toolchain.py 20 KB

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