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