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