1# SPDX-License-Identifier: GPL-2.0+
2# Copyright (c) 2012 The Chromium OS Authors.
3#
4
5import re
6import glob
7from html.parser import HTMLParser
8import os
9import sys
10import tempfile
11import urllib.request, urllib.error, urllib.parse
12
13from buildman import bsettings
14from patman import command
15from patman import terminal
16from 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
24class 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
50class 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
223class 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 = ['9.2.0', '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
638