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