1 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
6 """Script that deploys a Chrome build to a device.
8 The script supports deploying Chrome from these sources:
10 1. A local build output directory, such as chromium/src/out/[Debug|Release].
11 2. A Chrome tarball uploaded by a trybot/official-builder to GoogleStorage.
12 3. A Chrome tarball existing locally.
14 The script copies the necessary contents of the source location (tarball or
15 build directory) and rsyncs the contents of the staging directory onto your
19 from __future__ import print_function
26 import multiprocessing
34 from chromite.cbuildbot import constants
35 from chromite.cbuildbot import failures_lib
36 from chromite.cros.commands import cros_chrome_sdk
37 from chromite.lib import chrome_util
38 from chromite.lib import cros_build_lib
39 from chromite.lib import commandline
40 from chromite.lib import gs
41 from chromite.lib import osutils
42 from chromite.lib import parallel
43 from chromite.lib import remote_access as remote
44 from chromite.lib import stats
45 from chromite.lib import timeout_util
46 from chromite.scripts import lddtree
49 _USAGE = "deploy_chrome [--]\n\n %s" % __doc__
51 KERNEL_A_PARTITION = 2
52 KERNEL_B_PARTITION = 4
54 KILL_PROC_MAX_WAIT = 10
57 MOUNT_RW_COMMAND = 'mount -o remount,rw /'
58 LSOF_COMMAND = 'lsof %s/chrome'
60 _ANDROID_DIR = '/system/chrome'
61 _ANDROID_DIR_EXTRACT_PATH = 'system/chrome/*'
63 _CHROME_DIR = '/opt/google/chrome'
64 _CHROME_DIR_MOUNT = '/mnt/stateful_partition/deploy_rootfs/opt/google/chrome'
66 _BIND_TO_FINAL_DIR_CMD = 'mount --rbind %s %s'
67 _SET_MOUNT_FLAGS_CMD = 'mount -o remount,exec,suid %s'
69 DF_COMMAND = 'df -k %s'
71 def _UrlBaseName(url):
72 """Return the last component of the URL."""
73 return url.rstrip('/').rpartition('/')[-1]
76 class DeployFailure(failures_lib.StepFailure):
77 """Raised whenever the deploy fails."""
80 DeviceInfo = collections.namedtuple(
81 'DeviceInfo', ['target_dir_size', 'target_fs_free'])
84 class DeployChrome(object):
85 """Wraps the core deployment functionality."""
86 def __init__(self, options, tempdir, staging_dir):
87 """Initialize the class.
90 options: Optparse result structure.
91 tempdir: Scratch space for the class. Caller has responsibility to clean
93 staging_dir: Directory to stage the files to.
95 self.tempdir = tempdir
96 self.options = options
97 self.staging_dir = staging_dir
98 self.host = remote.RemoteAccess(options.to, tempdir, port=options.port)
99 self._rootfs_is_still_readonly = multiprocessing.Event()
101 self.copy_paths = chrome_util.GetCopyPaths('chrome')
102 self.chrome_dir = _CHROME_DIR
104 def _GetRemoteMountFree(self, remote_dir):
105 result = self.host.RemoteSh(DF_COMMAND % remote_dir, capture_output=True)
106 line = result.output.splitlines()[1]
107 value = line.split()[3]
109 'G': 1024 * 1024 * 1024,
113 return int(value.rstrip('GMK')) * multipliers.get(value[-1], 1)
115 def _GetRemoteDirSize(self, remote_dir):
116 result = self.host.RemoteSh('du -ks %s' % remote_dir, capture_output=True)
117 return int(result.output.split()[0])
119 def _GetStagingDirSize(self):
120 result = cros_build_lib.DebugRunCommand(['du', '-ks', self.staging_dir],
121 redirect_stdout=True,
123 return int(result.output.split()[0])
125 def _ChromeFileInUse(self):
126 result = self.host.RemoteSh(LSOF_COMMAND % (self.options.target_dir,),
127 error_code_ok=True, capture_output=True)
128 return result.returncode == 0
130 def _DisableRootfsVerification(self):
131 if not self.options.force:
132 logging.error('Detected that the device has rootfs verification enabled.')
133 logging.info('This script can automatically remove the rootfs '
134 'verification, which requires that it reboot the device.')
135 logging.info('Make sure the device is in developer mode!')
136 logging.info('Skip this prompt by specifying --force.')
137 if not cros_build_lib.BooleanPrompt('Remove roots verification?', False):
138 # Since we stopped Chrome earlier, it's good form to start it up again.
139 if self.options.startui:
140 logging.info('Starting Chrome...')
141 self.host.RemoteSh('start ui')
142 raise DeployFailure('Need rootfs verification to be disabled. '
145 logging.info('Removing rootfs verification from %s', self.options.to)
146 # Running in VM's cause make_dev_ssd's firmware sanity checks to fail.
147 # Use --force to bypass the checks.
148 cmd = ('/usr/share/vboot/bin/make_dev_ssd.sh --partitions %d '
149 '--remove_rootfs_verification --force')
150 for partition in (KERNEL_A_PARTITION, KERNEL_B_PARTITION):
151 self.host.RemoteSh(cmd % partition, error_code_ok=True)
153 # A reboot in developer mode takes a while (and has delays), so the user
154 # will have time to read and act on the USB boot instructions below.
155 logging.info('Please remember to press Ctrl-U if you are booting from USB.')
156 self.host.RemoteReboot()
158 # Now that the machine has been rebooted, we need to kill Chrome again.
159 self._KillProcsIfNeeded()
161 # Make sure the rootfs is writable now.
162 self._MountRootfsAsWritable(error_code_ok=False)
164 def _CheckUiJobStarted(self):
165 # status output is in the format:
166 # <job_name> <status> ['process' <pid>].
167 # <status> is in the format <goal>/<state>.
169 result = self.host.RemoteSh('status ui', capture_output=True)
170 except cros_build_lib.RunCommandError as e:
171 if 'Unknown job' in e.result.error:
176 return result.output.split()[1].split('/')[0] == 'start'
178 def _KillProcsIfNeeded(self):
179 if self._CheckUiJobStarted():
180 logging.info('Shutting down Chrome...')
181 self.host.RemoteSh('stop ui')
183 # Developers sometimes run session_manager manually, in which case we'll
184 # need to help shut the chrome processes down.
186 with timeout_util.Timeout(KILL_PROC_MAX_WAIT):
187 while self._ChromeFileInUse():
188 logging.warning('The chrome binary on the device is in use.')
189 logging.warning('Killing chrome and session_manager processes...\n')
191 self.host.RemoteSh("pkill 'chrome|session_manager'",
193 # Wait for processes to actually terminate
194 time.sleep(POST_KILL_WAIT)
195 logging.info('Rechecking the chrome binary...')
196 except timeout_util.TimeoutError:
197 msg = ('Could not kill processes after %s seconds. Please exit any '
198 'running chrome processes and try again.' % KILL_PROC_MAX_WAIT)
199 raise DeployFailure(msg)
201 def _MountRootfsAsWritable(self, error_code_ok=True):
202 """Mount the rootfs as writable.
204 If the command fails, and error_code_ok is True, then this function sets
205 self._rootfs_is_still_readonly.
208 error_code_ok: See remote.RemoteAccess.RemoteSh for details.
210 # TODO: Should migrate to use the remount functions in remote_access.
211 result = self.host.RemoteSh(MOUNT_RW_COMMAND,
212 error_code_ok=error_code_ok,
214 if result.returncode:
215 self._rootfs_is_still_readonly.set()
217 def _GetDeviceInfo(self):
219 functools.partial(self._GetRemoteDirSize, self.options.target_dir),
220 functools.partial(self._GetRemoteMountFree, self.options.target_dir)
222 return_values = parallel.RunParallelSteps(steps, return_values=True)
223 return DeviceInfo(*return_values)
225 def _CheckDeviceFreeSpace(self, device_info):
226 """See if target device has enough space for Chrome.
229 device_info: A DeviceInfo named tuple.
231 effective_free = device_info.target_dir_size + device_info.target_fs_free
232 staging_size = self._GetStagingDirSize()
233 if effective_free < staging_size:
235 'Not enough free space on the device. Required: %s MiB, '
236 'actual: %s MiB.' % (staging_size / 1024, effective_free / 1024))
237 if device_info.target_fs_free < (100 * 1024):
238 logging.warning('The device has less than 100MB free. deploy_chrome may '
239 'hang during the transfer.')
242 logging.info('Copying Chrome to %s on device...', self.options.target_dir)
243 # Show the output (status) for this command.
244 dest_path = _CHROME_DIR
245 self.host.Rsync('%s/' % os.path.abspath(self.staging_dir),
246 self.options.target_dir,
247 inplace=True, debug_level=logging.INFO,
248 verbose=self.options.verbose)
250 for p in self.copy_paths:
252 # Set mode if necessary.
253 self.host.RemoteSh('chmod %o %s/%s' % (p.mode, dest_path,
254 p.src if not p.dest else p.dest))
257 if self.options.startui:
258 logging.info('Starting UI...')
259 self.host.RemoteSh('start ui')
261 def _CheckConnection(self):
263 logging.info('Testing connection to the device...')
264 self.host.RemoteSh('true')
265 except cros_build_lib.RunCommandError as ex:
266 logging.error('Error connecting to the test device.')
267 raise DeployFailure(ex)
269 def _CheckDeployType(self):
270 if self.options.build_dir:
271 def BinaryExists(filename):
272 """Checks if the passed-in file is present in the build directory."""
273 return os.path.exists(os.path.join(self.options.build_dir, filename))
275 if BinaryExists('app_shell') and not BinaryExists('chrome'):
276 # app_shell deployment.
277 self.copy_paths = chrome_util.GetCopyPaths('app_shell')
278 # TODO(derat): Update _Deploy() and remove this after figuring out how
279 # app_shell should be executed.
280 self.options.startui = False
282 def _PrepareStagingDir(self):
283 _PrepareStagingDir(self.options, self.tempdir, self.staging_dir,
284 self.copy_paths, self.chrome_dir)
286 def _MountTarget(self):
287 logging.info('Mounting Chrome...')
289 # Create directory if does not exist
290 self.host.RemoteSh('mkdir -p --mode 0775 %s' % (self.options.mount_dir,))
291 self.host.RemoteSh(_BIND_TO_FINAL_DIR_CMD % (self.options.target_dir,
292 self.options.mount_dir))
293 # Chrome needs partition to have exec and suid flags set
294 self.host.RemoteSh(_SET_MOUNT_FLAGS_CMD % (self.options.mount_dir,))
297 self._CheckDeployType()
299 # If requested, just do the staging step.
300 if self.options.staging_only:
301 self._PrepareStagingDir()
304 # Run setup steps in parallel. If any step fails, RunParallelSteps will
305 # stop printing output at that point, and halt any running steps.
306 steps = [self._GetDeviceInfo, self._CheckConnection,
307 self._KillProcsIfNeeded, self._MountRootfsAsWritable,
308 self._PrepareStagingDir]
309 ret = parallel.RunParallelSteps(steps, halt_on_error=True,
311 self._CheckDeviceFreeSpace(ret[0])
313 # If we failed to mark the rootfs as writable, try disabling rootfs
315 if self._rootfs_is_still_readonly.is_set():
316 self._DisableRootfsVerification()
318 if self.options.mount_dir is not None:
321 # Actually deploy Chrome to the device.
325 def ValidateGypDefines(_option, _opt, value):
326 """Convert GYP_DEFINES-formatted string to dictionary."""
327 return chrome_util.ProcessGypDefines(value)
330 class CustomOption(commandline.Option):
331 """Subclass Option class to implement path evaluation."""
332 TYPES = commandline.Option.TYPES + ('gyp_defines',)
333 TYPE_CHECKER = commandline.Option.TYPE_CHECKER.copy()
334 TYPE_CHECKER['gyp_defines'] = ValidateGypDefines
338 """Create our custom parser."""
339 parser = commandline.OptionParser(usage=_USAGE, option_class=CustomOption,
342 # TODO(rcui): Have this use the UI-V2 format of having source and target
343 # device be specified as positional arguments.
344 parser.add_option('--force', action='store_true', default=False,
345 help='Skip all prompts (i.e., for disabling of rootfs '
346 'verification). This may result in the target '
347 'machine being rebooted.')
348 sdk_board_env = os.environ.get(cros_chrome_sdk.SDKFetcher.SDK_BOARD_ENV)
349 parser.add_option('--board', default=sdk_board_env,
350 help="The board the Chrome build is targeted for. When in "
351 "a 'cros chrome-sdk' shell, defaults to the SDK "
353 parser.add_option('--build-dir', type='path',
354 help='The directory with Chrome build artifacts to deploy '
355 'from. Typically of format <chrome_root>/out/Debug. '
356 'When this option is used, the GYP_DEFINES '
357 'environment variable must be set.')
358 parser.add_option('--target-dir', type='path',
359 help='Target directory on device to deploy Chrome into.',
361 parser.add_option('-g', '--gs-path', type='gs_path',
362 help='GS path that contains the chrome to deploy.')
363 parser.add_option('--nostartui', action='store_false', dest='startui',
365 help="Don't restart the ui daemon after deployment.")
366 parser.add_option('--nostrip', action='store_false', dest='dostrip',
368 help="Don't strip binaries during deployment. Warning: "
369 "the resulting binaries will be very large!")
370 parser.add_option('-p', '--port', type=int, default=remote.DEFAULT_SSH_PORT,
371 help='Port of the target device to connect to.')
372 parser.add_option('-t', '--to',
373 help='The IP address of the CrOS device to deploy to.')
374 parser.add_option('-v', '--verbose', action='store_true', default=False,
375 help='Show more debug output.')
376 parser.add_option('--mount-dir', type='path', default=None,
377 help='Deploy Chrome in target directory and bind it '
378 'to the directory specified by this flag.')
379 parser.add_option('--mount', action='store_true', default=False,
380 help='Deploy Chrome to default target directory and bind '
381 'it to the default mount directory.')
383 group = optparse.OptionGroup(parser, 'Advanced Options')
384 group.add_option('-l', '--local-pkg-path', type='path',
385 help='Path to local chrome prebuilt package to deploy.')
386 group.add_option('--sloppy', action='store_true', default=False,
387 help='Ignore when mandatory artifacts are missing.')
388 group.add_option('--staging-flags', default=None, type='gyp_defines',
389 help='Extra flags to control staging. Valid flags are - %s'
390 % ', '.join(chrome_util.STAGING_FLAGS))
391 group.add_option('--strict', action='store_true', default=False,
392 help='Stage artifacts based on the GYP_DEFINES environment '
393 'variable and --staging-flags, if set. Enforce that '
394 'all optional artifacts are deployed.')
395 group.add_option('--strip-flags', default=None,
396 help="Flags to call the 'strip' binutil tool with. "
397 "Overrides the default arguments.")
398 parser.add_option_group(group)
400 group = optparse.OptionGroup(parser, 'Metadata Overrides (Advanced)',
401 description='Provide all of these overrides '
402 'in order to remove dependencies on '
403 'metadata.json existence.')
404 group.add_option('--target-tc', action='store', default=None,
405 help='Override target toolchain name, e.g. '
406 'x86_64-cros-linux-gnu')
407 group.add_option('--toolchain-url', action='store', default=None,
408 help='Override toolchain url format pattern, e.g. '
409 '2014/04/%%(target)s-2014.04.23.220740.tar.xz')
410 parser.add_option_group(group)
413 # GYP_DEFINES that Chrome was built with. Influences which files are staged
414 # when --build-dir is set. Defaults to reading from the GYP_DEFINES
415 # enviroment variable.
416 parser.add_option('--gyp-defines', default=None, type='gyp_defines',
417 help=optparse.SUPPRESS_HELP)
418 # Path of an empty directory to stage chrome artifacts to. Defaults to a
419 # temporary directory that is removed when the script finishes. If the path
420 # is specified, then it will not be removed.
421 parser.add_option('--staging-dir', type='path', default=None,
422 help=optparse.SUPPRESS_HELP)
423 # Only prepare the staging directory, and skip deploying to the device.
424 parser.add_option('--staging-only', action='store_true', default=False,
425 help=optparse.SUPPRESS_HELP)
426 # Path to a binutil 'strip' tool to strip binaries with. The passed-in path
427 # is used as-is, and not normalized. Used by the Chrome ebuild to skip
428 # fetching the SDK toolchain.
429 parser.add_option('--strip-bin', default=None, help=optparse.SUPPRESS_HELP)
433 def _ParseCommandLine(argv):
434 """Parse args, and run environment-independent checks."""
435 parser = _CreateParser()
436 (options, args) = parser.parse_args(argv)
438 if not any([options.gs_path, options.local_pkg_path, options.build_dir]):
439 parser.error('Need to specify either --gs-path, --local-pkg-path, or '
441 if options.build_dir and any([options.gs_path, options.local_pkg_path]):
442 parser.error('Cannot specify both --build_dir and '
443 '--gs-path/--local-pkg-patch')
444 if options.build_dir and not options.board:
445 parser.error('--board is required when --build-dir is specified.')
446 if options.gs_path and options.local_pkg_path:
447 parser.error('Cannot specify both --gs-path and --local-pkg-path')
448 if not (options.staging_only or options.to):
449 parser.error('Need to specify --to')
450 if (options.strict or options.staging_flags) and not options.build_dir:
451 parser.error('--strict and --staging-flags require --build-dir to be '
453 if options.staging_flags and not options.strict:
454 parser.error('--staging-flags requires --strict to be set.')
455 if options.sloppy and options.strict:
456 parser.error('Cannot specify both --strict and --sloppy.')
458 if options.mount or options.mount_dir:
459 if not options.target_dir:
460 options.target_dir = _CHROME_DIR_MOUNT
462 if not options.target_dir:
463 options.target_dir = _CHROME_DIR
465 if options.mount and not options.mount_dir:
466 options.mount_dir = _CHROME_DIR
471 def _PostParseCheck(options, _args):
472 """Perform some usage validation (after we've parsed the arguments).
475 options: The options object returned by optparse.
476 _args: The args object returned by optparse.
478 if options.local_pkg_path and not os.path.isfile(options.local_pkg_path):
479 cros_build_lib.Die('%s is not a file.', options.local_pkg_path)
481 if not options.gyp_defines:
482 gyp_env = os.getenv('GYP_DEFINES', None)
483 if gyp_env is not None:
484 options.gyp_defines = chrome_util.ProcessGypDefines(gyp_env)
485 logging.debug('GYP_DEFINES taken from environment: %s',
488 if options.strict and not options.gyp_defines:
489 cros_build_lib.Die('When --strict is set, the GYP_DEFINES environment '
490 'variable must be set.')
492 if options.build_dir:
493 chrome_path = os.path.join(options.build_dir, 'chrome')
494 if os.path.isfile(chrome_path):
495 deps = lddtree.ParseELF(chrome_path)
496 if 'libbase.so' in deps['libs']:
497 cros_build_lib.Warning(
498 'Detected a component build of Chrome. component build is '
499 'not working properly for Chrome OS. See crbug.com/196317. '
500 'Use at your own risk!')
503 def _FetchChromePackage(cache_dir, tempdir, gs_path):
504 """Get the chrome prebuilt tarball from GS.
507 Path to the fetched chrome tarball.
509 gs_ctx = gs.GSContext(cache_dir=cache_dir, init_boto=True)
510 files = gs_ctx.LS(gs_path)
511 files = [found for found in files if
512 _UrlBaseName(found).startswith('%s-' % constants.CHROME_PN)]
514 raise Exception('No chrome package found at %s' % gs_path)
516 # - Users should provide us with a direct link to either a stripped or
517 # unstripped chrome package.
518 # - In the case of being provided with an archive directory, where both
519 # stripped and unstripped chrome available, use the stripped chrome
521 # - Stripped chrome pkg is chromeos-chrome-<version>.tar.gz
522 # - Unstripped chrome pkg is chromeos-chrome-<version>-unstripped.tar.gz.
523 files = [f for f in files if not 'unstripped' in f]
524 assert len(files) == 1
525 logging.warning('Multiple chrome packages found. Using %s', files[0])
527 filename = _UrlBaseName(files[0])
528 logging.info('Fetching %s...', filename)
529 gs_ctx.Copy(files[0], tempdir, print_cmd=False)
530 chrome_path = os.path.join(tempdir, filename)
531 assert os.path.exists(chrome_path)
535 @contextlib.contextmanager
536 def _StripBinContext(options):
537 if not options.dostrip:
539 elif options.strip_bin:
540 yield options.strip_bin
542 sdk = cros_chrome_sdk.SDKFetcher(options.cache_dir, options.board)
543 components = (sdk.TARGET_TOOLCHAIN_KEY, constants.CHROME_ENV_TAR)
544 with sdk.Prepare(components=components, target_tc=options.target_tc,
545 toolchain_url=options.toolchain_url) as ctx:
546 env_path = os.path.join(ctx.key_map[constants.CHROME_ENV_TAR].path,
547 constants.CHROME_ENV_FILE)
548 strip_bin = osutils.SourceEnvironment(env_path, ['STRIP'])['STRIP']
549 strip_bin = os.path.join(ctx.key_map[sdk.TARGET_TOOLCHAIN_KEY].path,
550 'bin', os.path.basename(strip_bin))
554 def _PrepareStagingDir(options, tempdir, staging_dir, copy_paths=None,
555 chrome_dir=_CHROME_DIR):
556 """Place the necessary files in the staging directory.
558 The staging directory is the directory used to rsync the build artifacts over
559 to the device. Only the necessary Chrome build artifacts are put into the
562 osutils.SafeMakedirs(staging_dir)
563 os.chmod(staging_dir, 0o755)
564 if options.build_dir:
565 with _StripBinContext(options) as strip_bin:
566 strip_flags = (None if options.strip_flags is None else
567 shlex.split(options.strip_flags))
568 chrome_util.StageChromeFromBuildDir(
569 staging_dir, options.build_dir, strip_bin, strict=options.strict,
570 sloppy=options.sloppy, gyp_defines=options.gyp_defines,
571 staging_flags=options.staging_flags,
572 strip_flags=strip_flags, copy_paths=copy_paths)
574 pkg_path = options.local_pkg_path
576 pkg_path = _FetchChromePackage(options.cache_dir, tempdir,
580 logging.info('Extracting %s...', pkg_path)
581 # Extract only the ./opt/google/chrome contents, directly into the staging
582 # dir, collapsing the directory hierarchy.
583 if pkg_path[-4:] == '.zip':
584 cros_build_lib.DebugRunCommand(
585 ['unzip', '-X', pkg_path, _ANDROID_DIR_EXTRACT_PATH, '-d',
587 for filename in glob.glob(os.path.join(staging_dir, 'system/chrome/*')):
588 shutil.move(filename, staging_dir)
589 osutils.RmDir(os.path.join(staging_dir, 'system'), ignore_missing=True)
591 cros_build_lib.DebugRunCommand(
592 ['tar', '--strip-components', '4', '--extract',
593 '--preserve-permissions', '--file', pkg_path, '.%s' % chrome_dir],
598 options, args = _ParseCommandLine(argv)
599 _PostParseCheck(options, args)
601 # Set cros_build_lib debug level to hide RunCommand spew.
603 logging.getLogger().setLevel(logging.DEBUG)
605 logging.getLogger().setLevel(logging.INFO)
607 with stats.UploadContext() as queue:
608 cmd_stats = stats.Stats.SafeInit(cmd_line=argv, cmd_base='deploy_chrome')
610 queue.put([cmd_stats, stats.StatsUploader.URL, 1])
612 with osutils.TempDir(set_global=True) as tempdir:
613 staging_dir = options.staging_dir
615 staging_dir = os.path.join(tempdir, 'chrome')
617 deploy = DeployChrome(options, tempdir, staging_dir)
620 except failures_lib.StepFailure as ex:
621 raise SystemExit(str(ex).strip())