blob: 117c34c28fe3b1fc692f6034f01d1348e24935e8 [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
Ryan Cui3045c5d2012-07-13 18:00:33 -070087 def _ChromeFileInUse(self):
88 result = self.host.RemoteSh('lsof /opt/google/chrome/chrome',
89 error_code_ok=True)
90 return result.returncode == 0
91
92 def _DisableRootfsVerification(self):
93 if not self.options.force:
94 logging.error('Detected that the device has rootfs verification enabled.')
95 logging.info('This script can automatically remove the rootfs '
96 'verification, which requires that it reboot the device.')
97 logging.info('Make sure the device is in developer mode!')
98 logging.info('Skip this prompt by specifying --force.')
Brian Harring521e7242012-11-01 16:57:42 -070099 if not cros_build_lib.BooleanPrompt('Remove roots verification?', False):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700100 cros_build_lib.Die('Need rootfs verification to be disabled. '
101 'Aborting.')
102
103 logging.info('Removing rootfs verification from %s', self.options.to)
104 # Running in VM's cause make_dev_ssd's firmware sanity checks to fail.
105 # Use --force to bypass the checks.
106 cmd = ('/usr/share/vboot/bin/make_dev_ssd.sh --partitions %d '
107 '--remove_rootfs_verification --force')
108 for partition in (KERNEL_A_PARTITION, KERNEL_B_PARTITION):
109 self.host.RemoteSh(cmd % partition, error_code_ok=True)
110
111 # A reboot in developer mode takes a while (and has delays), so the user
112 # will have time to read and act on the USB boot instructions below.
113 logging.info('Please remember to press Ctrl-U if you are booting from USB.')
114 self.host.RemoteReboot()
115
116 def _CheckRootfsWriteable(self):
117 # /proc/mounts is in the format:
118 # <device> <dir> <type> <options>
119 result = self.host.RemoteSh('cat /proc/mounts')
120 for line in result.output.splitlines():
121 components = line.split()
122 if components[0] == '/dev/root' and components[1] == '/':
123 return 'rw' in components[3].split(',')
124 else:
125 raise Exception('Internal error - rootfs mount not found!')
126
127 def _CheckUiJobStarted(self):
128 # status output is in the format:
129 # <job_name> <status> ['process' <pid>].
130 # <status> is in the format <goal>/<state>.
131 result = self.host.RemoteSh('status ui')
132 return result.output.split()[1].split('/')[0] == 'start'
133
134 def _KillProcsIfNeeded(self):
135 if self._CheckUiJobStarted():
136 logging.info('Shutting down Chrome.')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700137 self.host.RemoteSh('stop ui')
138
139 # Developers sometimes run session_manager manually, in which case we'll
140 # need to help shut the chrome processes down.
141 try:
142 with cros_build_lib.SubCommandTimeout(KILL_PROC_MAX_WAIT):
143 while self._ChromeFileInUse():
144 logging.warning('The chrome binary on the device is in use.')
145 logging.warning('Killing chrome and session_manager processes...\n')
146
147 self.host.RemoteSh("pkill 'chrome|session_manager'",
148 error_code_ok=True)
149 # Wait for processes to actually terminate
150 time.sleep(POST_KILL_WAIT)
151 logging.info('Rechecking the chrome binary...')
152 except cros_build_lib.TimeoutError:
153 cros_build_lib.Die('Could not kill processes after %s seconds. Please '
154 'exit any running chrome processes and try again.')
155
156 def _PrepareTarget(self):
157 # Mount root partition as read/write
158 if not self._CheckRootfsWriteable():
159 logging.info('Mounting rootfs as writeable...')
Ryan Cuie535b172012-10-19 18:25:03 -0700160 result = self.host.RemoteSh(MOUNT_RW_COMMAND, error_code_ok=True)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700161 if result.returncode:
162 self._DisableRootfsVerification()
163 logging.info('Trying again to mount rootfs as writeable...')
Ryan Cuie535b172012-10-19 18:25:03 -0700164 self.host.RemoteSh(MOUNT_RW_COMMAND)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700165
166 if not self._CheckRootfsWriteable():
167 cros_build_lib.Die('Root partition still read-only')
168
169 # This is needed because we're doing an 'rsync --inplace' of Chrome, but
170 # makes sense to have even when going the sshfs route.
171 self._KillProcsIfNeeded()
172
173 def _Deploy(self):
174 logging.info('Copying Chrome to device.')
175 # Show the output (status) for this command.
Ryan Cuia56a71e2012-10-18 18:40:35 -0700176 self.host.Rsync('%s/' % os.path.abspath(self.staging_dir), '/',
Ryan Cui61ca1602013-01-04 15:55:56 -0800177 inplace=True, debug_level=logging.INFO, sudo=True)
Ryan Cui82a1f2d2013-02-22 17:34:23 -0800178 if self.options.startui:
Ryan Cui3045c5d2012-07-13 18:00:33 -0700179 self.host.RemoteSh('start ui')
180
181 def Perform(self):
182 try:
183 logging.info('Testing connection to the device.')
184 self.host.RemoteSh('true')
185 except cros_build_lib.RunCommandError:
186 logging.error('Error connecting to the test device.')
187 raise
188
Ryan Cui3045c5d2012-07-13 18:00:33 -0700189 self._PrepareTarget()
190 self._Deploy()
191
192
Ryan Cuia56a71e2012-10-18 18:40:35 -0700193def ValidateGypDefines(_option, _opt, value):
194 """Convert GYP_DEFINES-formatted string to dictionary."""
195 return chrome_util.ProcessGypDefines(value)
196
197
198class CustomOption(commandline.Option):
199 """Subclass Option class to implement path evaluation."""
200 TYPES = commandline.Option.TYPES + ('gyp_defines',)
201 TYPE_CHECKER = commandline.Option.TYPE_CHECKER.copy()
202 TYPE_CHECKER['gyp_defines'] = ValidateGypDefines
203
204
Ryan Cuie535b172012-10-19 18:25:03 -0700205def _CreateParser():
206 """Create our custom parser."""
Ryan Cui504db722013-01-22 11:48:01 -0800207 parser = commandline.OptionParser(usage=_USAGE, option_class=CustomOption,
208 caching=True)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700209
Ryan Cuia56a71e2012-10-18 18:40:35 -0700210 # TODO(rcui): Have this use the UI-V2 format of having source and target
211 # device be specified as positional arguments.
Ryan Cui3045c5d2012-07-13 18:00:33 -0700212 parser.add_option('--force', action='store_true', default=False,
Ryan Cui686ec052013-02-12 16:39:41 -0800213 help='Skip all prompts (i.e., for disabling of rootfs '
214 'verification). This may result in the target '
215 'machine being rebooted.')
Ryan Cuia0215a72013-02-14 16:20:45 -0800216 sdk_board_env = os.environ.get(cros_chrome_sdk.SDKFetcher.SDK_BOARD_ENV)
217 parser.add_option('--board', default=sdk_board_env,
Ryan Cui686ec052013-02-12 16:39:41 -0800218 help="The board the Chrome build is targeted for. When in "
219 "a 'cros chrome-sdk' shell, defaults to the SDK "
220 "board.")
Ryan Cuia56a71e2012-10-18 18:40:35 -0700221 parser.add_option('--build-dir', type='path',
Ryan Cui686ec052013-02-12 16:39:41 -0800222 help='The directory with Chrome build artifacts to deploy '
223 'from. Typically of format <chrome_root>/out/Debug. '
224 'When this option is used, the GYP_DEFINES '
225 'environment variable must be set.')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700226 parser.add_option('-g', '--gs-path', type='gs_path',
Ryan Cui686ec052013-02-12 16:39:41 -0800227 help='GS path that contains the chrome to deploy.')
Ryan Cui82a1f2d2013-02-22 17:34:23 -0800228 parser.add_option('--nostartui', action='store_false', dest='startui',
229 default=True,
230 help="Don't restart the ui daemon after deployment.")
Ryan Cui3045c5d2012-07-13 18:00:33 -0700231 parser.add_option('-p', '--port', type=int, default=remote.DEFAULT_SSH_PORT,
Ryan Cui686ec052013-02-12 16:39:41 -0800232 help='Port of the target device to connect to.')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700233 parser.add_option('-t', '--to',
Ryan Cui686ec052013-02-12 16:39:41 -0800234 help='The IP address of the CrOS device to deploy to.')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700235 parser.add_option('-v', '--verbose', action='store_true', default=False,
Ryan Cui686ec052013-02-12 16:39:41 -0800236 help='Show more debug output.')
Ryan Cuia56a71e2012-10-18 18:40:35 -0700237
238 group = optparse.OptionGroup(parser, 'Advanced Options')
239 group.add_option('-l', '--local-pkg-path', type='path',
Ryan Cui686ec052013-02-12 16:39:41 -0800240 help='Path to local chrome prebuilt package to deploy.')
Ryan Cuief91e702013-02-04 12:06:36 -0800241 group.add_option('--strict', action='store_true', default=False,
Ryan Cui686ec052013-02-12 16:39:41 -0800242 help='Stage artifacts based on the GYP_DEFINES environment '
243 'variable and --staging-flags, if set.')
Ryan Cuief91e702013-02-04 12:06:36 -0800244 group.add_option('--staging-flags', default=None, type='gyp_defines',
Ryan Cui686ec052013-02-12 16:39:41 -0800245 help='Requires --strict to be set. Extra flags to '
246 'control staging. Valid flags are - %s'
247 % ', '.join(chrome_util.STAGING_FLAGS))
Ryan Cuief91e702013-02-04 12:06:36 -0800248
Ryan Cuia56a71e2012-10-18 18:40:35 -0700249 parser.add_option_group(group)
250
251 # Path of an empty directory to stage chrome artifacts to. Defaults to a
252 # temporary directory that is removed when the script finishes. If the path
253 # is specified, then it will not be removed.
254 parser.add_option('--staging-dir', type='path', default=None,
255 help=optparse.SUPPRESS_HELP)
256 # Only prepare the staging directory, and skip deploying to the device.
257 parser.add_option('--staging-only', action='store_true', default=False,
258 help=optparse.SUPPRESS_HELP)
259 # GYP_DEFINES that Chrome was built with. Influences which files are staged
260 # when --build-dir is set. Defaults to reading from the GYP_DEFINES
261 # enviroment variable.
Ryan Cuief91e702013-02-04 12:06:36 -0800262 parser.add_option('--gyp-defines', default=None, type='gyp_defines',
Ryan Cuia56a71e2012-10-18 18:40:35 -0700263 help=optparse.SUPPRESS_HELP)
Ryan Cuie535b172012-10-19 18:25:03 -0700264 return parser
Ryan Cui3045c5d2012-07-13 18:00:33 -0700265
Ryan Cuie535b172012-10-19 18:25:03 -0700266
267def _ParseCommandLine(argv):
268 """Parse args, and run environment-independent checks."""
269 parser = _CreateParser()
Ryan Cui3045c5d2012-07-13 18:00:33 -0700270 (options, args) = parser.parse_args(argv)
271
Ryan Cuia56a71e2012-10-18 18:40:35 -0700272 if not any([options.gs_path, options.local_pkg_path, options.build_dir]):
273 parser.error('Need to specify either --gs-path, --local-pkg-path, or '
Ryan Cuief91e702013-02-04 12:06:36 -0800274 '--build-dir')
Ryan Cuia56a71e2012-10-18 18:40:35 -0700275 if options.build_dir and any([options.gs_path, options.local_pkg_path]):
276 parser.error('Cannot specify both --build_dir and '
277 '--gs-path/--local-pkg-patch')
Ryan Cui686ec052013-02-12 16:39:41 -0800278 if options.build_dir and not options.board:
279 parser.error('--board is required when --build-dir is specified.')
Ryan Cuia56a71e2012-10-18 18:40:35 -0700280 if options.gs_path and options.local_pkg_path:
281 parser.error('Cannot specify both --gs-path and --local-pkg-path')
282 if not (options.staging_only or options.to):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700283 parser.error('Need to specify --to')
Ryan Cuief91e702013-02-04 12:06:36 -0800284 if (options.strict or options.staging_flags) and not options.build_dir:
285 parser.error('--strict and --staging-flags require --build-dir to be '
286 'set.')
287 if options.staging_flags and not options.strict:
288 parser.error('--strict requires --staging-flags to be set.')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700289 return options, args
290
291
Ryan Cuie535b172012-10-19 18:25:03 -0700292def _PostParseCheck(options, _args):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700293 """Perform some usage validation (after we've parsed the arguments
294
295 Args:
296 options/args: The options/args object returned by optparse
297 """
Ryan Cuia56a71e2012-10-18 18:40:35 -0700298 if options.local_pkg_path and not os.path.isfile(options.local_pkg_path):
299 cros_build_lib.Die('%s is not a file.', options.local_pkg_path)
300
Ryan Cui686ec052013-02-12 16:39:41 -0800301 if options.strict and not options.gyp_defines:
Ryan Cuia56a71e2012-10-18 18:40:35 -0700302 gyp_env = os.getenv('GYP_DEFINES', None)
303 if gyp_env is not None:
304 options.gyp_defines = chrome_util.ProcessGypDefines(gyp_env)
305 logging.info('GYP_DEFINES taken from environment: %s',
306 options.gyp_defines)
307 else:
Ryan Cui686ec052013-02-12 16:39:41 -0800308 cros_build_lib.Die('When --strict is set, the GYP_DEFINES environment '
309 'variable must be set.')
Ryan Cuia56a71e2012-10-18 18:40:35 -0700310
311
Ryan Cui504db722013-01-22 11:48:01 -0800312def _FetchChromePackage(cache_dir, tempdir, gs_path):
Ryan Cuia56a71e2012-10-18 18:40:35 -0700313 """Get the chrome prebuilt tarball from GS.
314
315 Returns: Path to the fetched chrome tarball.
316 """
Ryan Cui4c2d42c2013-02-08 16:22:26 -0800317 gs_ctx = gs.GSContext.Cached(cache_dir, init_boto=True)
318 files = gs_ctx.LS(gs_path).output.splitlines()
319 files = [found for found in files if
320 _UrlBaseName(found).startswith('%s-' % constants.CHROME_PN)]
321 if not files:
322 raise Exception('No chrome package found at %s' % gs_path)
323 elif len(files) > 1:
324 # - Users should provide us with a direct link to either a stripped or
325 # unstripped chrome package.
326 # - In the case of being provided with an archive directory, where both
327 # stripped and unstripped chrome available, use the stripped chrome
328 # package.
329 # - Stripped chrome pkg is chromeos-chrome-<version>.tar.gz
330 # - Unstripped chrome pkg is chromeos-chrome-<version>-unstripped.tar.gz.
331 files = [f for f in files if not 'unstripped' in f]
332 assert len(files) == 1
333 logging.warning('Multiple chrome packages found. Using %s', files[0])
Ryan Cui777ff422012-12-07 13:12:54 -0800334
Ryan Cui4c2d42c2013-02-08 16:22:26 -0800335 filename = _UrlBaseName(files[0])
336 logging.info('Fetching %s.', filename)
337 gs_ctx.Copy(files[0], tempdir, print_cmd=False)
338 chrome_path = os.path.join(tempdir, filename)
339 assert os.path.exists(chrome_path)
340 return chrome_path
Ryan Cuia56a71e2012-10-18 18:40:35 -0700341
342
343def _PrepareStagingDir(options, tempdir, staging_dir):
344 """Place the necessary files in the staging directory.
345
346 The staging directory is the directory used to rsync the build artifacts over
347 to the device. Only the necessary Chrome build artifacts are put into the
348 staging directory.
349 """
350 if options.build_dir:
Ryan Cuia0215a72013-02-14 16:20:45 -0800351 sdk = cros_chrome_sdk.SDKFetcher(options.cache_dir, options.board)
Ryan Cui686ec052013-02-12 16:39:41 -0800352 components = (sdk.TARGET_TOOLCHAIN_KEY, constants.CHROME_ENV_TAR)
353 with sdk.Prepare(components=components) as ctx:
354 env_path = os.path.join(ctx.key_map[constants.CHROME_ENV_TAR].path,
355 constants.CHROME_ENV_FILE)
356 strip_bin = osutils.SourceEnvironment(env_path, ['STRIP'])['STRIP']
357 strip_bin = os.path.join(ctx.key_map[sdk.TARGET_TOOLCHAIN_KEY].path,
358 'bin', os.path.basename(strip_bin))
359 chrome_util.StageChromeFromBuildDir(
360 staging_dir, options.build_dir, strip_bin, strict=options.strict,
361 gyp_defines=options.gyp_defines, staging_flags=options.staging_flags)
Ryan Cuia56a71e2012-10-18 18:40:35 -0700362 else:
363 pkg_path = options.local_pkg_path
364 if options.gs_path:
Ryan Cui504db722013-01-22 11:48:01 -0800365 pkg_path = _FetchChromePackage(options.cache_dir, tempdir,
366 options.gs_path)
Ryan Cuia56a71e2012-10-18 18:40:35 -0700367
368 assert pkg_path
369 logging.info('Extracting %s.', pkg_path)
370 _ExtractChrome(pkg_path, staging_dir)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700371
372
373def main(argv):
374 options, args = _ParseCommandLine(argv)
375 _PostParseCheck(options, args)
376
377 # Set cros_build_lib debug level to hide RunCommand spew.
378 if options.verbose:
Ryan Cuia56a71e2012-10-18 18:40:35 -0700379 logging.getLogger().setLevel(logging.DEBUG)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700380 else:
Ryan Cuia56a71e2012-10-18 18:40:35 -0700381 logging.getLogger().setLevel(logging.WARNING)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700382
David James891dccf2012-08-20 14:19:54 -0700383 with sudo.SudoKeepAlive(ttyless_sudo=False):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700384 with osutils.TempDirContextManager(sudo_rm=True) as tempdir:
Ryan Cuia56a71e2012-10-18 18:40:35 -0700385 staging_dir = options.staging_dir
386 if not staging_dir:
387 staging_dir = os.path.join(tempdir, 'chrome')
388 _PrepareStagingDir(options, tempdir, staging_dir)
389
390 if options.staging_only:
391 return 0
392
393 deploy = DeployChrome(options, tempdir, staging_dir)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700394 deploy.Perform()