blob: 53e8e16d2d0b135662e2a1f23b39a4370476a82d [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
Ryan Cui3045c5d2012-07-13 18:00:33 -070021import logging
Ryan Cui3045c5d2012-07-13 18:00:33 -070022import os
Ryan Cuia56a71e2012-10-18 18:40:35 -070023import optparse
Ryan Cui3045c5d2012-07-13 18:00:33 -070024import time
Ryan Cui3045c5d2012-07-13 18:00:33 -070025
Ryan Cuia56a71e2012-10-18 18:40:35 -070026
David James629febb2012-11-25 13:07:34 -080027from chromite.buildbot import constants
Ryan Cui686ec052013-02-12 16:39:41 -080028from chromite.cros.commands import cros_chrome_sdk
Ryan Cuia56a71e2012-10-18 18:40:35 -070029from chromite.lib import chrome_util
Ryan Cui3045c5d2012-07-13 18:00:33 -070030from chromite.lib import cros_build_lib
Ryan Cuie535b172012-10-19 18:25:03 -070031from chromite.lib import commandline
Ryan Cui777ff422012-12-07 13:12:54 -080032from chromite.lib import gs
Ryan Cui3045c5d2012-07-13 18:00:33 -070033from chromite.lib import osutils
34from chromite.lib import remote_access as remote
Ryan Cui3045c5d2012-07-13 18:00:33 -070035
36
Ryan Cuia56a71e2012-10-18 18:40:35 -070037_USAGE = "deploy_chrome [--]\n\n %s" % __doc__
38
Ryan Cui3045c5d2012-07-13 18:00:33 -070039KERNEL_A_PARTITION = 2
40KERNEL_B_PARTITION = 4
41
42KILL_PROC_MAX_WAIT = 10
43POST_KILL_WAIT = 2
44
Ryan Cuie535b172012-10-19 18:25:03 -070045MOUNT_RW_COMMAND = 'mount -o remount,rw /'
Ryan Cui3045c5d2012-07-13 18:00:33 -070046
David James2cb34002013-03-01 18:42:40 -080047_CHROME_DIR = '/opt/google/chrome'
48
Ryan Cui3045c5d2012-07-13 18:00:33 -070049
Ryan Cui3045c5d2012-07-13 18:00:33 -070050def _UrlBaseName(url):
51 """Return the last component of the URL."""
52 return url.rstrip('/').rpartition('/')[-1]
53
54
Ryan Cui3045c5d2012-07-13 18:00:33 -070055class DeployChrome(object):
56 """Wraps the core deployment functionality."""
Ryan Cuia56a71e2012-10-18 18:40:35 -070057 def __init__(self, options, tempdir, staging_dir):
Ryan Cuie535b172012-10-19 18:25:03 -070058 """Initialize the class.
59
60 Arguments:
61 options: Optparse result structure.
62 tempdir: Scratch space for the class. Caller has responsibility to clean
63 it up.
Ryan Cuie535b172012-10-19 18:25:03 -070064 """
Ryan Cui3045c5d2012-07-13 18:00:33 -070065 self.tempdir = tempdir
66 self.options = options
Ryan Cuia56a71e2012-10-18 18:40:35 -070067 self.staging_dir = staging_dir
Ryan Cuiafd6c5c2012-07-30 17:48:22 -070068 self.host = remote.RemoteAccess(options.to, tempdir, port=options.port)
Ryan Cui3045c5d2012-07-13 18:00:33 -070069
Ryan Cui3045c5d2012-07-13 18:00:33 -070070 def _ChromeFileInUse(self):
71 result = self.host.RemoteSh('lsof /opt/google/chrome/chrome',
72 error_code_ok=True)
73 return result.returncode == 0
74
75 def _DisableRootfsVerification(self):
76 if not self.options.force:
77 logging.error('Detected that the device has rootfs verification enabled.')
78 logging.info('This script can automatically remove the rootfs '
79 'verification, which requires that it reboot the device.')
80 logging.info('Make sure the device is in developer mode!')
81 logging.info('Skip this prompt by specifying --force.')
Brian Harring521e7242012-11-01 16:57:42 -070082 if not cros_build_lib.BooleanPrompt('Remove roots verification?', False):
Ryan Cui3045c5d2012-07-13 18:00:33 -070083 cros_build_lib.Die('Need rootfs verification to be disabled. '
84 'Aborting.')
85
86 logging.info('Removing rootfs verification from %s', self.options.to)
87 # Running in VM's cause make_dev_ssd's firmware sanity checks to fail.
88 # Use --force to bypass the checks.
89 cmd = ('/usr/share/vboot/bin/make_dev_ssd.sh --partitions %d '
90 '--remove_rootfs_verification --force')
91 for partition in (KERNEL_A_PARTITION, KERNEL_B_PARTITION):
92 self.host.RemoteSh(cmd % partition, error_code_ok=True)
93
94 # A reboot in developer mode takes a while (and has delays), so the user
95 # will have time to read and act on the USB boot instructions below.
96 logging.info('Please remember to press Ctrl-U if you are booting from USB.')
97 self.host.RemoteReboot()
98
99 def _CheckRootfsWriteable(self):
100 # /proc/mounts is in the format:
101 # <device> <dir> <type> <options>
102 result = self.host.RemoteSh('cat /proc/mounts')
103 for line in result.output.splitlines():
104 components = line.split()
105 if components[0] == '/dev/root' and components[1] == '/':
106 return 'rw' in components[3].split(',')
107 else:
108 raise Exception('Internal error - rootfs mount not found!')
109
110 def _CheckUiJobStarted(self):
111 # status output is in the format:
112 # <job_name> <status> ['process' <pid>].
113 # <status> is in the format <goal>/<state>.
Ryan Cuif2d1a582013-02-19 14:08:13 -0800114 try:
115 result = self.host.RemoteSh('status ui')
116 except cros_build_lib.RunCommandError as e:
117 if 'Unknown job' in e.result.error:
118 return False
119 else:
120 raise e
121
Ryan Cui3045c5d2012-07-13 18:00:33 -0700122 return result.output.split()[1].split('/')[0] == 'start'
123
124 def _KillProcsIfNeeded(self):
125 if self._CheckUiJobStarted():
Ryan Cui4d6b0db2013-02-28 15:13:24 -0800126 logging.info('Shutting down Chrome...')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700127 self.host.RemoteSh('stop ui')
128
129 # Developers sometimes run session_manager manually, in which case we'll
130 # need to help shut the chrome processes down.
131 try:
132 with cros_build_lib.SubCommandTimeout(KILL_PROC_MAX_WAIT):
133 while self._ChromeFileInUse():
134 logging.warning('The chrome binary on the device is in use.')
135 logging.warning('Killing chrome and session_manager processes...\n')
136
137 self.host.RemoteSh("pkill 'chrome|session_manager'",
138 error_code_ok=True)
139 # Wait for processes to actually terminate
140 time.sleep(POST_KILL_WAIT)
141 logging.info('Rechecking the chrome binary...')
142 except cros_build_lib.TimeoutError:
143 cros_build_lib.Die('Could not kill processes after %s seconds. Please '
144 'exit any running chrome processes and try again.')
145
146 def _PrepareTarget(self):
147 # Mount root partition as read/write
148 if not self._CheckRootfsWriteable():
149 logging.info('Mounting rootfs as writeable...')
Ryan Cuie535b172012-10-19 18:25:03 -0700150 result = self.host.RemoteSh(MOUNT_RW_COMMAND, error_code_ok=True)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700151 if result.returncode:
152 self._DisableRootfsVerification()
153 logging.info('Trying again to mount rootfs as writeable...')
Ryan Cuie535b172012-10-19 18:25:03 -0700154 self.host.RemoteSh(MOUNT_RW_COMMAND)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700155
156 if not self._CheckRootfsWriteable():
157 cros_build_lib.Die('Root partition still read-only')
158
159 # This is needed because we're doing an 'rsync --inplace' of Chrome, but
160 # makes sense to have even when going the sshfs route.
161 self._KillProcsIfNeeded()
162
163 def _Deploy(self):
Ryan Cui4d6b0db2013-02-28 15:13:24 -0800164 logging.info('Copying Chrome to device...')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700165 # Show the output (status) for this command.
David James2cb34002013-03-01 18:42:40 -0800166 self.host.Rsync('%s/' % os.path.abspath(self.staging_dir), _CHROME_DIR,
David Jamesa6e08892013-03-01 13:34:11 -0800167 inplace=True, debug_level=logging.INFO,
168 verbose=self.options.verbose)
Ryan Cui82a1f2d2013-02-22 17:34:23 -0800169 if self.options.startui:
Ryan Cui4d6b0db2013-02-28 15:13:24 -0800170 logging.info('Starting Chrome...')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700171 self.host.RemoteSh('start ui')
172
173 def Perform(self):
174 try:
Ryan Cui4d6b0db2013-02-28 15:13:24 -0800175 logging.info('Testing connection to the device...')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700176 self.host.RemoteSh('true')
177 except cros_build_lib.RunCommandError:
178 logging.error('Error connecting to the test device.')
179 raise
180
Ryan Cui3045c5d2012-07-13 18:00:33 -0700181 self._PrepareTarget()
182 self._Deploy()
183
184
Ryan Cuia56a71e2012-10-18 18:40:35 -0700185def ValidateGypDefines(_option, _opt, value):
186 """Convert GYP_DEFINES-formatted string to dictionary."""
187 return chrome_util.ProcessGypDefines(value)
188
189
190class CustomOption(commandline.Option):
191 """Subclass Option class to implement path evaluation."""
192 TYPES = commandline.Option.TYPES + ('gyp_defines',)
193 TYPE_CHECKER = commandline.Option.TYPE_CHECKER.copy()
194 TYPE_CHECKER['gyp_defines'] = ValidateGypDefines
195
196
Ryan Cuie535b172012-10-19 18:25:03 -0700197def _CreateParser():
198 """Create our custom parser."""
Ryan Cui504db722013-01-22 11:48:01 -0800199 parser = commandline.OptionParser(usage=_USAGE, option_class=CustomOption,
200 caching=True)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700201
Ryan Cuia56a71e2012-10-18 18:40:35 -0700202 # TODO(rcui): Have this use the UI-V2 format of having source and target
203 # device be specified as positional arguments.
Ryan Cui3045c5d2012-07-13 18:00:33 -0700204 parser.add_option('--force', action='store_true', default=False,
Ryan Cui686ec052013-02-12 16:39:41 -0800205 help='Skip all prompts (i.e., for disabling of rootfs '
206 'verification). This may result in the target '
207 'machine being rebooted.')
Ryan Cuia0215a72013-02-14 16:20:45 -0800208 sdk_board_env = os.environ.get(cros_chrome_sdk.SDKFetcher.SDK_BOARD_ENV)
209 parser.add_option('--board', default=sdk_board_env,
Ryan Cui686ec052013-02-12 16:39:41 -0800210 help="The board the Chrome build is targeted for. When in "
211 "a 'cros chrome-sdk' shell, defaults to the SDK "
212 "board.")
Ryan Cuia56a71e2012-10-18 18:40:35 -0700213 parser.add_option('--build-dir', type='path',
Ryan Cui686ec052013-02-12 16:39:41 -0800214 help='The directory with Chrome build artifacts to deploy '
215 'from. Typically of format <chrome_root>/out/Debug. '
216 'When this option is used, the GYP_DEFINES '
217 'environment variable must be set.')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700218 parser.add_option('-g', '--gs-path', type='gs_path',
Ryan Cui686ec052013-02-12 16:39:41 -0800219 help='GS path that contains the chrome to deploy.')
Ryan Cui82a1f2d2013-02-22 17:34:23 -0800220 parser.add_option('--nostartui', action='store_false', dest='startui',
221 default=True,
222 help="Don't restart the ui daemon after deployment.")
Ryan Cui3045c5d2012-07-13 18:00:33 -0700223 parser.add_option('-p', '--port', type=int, default=remote.DEFAULT_SSH_PORT,
Ryan Cui686ec052013-02-12 16:39:41 -0800224 help='Port of the target device to connect to.')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700225 parser.add_option('-t', '--to',
Ryan Cui686ec052013-02-12 16:39:41 -0800226 help='The IP address of the CrOS device to deploy to.')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700227 parser.add_option('-v', '--verbose', action='store_true', default=False,
Ryan Cui686ec052013-02-12 16:39:41 -0800228 help='Show more debug output.')
Ryan Cuia56a71e2012-10-18 18:40:35 -0700229
230 group = optparse.OptionGroup(parser, 'Advanced Options')
231 group.add_option('-l', '--local-pkg-path', type='path',
Ryan Cui686ec052013-02-12 16:39:41 -0800232 help='Path to local chrome prebuilt package to deploy.')
David Jamesa6e08892013-03-01 13:34:11 -0800233 group.add_option('--sloppy', action='store_true', default=False,
234 help='Ignore when mandatory artifacts are missing.')
Ryan Cuief91e702013-02-04 12:06:36 -0800235 group.add_option('--strict', action='store_true', default=False,
Ryan Cui686ec052013-02-12 16:39:41 -0800236 help='Stage artifacts based on the GYP_DEFINES environment '
David Jamesa6e08892013-03-01 13:34:11 -0800237 'variable and --staging-flags, if set. Enforce that '
238 'all optional artifacts are deployed.')
Ryan Cuief91e702013-02-04 12:06:36 -0800239 group.add_option('--staging-flags', default=None, type='gyp_defines',
David Jamesa6e08892013-03-01 13:34:11 -0800240 help='Extra flags to control staging. Valid flags are - %s'
Ryan Cui686ec052013-02-12 16:39:41 -0800241 % ', '.join(chrome_util.STAGING_FLAGS))
Ryan Cuief91e702013-02-04 12:06:36 -0800242
Ryan Cuia56a71e2012-10-18 18:40:35 -0700243 parser.add_option_group(group)
244
245 # Path of an empty directory to stage chrome artifacts to. Defaults to a
246 # temporary directory that is removed when the script finishes. If the path
247 # is specified, then it will not be removed.
248 parser.add_option('--staging-dir', type='path', default=None,
249 help=optparse.SUPPRESS_HELP)
250 # Only prepare the staging directory, and skip deploying to the device.
251 parser.add_option('--staging-only', action='store_true', default=False,
252 help=optparse.SUPPRESS_HELP)
253 # GYP_DEFINES that Chrome was built with. Influences which files are staged
254 # when --build-dir is set. Defaults to reading from the GYP_DEFINES
255 # enviroment variable.
Ryan Cuief91e702013-02-04 12:06:36 -0800256 parser.add_option('--gyp-defines', default=None, type='gyp_defines',
Ryan Cuia56a71e2012-10-18 18:40:35 -0700257 help=optparse.SUPPRESS_HELP)
Ryan Cuie535b172012-10-19 18:25:03 -0700258 return parser
Ryan Cui3045c5d2012-07-13 18:00:33 -0700259
Ryan Cuie535b172012-10-19 18:25:03 -0700260
261def _ParseCommandLine(argv):
262 """Parse args, and run environment-independent checks."""
263 parser = _CreateParser()
Ryan Cui3045c5d2012-07-13 18:00:33 -0700264 (options, args) = parser.parse_args(argv)
265
Ryan Cuia56a71e2012-10-18 18:40:35 -0700266 if not any([options.gs_path, options.local_pkg_path, options.build_dir]):
267 parser.error('Need to specify either --gs-path, --local-pkg-path, or '
Ryan Cuief91e702013-02-04 12:06:36 -0800268 '--build-dir')
Ryan Cuia56a71e2012-10-18 18:40:35 -0700269 if options.build_dir and any([options.gs_path, options.local_pkg_path]):
270 parser.error('Cannot specify both --build_dir and '
271 '--gs-path/--local-pkg-patch')
Ryan Cui686ec052013-02-12 16:39:41 -0800272 if options.build_dir and not options.board:
273 parser.error('--board is required when --build-dir is specified.')
Ryan Cuia56a71e2012-10-18 18:40:35 -0700274 if options.gs_path and options.local_pkg_path:
275 parser.error('Cannot specify both --gs-path and --local-pkg-path')
276 if not (options.staging_only or options.to):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700277 parser.error('Need to specify --to')
Ryan Cuief91e702013-02-04 12:06:36 -0800278 if (options.strict or options.staging_flags) and not options.build_dir:
279 parser.error('--strict and --staging-flags require --build-dir to be '
280 'set.')
281 if options.staging_flags and not options.strict:
David Jamesa6e08892013-03-01 13:34:11 -0800282 parser.error('--staging-flags requires --strict to be set.')
283 if options.sloppy and options.strict:
284 parser.error('Cannot specify both --strict and --sloppy.')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700285 return options, args
286
287
Ryan Cuie535b172012-10-19 18:25:03 -0700288def _PostParseCheck(options, _args):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700289 """Perform some usage validation (after we've parsed the arguments
290
291 Args:
292 options/args: The options/args object returned by optparse
293 """
Ryan Cuia56a71e2012-10-18 18:40:35 -0700294 if options.local_pkg_path and not os.path.isfile(options.local_pkg_path):
295 cros_build_lib.Die('%s is not a file.', options.local_pkg_path)
296
Ryan Cui686ec052013-02-12 16:39:41 -0800297 if options.strict and not options.gyp_defines:
Ryan Cuia56a71e2012-10-18 18:40:35 -0700298 gyp_env = os.getenv('GYP_DEFINES', None)
299 if gyp_env is not None:
300 options.gyp_defines = chrome_util.ProcessGypDefines(gyp_env)
301 logging.info('GYP_DEFINES taken from environment: %s',
302 options.gyp_defines)
303 else:
Ryan Cui686ec052013-02-12 16:39:41 -0800304 cros_build_lib.Die('When --strict is set, the GYP_DEFINES environment '
305 'variable must be set.')
Ryan Cuia56a71e2012-10-18 18:40:35 -0700306
307
Ryan Cui504db722013-01-22 11:48:01 -0800308def _FetchChromePackage(cache_dir, tempdir, gs_path):
Ryan Cuia56a71e2012-10-18 18:40:35 -0700309 """Get the chrome prebuilt tarball from GS.
310
311 Returns: Path to the fetched chrome tarball.
312 """
Ryan Cui4c2d42c2013-02-08 16:22:26 -0800313 gs_ctx = gs.GSContext.Cached(cache_dir, init_boto=True)
314 files = gs_ctx.LS(gs_path).output.splitlines()
315 files = [found for found in files if
316 _UrlBaseName(found).startswith('%s-' % constants.CHROME_PN)]
317 if not files:
318 raise Exception('No chrome package found at %s' % gs_path)
319 elif len(files) > 1:
320 # - Users should provide us with a direct link to either a stripped or
321 # unstripped chrome package.
322 # - In the case of being provided with an archive directory, where both
323 # stripped and unstripped chrome available, use the stripped chrome
324 # package.
325 # - Stripped chrome pkg is chromeos-chrome-<version>.tar.gz
326 # - Unstripped chrome pkg is chromeos-chrome-<version>-unstripped.tar.gz.
327 files = [f for f in files if not 'unstripped' in f]
328 assert len(files) == 1
329 logging.warning('Multiple chrome packages found. Using %s', files[0])
Ryan Cui777ff422012-12-07 13:12:54 -0800330
Ryan Cui4c2d42c2013-02-08 16:22:26 -0800331 filename = _UrlBaseName(files[0])
Ryan Cui4d6b0db2013-02-28 15:13:24 -0800332 logging.info('Fetching %s...', filename)
Ryan Cui4c2d42c2013-02-08 16:22:26 -0800333 gs_ctx.Copy(files[0], tempdir, print_cmd=False)
334 chrome_path = os.path.join(tempdir, filename)
335 assert os.path.exists(chrome_path)
336 return chrome_path
Ryan Cuia56a71e2012-10-18 18:40:35 -0700337
338
339def _PrepareStagingDir(options, tempdir, staging_dir):
340 """Place the necessary files in the staging directory.
341
342 The staging directory is the directory used to rsync the build artifacts over
343 to the device. Only the necessary Chrome build artifacts are put into the
344 staging directory.
345 """
346 if options.build_dir:
Ryan Cuia0215a72013-02-14 16:20:45 -0800347 sdk = cros_chrome_sdk.SDKFetcher(options.cache_dir, options.board)
Ryan Cui686ec052013-02-12 16:39:41 -0800348 components = (sdk.TARGET_TOOLCHAIN_KEY, constants.CHROME_ENV_TAR)
349 with sdk.Prepare(components=components) as ctx:
350 env_path = os.path.join(ctx.key_map[constants.CHROME_ENV_TAR].path,
351 constants.CHROME_ENV_FILE)
352 strip_bin = osutils.SourceEnvironment(env_path, ['STRIP'])['STRIP']
353 strip_bin = os.path.join(ctx.key_map[sdk.TARGET_TOOLCHAIN_KEY].path,
354 'bin', os.path.basename(strip_bin))
355 chrome_util.StageChromeFromBuildDir(
356 staging_dir, options.build_dir, strip_bin, strict=options.strict,
David Jamesa6e08892013-03-01 13:34:11 -0800357 sloppy=options.sloppy, gyp_defines=options.gyp_defines,
358 staging_flags=options.staging_flags)
Ryan Cuia56a71e2012-10-18 18:40:35 -0700359 else:
360 pkg_path = options.local_pkg_path
361 if options.gs_path:
Ryan Cui504db722013-01-22 11:48:01 -0800362 pkg_path = _FetchChromePackage(options.cache_dir, tempdir,
363 options.gs_path)
Ryan Cuia56a71e2012-10-18 18:40:35 -0700364
365 assert pkg_path
Ryan Cui4d6b0db2013-02-28 15:13:24 -0800366 logging.info('Extracting %s...', pkg_path)
Ryan Cuif2d1a582013-02-19 14:08:13 -0800367 osutils.SafeMakedirs(staging_dir)
Ryan Cui4d6b0db2013-02-28 15:13:24 -0800368 cros_build_lib.DebugRunCommand(['tar', '-xpf', pkg_path], cwd=staging_dir)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700369
370
371def main(argv):
372 options, args = _ParseCommandLine(argv)
373 _PostParseCheck(options, args)
374
375 # Set cros_build_lib debug level to hide RunCommand spew.
376 if options.verbose:
Ryan Cuia56a71e2012-10-18 18:40:35 -0700377 logging.getLogger().setLevel(logging.DEBUG)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700378 else:
Ryan Cui4d6b0db2013-02-28 15:13:24 -0800379 logging.getLogger().setLevel(logging.INFO)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700380
Ryan Cuif2d1a582013-02-19 14:08:13 -0800381 with osutils.TempDirContextManager() as tempdir:
382 staging_dir = options.staging_dir
383 if not staging_dir:
384 staging_dir = os.path.join(tempdir, 'chrome')
385 _PrepareStagingDir(options, tempdir, staging_dir)
Ryan Cuia56a71e2012-10-18 18:40:35 -0700386
Ryan Cuif2d1a582013-02-19 14:08:13 -0800387 if options.staging_only:
388 return 0
Ryan Cuia56a71e2012-10-18 18:40:35 -0700389
Ryan Cuif2d1a582013-02-19 14:08:13 -0800390 deploy = DeployChrome(options, tempdir, staging_dir)
391 deploy.Perform()