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        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
231class 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 = ['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 Download(self, url):
519        """Download a file to a temporary directory
520
521        Args:
522            url: URL to download
523        Returns:
524            Tuple:
525                Temporary directory name
526                Full path to the downloaded archive file in that directory,
527                    or None if there was an error while downloading
528        """
529        print('Downloading: %s' % url)
530        leaf = url.split('/')[-1]
531        tmpdir = tempfile.mkdtemp('.buildman')
532        response = urllib.request.urlopen(url)
533        fname = os.path.join(tmpdir, leaf)
534        fd = open(fname, 'wb')
535        meta = response.info()
536        size = int(meta.get('Content-Length'))
537        done = 0
538        block_size = 1 << 16
539        status = ''
540
541        # Read the file in chunks and show progress as we go
542        while True:
543            buffer = response.read(block_size)
544            if not buffer:
545                print(chr(8) * (len(status) + 1), '\r', end=' ')
546                break
547
548            done += len(buffer)
549            fd.write(buffer)
550            status = r'%10d MiB  [%3d%%]' % (done // 1024 // 1024,
551                                             done * 100 // size)
552            status = status + chr(8) * (len(status) + 1)
553            print(status, end=' ')
554            sys.stdout.flush()
555        fd.close()
556        if done != size:
557            print('Error, failed to download')
558            os.remove(fname)
559            fname = None
560        return tmpdir, fname
561
562    def Unpack(self, fname, dest):
563        """Unpack a tar file
564
565        Args:
566            fname: Filename to unpack
567            dest: Destination directory
568        Returns:
569            Directory name of the first entry in the archive, without the
570            trailing /
571        """
572        stdout = command.Output('tar', 'xvfJ', fname, '-C', dest)
573        dirs = stdout.splitlines()[1].split('/')[:2]
574        return '/'.join(dirs)
575
576    def TestSettingsHasPath(self, path):
577        """Check if buildman will find this toolchain
578
579        Returns:
580            True if the path is in settings, False if not
581        """
582        paths = self.GetPathList(False)
583        return path in paths
584
585    def ListArchs(self):
586        """List architectures with available toolchains to download"""
587        host_arch, archives = self.LocateArchUrl('list')
588        re_arch = re.compile('[-a-z0-9.]*[-_]([^-]*)-.*')
589        arch_set = set()
590        for archive in archives:
591            # Remove the host architecture from the start
592            arch = re_arch.match(archive[len(host_arch):])
593            if arch:
594                if arch.group(1) != '2.0' and arch.group(1) != '64':
595                    arch_set.add(arch.group(1))
596        return sorted(arch_set)
597
598    def FetchAndInstall(self, arch):
599        """Fetch and install a new toolchain
600
601        arch:
602            Architecture to fetch, or 'list' to list
603        """
604        # Fist get the URL for this architecture
605        col = terminal.Color()
606        print(col.Color(col.BLUE, "Downloading toolchain for arch '%s'" % arch))
607        url = self.LocateArchUrl(arch)
608        if not url:
609            print(("Cannot find toolchain for arch '%s' - use 'list' to list" %
610                   arch))
611            return 2
612        home = os.environ['HOME']
613        dest = os.path.join(home, '.buildman-toolchains')
614        if not os.path.exists(dest):
615            os.mkdir(dest)
616
617        # Download the tar file for this toolchain and unpack it
618        tmpdir, tarfile = self.Download(url)
619        if not tarfile:
620            return 1
621        print(col.Color(col.GREEN, 'Unpacking to: %s' % dest), end=' ')
622        sys.stdout.flush()
623        path = self.Unpack(tarfile, dest)
624        os.remove(tarfile)
625        os.rmdir(tmpdir)
626        print()
627
628        # Check that the toolchain works
629        print(col.Color(col.GREEN, 'Testing'))
630        dirpath = os.path.join(dest, path)
631        compiler_fname_list = self.ScanPath(dirpath, True)
632        if not compiler_fname_list:
633            print('Could not locate C compiler - fetch failed.')
634            return 1
635        if len(compiler_fname_list) != 1:
636            print(col.Color(col.RED, 'Warning, ambiguous toolchains: %s' %
637                            ', '.join(compiler_fname_list)))
638        toolchain = Toolchain(compiler_fname_list[0], True, True)
639
640        # Make sure that it will be found by buildman
641        if not self.TestSettingsHasPath(dirpath):
642            print(("Adding 'download' to config file '%s'" %
643                   bsettings.config_fname))
644            bsettings.SetItem('toolchain', 'download', '%s/*/*' % dest)
645        return 0
646