toolchain.py 20 KB

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