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