toolchain.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571
  1. # Copyright (c) 2012 The Chromium OS Authors.
  2. #
  3. # SPDX-License-Identifier: GPL-2.0+
  4. #
  5. import re
  6. import glob
  7. from HTMLParser import HTMLParser
  8. import os
  9. import sys
  10. import tempfile
  11. import urllib2
  12. import bsettings
  13. import command
  14. import terminal
  15. (PRIORITY_FULL_PREFIX, PRIORITY_PREFIX_GCC, PRIORITY_PREFIX_GCC_PATH,
  16. PRIORITY_CALC) = range(4)
  17. # Simple class to collect links from a page
  18. class MyHTMLParser(HTMLParser):
  19. def __init__(self, arch):
  20. """Create a new parser
  21. After the parser runs, self.links will be set to a list of the links
  22. to .xz archives found in the page, and self.arch_link will be set to
  23. the one for the given architecture (or None if not found).
  24. Args:
  25. arch: Architecture to search for
  26. """
  27. HTMLParser.__init__(self)
  28. self.arch_link = None
  29. self.links = []
  30. self._match = '_%s-' % arch
  31. def handle_starttag(self, tag, attrs):
  32. if tag == 'a':
  33. for tag, value in attrs:
  34. if tag == 'href':
  35. if value and value.endswith('.xz'):
  36. self.links.append(value)
  37. if self._match in value:
  38. self.arch_link = value
  39. class Toolchain:
  40. """A single toolchain
  41. Public members:
  42. gcc: Full path to C compiler
  43. path: Directory path containing C compiler
  44. cross: Cross compile string, e.g. 'arm-linux-'
  45. arch: Architecture of toolchain as determined from the first
  46. component of the filename. E.g. arm-linux-gcc becomes arm
  47. priority: Toolchain priority (0=highest, 20=lowest)
  48. """
  49. def __init__(self, fname, test, verbose=False, priority=PRIORITY_CALC,
  50. arch=None):
  51. """Create a new toolchain object.
  52. Args:
  53. fname: Filename of the gcc component
  54. test: True to run the toolchain to test it
  55. verbose: True to print out the information
  56. priority: Priority to use for this toolchain, or PRIORITY_CALC to
  57. calculate it
  58. """
  59. self.gcc = fname
  60. self.path = os.path.dirname(fname)
  61. # Find the CROSS_COMPILE prefix to use for U-Boot. For example,
  62. # 'arm-linux-gnueabihf-gcc' turns into 'arm-linux-gnueabihf-'.
  63. basename = os.path.basename(fname)
  64. pos = basename.rfind('-')
  65. self.cross = basename[:pos + 1] if pos != -1 else ''
  66. # The architecture is the first part of the name
  67. pos = self.cross.find('-')
  68. if arch:
  69. self.arch = arch
  70. else:
  71. self.arch = self.cross[:pos] if pos != -1 else 'sandbox'
  72. env = self.MakeEnvironment(False)
  73. # As a basic sanity check, run the C compiler with --version
  74. cmd = [fname, '--version']
  75. if priority == PRIORITY_CALC:
  76. self.priority = self.GetPriority(fname)
  77. else:
  78. self.priority = priority
  79. if test:
  80. result = command.RunPipe([cmd], capture=True, env=env,
  81. raise_on_error=False)
  82. self.ok = result.return_code == 0
  83. if verbose:
  84. print 'Tool chain test: ',
  85. if self.ok:
  86. print "OK, arch='%s', priority %d" % (self.arch,
  87. self.priority)
  88. else:
  89. print 'BAD'
  90. print 'Command: ', cmd
  91. print result.stdout
  92. print result.stderr
  93. else:
  94. self.ok = True
  95. def GetPriority(self, fname):
  96. """Return the priority of the toolchain.
  97. Toolchains are ranked according to their suitability by their
  98. filename prefix.
  99. Args:
  100. fname: Filename of toolchain
  101. Returns:
  102. Priority of toolchain, PRIORITY_CALC=highest, 20=lowest.
  103. """
  104. priority_list = ['-elf', '-unknown-linux-gnu', '-linux',
  105. '-none-linux-gnueabi', '-uclinux', '-none-eabi',
  106. '-gentoo-linux-gnu', '-linux-gnueabi', '-le-linux', '-uclinux']
  107. for prio in range(len(priority_list)):
  108. if priority_list[prio] in fname:
  109. return PRIORITY_CALC + prio
  110. return PRIORITY_CALC + prio
  111. def GetWrapper(self, show_warning=True):
  112. """Get toolchain wrapper from the setting file.
  113. """
  114. value = ''
  115. for name, value in bsettings.GetItems('toolchain-wrapper'):
  116. if not value:
  117. print "Warning: Wrapper not found"
  118. if value:
  119. value = value + ' '
  120. return value
  121. def MakeEnvironment(self, full_path):
  122. """Returns an environment for using the toolchain.
  123. Thie takes the current environment and adds CROSS_COMPILE so that
  124. the tool chain will operate correctly.
  125. Args:
  126. full_path: Return the full path in CROSS_COMPILE and don't set
  127. PATH
  128. """
  129. env = dict(os.environ)
  130. wrapper = self.GetWrapper()
  131. if full_path:
  132. env['CROSS_COMPILE'] = wrapper + os.path.join(self.path, self.cross)
  133. else:
  134. env['CROSS_COMPILE'] = wrapper + self.cross
  135. env['PATH'] = self.path + ':' + env['PATH']
  136. return env
  137. class Toolchains:
  138. """Manage a list of toolchains for building U-Boot
  139. We select one toolchain for each architecture type
  140. Public members:
  141. toolchains: Dict of Toolchain objects, keyed by architecture name
  142. prefixes: Dict of prefixes to check, keyed by architecture. This can
  143. be a full path and toolchain prefix, for example
  144. {'x86', 'opt/i386-linux/bin/i386-linux-'}, or the name of
  145. something on the search path, for example
  146. {'arm', 'arm-linux-gnueabihf-'}. Wildcards are not supported.
  147. paths: List of paths to check for toolchains (may contain wildcards)
  148. """
  149. def __init__(self):
  150. self.toolchains = {}
  151. self.prefixes = {}
  152. self.paths = []
  153. self._make_flags = dict(bsettings.GetItems('make-flags'))
  154. def GetPathList(self, show_warning=True):
  155. """Get a list of available toolchain paths
  156. Args:
  157. show_warning: True to show a warning if there are no tool chains.
  158. Returns:
  159. List of strings, each a path to a toolchain mentioned in the
  160. [toolchain] section of the settings file.
  161. """
  162. toolchains = bsettings.GetItems('toolchain')
  163. if show_warning and not toolchains:
  164. print ("Warning: No tool chains. Please run 'buildman "
  165. "--fetch-arch all' to download all available toolchains, or "
  166. "add a [toolchain] section to your buildman config file "
  167. "%s. See README for details" %
  168. bsettings.config_fname)
  169. paths = []
  170. for name, value in toolchains:
  171. if '*' in value:
  172. paths += glob.glob(value)
  173. else:
  174. paths.append(value)
  175. return paths
  176. def GetSettings(self, show_warning=True):
  177. """Get toolchain settings from the settings file.
  178. Args:
  179. show_warning: True to show a warning if there are no tool chains.
  180. """
  181. self.prefixes = bsettings.GetItems('toolchain-prefix')
  182. self.paths += self.GetPathList(show_warning)
  183. def Add(self, fname, test=True, verbose=False, priority=PRIORITY_CALC,
  184. arch=None):
  185. """Add a toolchain to our list
  186. We select the given toolchain as our preferred one for its
  187. architecture if it is a higher priority than the others.
  188. Args:
  189. fname: Filename of toolchain's gcc driver
  190. test: True to run the toolchain to test it
  191. priority: Priority to use for this toolchain
  192. arch: Toolchain architecture, or None if not known
  193. """
  194. toolchain = Toolchain(fname, test, verbose, priority, arch)
  195. add_it = toolchain.ok
  196. if toolchain.arch in self.toolchains:
  197. add_it = (toolchain.priority <
  198. self.toolchains[toolchain.arch].priority)
  199. if add_it:
  200. self.toolchains[toolchain.arch] = toolchain
  201. elif verbose:
  202. print ("Toolchain '%s' at priority %d will be ignored because "
  203. "another toolchain for arch '%s' has priority %d" %
  204. (toolchain.gcc, toolchain.priority, toolchain.arch,
  205. self.toolchains[toolchain.arch].priority))
  206. def ScanPath(self, path, verbose):
  207. """Scan a path for a valid toolchain
  208. Args:
  209. path: Path to scan
  210. verbose: True to print out progress information
  211. Returns:
  212. Filename of C compiler if found, else None
  213. """
  214. fnames = []
  215. for subdir in ['.', 'bin', 'usr/bin']:
  216. dirname = os.path.join(path, subdir)
  217. if verbose: print " - looking in '%s'" % dirname
  218. for fname in glob.glob(dirname + '/*gcc'):
  219. if verbose: print " - found '%s'" % fname
  220. fnames.append(fname)
  221. return fnames
  222. def ScanPathEnv(self, fname):
  223. """Scan the PATH environment variable for a given filename.
  224. Args:
  225. fname: Filename to scan for
  226. Returns:
  227. List of matching pathanames, or [] if none
  228. """
  229. pathname_list = []
  230. for path in os.environ["PATH"].split(os.pathsep):
  231. path = path.strip('"')
  232. pathname = os.path.join(path, fname)
  233. if os.path.exists(pathname):
  234. pathname_list.append(pathname)
  235. return pathname_list
  236. def Scan(self, verbose):
  237. """Scan for available toolchains and select the best for each arch.
  238. We look for all the toolchains we can file, figure out the
  239. architecture for each, and whether it works. Then we select the
  240. highest priority toolchain for each arch.
  241. Args:
  242. verbose: True to print out progress information
  243. """
  244. if verbose: print 'Scanning for tool chains'
  245. for name, value in self.prefixes:
  246. if verbose: print " - scanning prefix '%s'" % value
  247. if os.path.exists(value):
  248. self.Add(value, True, verbose, PRIORITY_FULL_PREFIX, name)
  249. continue
  250. fname = value + 'gcc'
  251. if os.path.exists(fname):
  252. self.Add(fname, True, verbose, PRIORITY_PREFIX_GCC, name)
  253. continue
  254. fname_list = self.ScanPathEnv(fname)
  255. for f in fname_list:
  256. self.Add(f, True, verbose, PRIORITY_PREFIX_GCC_PATH, name)
  257. if not fname_list:
  258. raise ValueError, ("No tool chain found for prefix '%s'" %
  259. value)
  260. for path in self.paths:
  261. if verbose: print " - scanning path '%s'" % path
  262. fnames = self.ScanPath(path, verbose)
  263. for fname in fnames:
  264. self.Add(fname, True, verbose)
  265. def List(self):
  266. """List out the selected toolchains for each architecture"""
  267. col = terminal.Color()
  268. print col.Color(col.BLUE, 'List of available toolchains (%d):' %
  269. len(self.toolchains))
  270. if len(self.toolchains):
  271. for key, value in sorted(self.toolchains.iteritems()):
  272. print '%-10s: %s' % (key, value.gcc)
  273. else:
  274. print 'None'
  275. def Select(self, arch):
  276. """Returns the toolchain for a given architecture
  277. Args:
  278. args: Name of architecture (e.g. 'arm', 'ppc_8xx')
  279. returns:
  280. toolchain object, or None if none found
  281. """
  282. for tag, value in bsettings.GetItems('toolchain-alias'):
  283. if arch == tag:
  284. for alias in value.split():
  285. if alias in self.toolchains:
  286. return self.toolchains[alias]
  287. if not arch in self.toolchains:
  288. raise ValueError, ("No tool chain found for arch '%s'" % arch)
  289. return self.toolchains[arch]
  290. def ResolveReferences(self, var_dict, args):
  291. """Resolve variable references in a string
  292. This converts ${blah} within the string to the value of blah.
  293. This function works recursively.
  294. Args:
  295. var_dict: Dictionary containing variables and their values
  296. args: String containing make arguments
  297. Returns:
  298. Resolved string
  299. >>> bsettings.Setup()
  300. >>> tcs = Toolchains()
  301. >>> tcs.Add('fred', False)
  302. >>> var_dict = {'oblique' : 'OBLIQUE', 'first' : 'fi${second}rst', \
  303. 'second' : '2nd'}
  304. >>> tcs.ResolveReferences(var_dict, 'this=${oblique}_set')
  305. 'this=OBLIQUE_set'
  306. >>> tcs.ResolveReferences(var_dict, 'this=${oblique}_set${first}nd')
  307. 'this=OBLIQUE_setfi2ndrstnd'
  308. """
  309. re_var = re.compile('(\$\{[-_a-z0-9A-Z]{1,}\})')
  310. while True:
  311. m = re_var.search(args)
  312. if not m:
  313. break
  314. lookup = m.group(0)[2:-1]
  315. value = var_dict.get(lookup, '')
  316. args = args[:m.start(0)] + value + args[m.end(0):]
  317. return args
  318. def GetMakeArguments(self, board):
  319. """Returns 'make' arguments for a given board
  320. The flags are in a section called 'make-flags'. Flags are named
  321. after the target they represent, for example snapper9260=TESTING=1
  322. will pass TESTING=1 to make when building the snapper9260 board.
  323. References to other boards can be added in the string also. For
  324. example:
  325. [make-flags]
  326. at91-boards=ENABLE_AT91_TEST=1
  327. snapper9260=${at91-boards} BUILD_TAG=442
  328. snapper9g45=${at91-boards} BUILD_TAG=443
  329. This will return 'ENABLE_AT91_TEST=1 BUILD_TAG=442' for snapper9260
  330. and 'ENABLE_AT91_TEST=1 BUILD_TAG=443' for snapper9g45.
  331. A special 'target' variable is set to the board target.
  332. Args:
  333. board: Board object for the board to check.
  334. Returns:
  335. 'make' flags for that board, or '' if none
  336. """
  337. self._make_flags['target'] = board.target
  338. arg_str = self.ResolveReferences(self._make_flags,
  339. self._make_flags.get(board.target, ''))
  340. args = arg_str.split(' ')
  341. i = 0
  342. while i < len(args):
  343. if not args[i]:
  344. del args[i]
  345. else:
  346. i += 1
  347. return args
  348. def LocateArchUrl(self, fetch_arch):
  349. """Find a toolchain available online
  350. Look in standard places for available toolchains. At present the
  351. only standard place is at kernel.org.
  352. Args:
  353. arch: Architecture to look for, or 'list' for all
  354. Returns:
  355. If fetch_arch is 'list', a tuple:
  356. Machine architecture (e.g. x86_64)
  357. List of toolchains
  358. else
  359. URL containing this toolchain, if avaialble, else None
  360. """
  361. arch = command.OutputOneLine('uname', '-m')
  362. base = 'https://www.kernel.org/pub/tools/crosstool/files/bin'
  363. versions = ['4.9.0', '4.6.3', '4.6.2', '4.5.1', '4.2.4']
  364. links = []
  365. for version in versions:
  366. url = '%s/%s/%s/' % (base, arch, version)
  367. print 'Checking: %s' % url
  368. response = urllib2.urlopen(url)
  369. html = response.read()
  370. parser = MyHTMLParser(fetch_arch)
  371. parser.feed(html)
  372. if fetch_arch == 'list':
  373. links += parser.links
  374. elif parser.arch_link:
  375. return url + parser.arch_link
  376. if fetch_arch == 'list':
  377. return arch, links
  378. return None
  379. def Download(self, url):
  380. """Download a file to a temporary directory
  381. Args:
  382. url: URL to download
  383. Returns:
  384. Tuple:
  385. Temporary directory name
  386. Full path to the downloaded archive file in that directory,
  387. or None if there was an error while downloading
  388. """
  389. print 'Downloading: %s' % url
  390. leaf = url.split('/')[-1]
  391. tmpdir = tempfile.mkdtemp('.buildman')
  392. response = urllib2.urlopen(url)
  393. fname = os.path.join(tmpdir, leaf)
  394. fd = open(fname, 'wb')
  395. meta = response.info()
  396. size = int(meta.getheaders('Content-Length')[0])
  397. done = 0
  398. block_size = 1 << 16
  399. status = ''
  400. # Read the file in chunks and show progress as we go
  401. while True:
  402. buffer = response.read(block_size)
  403. if not buffer:
  404. print chr(8) * (len(status) + 1), '\r',
  405. break
  406. done += len(buffer)
  407. fd.write(buffer)
  408. status = r'%10d MiB [%3d%%]' % (done / 1024 / 1024,
  409. done * 100 / size)
  410. status = status + chr(8) * (len(status) + 1)
  411. print status,
  412. sys.stdout.flush()
  413. fd.close()
  414. if done != size:
  415. print 'Error, failed to download'
  416. os.remove(fname)
  417. fname = None
  418. return tmpdir, fname
  419. def Unpack(self, fname, dest):
  420. """Unpack a tar file
  421. Args:
  422. fname: Filename to unpack
  423. dest: Destination directory
  424. Returns:
  425. Directory name of the first entry in the archive, without the
  426. trailing /
  427. """
  428. stdout = command.Output('tar', 'xvfJ', fname, '-C', dest)
  429. return stdout.splitlines()[0][:-1]
  430. def TestSettingsHasPath(self, path):
  431. """Check if buildman will find this toolchain
  432. Returns:
  433. True if the path is in settings, False if not
  434. """
  435. paths = self.GetPathList(False)
  436. return path in paths
  437. def ListArchs(self):
  438. """List architectures with available toolchains to download"""
  439. host_arch, archives = self.LocateArchUrl('list')
  440. re_arch = re.compile('[-a-z0-9.]*_([^-]*)-.*')
  441. arch_set = set()
  442. for archive in archives:
  443. # Remove the host architecture from the start
  444. arch = re_arch.match(archive[len(host_arch):])
  445. if arch:
  446. arch_set.add(arch.group(1))
  447. return sorted(arch_set)
  448. def FetchAndInstall(self, arch):
  449. """Fetch and install a new toolchain
  450. arch:
  451. Architecture to fetch, or 'list' to list
  452. """
  453. # Fist get the URL for this architecture
  454. col = terminal.Color()
  455. print col.Color(col.BLUE, "Downloading toolchain for arch '%s'" % arch)
  456. url = self.LocateArchUrl(arch)
  457. if not url:
  458. print ("Cannot find toolchain for arch '%s' - use 'list' to list" %
  459. arch)
  460. return 2
  461. home = os.environ['HOME']
  462. dest = os.path.join(home, '.buildman-toolchains')
  463. if not os.path.exists(dest):
  464. os.mkdir(dest)
  465. # Download the tar file for this toolchain and unpack it
  466. tmpdir, tarfile = self.Download(url)
  467. if not tarfile:
  468. return 1
  469. print col.Color(col.GREEN, 'Unpacking to: %s' % dest),
  470. sys.stdout.flush()
  471. path = self.Unpack(tarfile, dest)
  472. os.remove(tarfile)
  473. os.rmdir(tmpdir)
  474. print
  475. # Check that the toolchain works
  476. print col.Color(col.GREEN, 'Testing')
  477. dirpath = os.path.join(dest, path)
  478. compiler_fname_list = self.ScanPath(dirpath, True)
  479. if not compiler_fname_list:
  480. print 'Could not locate C compiler - fetch failed.'
  481. return 1
  482. if len(compiler_fname_list) != 1:
  483. print col.Color(col.RED, 'Warning, ambiguous toolchains: %s' %
  484. ', '.join(compiler_fname_list))
  485. toolchain = Toolchain(compiler_fname_list[0], True, True)
  486. # Make sure that it will be found by buildman
  487. if not self.TestSettingsHasPath(dirpath):
  488. print ("Adding 'download' to config file '%s'" %
  489. bsettings.config_fname)
  490. bsettings.SetItem('toolchain', 'download', '%s/*/*' % dest)
  491. return 0