David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 1 | #!/usr/bin/env python |
Ryan Cairns | dd1ceb8 | 2010-03-03 05:35:01 | [diff] [blame] | 2 | |
David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 3 | # Copyright (c) 2011 The Chromium OS Authors. All rights reserved. |
Ryan Cairns | dd1ceb8 | 2010-03-03 05:35:01 | [diff] [blame] | 4 | # Use of this source code is governed by a BSD-style license that can be |
| 5 | # found in the LICENSE file. |
| 6 | |
David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 7 | """Build packages on a host machine, then install them on the local target. |
Chris Sosa | 136418c | 2010-11-11 00:27:14 | [diff] [blame] | 8 | |
David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 9 | Contacts a devserver (trunk/src/platform/dev/devserver.py) and |
| 10 | requests that it build a package, then performs a binary install of |
| 11 | that package on the local machine. |
| 12 | """ |
Chris Sosa | 4a1e819 | 2010-12-13 22:22:41 | [diff] [blame] | 13 | |
David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 14 | import optparse |
| 15 | import os |
| 16 | import shutil |
| 17 | import subprocess |
| 18 | import sys |
| 19 | import urllib |
| 20 | import urllib2 |
Chris Sosa | 4c9b2f9 | 2010-12-14 00:09:06 | [diff] [blame] | 21 | |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 22 | LSB_RELEASE_PATH = '/etc/lsb-release' |
| 23 | STATEFUL_LSB_RELEASE_PATH = '/mnt/stateful_partition/etc/lsb-release' |
| 24 | |
| 25 | |
| 26 | class GMergeParsingException(Exception): |
| 27 | """A fatal exception raised when an expected variable is not parsed.""" |
| 28 | |
Ryan Cairns | dd1ceb8 | 2010-03-03 05:35:01 | [diff] [blame] | 29 | |
David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 30 | class GMerger(object): |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 31 | """emerges a package from the devserver.""" |
| 32 | def __init__(self, devserver_url, board): |
| 33 | self.devserver_url = devserver_url |
| 34 | self.board_name = board |
Chris Sosa | 136418c | 2010-11-11 00:27:14 | [diff] [blame] | 35 | |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 36 | @staticmethod |
| 37 | def RemountOrChangeRoot(environ): |
| 38 | """Remount the root filesystem rw; install into /usr/local if this fails. |
Chris Sosa | 4a1e819 | 2010-12-13 22:22:41 | [diff] [blame] | 39 | |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 40 | Args: |
| 41 | environ: The environment dictionary. |
| 42 | """ |
David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 43 | rc = subprocess.call(['mount', '-o', 'remount,rw', '/']) |
| 44 | if rc == 0: |
| 45 | return |
Chris Sosa | ee1e972 | 2013-03-06 19:04:31 | [diff] [blame] | 46 | try: |
| 47 | answer = raw_input( |
| 48 | 'Could not mount / as writable. Install into /usr/local? (Y/n)') |
| 49 | except EOFError: |
| 50 | # Received if stdin is piped through /dev/null. |
| 51 | answer = None |
| 52 | |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 53 | if answer and answer[0] not in 'Yy': |
David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 54 | sys.exit('Better safe than sorry.') |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 55 | |
David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 56 | environ['ROOT'] = '/usr/local' |
Chris Sosa | 605fe885 | 2010-04-23 00:01:32 | [diff] [blame] | 57 | |
David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 58 | def ParseLsbRelease(self, lsb_release_lines): |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 59 | """Parses LSB release and set out internal variables accordingly |
Ryan Cairns | dd1ceb8 | 2010-03-03 05:35:01 | [diff] [blame] | 60 | |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 61 | Args: |
| 62 | lsb_release_lines: a list of key=val lines e.g. the output of opening |
| 63 | /etc/lsb-release and using readlines(). |
| 64 | """ |
| 65 | lsb_release = dict((k, v) for k, _, v in [line.rstrip().partition('=') |
| 66 | for line in lsb_release_lines]) |
| 67 | |
| 68 | parsing_msg = ('%(variable)s not set. Please set by using a command line ' |
| 69 | 'option or overriding in ' + STATEFUL_LSB_RELEASE_PATH) |
| 70 | if not self.devserver_url: |
| 71 | self.devserver_url = lsb_release['CHROMEOS_DEVSERVER'] |
| 72 | if not self.devserver_url: |
| 73 | raise GMergeParsingException(parsing_msg % dict( |
| 74 | variable='CHROMEOS_DEVSERVER')) |
| 75 | |
| 76 | if not self.board_name: |
| 77 | self.board_name = lsb_release['CHROMEOS_RELEASE_BOARD'] |
| 78 | if not self.board_name: |
| 79 | raise GMergeParsingException(parsing_msg % dict( |
| 80 | variable='CHROMEOS_RELEASE_BOARD')) |
| 81 | |
| 82 | def SetupPortageEnvironment(self, environ, include_masked_files): |
| 83 | """Setup portage to use stateful partition and fetch from dev server. |
| 84 | |
| 85 | Args: |
| 86 | environ: The environment dictionary to setup. |
| 87 | include_masked_files: If true, include masked files in package |
| 88 | (e.g. debug symbols). |
| 89 | """ |
| 90 | binhost_prefix = '%s/static/pkgroot/%s' % (self.devserver_url, |
| 91 | self.board_name) |
David James | e4f73a4 | 2011-05-19 19:18:33 | [diff] [blame] | 92 | binhost = '%s/packages' % binhost_prefix |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 93 | if not include_masked_files: |
David James | e4f73a4 | 2011-05-19 19:18:33 | [diff] [blame] | 94 | binhost += ' %s/gmerge-packages' % binhost_prefix |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 95 | |
David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 96 | environ.update({ |
| 97 | 'PORTDIR': '/usr/local/portage', |
| 98 | 'PKGDIR': '/usr/local/portage', |
| 99 | 'DISTDIR': '/usr/local/portage/distfiles', |
David James | e4f73a4 | 2011-05-19 19:18:33 | [diff] [blame] | 100 | 'PORTAGE_BINHOST': binhost, |
David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 101 | 'PORTAGE_TMPDIR': '/tmp', |
| 102 | 'CONFIG_PROTECT': '-*', |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 103 | 'FEATURES': '-sandbox -usersandbox', |
| 104 | 'ACCEPT_KEYWORDS': 'arm x86 amd64 ~arm ~x86 ~amd64', |
David James | ff33944 | 2011-05-19 21:08:35 | [diff] [blame] | 105 | 'ROOT': os.environ.get('ROOT', '/'), |
| 106 | 'PORTAGE_CONFIGROOT': '/usr/local' |
David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 107 | }) |
Ryan Cairns | dd1ceb8 | 2010-03-03 05:35:01 | [diff] [blame] | 108 | |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 109 | def RequestPackageBuild(self, package_name, deep, accept_stable, usepkg): |
| 110 | """Contacts devserver to request a build. |
| 111 | |
| 112 | Args: |
| 113 | package_name: The name of the package to build. |
| 114 | deep: Update package and all dependencies. |
| 115 | accept_stable: Allow non-workon packages. |
| 116 | usepkg: Use currently built binary packages on server. |
| 117 | """ |
| 118 | def GeneratePackageRequest(): |
| 119 | """Build the POST string that conveys our options to the devserver.""" |
| 120 | post_data = {'board': self.board_name, |
| 121 | 'deep': deep or '', |
David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 122 | 'pkg': package_name, |
David James | ed079b1 | 2011-05-17 21:53:15 | [diff] [blame] | 123 | 'features': os.environ.get('FEATURES'), |
| 124 | 'use': os.environ.get('USE'), |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 125 | 'accept_stable': accept_stable or '', |
| 126 | 'usepkg': usepkg or '', |
David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 127 | } |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 128 | post_data = dict([(key, value) for (key, value) in post_data.iteritems() |
David James | ed079b1 | 2011-05-17 21:53:15 | [diff] [blame] | 129 | if value is not None]) |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 130 | return urllib.urlencode(post_data) |
Frank Swiderski | dc13081 | 2010-10-08 22:42:28 | [diff] [blame] | 131 | |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 132 | print 'Sending build request to', self.devserver_url |
David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 133 | try: |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 134 | result = urllib2.urlopen( |
| 135 | self.devserver_url + '/build', |
| 136 | data=GeneratePackageRequest()) |
David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 137 | print result.read() |
| 138 | result.close() |
Mandeep Singh Baines | ea6b7a5 | 2010-08-17 21:03:57 | [diff] [blame] | 139 | |
David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 140 | except urllib2.HTTPError, e: |
| 141 | # The exception includes the content, which is the error mesage |
| 142 | sys.exit(e.read()) |
David James | 3556d22 | 2011-05-20 22:58:41 | [diff] [blame] | 143 | except urllib2.URLError, e: |
| 144 | sys.exit('Could not reach devserver. Reason: %s' % e.reason) |
Ryan Cairns | dd1ceb8 | 2010-03-03 05:35:01 | [diff] [blame] | 145 | |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 146 | @staticmethod |
| 147 | def EmergePackage(package_name, deep, extra): |
| 148 | """Emerges the package from the binhost. |
| 149 | |
| 150 | Args: |
| 151 | package_name: The name of the package to build. |
| 152 | deep: Update package and all dependencies. |
| 153 | extra: Extra arguments to emerge. |
| 154 | """ |
| 155 | # In case the version is the same as the one that's installed, don't re-use |
| 156 | # it. |
| 157 | print 'Emerging ', package_name |
| 158 | shutil.rmtree('/usr/local/portage', ignore_errors=True) |
David James | f871124 | 2013-07-23 03:22:36 | [diff] [blame] | 159 | os.makedirs('/usr/local/portage') |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 160 | shutil.rmtree('/var/cache/edb/binhost', ignore_errors=True) |
| 161 | |
| 162 | emerge_args = ['emerge', '--getbinpkgonly', '--usepkgonly', '--verbose'] |
| 163 | if deep: |
| 164 | emerge_args.extend(['--update', '--deep']) |
| 165 | |
| 166 | if extra: |
| 167 | emerge_args.extend(extra.split()) |
| 168 | |
| 169 | emerge_args.append(package_name) |
| 170 | subprocess.check_call(emerge_args) |
| 171 | |
Ryan Cairns | dd1ceb8 | 2010-03-03 05:35:01 | [diff] [blame] | 172 | |
David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 173 | def main(): |
David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 174 | parser = optparse.OptionParser(usage='usage: %prog [options] package_name') |
| 175 | parser.add_option('--accept_stable', |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 176 | action='store_true', default=False, |
David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 177 | help=('Build even if a cros_workon package is not ' |
| 178 | 'using the live package')) |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 179 | parser.add_option('-b', '--board', default=None, |
| 180 | help='Specify a different board to use when building.') |
| 181 | parser.add_option('-d', '--devserver_url', default=None, |
| 182 | help='Specify a different devserver(binhost) url to use' |
| 183 | 'to build and download packages.') |
David James | e4f73a4 | 2011-05-19 19:18:33 | [diff] [blame] | 184 | parser.add_option('--include_masked_files', |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 185 | action='store_true', |
David James | e4f73a4 | 2011-05-19 19:18:33 | [diff] [blame] | 186 | default=False, help=('Include masked files in package ' |
| 187 | '(e.g. debug symbols)')) |
David James | ed079b1 | 2011-05-17 21:53:15 | [diff] [blame] | 188 | parser.add_option('-n', '--usepkg', |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 189 | action='store_true', default=False, |
David James | 3556d22 | 2011-05-20 22:58:41 | [diff] [blame] | 190 | help='Use currently built binary packages on server.') |
| 191 | parser.add_option('-D', '--deep', |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 192 | action='store_true', default=False, |
David James | 3556d22 | 2011-05-20 22:58:41 | [diff] [blame] | 193 | help='Update package and all dependencies ' |
| 194 | '(requires --usepkg).') |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 195 | parser.add_option('-x', '--extra', default='', |
David James | 3556d22 | 2011-05-20 22:58:41 | [diff] [blame] | 196 | help='Extra arguments to pass to emerge command.') |
David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 197 | |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 198 | options, remaining_arguments = parser.parse_args() |
David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 199 | if len(remaining_arguments) != 1: |
| 200 | parser.print_help() |
| 201 | sys.exit('Need exactly one package name') |
| 202 | |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 203 | |
David James | 3556d22 | 2011-05-20 22:58:41 | [diff] [blame] | 204 | # TODO(davidjames): Should we allow --deep without --usepkg? Not sure what |
| 205 | # the desired behavior should be in this case, so disabling the combo for |
| 206 | # now. |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 207 | if options.deep and not options.usepkg: |
David James | 3556d22 | 2011-05-20 22:58:41 | [diff] [blame] | 208 | sys.exit('If using --deep, --usepkg must also be enabled.') |
| 209 | |
David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 210 | package_name = remaining_arguments[0] |
| 211 | |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 212 | subprocess.check_call(['mount', '-o', 'remount,exec', '/tmp']) |
David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 213 | try: |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 214 | etc_lsb_release_lines = open(LSB_RELEASE_PATH).readlines() |
| 215 | # Allow overrides from the stateful partition. |
| 216 | if os.path.exists(STATEFUL_LSB_RELEASE_PATH): |
| 217 | etc_lsb_release_lines += open(STATEFUL_LSB_RELEASE_PATH).readlines() |
| 218 | print 'Stateful lsb release file found', STATEFUL_LSB_RELEASE_PATH |
David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 219 | |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 220 | merger = GMerger(options.devserver_url, options.board) |
| 221 | merger.ParseLsbRelease(etc_lsb_release_lines) |
| 222 | merger.RequestPackageBuild(package_name, options.deep, |
| 223 | options.accept_stable, options.usepkg) |
| 224 | |
| 225 | merger.SetupPortageEnvironment(os.environ, options.include_masked_files) |
Chris Sosa | dda923d | 2011-04-13 20:12:01 | [diff] [blame] | 226 | merger.RemountOrChangeRoot(os.environ) |
Chris Sosa | 6c92214 | 2013-02-16 01:37:49 | [diff] [blame] | 227 | merger.EmergePackage(package_name, options.deep, options.extra) |
David Rochberg | 7c79a81 | 2011-01-19 19:24:45 | [diff] [blame] | 228 | finally: |
| 229 | subprocess.call(['mount', '-o', 'remount,noexec', '/tmp']) |
| 230 | |
| 231 | |
| 232 | if __name__ == '__main__': |
| 233 | main() |