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