blob: 406ca4bccf5eeba6bb35e493c26a27ca47de154e [file] [log] [blame]
Zdenek Behan508dcce2011-12-05 15:39:32 +01001#!/usr/bin/env python
2# Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""This script manages the installed toolchains in the chroot.
7"""
8
9import copy
10import optparse
11import os
12import sys
13
14
15sys.path.insert(0, os.path.abspath(__file__ + '/../..'))
16import lib.cros_build_lib as cros_build_lib
17import buildbot.constants as constants
18
19# Some sanity checks first.
20if not cros_build_lib.IsInsideChroot():
21 print '%s: This needs to be run inside the chroot' % sys.argv[0]
22 sys.exit(1)
23# Only import portage after we've checked that we're inside the chroot.
24# Outside may not have portage, in which case the above may not happen.
25import portage
26
27
28EMERGE_CMD = os.path.join(os.path.dirname(__file__), 'parallel_emerge')
29PACKAGE_STABLE = '[stable]'
30PACKAGE_NONE = '[none]'
31SRC_ROOT = os.path.realpath(constants.SOURCE_ROOT)
32TOOLCHAIN_PACKAGES = ('gcc', 'glibc', 'binutils', 'linux-headers', 'gdb')
33
34
35# TODO: The versions are stored here very much like in setup_board.
36# The goal for future is to differentiate these using a config file.
37# This is done essentially by messing with GetDesiredPackageVersions()
38DEFAULT_VERSION = PACKAGE_STABLE
39DEFAULT_TARGET_VERSION_MAP = {
40 'linux-headers' : '3.1',
41 'binutils' : '2.21-r4',
42}
43TARGET_VERSION_MAP = {
44 'arm-none-eabi' : {
45 'glibc' : PACKAGE_NONE,
46 'linux-headers' : PACKAGE_NONE,
47 'gdb' : PACKAGE_NONE,
48 },
49 'host' : {
50 'binutils' : '2.21.1',
51 'gdb' : PACKAGE_NONE,
52 },
53}
54# Overrides for {gcc,binutils}-config, pick a package with particular suffix.
55CONFIG_TARGET_SUFFIXES = {
56 'binutils' : {
57 'i686-pc-linux-gnu' : '-gold',
58 'x86_64-cros-linux-gnu' : '-gold',
59 },
60}
61# FIXME(zbehan): This is used to override the above. Before we compile
62# cross-glibc, we need to set the cross-binutils to GNU ld. Ebuilds should
63# handle this by themselves.
64CONFIG_TARGET_SUFFIXES_nongold = {
65 'binutils' : {
66 'i686-pc-linux-gnu' : '',
67 'x86_64-cros-linux-gnu' : '',
68 },
69}
70# Global per-run cache that will be filled ondemand in by GetPackageMap()
71# function as needed.
72target_version_map = {
73}
74
75
76def GetPackageMap(target):
77 """Compiles a package map for the given target from the constants.
78
79 Uses a cache in target_version_map, that is dynamically filled in as needed,
80 since here everything is static data and the structuring is for ease of
81 configurability only.
82
83 args:
84 target - the target for which to return a version map
85
86 returns a map between packages and desired versions in internal format
87 (using the PACKAGE_* constants)
88 """
89 if target in target_version_map:
90 return target_version_map[target]
91
92 # Start from copy of the global defaults.
93 result = copy.copy(DEFAULT_TARGET_VERSION_MAP)
94
95 for pkg in TOOLCHAIN_PACKAGES:
96 # prefer any specific overrides
97 if pkg in TARGET_VERSION_MAP.get(target, {}):
98 result[pkg] = TARGET_VERSION_MAP[target][pkg]
99 else:
100 # finally, if not already set, set a sane default
101 result.setdefault(pkg, DEFAULT_VERSION)
102 target_version_map[target] = result
103 return result
104
105
106# Helper functions:
107def GetPortageCategory(target, package):
108 """Creates a package name for the given target.
109
110 The function provides simple abstraction around the confusing fact that
111 cross packages all live under a single category, while host packages have
112 categories varied. This lets us further identify packages solely by the
113 (target, package) pair and worry less about portage names.
114
115 args:
116 target, package - the target/package to operate on eg. i686-pc-linux-gnu,gcc
117
118 returns string with the portage category
119 """
120 HOST_MAP = {
121 'gcc' : 'sys-devel',
122 'gdb' : 'sys-devel',
123 'glibc' : 'sys-libs',
124 'binutils' : 'sys-devel',
125 'linux-headers' : 'sys-kernel',
126 }
127
128 if target == 'host':
129 return HOST_MAP[package]
130 else:
131 return 'cross-' + target
132
133
134def GetHostTarget():
135 """Returns a string for the host target tuple."""
136 return portage.settings['CHOST']
137
138
139# Tree interface functions. They help with retrieving data about the current
140# state of the tree:
141def GetAllTargets():
142 """Get the complete list of targets.
143
144 returns the list of cross targets for the current tree
145 """
146 cmd = ['cros_overlay_list', '--all_boards']
147 overlays = cros_build_lib.RunCommand(cmd, print_cmd=False,
148 redirect_stdout=True).output.splitlines()
149 targets = set()
150 for overlay in overlays:
151 config = os.path.join(overlay, 'toolchain.conf')
152 if os.path.exists(config):
153 with open(config) as config_file:
154 lines = config_file.read().splitlines()
155 for line in lines:
156 # Remove empty lines and commented lines.
157 if line and not line.startswith('#'):
158 targets.add(line)
159
160 # Remove the host target as that is not a cross-target. Replace with 'host'.
161 targets.remove(GetHostTarget())
162 targets.add('host')
163 return targets
164
165
166def GetInstalledPackageVersions(target, package):
167 """Extracts the list of current versions of a target, package pair.
168
169 args:
170 target, package - the target/package to operate on eg. i686-pc-linux-gnu,gcc
171
172 returns the list of versions of the package currently installed.
173 """
174 category = GetPortageCategory(target, package)
175 versions = []
176 # This is the package name in terms of portage.
177 atom = '%s/%s' % (category, package)
178 for pkg in portage.db['/']['vartree'].dbapi.match(atom):
179 version = portage.versions.cpv_getversion(pkg)
180 versions.append(version)
181 return versions
182
183
184def GetPortageKeyword(_target):
185 """Returns a portage friendly keyword for a given target."""
186 # NOTE: This table is part of the one found in crossdev.
187 PORTAGE_KEYWORD_MAP = {
188 'x86_64-' : 'amd64',
189 'i686-' : 'x86',
190 'arm' : 'arm',
191 }
192 if _target == 'host':
193 target = GetHostTarget()
194 else:
195 target = _target
196
197 for prefix, arch in PORTAGE_KEYWORD_MAP.iteritems():
198 if target.startswith(prefix):
199 return arch
200 else:
201 raise RuntimeError("Unknown target: " + _target)
202
203
204def GetStablePackageVersion(target, package):
205 """Extracts the current stable version for a given package.
206
207 args:
208 target, package - the target/package to operate on eg. i686-pc-linux-gnu,gcc
209
210 returns a string containing the latest version.
211 """
212 category = GetPortageCategory(target, package)
213 keyword = GetPortageKeyword(target)
214 extra_env = {'ACCEPT_KEYWORDS' : '-* ' + keyword}
215 atom = '%s/%s' % (category, package)
216 cpv = cros_build_lib.RunCommand(['portageq', 'best_visible', '/', atom],
217 print_cmd=False, redirect_stdout=True,
218 extra_env=extra_env).output.splitlines()[0]
219 return portage.versions.cpv_getversion(cpv)
220
221
222def VersionListToNumeric(target, package, versions):
223 """Resolves keywords in a given version list for a particular package.
224
225 Resolving means replacing PACKAGE_STABLE with the actual number.
226
227 args:
228 target, package - the target/package to operate on eg. i686-pc-linux-gnu,gcc
229 versions - list of versions to resolve
230
231 returns list of purely numeric versions equivalent to argument
232 """
233 resolved = []
234 for version in versions:
235 if version == PACKAGE_STABLE:
236 resolved.append(GetStablePackageVersion(target, package))
237 elif version != PACKAGE_NONE:
238 resolved.append(version)
239 return resolved
240
241
242def GetDesiredPackageVersions(target, package):
243 """Produces the list of desired versions for each target, package pair.
244
245 The first version in the list is implicitly treated as primary, ie.
246 the version that will be initialized by crossdev and selected.
247
248 If the version is PACKAGE_STABLE, it really means the current version which
249 is emerged by using the package atom with no particular version key.
250 Since crossdev unmasks all packages by default, this will actually
251 mean 'unstable' in most cases.
252
253 args:
254 target, package - the target/package to operate on eg. i686-pc-linux-gnu,gcc
255
256 returns a list composed of either a version string, PACKAGE_STABLE
257 """
258 packagemap = GetPackageMap(target)
259
260 versions = []
261 if package in packagemap:
262 versions.append(packagemap[package])
263
264 return versions
265
266
267def TargetIsInitialized(target):
268 """Verifies if the given list of targets has been correctly initialized.
269
270 This determines whether we have to call crossdev while emerging
271 toolchain packages or can do it using emerge. Emerge is naturally
272 preferred, because all packages can be updated in a single pass.
273
274 args:
275 targets - list of individual cross targets which are checked
276
277 returns True if target is completely initialized
278 returns False otherwise
279 """
280 # Check if packages for the given target all have a proper version.
281 try:
282 for package in TOOLCHAIN_PACKAGES:
283 if not GetInstalledPackageVersions(target, package) and \
284 GetDesiredPackageVersions(target, package) != [PACKAGE_NONE]:
285 return False
286 return True
287 except cros_build_lib.RunCommandError:
288 # Fails - The target has likely never been initialized before.
289 return False
290
291
292def RemovePackageMask(target):
293 """Removes a package.mask file for the given platform.
294
295 The pre-existing package.mask files can mess with the keywords.
296
297 args:
298 target - the target for which to remove the file
299 """
300 maskfile = os.path.join('/etc/portage/package.mask', 'cross-' + target)
301 # Note: We have to use sudo here, in case the file is created with
302 # root ownership. However, sudo in chroot is always passwordless.
303 cros_build_lib.SudoRunCommand(['rm', '-f', maskfile],
304 redirect_stdout=True, print_cmd=False)
305
306
307def CreatePackageMask(target, masks):
308 """[Re]creates a package.mask file for the given platform.
309
310 args:
311 target - the given target on which to operate
312 masks - a map of package : version,
313 where version is the highest permissible version (mask >)
314 """
315 maskfile = os.path.join('/etc/portage/package.mask', 'cross-' + target)
316 assert not os.path.exists(maskfile)
317 # package.mask/ isn't writable, so we need to create and
318 # chown the file before we use it.
319 cros_build_lib.SudoRunCommand(['touch', maskfile],
320 redirect_stdout=True, print_cmd=False)
321 cros_build_lib.SudoRunCommand(['chown', str(os.getuid()), maskfile],
322 redirect_stdout=True, print_cmd=False)
323
324 with open(maskfile, 'w') as f:
325 for pkg, m in masks.items():
326 f.write('>%s-%s\n' % (pkg, m))
327
328
329def CreatePackageKeywords(target):
330 """[Re]create a package.keywords file for the platform.
331
332 This sets all package.keywords files to unmask all stable/testing packages.
333 TODO: Note that this approach should be deprecated and is only done for
334 compatibility reasons. In the future, we'd like to stop using keywords
335 altogether, and keep just stable unmasked.
336
337 args:
338 target - target for which to recreate package.keywords
339 """
340 maskfile = os.path.join('/etc/portage/package.keywords', 'cross-' + target)
341 cros_build_lib.SudoRunCommand(['rm', '-f', maskfile],
342 redirect_stdout=True, print_cmd=False)
343 cros_build_lib.SudoRunCommand(['touch', maskfile],
344 redirect_stdout=True, print_cmd=False)
345 cros_build_lib.SudoRunCommand(['chown', '%d' % os.getuid(), maskfile],
346 redirect_stdout=True, print_cmd=False)
347
348 keyword = GetPortageKeyword(target)
349
350 with open(maskfile, 'w') as f:
351 for pkg in TOOLCHAIN_PACKAGES:
352 f.write('%s/%s %s ~%s\n' %
353 (GetPortageCategory(target, pkg), pkg, keyword, keyword))
354
355
356# Main functions performing the actual update steps.
357def InitializeCrossdevTargets(targets, usepkg):
358 """Calls crossdev to initialize a cross target.
359 args:
360 targets - the list of targets to initialize using crossdev
361 usepkg - copies the commandline opts
362 """
363 print 'The following targets need to be re-initialized:'
364 print targets
365 CROSSDEV_FLAG_MAP = {
366 'gcc' : '--gcc',
367 'glibc' : '--libc',
368 'linux-headers' : '--kernel',
369 'binutils' : '--binutils',
370 }
371
372 for target in targets:
373 cmd = ['sudo', 'FEATURES=splitdebug', 'crossdev', '-v', '-t', target]
374 # Pick stable by default, and override as necessary.
375 cmd.extend(['-P', '--oneshot'])
376 if usepkg:
377 cmd.extend(['-P', '--getbinpkg',
378 '-P', '--usepkgonly',
379 '--without-headers'])
380 cmd.append('--ex-gdb')
381
382 # HACK(zbehan): arm-none-eabi uses newlib which doesn't like headers-only.
383 if target == 'arm-none-eabi':
384 cmd.append('--without-headers')
385
386 for pkg in CROSSDEV_FLAG_MAP:
387 # The first of the desired versions is the "primary" one.
388 version = GetDesiredPackageVersions(target, pkg)[0]
389 if version != PACKAGE_NONE:
390 cmd.extend([CROSSDEV_FLAG_MAP[pkg], version])
391 cros_build_lib.RunCommand(cmd)
392
393
394def UpdateTargets(targets, usepkg):
395 """Determines which packages need update/unmerge and defers to portage.
396
397 args:
398 targets - the list of targets to update
399 usepkg - copies the commandline option
400 """
401 # TODO(zbehan): This process is rather complex due to package.* handling.
402 # With some semantic changes over the original setup_board functionality,
403 # it can be considerably cleaned up.
404 mergemap = {}
405
406 # For each target, we do two things. Figure out the list of updates,
407 # and figure out the appropriate keywords/masks. Crossdev will initialize
408 # these, but they need to be regenerated on every update.
409 print 'Determining required toolchain updates...'
410 for target in targets:
411 # Record the highest needed version for each target, for masking purposes.
412 RemovePackageMask(target)
413 CreatePackageKeywords(target)
414 packagemasks = {}
415 for package in TOOLCHAIN_PACKAGES:
416 # Portage name for the package
417 pkg = GetPortageCategory(target, package) + '/' + package
418 current = GetInstalledPackageVersions(target, package)
419 desired = GetDesiredPackageVersions(target, package)
420 if desired != [PACKAGE_NONE]:
421 desired_num = VersionListToNumeric(target, package, desired)
422 mergemap[pkg] = set(desired_num).difference(current)
423
424 # Pick the highest version for mask.
425 packagemasks[pkg] = portage.versions.best(desired_num)
426
427 CreatePackageMask(target, packagemasks)
428
429 packages = []
430 for pkg in mergemap:
431 for ver in mergemap[pkg]:
432 if ver == PACKAGE_STABLE:
433 packages.append(pkg)
434 elif ver != PACKAGE_NONE:
435 packages.append('=%s-%s' % (pkg, ver))
436
437 if not packages:
438 print 'Nothing to update!'
439 return
440
441 print 'Updating packages:'
442 print packages
443
444 # FIXME(zbehan): Override gold if we're doing source builds. See comment
445 # at the constant definition.
446 if not usepkg:
447 SelectActiveToolchains(targets, CONFIG_TARGET_SUFFIXES_nongold)
448
449 cmd = ['sudo', '-E', 'FEATURES=splitdebug', EMERGE_CMD,
450 '--oneshot', '--update']
451 if usepkg:
452 cmd.extend(['--getbinpkg', '--usepkgonly'])
453
454 cmd.extend(packages)
455 cros_build_lib.RunCommand(cmd)
456
457
458def CleanTargets(targets):
459 """Unmerges old packages that are assumed unnecessary."""
460 unmergemap = {}
461 for target in targets:
462 for package in TOOLCHAIN_PACKAGES:
463 pkg = GetPortageCategory(target, package) + '/' + package
464 current = GetInstalledPackageVersions(target, package)
465 desired = GetDesiredPackageVersions(target, package)
466 desired_num = VersionListToNumeric(target, package, desired)
467 assert set(desired).issubset(set(desired))
468 unmergemap[pkg] = set(current).difference(desired_num)
469
470 # Cleaning doesn't care about consistency and rebuilding package.* files.
471 packages = []
472 for pkg, vers in unmergemap.iteritems():
473 packages.extend('=%s-%s' % (pkg, ver) for ver in vers if ver != '9999')
474
475 if packages:
476 print 'Cleaning packages:'
477 print packages
478 cmd = ['sudo', '-E', EMERGE_CMD, '--unmerge']
479 cmd.extend(packages)
480 cros_build_lib.RunCommand(cmd)
481 else:
482 print 'Nothing to clean!'
483
484
485def SelectActiveToolchains(targets, suffixes):
486 """Runs gcc-config and binutils-config to select the desired.
487
488 args:
489 targets - the targets to select
490 """
491 for package in ['gcc', 'binutils']:
492 for target in targets:
493 # Pick the first version in the numbered list as the selected one.
494 desired = GetDesiredPackageVersions(target, package)
495 desired_num = VersionListToNumeric(target, package, desired)
496 desired = desired_num[0]
497 # *-config does not play revisions, strip them, keep just PV.
498 desired = portage.versions.pkgsplit('%s-%s' % (package, desired))[1]
499
500 if target == 'host':
501 # *-config is the only tool treating host identically (by tuple).
502 target = GetHostTarget()
503
504 # And finally, attach target to it.
505 desired = '%s-%s' % (target, desired)
506
507 # Target specific hacks
508 if package in suffixes:
509 if target in suffixes[package]:
510 desired += suffixes[package][target]
511
512 cmd = [ package + '-config', '-c', target ]
513 current = cros_build_lib.RunCommand(cmd, print_cmd=False,
514 redirect_stdout=True).output.splitlines()[0]
515 # Do not gcc-config when the current is live or nothing needs to be done.
516 if current != desired and current != '9999':
517 cmd = [ package + '-config', desired ]
518 cros_build_lib.SudoRunCommand(cmd, print_cmd=False)
519
520
521def UpdateToolchains(usepkg, deleteold, targets):
522 """Performs all steps to create a synchronized toolchain enviroment.
523
524 args:
525 arguments correspond to the given commandline flags
526 """
527 alltargets = GetAllTargets()
528 nonexistant = []
529 if targets == set(['all']):
530 targets = alltargets
531 else:
532 # Verify user input.
533 for target in targets:
534 if target not in alltargets:
535 nonexistant.append(target)
536 if nonexistant:
537 raise Exception("Invalid targets: " + ','.join(nonexistant))
538
539 # First check and initialize all cross targets that need to be.
540 crossdev_targets = \
541 [t for t in targets if not TargetIsInitialized(t) and not 'host' == t]
542 if crossdev_targets:
543 try:
544 SelectActiveToolchains(crossdev_targets, CONFIG_TARGET_SUFFIXES_nongold)
545 except cros_build_lib.RunCommandError:
546 # This can fail if the target has never been initialized before.
547 pass
548 InitializeCrossdevTargets(crossdev_targets, usepkg)
549
550 # Now update all packages, including host.
551 targets.add('host')
552 UpdateTargets(targets, usepkg)
553 SelectActiveToolchains(targets, CONFIG_TARGET_SUFFIXES)
554
555 if deleteold:
556 CleanTargets(targets)
557
558
559def main():
560 usage = """usage: %prog [options]
561
562 The script installs and updates the toolchains in your chroot.
563 """
564 parser = optparse.OptionParser(usage)
565 parser.add_option('-u', '--nousepkg',
566 action='store_false', dest='usepkg', default=True,
567 help=('Use prebuilt packages if possible.'))
568 parser.add_option('-d', '--deleteold',
569 action='store_true', dest='deleteold', default=False,
570 help=('Unmerge deprecated packages.'))
571 parser.add_option('-t', '--targets',
572 dest='targets', default='all',
573 help=('Comma separated list of targets. Default: all'))
574
575 (options, _remaining_arguments) = parser.parse_args()
576
577 targets = set(options.targets.split(','))
578 UpdateToolchains(options.usepkg, options.deleteold, targets)
579
580if __name__ == '__main__':
581 main()