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