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