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