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