| #!/usr/bin/env python |
| |
| # Copyright (c) 2011 The Chromium OS Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Build packages on a host machine, then install them on the local target. |
| |
| Contacts a devserver (trunk/src/platform/dev/devserver.py) and |
| requests that it build a package, then performs a binary install of |
| that package on the local machine. |
| """ |
| |
| import optparse |
| import os |
| import shutil |
| import subprocess |
| import sys |
| import urllib |
| import urllib2 |
| |
| LSB_RELEASE_PATH = '/etc/lsb-release' |
| STATEFUL_LSB_RELEASE_PATH = '/mnt/stateful_partition/etc/lsb-release' |
| |
| |
| class GMergeParsingException(Exception): |
| """A fatal exception raised when an expected variable is not parsed.""" |
| |
| |
| class GMerger(object): |
| """emerges a package from the devserver.""" |
| def __init__(self, devserver_url, board): |
| self.devserver_url = devserver_url |
| self.board_name = board |
| |
| @staticmethod |
| def RemountOrChangeRoot(environ): |
| """Remount the root filesystem rw; install into /usr/local if this fails. |
| |
| Args: |
| environ: The environment dictionary. |
| """ |
| rc = subprocess.call(['mount', '-o', 'remount,rw', '/']) |
| if rc == 0: |
| return |
| try: |
| answer = raw_input( |
| 'Could not mount / as writable. Install into /usr/local? (Y/n)') |
| except EOFError: |
| # Received if stdin is piped through /dev/null. |
| answer = None |
| |
| if answer and answer[0] not in 'Yy': |
| sys.exit('Better safe than sorry.') |
| |
| environ['ROOT'] = '/usr/local' |
| |
| def ParseLsbRelease(self, lsb_release_lines): |
| """Parses LSB release and set out internal variables accordingly |
| |
| Args: |
| lsb_release_lines: a list of key=val lines e.g. the output of opening |
| /etc/lsb-release and using readlines(). |
| """ |
| lsb_release = dict((k, v) for k, _, v in [line.rstrip().partition('=') |
| for line in lsb_release_lines]) |
| |
| parsing_msg = ('%(variable)s not set. Please set by using a command line ' |
| 'option or overriding in ' + STATEFUL_LSB_RELEASE_PATH) |
| if not self.devserver_url: |
| self.devserver_url = lsb_release['CHROMEOS_DEVSERVER'] |
| if not self.devserver_url: |
| raise GMergeParsingException(parsing_msg % dict( |
| variable='CHROMEOS_DEVSERVER')) |
| |
| if not self.board_name: |
| self.board_name = lsb_release['CHROMEOS_RELEASE_BOARD'] |
| if not self.board_name: |
| raise GMergeParsingException(parsing_msg % dict( |
| variable='CHROMEOS_RELEASE_BOARD')) |
| |
| def SetupPortageEnvironment(self, environ, include_masked_files): |
| """Setup portage to use stateful partition and fetch from dev server. |
| |
| Args: |
| environ: The environment dictionary to setup. |
| include_masked_files: If true, include masked files in package |
| (e.g. debug symbols). |
| """ |
| binhost_prefix = '%s/static/pkgroot/%s' % (self.devserver_url, |
| self.board_name) |
| binhost = '%s/packages' % binhost_prefix |
| if not include_masked_files: |
| binhost += ' %s/gmerge-packages' % binhost_prefix |
| |
| environ.update({ |
| 'PORTDIR': '/usr/local/portage', |
| 'PKGDIR': '/usr/local/portage', |
| 'DISTDIR': '/usr/local/portage/distfiles', |
| 'PORTAGE_BINHOST': binhost, |
| 'PORTAGE_TMPDIR': '/tmp', |
| 'CONFIG_PROTECT': '-*', |
| 'FEATURES': '-sandbox -usersandbox', |
| 'ACCEPT_KEYWORDS': 'arm x86 amd64 ~arm ~x86 ~amd64', |
| 'ROOT': os.environ.get('ROOT', '/'), |
| 'PORTAGE_CONFIGROOT': '/usr/local' |
| }) |
| |
| def RequestPackageBuild(self, package_name, deep, accept_stable, usepkg): |
| """Contacts devserver to request a build. |
| |
| Args: |
| package_name: The name of the package to build. |
| deep: Update package and all dependencies. |
| accept_stable: Allow non-workon packages. |
| usepkg: Use currently built binary packages on server. |
| """ |
| def GeneratePackageRequest(): |
| """Build the POST string that conveys our options to the devserver.""" |
| post_data = {'board': self.board_name, |
| 'deep': deep or '', |
| 'pkg': package_name, |
| 'features': os.environ.get('FEATURES'), |
| 'use': os.environ.get('USE'), |
| 'accept_stable': accept_stable or '', |
| 'usepkg': usepkg or '', |
| } |
| post_data = dict([(key, value) for (key, value) in post_data.iteritems() |
| if value is not None]) |
| return urllib.urlencode(post_data) |
| |
| print 'Sending build request to', self.devserver_url |
| try: |
| result = urllib2.urlopen( |
| self.devserver_url + '/build', |
| data=GeneratePackageRequest()) |
| print result.read() |
| result.close() |
| |
| except urllib2.HTTPError, e: |
| # The exception includes the content, which is the error mesage |
| sys.exit(e.read()) |
| except urllib2.URLError, e: |
| sys.exit('Could not reach devserver. Reason: %s' % e.reason) |
| |
| @staticmethod |
| def EmergePackage(package_name, deep, extra): |
| """Emerges the package from the binhost. |
| |
| Args: |
| package_name: The name of the package to build. |
| deep: Update package and all dependencies. |
| extra: Extra arguments to emerge. |
| """ |
| # In case the version is the same as the one that's installed, don't re-use |
| # it. |
| print 'Emerging ', package_name |
| shutil.rmtree('/usr/local/portage', ignore_errors=True) |
| os.makedirs('/usr/local/portage') |
| shutil.rmtree('/var/cache/edb/binhost', ignore_errors=True) |
| |
| emerge_args = ['emerge', '--getbinpkgonly', '--usepkgonly', '--verbose'] |
| if deep: |
| emerge_args.extend(['--update', '--deep']) |
| |
| if extra: |
| emerge_args.extend(extra.split()) |
| |
| emerge_args.append(package_name) |
| subprocess.check_call(emerge_args) |
| |
| |
| def main(): |
| parser = optparse.OptionParser(usage='usage: %prog [options] package_name') |
| parser.add_option('--accept_stable', |
| action='store_true', default=False, |
| help=('Build even if a cros_workon package is not ' |
| 'using the live package')) |
| parser.add_option('-b', '--board', default=None, |
| help='Specify a different board to use when building.') |
| parser.add_option('-d', '--devserver_url', default=None, |
| help='Specify a different devserver(binhost) url to use' |
| 'to build and download packages.') |
| parser.add_option('--include_masked_files', |
| action='store_true', |
| default=False, help=('Include masked files in package ' |
| '(e.g. debug symbols)')) |
| parser.add_option('-n', '--usepkg', |
| action='store_true', default=False, |
| help='Use currently built binary packages on server.') |
| parser.add_option('-D', '--deep', |
| action='store_true', default=False, |
| help='Update package and all dependencies ' |
| '(requires --usepkg).') |
| parser.add_option('-x', '--extra', default='', |
| help='Extra arguments to pass to emerge command.') |
| |
| options, remaining_arguments = parser.parse_args() |
| if len(remaining_arguments) != 1: |
| parser.print_help() |
| sys.exit('Need exactly one package name') |
| |
| |
| # TODO(davidjames): Should we allow --deep without --usepkg? Not sure what |
| # the desired behavior should be in this case, so disabling the combo for |
| # now. |
| if options.deep and not options.usepkg: |
| sys.exit('If using --deep, --usepkg must also be enabled.') |
| |
| package_name = remaining_arguments[0] |
| |
| subprocess.check_call(['mount', '-o', 'remount,exec', '/tmp']) |
| try: |
| etc_lsb_release_lines = open(LSB_RELEASE_PATH).readlines() |
| # Allow overrides from the stateful partition. |
| if os.path.exists(STATEFUL_LSB_RELEASE_PATH): |
| etc_lsb_release_lines += open(STATEFUL_LSB_RELEASE_PATH).readlines() |
| print 'Stateful lsb release file found', STATEFUL_LSB_RELEASE_PATH |
| |
| merger = GMerger(options.devserver_url, options.board) |
| merger.ParseLsbRelease(etc_lsb_release_lines) |
| merger.RequestPackageBuild(package_name, options.deep, |
| options.accept_stable, options.usepkg) |
| |
| merger.SetupPortageEnvironment(os.environ, options.include_masked_files) |
| merger.RemountOrChangeRoot(os.environ) |
| merger.EmergePackage(package_name, options.deep, options.extra) |
| finally: |
| subprocess.call(['mount', '-o', 'remount,noexec', '/tmp']) |
| |
| |
| if __name__ == '__main__': |
| main() |