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 = ['11.1.0', '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