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