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