Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / third_party / chromite / scripts / deploy_chrome.py
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.
4
5
6 """Script that deploys a Chrome build to a device.
7
8 The script supports deploying Chrome from these sources:
9
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.
13
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
16 device's rootfs.
17 """
18
19 from __future__ import print_function
20
21 import collections
22 import contextlib
23 import functools
24 import glob
25 import logging
26 import multiprocessing
27 import os
28 import optparse
29 import shlex
30 import shutil
31 import time
32
33
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
47
48
49 _USAGE = "deploy_chrome [--]\n\n %s" % __doc__
50
51 KERNEL_A_PARTITION = 2
52 KERNEL_B_PARTITION = 4
53
54 KILL_PROC_MAX_WAIT = 10
55 POST_KILL_WAIT = 2
56
57 MOUNT_RW_COMMAND = 'mount -o remount,rw /'
58 LSOF_COMMAND = 'lsof %s/chrome'
59
60 _ANDROID_DIR = '/system/chrome'
61 _ANDROID_DIR_EXTRACT_PATH = 'system/chrome/*'
62
63 _CHROME_DIR = '/opt/google/chrome'
64 _CHROME_DIR_MOUNT = '/mnt/stateful_partition/deploy_rootfs/opt/google/chrome'
65
66 _BIND_TO_FINAL_DIR_CMD = 'mount --rbind %s %s'
67 _SET_MOUNT_FLAGS_CMD = 'mount -o remount,exec,suid %s'
68
69 DF_COMMAND = 'df -k %s'
70
71 def _UrlBaseName(url):
72   """Return the last component of the URL."""
73   return url.rstrip('/').rpartition('/')[-1]
74
75
76 class DeployFailure(failures_lib.StepFailure):
77   """Raised whenever the deploy fails."""
78
79
80 DeviceInfo = collections.namedtuple(
81     'DeviceInfo', ['target_dir_size', 'target_fs_free'])
82
83
84 class DeployChrome(object):
85   """Wraps the core deployment functionality."""
86   def __init__(self, options, tempdir, staging_dir):
87     """Initialize the class.
88
89     Args:
90       options: Optparse result structure.
91       tempdir: Scratch space for the class.  Caller has responsibility to clean
92         it up.
93       staging_dir: Directory to stage the files to.
94     """
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()
100
101     self.copy_paths = chrome_util.GetCopyPaths('chrome')
102     self.chrome_dir = _CHROME_DIR
103
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]
108     multipliers = {
109         'G': 1024 * 1024 * 1024,
110         'M': 1024 * 1024,
111         'K': 1024,
112     }
113     return int(value.rstrip('GMK')) * multipliers.get(value[-1], 1)
114
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])
118
119   def _GetStagingDirSize(self):
120     result = cros_build_lib.DebugRunCommand(['du', '-ks', self.staging_dir],
121                                             redirect_stdout=True,
122                                             capture_output=True)
123     return int(result.output.split()[0])
124
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
129
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. '
143                             'Aborting.')
144
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)
152
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()
157
158     # Now that the machine has been rebooted, we need to kill Chrome again.
159     self._KillProcsIfNeeded()
160
161     # Make sure the rootfs is writable now.
162     self._MountRootfsAsWritable(error_code_ok=False)
163
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>.
168     try:
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:
172         return False
173       else:
174         raise e
175
176     return result.output.split()[1].split('/')[0] == 'start'
177
178   def _KillProcsIfNeeded(self):
179     if self._CheckUiJobStarted():
180       logging.info('Shutting down Chrome...')
181       self.host.RemoteSh('stop ui')
182
183     # Developers sometimes run session_manager manually, in which case we'll
184     # need to help shut the chrome processes down.
185     try:
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')
190
191           self.host.RemoteSh("pkill 'chrome|session_manager'",
192                              error_code_ok=True)
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)
200
201   def _MountRootfsAsWritable(self, error_code_ok=True):
202     """Mount the rootfs as writable.
203
204     If the command fails, and error_code_ok is True, then this function sets
205     self._rootfs_is_still_readonly.
206
207     Args:
208       error_code_ok: See remote.RemoteAccess.RemoteSh for details.
209     """
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,
213                                 capture_output=True)
214     if result.returncode:
215       self._rootfs_is_still_readonly.set()
216
217   def _GetDeviceInfo(self):
218     steps = [
219         functools.partial(self._GetRemoteDirSize, self.options.target_dir),
220         functools.partial(self._GetRemoteMountFree, self.options.target_dir)
221     ]
222     return_values = parallel.RunParallelSteps(steps, return_values=True)
223     return DeviceInfo(*return_values)
224
225   def _CheckDeviceFreeSpace(self, device_info):
226     """See if target device has enough space for Chrome.
227
228     Args:
229       device_info: A DeviceInfo named tuple.
230     """
231     effective_free = device_info.target_dir_size + device_info.target_fs_free
232     staging_size = self._GetStagingDirSize()
233     if effective_free < staging_size:
234       raise DeployFailure(
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.')
240
241   def _Deploy(self):
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)
249
250     for p in self.copy_paths:
251       if p.mode:
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))
255
256
257     if self.options.startui:
258       logging.info('Starting UI...')
259       self.host.RemoteSh('start ui')
260
261   def _CheckConnection(self):
262     try:
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)
268
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))
274
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
281
282   def _PrepareStagingDir(self):
283     _PrepareStagingDir(self.options, self.tempdir, self.staging_dir,
284                        self.copy_paths, self.chrome_dir)
285
286   def _MountTarget(self):
287     logging.info('Mounting Chrome...')
288
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,))
295
296   def Perform(self):
297     self._CheckDeployType()
298
299     # If requested, just do the staging step.
300     if self.options.staging_only:
301       self._PrepareStagingDir()
302       return 0
303
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,
310                                     return_values=True)
311     self._CheckDeviceFreeSpace(ret[0])
312
313     # If we failed to mark the rootfs as writable, try disabling rootfs
314     # verification.
315     if self._rootfs_is_still_readonly.is_set():
316       self._DisableRootfsVerification()
317
318     if self.options.mount_dir is not None:
319       self._MountTarget()
320
321     # Actually deploy Chrome to the device.
322     self._Deploy()
323
324
325 def ValidateGypDefines(_option, _opt, value):
326   """Convert GYP_DEFINES-formatted string to dictionary."""
327   return chrome_util.ProcessGypDefines(value)
328
329
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
335
336
337 def _CreateParser():
338   """Create our custom parser."""
339   parser = commandline.OptionParser(usage=_USAGE, option_class=CustomOption,
340                                     caching=True)
341
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 "
352                          "board.")
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.',
360                     default=None)
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',
364                     default=True,
365                     help="Don't restart the ui daemon after deployment.")
366   parser.add_option('--nostrip', action='store_false', dest='dostrip',
367                     default=True,
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.')
382
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)
399
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)
411
412
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)
430   return parser
431
432
433 def _ParseCommandLine(argv):
434   """Parse args, and run environment-independent checks."""
435   parser = _CreateParser()
436   (options, args) = parser.parse_args(argv)
437
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 '
440                  '--build-dir')
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 '
452                  'set.')
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.')
457
458   if options.mount or options.mount_dir:
459     if not options.target_dir:
460       options.target_dir = _CHROME_DIR_MOUNT
461   else:
462     if not options.target_dir:
463       options.target_dir = _CHROME_DIR
464
465   if options.mount and not options.mount_dir:
466     options.mount_dir = _CHROME_DIR
467
468   return options, args
469
470
471 def _PostParseCheck(options, _args):
472   """Perform some usage validation (after we've parsed the arguments).
473
474   Args:
475     options: The options object returned by optparse.
476     _args: The args object returned by optparse.
477   """
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)
480
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',
486                    options.gyp_defines)
487
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.')
491
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!')
501
502
503 def _FetchChromePackage(cache_dir, tempdir, gs_path):
504   """Get the chrome prebuilt tarball from GS.
505
506   Returns:
507     Path to the fetched chrome tarball.
508   """
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)]
513   if not files:
514     raise Exception('No chrome package found at %s' % gs_path)
515   elif len(files) > 1:
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
520     #   package.
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])
526
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)
532   return chrome_path
533
534
535 @contextlib.contextmanager
536 def _StripBinContext(options):
537   if not options.dostrip:
538     yield None
539   elif options.strip_bin:
540     yield options.strip_bin
541   else:
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))
551       yield strip_bin
552
553
554 def _PrepareStagingDir(options, tempdir, staging_dir, copy_paths=None,
555                        chrome_dir=_CHROME_DIR):
556   """Place the necessary files in the staging directory.
557
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
560   staging directory.
561   """
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)
573   else:
574     pkg_path = options.local_pkg_path
575     if options.gs_path:
576       pkg_path = _FetchChromePackage(options.cache_dir, tempdir,
577                                      options.gs_path)
578
579     assert pkg_path
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',
586            staging_dir])
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)
590     else:
591       cros_build_lib.DebugRunCommand(
592           ['tar', '--strip-components', '4', '--extract',
593            '--preserve-permissions', '--file', pkg_path, '.%s' % chrome_dir],
594           cwd=staging_dir)
595
596
597 def main(argv):
598   options, args = _ParseCommandLine(argv)
599   _PostParseCheck(options, args)
600
601   # Set cros_build_lib debug level to hide RunCommand spew.
602   if options.verbose:
603     logging.getLogger().setLevel(logging.DEBUG)
604   else:
605     logging.getLogger().setLevel(logging.INFO)
606
607   with stats.UploadContext() as queue:
608     cmd_stats = stats.Stats.SafeInit(cmd_line=argv, cmd_base='deploy_chrome')
609     if cmd_stats:
610       queue.put([cmd_stats, stats.StatsUploader.URL, 1])
611
612     with osutils.TempDir(set_global=True) as tempdir:
613       staging_dir = options.staging_dir
614       if not staging_dir:
615         staging_dir = os.path.join(tempdir, 'chrome')
616
617       deploy = DeployChrome(options, tempdir, staging_dir)
618       try:
619         deploy.Perform()
620       except failures_lib.StepFailure as ex:
621         raise SystemExit(str(ex).strip())