Upstream version 8.36.161.0
[platform/framework/web/crosswalk.git] / src / third_party / chromite / scripts / deploy_chrome.py
1 #!/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
6
7 """Script that deploys a Chrome build to a device.
8
9 The script supports deploying Chrome from these sources:
10
11 1. A local build output directory, such as chromium/src/out/[Debug|Release].
12 2. A Chrome tarball uploaded by a trybot/official-builder to GoogleStorage.
13 3. A Chrome tarball existing locally.
14
15 The script copies the necessary contents of the source location (tarball or
16 build directory) and rsyncs the contents of the staging directory onto your
17 device's rootfs.
18 """
19
20 import collections
21 import contextlib
22 import functools
23 import glob
24 import logging
25 import multiprocessing
26 import os
27 import optparse
28 import shlex
29 import shutil
30 import tarfile
31 import time
32 import zipfile
33
34
35 from chromite.buildbot import constants
36 from chromite.buildbot import cbuildbot_results as results_lib
37 from chromite.cros.commands import cros_chrome_sdk
38 from chromite.lib import chrome_util
39 from chromite.lib import cros_build_lib
40 from chromite.lib import commandline
41 from chromite.lib import gs
42 from chromite.lib import osutils
43 from chromite.lib import parallel
44 from chromite.lib import remote_access as remote
45 from chromite.lib import stats
46 from chromite.lib import timeout_util
47 from chromite.scripts import lddtree
48
49
50 _USAGE = "deploy_chrome [--]\n\n %s" % __doc__
51
52 KERNEL_A_PARTITION = 2
53 KERNEL_B_PARTITION = 4
54
55 KILL_PROC_MAX_WAIT = 10
56 POST_KILL_WAIT = 2
57
58 MOUNT_RW_COMMAND = 'mount -o remount,rw /'
59 LSOF_COMMAND = 'lsof %s/chrome'
60
61 MOUNT_RW_COMMAND_ANDROID = 'mount -o remount,rw /system'
62
63 _ANDROID_DIR = '/system/chrome'
64 _ANDROID_DIR_EXTRACT_PATH = 'system/chrome/*'
65
66 _CHROME_DIR = '/opt/google/chrome'
67 _CHROME_DIR_MOUNT = '/mnt/stateful_partition/deploy_rootfs/opt/google/chrome'
68
69 _BIND_TO_FINAL_DIR_CMD = 'mount --rbind %s %s'
70 _SET_MOUNT_FLAGS_CMD = 'mount -o remount,exec,suid %s'
71
72 DF_COMMAND = 'df -k %s'
73 DF_COMMAND_ANDROID = 'df %s'
74
75 def _UrlBaseName(url):
76   """Return the last component of the URL."""
77   return url.rstrip('/').rpartition('/')[-1]
78
79
80 class DeployFailure(results_lib.StepFailure):
81   """Raised whenever the deploy fails."""
82
83
84 DeviceInfo = collections.namedtuple(
85     'DeviceInfo', ['target_dir_size', 'target_fs_free'])
86
87
88 class DeployChrome(object):
89   """Wraps the core deployment functionality."""
90   def __init__(self, options, tempdir, staging_dir):
91     """Initialize the class.
92
93     Args:
94       options: Optparse result structure.
95       tempdir: Scratch space for the class.  Caller has responsibility to clean
96         it up.
97       staging_dir: Directory to stage the files to.
98     """
99     self.tempdir = tempdir
100     self.options = options
101     self.staging_dir = staging_dir
102     self.host = remote.RemoteAccess(options.to, tempdir, port=options.port)
103     self._rootfs_is_still_readonly = multiprocessing.Event()
104
105     # Used to track whether deploying content_shell or chrome to a device.
106     self.content_shell = False
107     self.copy_paths = chrome_util.GetCopyPaths(False)
108     self.chrome_dir = _CHROME_DIR
109
110   def _GetRemoteMountFree(self, remote_dir):
111     result = self.host.RemoteSh((DF_COMMAND if not self.content_shell
112                                  else DF_COMMAND_ANDROID) % remote_dir,
113                                 capture_output=True)
114     line = result.output.splitlines()[1]
115     value = line.split()[3]
116     multipliers = {
117         'G': 1024 * 1024 * 1024,
118         'M': 1024 * 1024,
119         'K': 1024,
120     }
121     return int(value.rstrip('GMK')) * multipliers.get(value[-1], 1)
122
123   def _GetRemoteDirSize(self, remote_dir):
124     if self.content_shell:
125       # Content Shell devices currently do not contain the du binary.
126       logging.warning('Remote host does not contain du; cannot get remote '
127                       'directory size to properly calculate available free '
128                       'space.')
129       return 0
130     result = self.host.RemoteSh('du -ks %s' % remote_dir, capture_output=True)
131     return int(result.output.split()[0])
132
133   def _GetStagingDirSize(self):
134     result = cros_build_lib.DebugRunCommand(['du', '-ks', self.staging_dir],
135                                             redirect_stdout=True,
136                                             capture_output=True)
137     return int(result.output.split()[0])
138
139   def _ChromeFileInUse(self):
140     result = self.host.RemoteSh(LSOF_COMMAND % (self.options.target_dir,),
141                                 error_code_ok=True, capture_output=True)
142     return result.returncode == 0
143
144   def _DisableRootfsVerification(self):
145     if not self.options.force:
146       logging.error('Detected that the device has rootfs verification enabled.')
147       logging.info('This script can automatically remove the rootfs '
148                    'verification, which requires that it reboot the device.')
149       logging.info('Make sure the device is in developer mode!')
150       logging.info('Skip this prompt by specifying --force.')
151       if not cros_build_lib.BooleanPrompt('Remove roots verification?', False):
152         # Since we stopped Chrome earlier, it's good form to start it up again.
153         if self.options.startui:
154           logging.info('Starting Chrome...')
155           self.host.RemoteSh('start ui')
156         raise DeployFailure('Need rootfs verification to be disabled. '
157                             'Aborting.')
158
159     logging.info('Removing rootfs verification from %s', self.options.to)
160     # Running in VM's cause make_dev_ssd's firmware sanity checks to fail.
161     # Use --force to bypass the checks.
162     cmd = ('/usr/share/vboot/bin/make_dev_ssd.sh --partitions %d '
163            '--remove_rootfs_verification --force')
164     for partition in (KERNEL_A_PARTITION, KERNEL_B_PARTITION):
165       self.host.RemoteSh(cmd % partition, error_code_ok=True)
166
167     # A reboot in developer mode takes a while (and has delays), so the user
168     # will have time to read and act on the USB boot instructions below.
169     logging.info('Please remember to press Ctrl-U if you are booting from USB.')
170     self.host.RemoteReboot()
171
172     # Now that the machine has been rebooted, we need to kill Chrome again.
173     self._KillProcsIfNeeded()
174
175     # Make sure the rootfs is writable now.
176     self._MountRootfsAsWritable(error_code_ok=False)
177
178   def _CheckUiJobStarted(self):
179     # status output is in the format:
180     # <job_name> <status> ['process' <pid>].
181     # <status> is in the format <goal>/<state>.
182     try:
183       result = self.host.RemoteSh('status ui', capture_output=True)
184     except cros_build_lib.RunCommandError as e:
185       if 'Unknown job' in e.result.error:
186         return False
187       else:
188         raise e
189
190     return result.output.split()[1].split('/')[0] == 'start'
191
192   def _KillProcsIfNeeded(self):
193     if self.content_shell:
194       logging.info('Shutting down content_shell...')
195       self.host.RemoteSh('stop content_shell')
196       return
197
198     if self._CheckUiJobStarted():
199       logging.info('Shutting down Chrome...')
200       self.host.RemoteSh('stop ui')
201
202     # Developers sometimes run session_manager manually, in which case we'll
203     # need to help shut the chrome processes down.
204     try:
205       with timeout_util.Timeout(KILL_PROC_MAX_WAIT):
206         while self._ChromeFileInUse():
207           logging.warning('The chrome binary on the device is in use.')
208           logging.warning('Killing chrome and session_manager processes...\n')
209
210           self.host.RemoteSh("pkill 'chrome|session_manager'",
211                              error_code_ok=True)
212           # Wait for processes to actually terminate
213           time.sleep(POST_KILL_WAIT)
214           logging.info('Rechecking the chrome binary...')
215     except timeout_util.TimeoutError:
216       msg = ('Could not kill processes after %s seconds.  Please exit any '
217              'running chrome processes and try again.' % KILL_PROC_MAX_WAIT)
218       raise DeployFailure(msg)
219
220   def _MountRootfsAsWritable(self, error_code_ok=True):
221     """Mount the rootfs as writable.
222
223     If the command fails, and error_code_ok is True, then this function sets
224     self._rootfs_is_still_readonly.
225
226     Args:
227       error_code_ok: See remote.RemoteAccess.RemoteSh for details.
228     """
229     # TODO: Should migrate to use the remount functions in remote_access.
230     result = self.host.RemoteSh(MOUNT_RW_COMMAND if not self.content_shell
231                                 else MOUNT_RW_COMMAND_ANDROID,
232                                 error_code_ok=error_code_ok,
233                                 capture_output=True)
234     if result.returncode:
235       self._rootfs_is_still_readonly.set()
236
237   def _GetDeviceInfo(self):
238     steps = [
239         functools.partial(self._GetRemoteDirSize, self.options.target_dir),
240         functools.partial(self._GetRemoteMountFree, self.options.target_dir)
241     ]
242     return_values = parallel.RunParallelSteps(steps, return_values=True)
243     return DeviceInfo(*return_values)
244
245   def _CheckDeviceFreeSpace(self, device_info):
246     """See if target device has enough space for Chrome.
247
248     Args:
249       device_info: A DeviceInfo named tuple.
250     """
251     effective_free = device_info.target_dir_size + device_info.target_fs_free
252     staging_size = self._GetStagingDirSize()
253     # For content shell deployments, which do not contain the du binary,
254     # do not raise DeployFailure since can't get exact free space available
255     # for staging files.
256     if effective_free < staging_size:
257       if self.content_shell:
258         logging.warning('Not enough free space on the device.  If overwriting '
259                         'files, deployment may still succeed.  Required: %s '
260                         'MiB, actual: %s MiB.', staging_size / 1024,
261                         effective_free / 1024)
262       else:
263         raise DeployFailure(
264             'Not enough free space on the device.  Required: %s MiB, '
265             'actual: %s MiB.' % (staging_size / 1024, effective_free / 1024))
266     if device_info.target_fs_free < (100 * 1024):
267       logging.warning('The device has less than 100MB free.  deploy_chrome may '
268                       'hang during the transfer.')
269
270   def _Deploy(self):
271     logging.info('Copying Chrome to %s on device...', self.options.target_dir)
272     # Show the output (status) for this command.
273     dest_path = _CHROME_DIR
274     if self.content_shell:
275       try:
276         self.host.Scp('%s/*' % os.path.abspath(self.staging_dir),
277                       '%s/' % self.options.target_dir,
278                       debug_level=logging.INFO,
279                       verbose=self.options.verbose)
280       except cros_build_lib.RunCommandError as ex:
281         if ex.result.returncode != 1:
282           logging.error('Scp failure [%s]', ex.result.returncode)
283           raise DeployFailure(ex)
284         else:
285           # TODO(stevefung): Update Dropbear SSHD on device.
286           # http://crbug.com/329656
287           logging.info('Potential conflict with DropBear SSHD return status')
288
289       dest_path = _ANDROID_DIR
290     else:
291       self.host.Rsync('%s/' % os.path.abspath(self.staging_dir),
292                       self.options.target_dir,
293                       inplace=True, debug_level=logging.INFO,
294                       verbose=self.options.verbose)
295
296     for p in self.copy_paths:
297       if p.owner:
298         self.host.RemoteSh('chown %s %s/%s' % (p.owner, dest_path,
299                                                p.src if not p.dest else p.dest))
300       if p.mode:
301         # Set mode if necessary.
302         self.host.RemoteSh('chmod %o %s/%s' % (p.mode, dest_path,
303                                                p.src if not p.dest else p.dest))
304
305
306     if self.options.startui:
307       logging.info('Starting UI...')
308       if self.content_shell:
309         self.host.RemoteSh('start content_shell')
310       else:
311         self.host.RemoteSh('start ui')
312
313   def _CheckConnection(self):
314     try:
315       logging.info('Testing connection to the device...')
316       if self.content_shell:
317         # true command over ssh returns error code 255, so as workaround
318         #   use `sleep 0` as no-op.
319         self.host.RemoteSh('sleep 0')
320       else:
321         self.host.RemoteSh('true')
322     except cros_build_lib.RunCommandError as ex:
323       logging.error('Error connecting to the test device.')
324       raise DeployFailure(ex)
325
326   def _CheckDeployType(self):
327     if self.options.build_dir:
328       if os.path.exists(os.path.join(self.options.build_dir, 'system.unand')):
329         # Unand Content shell deployment.
330         self.content_shell = True
331         self.options.build_dir = os.path.join(self.options.build_dir,
332                                               'system.unand/chrome/')
333         self.options.dostrip = False
334         self.options.target_dir = _ANDROID_DIR
335         self.copy_paths = chrome_util.GetCopyPaths(True)
336       elif os.path.exists(os.path.join(self.options.build_dir,
337                                        'content_shell')) and not os.path.exists(
338           os.path.join(self.options.build_dir, 'chrome')):
339         # Content shell deployment
340         self.content_shell = True
341         self.copy_paths = chrome_util.GetCopyPaths(True)
342     elif self.options.local_pkg_path or self.options.gs_path:
343       # Package deployment.
344       pkg_path = self.options.local_pkg_path
345       if self.options.gs_path:
346         pkg_path = _FetchChromePackage(self.options.cache_dir, self.tempdir,
347                                        self.options.gs_path)
348
349       assert pkg_path
350       logging.info('Checking %s for content_shell...', pkg_path)
351       if pkg_path[-4:] == '.zip':
352         zip_pkg = zipfile.ZipFile(pkg_path)
353         if any('eureka_shell' in name for name in zip_pkg.namelist()):
354           self.content_shell = True
355         zip_pkg.close()
356       else:
357         tar = tarfile.open(pkg_path)
358         if any('eureka_shell' in member.name for member in tar.getmembers()):
359           self.content_shell = True
360         tar.close()
361
362       if self.content_shell:
363         self.options.dostrip = False
364         self.options.target_dir = _ANDROID_DIR
365         self.copy_paths = chrome_util.GetCopyPaths(True)
366         self.chrome_dir = _ANDROID_DIR
367
368   def _PrepareStagingDir(self):
369     _PrepareStagingDir(self.options, self.tempdir, self.staging_dir,
370                        self.copy_paths, self.chrome_dir)
371
372   def _MountTarget(self):
373     logging.info('Mounting Chrome...')
374
375     # Create directory if does not exist
376     self.host.RemoteSh('mkdir -p --mode 0775 %s' % (self.options.mount_dir,))
377     self.host.RemoteSh(_BIND_TO_FINAL_DIR_CMD % (self.options.target_dir,
378                                                  self.options.mount_dir))
379     # Chrome needs partition to have exec and suid flags set
380     self.host.RemoteSh(_SET_MOUNT_FLAGS_CMD % (self.options.mount_dir,))
381
382   def Perform(self):
383     self._CheckDeployType()
384
385     # If requested, just do the staging step.
386     if self.options.staging_only:
387       self._PrepareStagingDir()
388       return 0
389
390     # Run setup steps in parallel. If any step fails, RunParallelSteps will
391     # stop printing output at that point, and halt any running steps.
392     steps = [self._GetDeviceInfo, self._CheckConnection,
393              self._KillProcsIfNeeded, self._MountRootfsAsWritable,
394              self._PrepareStagingDir]
395     ret = parallel.RunParallelSteps(steps, halt_on_error=True,
396                                     return_values=True)
397     self._CheckDeviceFreeSpace(ret[0])
398
399     # If we failed to mark the rootfs as writable, try disabling rootfs
400     # verification.
401     if self._rootfs_is_still_readonly.is_set():
402       self._DisableRootfsVerification()
403
404     if self.options.mount_dir is not None:
405       self._MountTarget()
406
407     # Actually deploy Chrome to the device.
408     self._Deploy()
409
410
411 def ValidateGypDefines(_option, _opt, value):
412   """Convert GYP_DEFINES-formatted string to dictionary."""
413   return chrome_util.ProcessGypDefines(value)
414
415
416 class CustomOption(commandline.Option):
417   """Subclass Option class to implement path evaluation."""
418   TYPES = commandline.Option.TYPES + ('gyp_defines',)
419   TYPE_CHECKER = commandline.Option.TYPE_CHECKER.copy()
420   TYPE_CHECKER['gyp_defines'] = ValidateGypDefines
421
422
423 def _CreateParser():
424   """Create our custom parser."""
425   parser = commandline.OptionParser(usage=_USAGE, option_class=CustomOption,
426                                     caching=True)
427
428   # TODO(rcui): Have this use the UI-V2 format of having source and target
429   # device be specified as positional arguments.
430   parser.add_option('--force', action='store_true', default=False,
431                     help='Skip all prompts (i.e., for disabling of rootfs '
432                          'verification).  This may result in the target '
433                          'machine being rebooted.')
434   sdk_board_env = os.environ.get(cros_chrome_sdk.SDKFetcher.SDK_BOARD_ENV)
435   parser.add_option('--board', default=sdk_board_env,
436                     help="The board the Chrome build is targeted for.  When in "
437                          "a 'cros chrome-sdk' shell, defaults to the SDK "
438                          "board.")
439   parser.add_option('--build-dir', type='path',
440                     help='The directory with Chrome build artifacts to deploy '
441                          'from.  Typically of format <chrome_root>/out/Debug. '
442                          'When this option is used, the GYP_DEFINES '
443                          'environment variable must be set.')
444   parser.add_option('--target-dir', type='path',
445                     help='Target directory on device to deploy Chrome into.',
446                     default=None)
447   parser.add_option('-g', '--gs-path', type='gs_path',
448                     help='GS path that contains the chrome to deploy.')
449   parser.add_option('--nostartui', action='store_false', dest='startui',
450                     default=True,
451                     help="Don't restart the ui daemon after deployment.")
452   parser.add_option('--nostrip', action='store_false', dest='dostrip',
453                     default=True,
454                     help="Don't strip binaries during deployment.  Warning: "
455                          "the resulting binaries will be very large!")
456   parser.add_option('-p', '--port', type=int, default=remote.DEFAULT_SSH_PORT,
457                     help='Port of the target device to connect to.')
458   parser.add_option('-t', '--to',
459                     help='The IP address of the CrOS device to deploy to.')
460   parser.add_option('-v', '--verbose', action='store_true', default=False,
461                     help='Show more debug output.')
462   parser.add_option('--mount-dir', type='path', default=None,
463                     help='Deploy Chrome in target directory and bind it'
464                          'to directory specified by this flag.')
465   parser.add_option('--mount', action='store_true', default=False,
466                     help='Deploy Chrome to default target directory and bind it'
467                          'to default mount directory.')
468
469   group = optparse.OptionGroup(parser, 'Advanced Options')
470   group.add_option('-l', '--local-pkg-path', type='path',
471                    help='Path to local chrome prebuilt package to deploy.')
472   group.add_option('--sloppy', action='store_true', default=False,
473                    help='Ignore when mandatory artifacts are missing.')
474   group.add_option('--staging-flags', default=None, type='gyp_defines',
475                    help='Extra flags to control staging.  Valid flags are - %s'
476                         % ', '.join(chrome_util.STAGING_FLAGS))
477   group.add_option('--strict', action='store_true', default=False,
478                    help='Stage artifacts based on the GYP_DEFINES environment '
479                         'variable and --staging-flags, if set. Enforce that '
480                         'all optional artifacts are deployed.')
481   group.add_option('--strip-flags', default=None,
482                    help="Flags to call the 'strip' binutil tool with.  "
483                         "Overrides the default arguments.")
484
485   parser.add_option_group(group)
486
487   # GYP_DEFINES that Chrome was built with.  Influences which files are staged
488   # when --build-dir is set.  Defaults to reading from the GYP_DEFINES
489   # enviroment variable.
490   parser.add_option('--gyp-defines', default=None, type='gyp_defines',
491                     help=optparse.SUPPRESS_HELP)
492   # Path of an empty directory to stage chrome artifacts to.  Defaults to a
493   # temporary directory that is removed when the script finishes. If the path
494   # is specified, then it will not be removed.
495   parser.add_option('--staging-dir', type='path', default=None,
496                     help=optparse.SUPPRESS_HELP)
497   # Only prepare the staging directory, and skip deploying to the device.
498   parser.add_option('--staging-only', action='store_true', default=False,
499                     help=optparse.SUPPRESS_HELP)
500   # Path to a binutil 'strip' tool to strip binaries with.  The passed-in path
501   # is used as-is, and not normalized.  Used by the Chrome ebuild to skip
502   # fetching the SDK toolchain.
503   parser.add_option('--strip-bin', default=None, help=optparse.SUPPRESS_HELP)
504   return parser
505
506
507 def _ParseCommandLine(argv):
508   """Parse args, and run environment-independent checks."""
509   parser = _CreateParser()
510   (options, args) = parser.parse_args(argv)
511
512   if not any([options.gs_path, options.local_pkg_path, options.build_dir]):
513     parser.error('Need to specify either --gs-path, --local-pkg-path, or '
514                  '--build-dir')
515   if options.build_dir and any([options.gs_path, options.local_pkg_path]):
516     parser.error('Cannot specify both --build_dir and '
517                  '--gs-path/--local-pkg-patch')
518   if options.build_dir and not options.board:
519     parser.error('--board is required when --build-dir is specified.')
520   if options.gs_path and options.local_pkg_path:
521     parser.error('Cannot specify both --gs-path and --local-pkg-path')
522   if not (options.staging_only or options.to):
523     parser.error('Need to specify --to')
524   if (options.strict or options.staging_flags) and not options.build_dir:
525     parser.error('--strict and --staging-flags require --build-dir to be '
526                  'set.')
527   if options.staging_flags and not options.strict:
528     parser.error('--staging-flags requires --strict to be set.')
529   if options.sloppy and options.strict:
530     parser.error('Cannot specify both --strict and --sloppy.')
531
532   if options.mount or options.mount_dir:
533     if not options.target_dir:
534       options.target_dir = _CHROME_DIR_MOUNT
535   else:
536     if not options.target_dir:
537       options.target_dir = _CHROME_DIR
538
539   if options.mount and not options.mount_dir:
540     options.mount_dir = _CHROME_DIR
541
542   return options, args
543
544
545 def _PostParseCheck(options, _args):
546   """Perform some usage validation (after we've parsed the arguments).
547
548   Args:
549     options: The options object returned by optparse.
550     _args: The args object returned by optparse.
551   """
552   if options.local_pkg_path and not os.path.isfile(options.local_pkg_path):
553     cros_build_lib.Die('%s is not a file.', options.local_pkg_path)
554
555   if not options.gyp_defines:
556     gyp_env = os.getenv('GYP_DEFINES', None)
557     if gyp_env is not None:
558       options.gyp_defines = chrome_util.ProcessGypDefines(gyp_env)
559       logging.debug('GYP_DEFINES taken from environment: %s',
560                    options.gyp_defines)
561
562   if options.strict and not options.gyp_defines:
563     cros_build_lib.Die('When --strict is set, the GYP_DEFINES environment '
564                          'variable must be set.')
565
566   if options.build_dir:
567     chrome_path = os.path.join(options.build_dir, 'chrome')
568     if os.path.isfile(chrome_path):
569       deps = lddtree.ParseELF(chrome_path)
570       if 'libbase.so' in deps['libs']:
571         cros_build_lib.Warning(
572             'Detected a component build of Chrome.  component build is '
573             'not working properly for Chrome OS.  See crbug.com/196317.  '
574             'Use at your own risk!')
575
576
577 def _FetchChromePackage(cache_dir, tempdir, gs_path):
578   """Get the chrome prebuilt tarball from GS.
579
580   Returns:
581     Path to the fetched chrome tarball.
582   """
583   gs_ctx = gs.GSContext(cache_dir=cache_dir, init_boto=True)
584   files = gs_ctx.LS(gs_path)
585   files = [found for found in files if
586            _UrlBaseName(found).startswith('%s-' % constants.CHROME_PN)]
587   if not files:
588     raise Exception('No chrome package found at %s' % gs_path)
589   elif len(files) > 1:
590     # - Users should provide us with a direct link to either a stripped or
591     #   unstripped chrome package.
592     # - In the case of being provided with an archive directory, where both
593     #   stripped and unstripped chrome available, use the stripped chrome
594     #   package.
595     # - Stripped chrome pkg is chromeos-chrome-<version>.tar.gz
596     # - Unstripped chrome pkg is chromeos-chrome-<version>-unstripped.tar.gz.
597     files = [f for f in files if not 'unstripped' in f]
598     assert len(files) == 1
599     logging.warning('Multiple chrome packages found.  Using %s', files[0])
600
601   filename = _UrlBaseName(files[0])
602   logging.info('Fetching %s...', filename)
603   gs_ctx.Copy(files[0], tempdir, print_cmd=False)
604   chrome_path = os.path.join(tempdir, filename)
605   assert os.path.exists(chrome_path)
606   return chrome_path
607
608
609 @contextlib.contextmanager
610 def _StripBinContext(options):
611   if not options.dostrip:
612     yield None
613   elif options.strip_bin:
614     yield options.strip_bin
615   else:
616     sdk = cros_chrome_sdk.SDKFetcher(options.cache_dir, options.board)
617     components = (sdk.TARGET_TOOLCHAIN_KEY, constants.CHROME_ENV_TAR)
618     with sdk.Prepare(components=components) as ctx:
619       env_path = os.path.join(ctx.key_map[constants.CHROME_ENV_TAR].path,
620                               constants.CHROME_ENV_FILE)
621       strip_bin = osutils.SourceEnvironment(env_path, ['STRIP'])['STRIP']
622       strip_bin = os.path.join(ctx.key_map[sdk.TARGET_TOOLCHAIN_KEY].path,
623                                'bin', os.path.basename(strip_bin))
624       yield strip_bin
625
626
627 def _PrepareStagingDir(options, tempdir, staging_dir, copy_paths=None,
628                        chrome_dir=_CHROME_DIR):
629   """Place the necessary files in the staging directory.
630
631   The staging directory is the directory used to rsync the build artifacts over
632   to the device.  Only the necessary Chrome build artifacts are put into the
633   staging directory.
634   """
635   osutils.SafeMakedirs(staging_dir)
636   os.chmod(staging_dir, 0o755)
637   if options.build_dir:
638     with _StripBinContext(options) as strip_bin:
639       strip_flags = (None if options.strip_flags is None else
640                      shlex.split(options.strip_flags))
641       chrome_util.StageChromeFromBuildDir(
642           staging_dir, options.build_dir, strip_bin, strict=options.strict,
643           sloppy=options.sloppy, gyp_defines=options.gyp_defines,
644           staging_flags=options.staging_flags,
645           strip_flags=strip_flags, copy_paths=copy_paths)
646   else:
647     pkg_path = options.local_pkg_path
648     if options.gs_path:
649       pkg_path = _FetchChromePackage(options.cache_dir, tempdir,
650                                      options.gs_path)
651
652     assert pkg_path
653     logging.info('Extracting %s...', pkg_path)
654     # Extract only the ./opt/google/chrome contents, directly into the staging
655     # dir, collapsing the directory hierarchy.
656     if pkg_path[-4:] == '.zip':
657       cros_build_lib.DebugRunCommand(
658           ['unzip', '-X', pkg_path, _ANDROID_DIR_EXTRACT_PATH, '-d',
659            staging_dir])
660       for filename in glob.glob(os.path.join(staging_dir, 'system/chrome/*')):
661         shutil.move(filename, staging_dir)
662       osutils.RmDir(os.path.join(staging_dir, 'system'), ignore_missing=True)
663     else:
664       cros_build_lib.DebugRunCommand(
665           ['tar', '--strip-components', '4', '--extract',
666            '--preserve-permissions', '--file', pkg_path, '.%s' % chrome_dir],
667           cwd=staging_dir)
668
669
670 def main(argv):
671   options, args = _ParseCommandLine(argv)
672   _PostParseCheck(options, args)
673
674   # Set cros_build_lib debug level to hide RunCommand spew.
675   if options.verbose:
676     logging.getLogger().setLevel(logging.DEBUG)
677   else:
678     logging.getLogger().setLevel(logging.INFO)
679
680   with stats.UploadContext() as queue:
681     cmd_stats = stats.Stats.SafeInit(cmd_line=argv, cmd_base='deploy_chrome')
682     if cmd_stats:
683       queue.put([cmd_stats, stats.StatsUploader.URL, 1])
684
685     with osutils.TempDir(set_global=True) as tempdir:
686       staging_dir = options.staging_dir
687       if not staging_dir:
688         staging_dir = os.path.join(tempdir, 'chrome')
689
690       deploy = DeployChrome(options, tempdir, staging_dir)
691       try:
692         deploy.Perform()
693       except results_lib.StepFailure as ex:
694         raise SystemExit(str(ex).strip())