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