blob: 824417ab0059e19efabfa97850ced68c9e6a36fc [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
Ryan Cui3045c5d2012-07-13 18:00:33 -070036
37
Ryan Cuia56a71e2012-10-18 18:40:35 -070038_USAGE = "deploy_chrome [--]\n\n %s" % __doc__
39
Ryan Cui3045c5d2012-07-13 18:00:33 -070040KERNEL_A_PARTITION = 2
41KERNEL_B_PARTITION = 4
42
43KILL_PROC_MAX_WAIT = 10
44POST_KILL_WAIT = 2
45
Ryan Cuie535b172012-10-19 18:25:03 -070046MOUNT_RW_COMMAND = 'mount -o remount,rw /'
Ryan Cui3045c5d2012-07-13 18:00:33 -070047
48# Convenience RunCommand methods
49DebugRunCommand = functools.partial(
50 cros_build_lib.RunCommand, debug_level=logging.DEBUG)
51
Ryan Cui3045c5d2012-07-13 18:00:33 -070052
Ryan Cui3045c5d2012-07-13 18:00:33 -070053def _UrlBaseName(url):
54 """Return the last component of the URL."""
55 return url.rstrip('/').rpartition('/')[-1]
56
57
Ryan Cui3045c5d2012-07-13 18:00:33 -070058class DeployChrome(object):
59 """Wraps the core deployment functionality."""
Ryan Cuia56a71e2012-10-18 18:40:35 -070060 def __init__(self, options, tempdir, staging_dir):
Ryan Cuie535b172012-10-19 18:25:03 -070061 """Initialize the class.
62
63 Arguments:
64 options: Optparse result structure.
65 tempdir: Scratch space for the class. Caller has responsibility to clean
66 it up.
Ryan Cuie535b172012-10-19 18:25:03 -070067 """
Ryan Cui3045c5d2012-07-13 18:00:33 -070068 self.tempdir = tempdir
69 self.options = options
Ryan Cuia56a71e2012-10-18 18:40:35 -070070 self.staging_dir = staging_dir
Ryan Cuiafd6c5c2012-07-30 17:48:22 -070071 self.host = remote.RemoteAccess(options.to, tempdir, port=options.port)
Ryan Cui3045c5d2012-07-13 18:00:33 -070072
Ryan Cui3045c5d2012-07-13 18:00:33 -070073 def _ChromeFileInUse(self):
74 result = self.host.RemoteSh('lsof /opt/google/chrome/chrome',
75 error_code_ok=True)
76 return result.returncode == 0
77
78 def _DisableRootfsVerification(self):
79 if not self.options.force:
80 logging.error('Detected that the device has rootfs verification enabled.')
81 logging.info('This script can automatically remove the rootfs '
82 'verification, which requires that it reboot the device.')
83 logging.info('Make sure the device is in developer mode!')
84 logging.info('Skip this prompt by specifying --force.')
Brian Harring521e7242012-11-01 16:57:42 -070085 if not cros_build_lib.BooleanPrompt('Remove roots verification?', False):
Ryan Cui3045c5d2012-07-13 18:00:33 -070086 cros_build_lib.Die('Need rootfs verification to be disabled. '
87 'Aborting.')
88
89 logging.info('Removing rootfs verification from %s', self.options.to)
90 # Running in VM's cause make_dev_ssd's firmware sanity checks to fail.
91 # Use --force to bypass the checks.
92 cmd = ('/usr/share/vboot/bin/make_dev_ssd.sh --partitions %d '
93 '--remove_rootfs_verification --force')
94 for partition in (KERNEL_A_PARTITION, KERNEL_B_PARTITION):
95 self.host.RemoteSh(cmd % partition, error_code_ok=True)
96
97 # A reboot in developer mode takes a while (and has delays), so the user
98 # will have time to read and act on the USB boot instructions below.
99 logging.info('Please remember to press Ctrl-U if you are booting from USB.')
100 self.host.RemoteReboot()
101
102 def _CheckRootfsWriteable(self):
103 # /proc/mounts is in the format:
104 # <device> <dir> <type> <options>
105 result = self.host.RemoteSh('cat /proc/mounts')
106 for line in result.output.splitlines():
107 components = line.split()
108 if components[0] == '/dev/root' and components[1] == '/':
109 return 'rw' in components[3].split(',')
110 else:
111 raise Exception('Internal error - rootfs mount not found!')
112
113 def _CheckUiJobStarted(self):
114 # status output is in the format:
115 # <job_name> <status> ['process' <pid>].
116 # <status> is in the format <goal>/<state>.
Ryan Cuif2d1a582013-02-19 14:08:13 -0800117 try:
118 result = self.host.RemoteSh('status ui')
119 except cros_build_lib.RunCommandError as e:
120 if 'Unknown job' in e.result.error:
121 return False
122 else:
123 raise e
124
Ryan Cui3045c5d2012-07-13 18:00:33 -0700125 return result.output.split()[1].split('/')[0] == 'start'
126
127 def _KillProcsIfNeeded(self):
128 if self._CheckUiJobStarted():
129 logging.info('Shutting down Chrome.')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700130 self.host.RemoteSh('stop ui')
131
132 # Developers sometimes run session_manager manually, in which case we'll
133 # need to help shut the chrome processes down.
134 try:
135 with cros_build_lib.SubCommandTimeout(KILL_PROC_MAX_WAIT):
136 while self._ChromeFileInUse():
137 logging.warning('The chrome binary on the device is in use.')
138 logging.warning('Killing chrome and session_manager processes...\n')
139
140 self.host.RemoteSh("pkill 'chrome|session_manager'",
141 error_code_ok=True)
142 # Wait for processes to actually terminate
143 time.sleep(POST_KILL_WAIT)
144 logging.info('Rechecking the chrome binary...')
145 except cros_build_lib.TimeoutError:
146 cros_build_lib.Die('Could not kill processes after %s seconds. Please '
147 'exit any running chrome processes and try again.')
148
149 def _PrepareTarget(self):
150 # Mount root partition as read/write
151 if not self._CheckRootfsWriteable():
152 logging.info('Mounting rootfs as writeable...')
Ryan Cuie535b172012-10-19 18:25:03 -0700153 result = self.host.RemoteSh(MOUNT_RW_COMMAND, error_code_ok=True)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700154 if result.returncode:
155 self._DisableRootfsVerification()
156 logging.info('Trying again to mount rootfs as writeable...')
Ryan Cuie535b172012-10-19 18:25:03 -0700157 self.host.RemoteSh(MOUNT_RW_COMMAND)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700158
159 if not self._CheckRootfsWriteable():
160 cros_build_lib.Die('Root partition still read-only')
161
162 # This is needed because we're doing an 'rsync --inplace' of Chrome, but
163 # makes sense to have even when going the sshfs route.
164 self._KillProcsIfNeeded()
165
166 def _Deploy(self):
167 logging.info('Copying Chrome to device.')
168 # Show the output (status) for this command.
Ryan Cuia56a71e2012-10-18 18:40:35 -0700169 self.host.Rsync('%s/' % os.path.abspath(self.staging_dir), '/',
Ryan Cuif2d1a582013-02-19 14:08:13 -0800170 inplace=True, debug_level=logging.INFO)
Ryan Cui82a1f2d2013-02-22 17:34:23 -0800171 if self.options.startui:
Ryan Cui3045c5d2012-07-13 18:00:33 -0700172 self.host.RemoteSh('start ui')
173
174 def Perform(self):
175 try:
176 logging.info('Testing connection to the device.')
177 self.host.RemoteSh('true')
178 except cros_build_lib.RunCommandError:
179 logging.error('Error connecting to the test device.')
180 raise
181
Ryan Cui3045c5d2012-07-13 18:00:33 -0700182 self._PrepareTarget()
183 self._Deploy()
184
185
Ryan Cuia56a71e2012-10-18 18:40:35 -0700186def ValidateGypDefines(_option, _opt, value):
187 """Convert GYP_DEFINES-formatted string to dictionary."""
188 return chrome_util.ProcessGypDefines(value)
189
190
191class CustomOption(commandline.Option):
192 """Subclass Option class to implement path evaluation."""
193 TYPES = commandline.Option.TYPES + ('gyp_defines',)
194 TYPE_CHECKER = commandline.Option.TYPE_CHECKER.copy()
195 TYPE_CHECKER['gyp_defines'] = ValidateGypDefines
196
197
Ryan Cuie535b172012-10-19 18:25:03 -0700198def _CreateParser():
199 """Create our custom parser."""
Ryan Cui504db722013-01-22 11:48:01 -0800200 parser = commandline.OptionParser(usage=_USAGE, option_class=CustomOption,
201 caching=True)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700202
Ryan Cuia56a71e2012-10-18 18:40:35 -0700203 # TODO(rcui): Have this use the UI-V2 format of having source and target
204 # device be specified as positional arguments.
Ryan Cui3045c5d2012-07-13 18:00:33 -0700205 parser.add_option('--force', action='store_true', default=False,
Ryan Cui686ec052013-02-12 16:39:41 -0800206 help='Skip all prompts (i.e., for disabling of rootfs '
207 'verification). This may result in the target '
208 'machine being rebooted.')
Ryan Cuia0215a72013-02-14 16:20:45 -0800209 sdk_board_env = os.environ.get(cros_chrome_sdk.SDKFetcher.SDK_BOARD_ENV)
210 parser.add_option('--board', default=sdk_board_env,
Ryan Cui686ec052013-02-12 16:39:41 -0800211 help="The board the Chrome build is targeted for. When in "
212 "a 'cros chrome-sdk' shell, defaults to the SDK "
213 "board.")
Ryan Cuia56a71e2012-10-18 18:40:35 -0700214 parser.add_option('--build-dir', type='path',
Ryan Cui686ec052013-02-12 16:39:41 -0800215 help='The directory with Chrome build artifacts to deploy '
216 'from. Typically of format <chrome_root>/out/Debug. '
217 'When this option is used, the GYP_DEFINES '
218 'environment variable must be set.')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700219 parser.add_option('-g', '--gs-path', type='gs_path',
Ryan Cui686ec052013-02-12 16:39:41 -0800220 help='GS path that contains the chrome to deploy.')
Ryan Cui82a1f2d2013-02-22 17:34:23 -0800221 parser.add_option('--nostartui', action='store_false', dest='startui',
222 default=True,
223 help="Don't restart the ui daemon after deployment.")
Ryan Cui3045c5d2012-07-13 18:00:33 -0700224 parser.add_option('-p', '--port', type=int, default=remote.DEFAULT_SSH_PORT,
Ryan Cui686ec052013-02-12 16:39:41 -0800225 help='Port of the target device to connect to.')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700226 parser.add_option('-t', '--to',
Ryan Cui686ec052013-02-12 16:39:41 -0800227 help='The IP address of the CrOS device to deploy to.')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700228 parser.add_option('-v', '--verbose', action='store_true', default=False,
Ryan Cui686ec052013-02-12 16:39:41 -0800229 help='Show more debug output.')
Ryan Cuia56a71e2012-10-18 18:40:35 -0700230
231 group = optparse.OptionGroup(parser, 'Advanced Options')
232 group.add_option('-l', '--local-pkg-path', type='path',
Ryan Cui686ec052013-02-12 16:39:41 -0800233 help='Path to local chrome prebuilt package to deploy.')
Ryan Cuief91e702013-02-04 12:06:36 -0800234 group.add_option('--strict', action='store_true', default=False,
Ryan Cui686ec052013-02-12 16:39:41 -0800235 help='Stage artifacts based on the GYP_DEFINES environment '
236 'variable and --staging-flags, if set.')
Ryan Cuief91e702013-02-04 12:06:36 -0800237 group.add_option('--staging-flags', default=None, type='gyp_defines',
Ryan Cui686ec052013-02-12 16:39:41 -0800238 help='Requires --strict to be set. Extra flags to '
239 'control staging. Valid flags are - %s'
240 % ', '.join(chrome_util.STAGING_FLAGS))
Ryan Cuief91e702013-02-04 12:06:36 -0800241
Ryan Cuia56a71e2012-10-18 18:40:35 -0700242 parser.add_option_group(group)
243
244 # Path of an empty directory to stage chrome artifacts to. Defaults to a
245 # temporary directory that is removed when the script finishes. If the path
246 # is specified, then it will not be removed.
247 parser.add_option('--staging-dir', type='path', default=None,
248 help=optparse.SUPPRESS_HELP)
249 # Only prepare the staging directory, and skip deploying to the device.
250 parser.add_option('--staging-only', action='store_true', default=False,
251 help=optparse.SUPPRESS_HELP)
252 # GYP_DEFINES that Chrome was built with. Influences which files are staged
253 # when --build-dir is set. Defaults to reading from the GYP_DEFINES
254 # enviroment variable.
Ryan Cuief91e702013-02-04 12:06:36 -0800255 parser.add_option('--gyp-defines', default=None, type='gyp_defines',
Ryan Cuia56a71e2012-10-18 18:40:35 -0700256 help=optparse.SUPPRESS_HELP)
Ryan Cuie535b172012-10-19 18:25:03 -0700257 return parser
Ryan Cui3045c5d2012-07-13 18:00:33 -0700258
Ryan Cuie535b172012-10-19 18:25:03 -0700259
260def _ParseCommandLine(argv):
261 """Parse args, and run environment-independent checks."""
262 parser = _CreateParser()
Ryan Cui3045c5d2012-07-13 18:00:33 -0700263 (options, args) = parser.parse_args(argv)
264
Ryan Cuia56a71e2012-10-18 18:40:35 -0700265 if not any([options.gs_path, options.local_pkg_path, options.build_dir]):
266 parser.error('Need to specify either --gs-path, --local-pkg-path, or '
Ryan Cuief91e702013-02-04 12:06:36 -0800267 '--build-dir')
Ryan Cuia56a71e2012-10-18 18:40:35 -0700268 if options.build_dir and any([options.gs_path, options.local_pkg_path]):
269 parser.error('Cannot specify both --build_dir and '
270 '--gs-path/--local-pkg-patch')
Ryan Cui686ec052013-02-12 16:39:41 -0800271 if options.build_dir and not options.board:
272 parser.error('--board is required when --build-dir is specified.')
Ryan Cuia56a71e2012-10-18 18:40:35 -0700273 if options.gs_path and options.local_pkg_path:
274 parser.error('Cannot specify both --gs-path and --local-pkg-path')
275 if not (options.staging_only or options.to):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700276 parser.error('Need to specify --to')
Ryan Cuief91e702013-02-04 12:06:36 -0800277 if (options.strict or options.staging_flags) and not options.build_dir:
278 parser.error('--strict and --staging-flags require --build-dir to be '
279 'set.')
280 if options.staging_flags and not options.strict:
281 parser.error('--strict requires --staging-flags to be set.')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700282 return options, args
283
284
Ryan Cuie535b172012-10-19 18:25:03 -0700285def _PostParseCheck(options, _args):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700286 """Perform some usage validation (after we've parsed the arguments
287
288 Args:
289 options/args: The options/args object returned by optparse
290 """
Ryan Cuia56a71e2012-10-18 18:40:35 -0700291 if options.local_pkg_path and not os.path.isfile(options.local_pkg_path):
292 cros_build_lib.Die('%s is not a file.', options.local_pkg_path)
293
Ryan Cui686ec052013-02-12 16:39:41 -0800294 if options.strict and not options.gyp_defines:
Ryan Cuia56a71e2012-10-18 18:40:35 -0700295 gyp_env = os.getenv('GYP_DEFINES', None)
296 if gyp_env is not None:
297 options.gyp_defines = chrome_util.ProcessGypDefines(gyp_env)
298 logging.info('GYP_DEFINES taken from environment: %s',
299 options.gyp_defines)
300 else:
Ryan Cui686ec052013-02-12 16:39:41 -0800301 cros_build_lib.Die('When --strict is set, the GYP_DEFINES environment '
302 'variable must be set.')
Ryan Cuia56a71e2012-10-18 18:40:35 -0700303
304
Ryan Cui504db722013-01-22 11:48:01 -0800305def _FetchChromePackage(cache_dir, tempdir, gs_path):
Ryan Cuia56a71e2012-10-18 18:40:35 -0700306 """Get the chrome prebuilt tarball from GS.
307
308 Returns: Path to the fetched chrome tarball.
309 """
Ryan Cui4c2d42c2013-02-08 16:22:26 -0800310 gs_ctx = gs.GSContext.Cached(cache_dir, init_boto=True)
311 files = gs_ctx.LS(gs_path).output.splitlines()
312 files = [found for found in files if
313 _UrlBaseName(found).startswith('%s-' % constants.CHROME_PN)]
314 if not files:
315 raise Exception('No chrome package found at %s' % gs_path)
316 elif len(files) > 1:
317 # - Users should provide us with a direct link to either a stripped or
318 # unstripped chrome package.
319 # - In the case of being provided with an archive directory, where both
320 # stripped and unstripped chrome available, use the stripped chrome
321 # package.
322 # - Stripped chrome pkg is chromeos-chrome-<version>.tar.gz
323 # - Unstripped chrome pkg is chromeos-chrome-<version>-unstripped.tar.gz.
324 files = [f for f in files if not 'unstripped' in f]
325 assert len(files) == 1
326 logging.warning('Multiple chrome packages found. Using %s', files[0])
Ryan Cui777ff422012-12-07 13:12:54 -0800327
Ryan Cui4c2d42c2013-02-08 16:22:26 -0800328 filename = _UrlBaseName(files[0])
329 logging.info('Fetching %s.', filename)
330 gs_ctx.Copy(files[0], tempdir, print_cmd=False)
331 chrome_path = os.path.join(tempdir, filename)
332 assert os.path.exists(chrome_path)
333 return chrome_path
Ryan Cuia56a71e2012-10-18 18:40:35 -0700334
335
336def _PrepareStagingDir(options, tempdir, staging_dir):
337 """Place the necessary files in the staging directory.
338
339 The staging directory is the directory used to rsync the build artifacts over
340 to the device. Only the necessary Chrome build artifacts are put into the
341 staging directory.
342 """
343 if options.build_dir:
Ryan Cuia0215a72013-02-14 16:20:45 -0800344 sdk = cros_chrome_sdk.SDKFetcher(options.cache_dir, options.board)
Ryan Cui686ec052013-02-12 16:39:41 -0800345 components = (sdk.TARGET_TOOLCHAIN_KEY, constants.CHROME_ENV_TAR)
346 with sdk.Prepare(components=components) as ctx:
347 env_path = os.path.join(ctx.key_map[constants.CHROME_ENV_TAR].path,
348 constants.CHROME_ENV_FILE)
349 strip_bin = osutils.SourceEnvironment(env_path, ['STRIP'])['STRIP']
350 strip_bin = os.path.join(ctx.key_map[sdk.TARGET_TOOLCHAIN_KEY].path,
351 'bin', os.path.basename(strip_bin))
352 chrome_util.StageChromeFromBuildDir(
353 staging_dir, options.build_dir, strip_bin, strict=options.strict,
354 gyp_defines=options.gyp_defines, staging_flags=options.staging_flags)
Ryan Cuia56a71e2012-10-18 18:40:35 -0700355 else:
356 pkg_path = options.local_pkg_path
357 if options.gs_path:
Ryan Cui504db722013-01-22 11:48:01 -0800358 pkg_path = _FetchChromePackage(options.cache_dir, tempdir,
359 options.gs_path)
Ryan Cuia56a71e2012-10-18 18:40:35 -0700360
361 assert pkg_path
362 logging.info('Extracting %s.', pkg_path)
Ryan Cuif2d1a582013-02-19 14:08:13 -0800363 osutils.SafeMakedirs(staging_dir)
364 DebugRunCommand(['tar', '-xpf', pkg_path], cwd=staging_dir)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700365
366
367def main(argv):
368 options, args = _ParseCommandLine(argv)
369 _PostParseCheck(options, args)
370
371 # Set cros_build_lib debug level to hide RunCommand spew.
372 if options.verbose:
Ryan Cuia56a71e2012-10-18 18:40:35 -0700373 logging.getLogger().setLevel(logging.DEBUG)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700374 else:
Ryan Cuia56a71e2012-10-18 18:40:35 -0700375 logging.getLogger().setLevel(logging.WARNING)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700376
Ryan Cuif2d1a582013-02-19 14:08:13 -0800377 with osutils.TempDirContextManager() as tempdir:
378 staging_dir = options.staging_dir
379 if not staging_dir:
380 staging_dir = os.path.join(tempdir, 'chrome')
381 _PrepareStagingDir(options, tempdir, staging_dir)
Ryan Cuia56a71e2012-10-18 18:40:35 -0700382
Ryan Cuif2d1a582013-02-19 14:08:13 -0800383 if options.staging_only:
384 return 0
Ryan Cuia56a71e2012-10-18 18:40:35 -0700385
Ryan Cuif2d1a582013-02-19 14:08:13 -0800386 deploy = DeployChrome(options, tempdir, staging_dir)
387 deploy.Perform()