toolchain.py 16 KB

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