blob: ab2a1ae8eb91208ad38db4530e499f84f489d653 [file] [log] [blame]
Ryan Cui3045c5d2012-07-13 18:00:33 -07001#!/usr/bin/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
Ryan Cuia56a71e2012-10-18 18:40:35 -07006
7"""
8Script that deploys a Chrome build to a device.
9
10The script supports deploying Chrome from these sources:
11
121. A local build output directory, such as chromium/src/out/[Debug|Release].
132. A Chrome tarball uploaded by a trybot/official-builder to GoogleStorage.
143. A Chrome tarball existing locally.
15
16The script copies the necessary contents of the source location (tarball or
17build directory) and rsyncs the contents of the staging directory onto your
18device's rootfs.
19"""
Ryan Cui3045c5d2012-07-13 18:00:33 -070020
21import functools
22import logging
Ryan Cui3045c5d2012-07-13 18:00:33 -070023import os
Ryan Cuia56a71e2012-10-18 18:40:35 -070024import optparse
Ryan Cui3045c5d2012-07-13 18:00:33 -070025import time
Ryan Cui3045c5d2012-07-13 18:00:33 -070026
Ryan Cuia56a71e2012-10-18 18:40:35 -070027
David James629febb2012-11-25 13:07:34 -080028from chromite.buildbot import constants
Ryan Cui686ec052013-02-12 16:39:41 -080029from chromite.cros.commands import cros_chrome_sdk
Ryan Cuia56a71e2012-10-18 18:40:35 -070030from chromite.lib import chrome_util
Ryan Cui3045c5d2012-07-13 18:00:33 -070031from chromite.lib import cros_build_lib
Ryan Cuie535b172012-10-19 18:25:03 -070032from chromite.lib import commandline
Ryan Cui777ff422012-12-07 13:12:54 -080033from chromite.lib import gs
Ryan Cui3045c5d2012-07-13 18:00:33 -070034from chromite.lib import osutils
35from chromite.lib import remote_access as remote
36from chromite.lib import sudo
37
38
Ryan Cuia56a71e2012-10-18 18:40:35 -070039_USAGE = "deploy_chrome [--]\n\n %s" % __doc__
40
Ryan Cui3045c5d2012-07-13 18:00:33 -070041KERNEL_A_PARTITION = 2
42KERNEL_B_PARTITION = 4
43
44KILL_PROC_MAX_WAIT = 10
45POST_KILL_WAIT = 2
46
Ryan Cuie535b172012-10-19 18:25:03 -070047MOUNT_RW_COMMAND = 'mount -o remount,rw /'
Ryan Cui3045c5d2012-07-13 18:00:33 -070048
49# Convenience RunCommand methods
50DebugRunCommand = functools.partial(
51 cros_build_lib.RunCommand, debug_level=logging.DEBUG)
52
53DebugRunCommandCaptureOutput = functools.partial(
54 cros_build_lib.RunCommandCaptureOutput, debug_level=logging.DEBUG)
55
56DebugSudoRunCommand = functools.partial(
57 cros_build_lib.SudoRunCommand, debug_level=logging.DEBUG)
58
59
Ryan Cui3045c5d2012-07-13 18:00:33 -070060def _UrlBaseName(url):
61 """Return the last component of the URL."""
62 return url.rstrip('/').rpartition('/')[-1]
63
64
65def _ExtractChrome(src, dest):
66 osutils.SafeMakedirs(dest)
67 # Preserve permissions (-p). This is default when running tar with 'sudo'.
68 DebugSudoRunCommand(['tar', '--checkpoint', '-xf', src],
69 cwd=dest)
70
71
72class DeployChrome(object):
73 """Wraps the core deployment functionality."""
Ryan Cuia56a71e2012-10-18 18:40:35 -070074 def __init__(self, options, tempdir, staging_dir):
Ryan Cuie535b172012-10-19 18:25:03 -070075 """Initialize the class.
76
77 Arguments:
78 options: Optparse result structure.
79 tempdir: Scratch space for the class. Caller has responsibility to clean
80 it up.
Ryan Cuie535b172012-10-19 18:25:03 -070081 """
Ryan Cui3045c5d2012-07-13 18:00:33 -070082 self.tempdir = tempdir
83 self.options = options
Ryan Cuia56a71e2012-10-18 18:40:35 -070084 self.staging_dir = staging_dir
Ryan Cuiafd6c5c2012-07-30 17:48:22 -070085 self.host = remote.RemoteAccess(options.to, tempdir, port=options.port)
Ryan Cui3045c5d2012-07-13 18:00:33 -070086 self.start_ui_needed = False
87
Ryan Cui3045c5d2012-07-13 18:00:33 -070088 def _ChromeFileInUse(self):
89 result = self.host.RemoteSh('lsof /opt/google/chrome/chrome',
90 error_code_ok=True)
91 return result.returncode == 0
92
93 def _DisableRootfsVerification(self):
94 if not self.options.force:
95 logging.error('Detected that the device has rootfs verification enabled.')
96 logging.info('This script can automatically remove the rootfs '
97 'verification, which requires that it reboot the device.')
98 logging.info('Make sure the device is in developer mode!')
99 logging.info('Skip this prompt by specifying --force.')
Brian Harring521e7242012-11-01 16:57:42 -0700100 if not cros_build_lib.BooleanPrompt('Remove roots verification?', False):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700101 cros_build_lib.Die('Need rootfs verification to be disabled. '
102 'Aborting.')
103
104 logging.info('Removing rootfs verification from %s', self.options.to)
105 # Running in VM's cause make_dev_ssd's firmware sanity checks to fail.
106 # Use --force to bypass the checks.
107 cmd = ('/usr/share/vboot/bin/make_dev_ssd.sh --partitions %d '
108 '--remove_rootfs_verification --force')
109 for partition in (KERNEL_A_PARTITION, KERNEL_B_PARTITION):
110 self.host.RemoteSh(cmd % partition, error_code_ok=True)
111
112 # A reboot in developer mode takes a while (and has delays), so the user
113 # will have time to read and act on the USB boot instructions below.
114 logging.info('Please remember to press Ctrl-U if you are booting from USB.')
115 self.host.RemoteReboot()
116
117 def _CheckRootfsWriteable(self):
118 # /proc/mounts is in the format:
119 # <device> <dir> <type> <options>
120 result = self.host.RemoteSh('cat /proc/mounts')
121 for line in result.output.splitlines():
122 components = line.split()
123 if components[0] == '/dev/root' and components[1] == '/':
124 return 'rw' in components[3].split(',')
125 else:
126 raise Exception('Internal error - rootfs mount not found!')
127
128 def _CheckUiJobStarted(self):
129 # status output is in the format:
130 # <job_name> <status> ['process' <pid>].
131 # <status> is in the format <goal>/<state>.
132 result = self.host.RemoteSh('status ui')
133 return result.output.split()[1].split('/')[0] == 'start'
134
135 def _KillProcsIfNeeded(self):
136 if self._CheckUiJobStarted():
137 logging.info('Shutting down Chrome.')
138 self.start_ui_needed = True
139 self.host.RemoteSh('stop ui')
140
141 # Developers sometimes run session_manager manually, in which case we'll
142 # need to help shut the chrome processes down.
143 try:
144 with cros_build_lib.SubCommandTimeout(KILL_PROC_MAX_WAIT):
145 while self._ChromeFileInUse():
146 logging.warning('The chrome binary on the device is in use.')
147 logging.warning('Killing chrome and session_manager processes...\n')
148
149 self.host.RemoteSh("pkill 'chrome|session_manager'",
150 error_code_ok=True)
151 # Wait for processes to actually terminate
152 time.sleep(POST_KILL_WAIT)
153 logging.info('Rechecking the chrome binary...')
154 except cros_build_lib.TimeoutError:
155 cros_build_lib.Die('Could not kill processes after %s seconds. Please '
156 'exit any running chrome processes and try again.')
157
158 def _PrepareTarget(self):
159 # Mount root partition as read/write
160 if not self._CheckRootfsWriteable():
161 logging.info('Mounting rootfs as writeable...')
Ryan Cuie535b172012-10-19 18:25:03 -0700162 result = self.host.RemoteSh(MOUNT_RW_COMMAND, error_code_ok=True)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700163 if result.returncode:
164 self._DisableRootfsVerification()
165 logging.info('Trying again to mount rootfs as writeable...')
Ryan Cuie535b172012-10-19 18:25:03 -0700166 self.host.RemoteSh(MOUNT_RW_COMMAND)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700167
168 if not self._CheckRootfsWriteable():
169 cros_build_lib.Die('Root partition still read-only')
170
171 # This is needed because we're doing an 'rsync --inplace' of Chrome, but
172 # makes sense to have even when going the sshfs route.
173 self._KillProcsIfNeeded()
174
175 def _Deploy(self):
176 logging.info('Copying Chrome to device.')
177 # Show the output (status) for this command.
Ryan Cuia56a71e2012-10-18 18:40:35 -0700178 self.host.Rsync('%s/' % os.path.abspath(self.staging_dir), '/',
Ryan Cui61ca1602013-01-04 15:55:56 -0800179 inplace=True, debug_level=logging.INFO, sudo=True)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700180 if self.start_ui_needed:
181 self.host.RemoteSh('start ui')
182
183 def Perform(self):
184 try:
185 logging.info('Testing connection to the device.')
186 self.host.RemoteSh('true')
187 except cros_build_lib.RunCommandError:
188 logging.error('Error connecting to the test device.')
189 raise
190
Ryan Cui3045c5d2012-07-13 18:00:33 -0700191 self._PrepareTarget()
192 self._Deploy()
193
194
Ryan Cuia56a71e2012-10-18 18:40:35 -0700195def ValidateGypDefines(_option, _opt, value):
196 """Convert GYP_DEFINES-formatted string to dictionary."""
197 return chrome_util.ProcessGypDefines(value)
198
199
200class CustomOption(commandline.Option):
201 """Subclass Option class to implement path evaluation."""
202 TYPES = commandline.Option.TYPES + ('gyp_defines',)
203 TYPE_CHECKER = commandline.Option.TYPE_CHECKER.copy()
204 TYPE_CHECKER['gyp_defines'] = ValidateGypDefines
205
206
Ryan Cuie535b172012-10-19 18:25:03 -0700207def _CreateParser():
208 """Create our custom parser."""
Ryan Cui504db722013-01-22 11:48:01 -0800209 parser = commandline.OptionParser(usage=_USAGE, option_class=CustomOption,
210 caching=True)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700211
Ryan Cuia56a71e2012-10-18 18:40:35 -0700212 # TODO(rcui): Have this use the UI-V2 format of having source and target
213 # device be specified as positional arguments.
Ryan Cui3045c5d2012-07-13 18:00:33 -0700214 parser.add_option('--force', action='store_true', default=False,
Ryan Cui686ec052013-02-12 16:39:41 -0800215 help='Skip all prompts (i.e., for disabling of rootfs '
216 'verification). This may result in the target '
217 'machine being rebooted.')
Ryan Cuia0215a72013-02-14 16:20:45 -0800218 sdk_board_env = os.environ.get(cros_chrome_sdk.SDKFetcher.SDK_BOARD_ENV)
219 parser.add_option('--board', default=sdk_board_env,
Ryan Cui686ec052013-02-12 16:39:41 -0800220 help="The board the Chrome build is targeted for. When in "
221 "a 'cros chrome-sdk' shell, defaults to the SDK "
222 "board.")
Ryan Cuia56a71e2012-10-18 18:40:35 -0700223 parser.add_option('--build-dir', type='path',
Ryan Cui686ec052013-02-12 16:39:41 -0800224 help='The directory with Chrome build artifacts to deploy '
225 'from. Typically of format <chrome_root>/out/Debug. '
226 'When this option is used, the GYP_DEFINES '
227 'environment variable must be set.')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700228 parser.add_option('-g', '--gs-path', type='gs_path',
Ryan Cui686ec052013-02-12 16:39:41 -0800229 help='GS path that contains the chrome to deploy.')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700230 parser.add_option('-p', '--port', type=int, default=remote.DEFAULT_SSH_PORT,
Ryan Cui686ec052013-02-12 16:39:41 -0800231 help='Port of the target device to connect to.')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700232 parser.add_option('-t', '--to',
Ryan Cui686ec052013-02-12 16:39:41 -0800233 help='The IP address of the CrOS device to deploy to.')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700234 parser.add_option('-v', '--verbose', action='store_true', default=False,
Ryan Cui686ec052013-02-12 16:39:41 -0800235 help='Show more debug output.')
Ryan Cuia56a71e2012-10-18 18:40:35 -0700236
237 group = optparse.OptionGroup(parser, 'Advanced Options')
238 group.add_option('-l', '--local-pkg-path', type='path',
Ryan Cui686ec052013-02-12 16:39:41 -0800239 help='Path to local chrome prebuilt package to deploy.')
Ryan Cuief91e702013-02-04 12:06:36 -0800240 group.add_option('--strict', action='store_true', default=False,
Ryan Cui686ec052013-02-12 16:39:41 -0800241 help='Stage artifacts based on the GYP_DEFINES environment '
242 'variable and --staging-flags, if set.')
Ryan Cuief91e702013-02-04 12:06:36 -0800243 group.add_option('--staging-flags', default=None, type='gyp_defines',
Ryan Cui686ec052013-02-12 16:39:41 -0800244 help='Requires --strict to be set. Extra flags to '
245 'control staging. Valid flags are - %s'
246 % ', '.join(chrome_util.STAGING_FLAGS))
Ryan Cuief91e702013-02-04 12:06:36 -0800247
Ryan Cuia56a71e2012-10-18 18:40:35 -0700248 parser.add_option_group(group)
249
250 # Path of an empty directory to stage chrome artifacts to. Defaults to a
251 # temporary directory that is removed when the script finishes. If the path
252 # is specified, then it will not be removed.
253 parser.add_option('--staging-dir', type='path', default=None,
254 help=optparse.SUPPRESS_HELP)
255 # Only prepare the staging directory, and skip deploying to the device.
256 parser.add_option('--staging-only', action='store_true', default=False,
257 help=optparse.SUPPRESS_HELP)
258 # GYP_DEFINES that Chrome was built with. Influences which files are staged
259 # when --build-dir is set. Defaults to reading from the GYP_DEFINES
260 # enviroment variable.
Ryan Cuief91e702013-02-04 12:06:36 -0800261 parser.add_option('--gyp-defines', default=None, type='gyp_defines',
Ryan Cuia56a71e2012-10-18 18:40:35 -0700262 help=optparse.SUPPRESS_HELP)
Ryan Cuie535b172012-10-19 18:25:03 -0700263 return parser
Ryan Cui3045c5d2012-07-13 18:00:33 -0700264
Ryan Cuie535b172012-10-19 18:25:03 -0700265
266def _ParseCommandLine(argv):
267 """Parse args, and run environment-independent checks."""
268 parser = _CreateParser()
Ryan Cui3045c5d2012-07-13 18:00:33 -0700269 (options, args) = parser.parse_args(argv)
270
Ryan Cuia56a71e2012-10-18 18:40:35 -0700271 if not any([options.gs_path, options.local_pkg_path, options.build_dir]):
272 parser.error('Need to specify either --gs-path, --local-pkg-path, or '
Ryan Cuief91e702013-02-04 12:06:36 -0800273 '--build-dir')
Ryan Cuia56a71e2012-10-18 18:40:35 -0700274 if options.build_dir and any([options.gs_path, options.local_pkg_path]):
275 parser.error('Cannot specify both --build_dir and '
276 '--gs-path/--local-pkg-patch')
Ryan Cui686ec052013-02-12 16:39:41 -0800277 if options.build_dir and not options.board:
278 parser.error('--board is required when --build-dir is specified.')
Ryan Cuia56a71e2012-10-18 18:40:35 -0700279 if options.gs_path and options.local_pkg_path:
280 parser.error('Cannot specify both --gs-path and --local-pkg-path')
281 if not (options.staging_only or options.to):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700282 parser.error('Need to specify --to')
Ryan Cuief91e702013-02-04 12:06:36 -0800283 if (options.strict or options.staging_flags) and not options.build_dir:
284 parser.error('--strict and --staging-flags require --build-dir to be '
285 'set.')
286 if options.staging_flags and not options.strict:
287 parser.error('--strict requires --staging-flags to be set.')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700288 return options, args
289
290
Ryan Cuie535b172012-10-19 18:25:03 -0700291def _PostParseCheck(options, _args):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700292 """Perform some usage validation (after we've parsed the arguments
293
294 Args:
295 options/args: The options/args object returned by optparse
296 """
Ryan Cuia56a71e2012-10-18 18:40:35 -0700297 if options.local_pkg_path and not os.path.isfile(options.local_pkg_path):
298 cros_build_lib.Die('%s is not a file.', options.local_pkg_path)
299
Ryan Cui686ec052013-02-12 16:39:41 -0800300 if options.strict and not options.gyp_defines:
Ryan Cuia56a71e2012-10-18 18:40:35 -0700301 gyp_env = os.getenv('GYP_DEFINES', None)
302 if gyp_env is not None:
303 options.gyp_defines = chrome_util.ProcessGypDefines(gyp_env)
304 logging.info('GYP_DEFINES taken from environment: %s',
305 options.gyp_defines)
306 else:
Ryan Cui686ec052013-02-12 16:39:41 -0800307 cros_build_lib.Die('When --strict is set, the GYP_DEFINES environment '
308 'variable must be set.')
Ryan Cuia56a71e2012-10-18 18:40:35 -0700309
310
Ryan Cui504db722013-01-22 11:48:01 -0800311def _FetchChromePackage(cache_dir, tempdir, gs_path):
Ryan Cuia56a71e2012-10-18 18:40:35 -0700312 """Get the chrome prebuilt tarball from GS.
313
314 Returns: Path to the fetched chrome tarball.
315 """
Ryan Cui4c2d42c2013-02-08 16:22:26 -0800316 gs_ctx = gs.GSContext.Cached(cache_dir, init_boto=True)
317 files = gs_ctx.LS(gs_path).output.splitlines()
318 files = [found for found in files if
319 _UrlBaseName(found).startswith('%s-' % constants.CHROME_PN)]
320 if not files:
321 raise Exception('No chrome package found at %s' % gs_path)
322 elif len(files) > 1:
323 # - Users should provide us with a direct link to either a stripped or
324 # unstripped chrome package.
325 # - In the case of being provided with an archive directory, where both
326 # stripped and unstripped chrome available, use the stripped chrome
327 # package.
328 # - Stripped chrome pkg is chromeos-chrome-<version>.tar.gz
329 # - Unstripped chrome pkg is chromeos-chrome-<version>-unstripped.tar.gz.
330 files = [f for f in files if not 'unstripped' in f]
331 assert len(files) == 1
332 logging.warning('Multiple chrome packages found. Using %s', files[0])
Ryan Cui777ff422012-12-07 13:12:54 -0800333
Ryan Cui4c2d42c2013-02-08 16:22:26 -0800334 filename = _UrlBaseName(files[0])
335 logging.info('Fetching %s.', filename)
336 gs_ctx.Copy(files[0], tempdir, print_cmd=False)
337 chrome_path = os.path.join(tempdir, filename)
338 assert os.path.exists(chrome_path)
339 return chrome_path
Ryan Cuia56a71e2012-10-18 18:40:35 -0700340
341
342def _PrepareStagingDir(options, tempdir, staging_dir):
343 """Place the necessary files in the staging directory.
344
345 The staging directory is the directory used to rsync the build artifacts over
346 to the device. Only the necessary Chrome build artifacts are put into the
347 staging directory.
348 """
349 if options.build_dir:
Ryan Cuia0215a72013-02-14 16:20:45 -0800350 sdk = cros_chrome_sdk.SDKFetcher(options.cache_dir, options.board)
Ryan Cui686ec052013-02-12 16:39:41 -0800351 components = (sdk.TARGET_TOOLCHAIN_KEY, constants.CHROME_ENV_TAR)
352 with sdk.Prepare(components=components) as ctx:
353 env_path = os.path.join(ctx.key_map[constants.CHROME_ENV_TAR].path,
354 constants.CHROME_ENV_FILE)
355 strip_bin = osutils.SourceEnvironment(env_path, ['STRIP'])['STRIP']
356 strip_bin = os.path.join(ctx.key_map[sdk.TARGET_TOOLCHAIN_KEY].path,
357 'bin', os.path.basename(strip_bin))
358 chrome_util.StageChromeFromBuildDir(
359 staging_dir, options.build_dir, strip_bin, strict=options.strict,
360 gyp_defines=options.gyp_defines, staging_flags=options.staging_flags)
Ryan Cuia56a71e2012-10-18 18:40:35 -0700361 else:
362 pkg_path = options.local_pkg_path
363 if options.gs_path:
Ryan Cui504db722013-01-22 11:48:01 -0800364 pkg_path = _FetchChromePackage(options.cache_dir, tempdir,
365 options.gs_path)
Ryan Cuia56a71e2012-10-18 18:40:35 -0700366
367 assert pkg_path
368 logging.info('Extracting %s.', pkg_path)
369 _ExtractChrome(pkg_path, staging_dir)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700370
371
372def main(argv):
373 options, args = _ParseCommandLine(argv)
374 _PostParseCheck(options, args)
375
376 # Set cros_build_lib debug level to hide RunCommand spew.
377 if options.verbose:
Ryan Cuia56a71e2012-10-18 18:40:35 -0700378 logging.getLogger().setLevel(logging.DEBUG)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700379 else:
Ryan Cuia56a71e2012-10-18 18:40:35 -0700380 logging.getLogger().setLevel(logging.WARNING)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700381
David James891dccf2012-08-20 14:19:54 -0700382 with sudo.SudoKeepAlive(ttyless_sudo=False):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700383 with osutils.TempDirContextManager(sudo_rm=True) as tempdir:
Ryan Cuia56a71e2012-10-18 18:40:35 -0700384 staging_dir = options.staging_dir
385 if not staging_dir:
386 staging_dir = os.path.join(tempdir, 'chrome')
387 _PrepareStagingDir(options, tempdir, staging_dir)
388
389 if options.staging_only:
390 return 0
391
392 deploy = DeployChrome(options, tempdir, staging_dir)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700393 deploy.Perform()