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