blob: 41041b3863aad69998877c38355828a3e50c52d6 [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
Ryan Cui3045c5d2012-07-13 18:00:33 -070047
Ryan Cui3045c5d2012-07-13 18:00:33 -070048def _UrlBaseName(url):
49 """Return the last component of the URL."""
50 return url.rstrip('/').rpartition('/')[-1]
51
52
Ryan Cui3045c5d2012-07-13 18:00:33 -070053class DeployChrome(object):
54 """Wraps the core deployment functionality."""
Ryan Cuia56a71e2012-10-18 18:40:35 -070055 def __init__(self, options, tempdir, staging_dir):
Ryan Cuie535b172012-10-19 18:25:03 -070056 """Initialize the class.
57
58 Arguments:
59 options: Optparse result structure.
60 tempdir: Scratch space for the class. Caller has responsibility to clean
61 it up.
Ryan Cuie535b172012-10-19 18:25:03 -070062 """
Ryan Cui3045c5d2012-07-13 18:00:33 -070063 self.tempdir = tempdir
64 self.options = options
Ryan Cuia56a71e2012-10-18 18:40:35 -070065 self.staging_dir = staging_dir
Ryan Cuiafd6c5c2012-07-30 17:48:22 -070066 self.host = remote.RemoteAccess(options.to, tempdir, port=options.port)
Ryan Cui3045c5d2012-07-13 18:00:33 -070067
Ryan Cui3045c5d2012-07-13 18:00:33 -070068 def _ChromeFileInUse(self):
69 result = self.host.RemoteSh('lsof /opt/google/chrome/chrome',
70 error_code_ok=True)
71 return result.returncode == 0
72
73 def _DisableRootfsVerification(self):
74 if not self.options.force:
75 logging.error('Detected that the device has rootfs verification enabled.')
76 logging.info('This script can automatically remove the rootfs '
77 'verification, which requires that it reboot the device.')
78 logging.info('Make sure the device is in developer mode!')
79 logging.info('Skip this prompt by specifying --force.')
Brian Harring521e7242012-11-01 16:57:42 -070080 if not cros_build_lib.BooleanPrompt('Remove roots verification?', False):
Ryan Cui3045c5d2012-07-13 18:00:33 -070081 cros_build_lib.Die('Need rootfs verification to be disabled. '
82 'Aborting.')
83
84 logging.info('Removing rootfs verification from %s', self.options.to)
85 # Running in VM's cause make_dev_ssd's firmware sanity checks to fail.
86 # Use --force to bypass the checks.
87 cmd = ('/usr/share/vboot/bin/make_dev_ssd.sh --partitions %d '
88 '--remove_rootfs_verification --force')
89 for partition in (KERNEL_A_PARTITION, KERNEL_B_PARTITION):
90 self.host.RemoteSh(cmd % partition, error_code_ok=True)
91
92 # A reboot in developer mode takes a while (and has delays), so the user
93 # will have time to read and act on the USB boot instructions below.
94 logging.info('Please remember to press Ctrl-U if you are booting from USB.')
95 self.host.RemoteReboot()
96
97 def _CheckRootfsWriteable(self):
98 # /proc/mounts is in the format:
99 # <device> <dir> <type> <options>
100 result = self.host.RemoteSh('cat /proc/mounts')
101 for line in result.output.splitlines():
102 components = line.split()
103 if components[0] == '/dev/root' and components[1] == '/':
104 return 'rw' in components[3].split(',')
105 else:
106 raise Exception('Internal error - rootfs mount not found!')
107
108 def _CheckUiJobStarted(self):
109 # status output is in the format:
110 # <job_name> <status> ['process' <pid>].
111 # <status> is in the format <goal>/<state>.
Ryan Cuif2d1a582013-02-19 14:08:13 -0800112 try:
113 result = self.host.RemoteSh('status ui')
114 except cros_build_lib.RunCommandError as e:
115 if 'Unknown job' in e.result.error:
116 return False
117 else:
118 raise e
119
Ryan Cui3045c5d2012-07-13 18:00:33 -0700120 return result.output.split()[1].split('/')[0] == 'start'
121
122 def _KillProcsIfNeeded(self):
123 if self._CheckUiJobStarted():
Ryan Cui4d6b0db2013-02-28 15:13:24 -0800124 logging.info('Shutting down Chrome...')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700125 self.host.RemoteSh('stop ui')
126
127 # Developers sometimes run session_manager manually, in which case we'll
128 # need to help shut the chrome processes down.
129 try:
130 with cros_build_lib.SubCommandTimeout(KILL_PROC_MAX_WAIT):
131 while self._ChromeFileInUse():
132 logging.warning('The chrome binary on the device is in use.')
133 logging.warning('Killing chrome and session_manager processes...\n')
134
135 self.host.RemoteSh("pkill 'chrome|session_manager'",
136 error_code_ok=True)
137 # Wait for processes to actually terminate
138 time.sleep(POST_KILL_WAIT)
139 logging.info('Rechecking the chrome binary...')
140 except cros_build_lib.TimeoutError:
141 cros_build_lib.Die('Could not kill processes after %s seconds. Please '
142 'exit any running chrome processes and try again.')
143
144 def _PrepareTarget(self):
145 # Mount root partition as read/write
146 if not self._CheckRootfsWriteable():
147 logging.info('Mounting rootfs as writeable...')
Ryan Cuie535b172012-10-19 18:25:03 -0700148 result = self.host.RemoteSh(MOUNT_RW_COMMAND, error_code_ok=True)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700149 if result.returncode:
150 self._DisableRootfsVerification()
151 logging.info('Trying again to mount rootfs as writeable...')
Ryan Cuie535b172012-10-19 18:25:03 -0700152 self.host.RemoteSh(MOUNT_RW_COMMAND)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700153
154 if not self._CheckRootfsWriteable():
155 cros_build_lib.Die('Root partition still read-only')
156
157 # This is needed because we're doing an 'rsync --inplace' of Chrome, but
158 # makes sense to have even when going the sshfs route.
159 self._KillProcsIfNeeded()
160
161 def _Deploy(self):
Ryan Cui4d6b0db2013-02-28 15:13:24 -0800162 logging.info('Copying Chrome to device...')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700163 # Show the output (status) for this command.
Ryan Cuia56a71e2012-10-18 18:40:35 -0700164 self.host.Rsync('%s/' % os.path.abspath(self.staging_dir), '/',
David Jamesa6e08892013-03-01 13:34:11 -0800165 inplace=True, debug_level=logging.INFO,
166 verbose=self.options.verbose)
Ryan Cui82a1f2d2013-02-22 17:34:23 -0800167 if self.options.startui:
Ryan Cui4d6b0db2013-02-28 15:13:24 -0800168 logging.info('Starting Chrome...')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700169 self.host.RemoteSh('start ui')
170
171 def Perform(self):
172 try:
Ryan Cui4d6b0db2013-02-28 15:13:24 -0800173 logging.info('Testing connection to the device...')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700174 self.host.RemoteSh('true')
175 except cros_build_lib.RunCommandError:
176 logging.error('Error connecting to the test device.')
177 raise
178
Ryan Cui3045c5d2012-07-13 18:00:33 -0700179 self._PrepareTarget()
180 self._Deploy()
181
182
Ryan Cuia56a71e2012-10-18 18:40:35 -0700183def ValidateGypDefines(_option, _opt, value):
184 """Convert GYP_DEFINES-formatted string to dictionary."""
185 return chrome_util.ProcessGypDefines(value)
186
187
188class CustomOption(commandline.Option):
189 """Subclass Option class to implement path evaluation."""
190 TYPES = commandline.Option.TYPES + ('gyp_defines',)
191 TYPE_CHECKER = commandline.Option.TYPE_CHECKER.copy()
192 TYPE_CHECKER['gyp_defines'] = ValidateGypDefines
193
194
Ryan Cuie535b172012-10-19 18:25:03 -0700195def _CreateParser():
196 """Create our custom parser."""
Ryan Cui504db722013-01-22 11:48:01 -0800197 parser = commandline.OptionParser(usage=_USAGE, option_class=CustomOption,
198 caching=True)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700199
Ryan Cuia56a71e2012-10-18 18:40:35 -0700200 # TODO(rcui): Have this use the UI-V2 format of having source and target
201 # device be specified as positional arguments.
Ryan Cui3045c5d2012-07-13 18:00:33 -0700202 parser.add_option('--force', action='store_true', default=False,
Ryan Cui686ec052013-02-12 16:39:41 -0800203 help='Skip all prompts (i.e., for disabling of rootfs '
204 'verification). This may result in the target '
205 'machine being rebooted.')
Ryan Cuia0215a72013-02-14 16:20:45 -0800206 sdk_board_env = os.environ.get(cros_chrome_sdk.SDKFetcher.SDK_BOARD_ENV)
207 parser.add_option('--board', default=sdk_board_env,
Ryan Cui686ec052013-02-12 16:39:41 -0800208 help="The board the Chrome build is targeted for. When in "
209 "a 'cros chrome-sdk' shell, defaults to the SDK "
210 "board.")
Ryan Cuia56a71e2012-10-18 18:40:35 -0700211 parser.add_option('--build-dir', type='path',
Ryan Cui686ec052013-02-12 16:39:41 -0800212 help='The directory with Chrome build artifacts to deploy '
213 'from. Typically of format <chrome_root>/out/Debug. '
214 'When this option is used, the GYP_DEFINES '
215 'environment variable must be set.')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700216 parser.add_option('-g', '--gs-path', type='gs_path',
Ryan Cui686ec052013-02-12 16:39:41 -0800217 help='GS path that contains the chrome to deploy.')
Ryan Cui82a1f2d2013-02-22 17:34:23 -0800218 parser.add_option('--nostartui', action='store_false', dest='startui',
219 default=True,
220 help="Don't restart the ui daemon after deployment.")
Ryan Cui3045c5d2012-07-13 18:00:33 -0700221 parser.add_option('-p', '--port', type=int, default=remote.DEFAULT_SSH_PORT,
Ryan Cui686ec052013-02-12 16:39:41 -0800222 help='Port of the target device to connect to.')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700223 parser.add_option('-t', '--to',
Ryan Cui686ec052013-02-12 16:39:41 -0800224 help='The IP address of the CrOS device to deploy to.')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700225 parser.add_option('-v', '--verbose', action='store_true', default=False,
Ryan Cui686ec052013-02-12 16:39:41 -0800226 help='Show more debug output.')
Ryan Cuia56a71e2012-10-18 18:40:35 -0700227
228 group = optparse.OptionGroup(parser, 'Advanced Options')
229 group.add_option('-l', '--local-pkg-path', type='path',
Ryan Cui686ec052013-02-12 16:39:41 -0800230 help='Path to local chrome prebuilt package to deploy.')
David Jamesa6e08892013-03-01 13:34:11 -0800231 group.add_option('--sloppy', action='store_true', default=False,
232 help='Ignore when mandatory artifacts are missing.')
Ryan Cuief91e702013-02-04 12:06:36 -0800233 group.add_option('--strict', action='store_true', default=False,
Ryan Cui686ec052013-02-12 16:39:41 -0800234 help='Stage artifacts based on the GYP_DEFINES environment '
David Jamesa6e08892013-03-01 13:34:11 -0800235 'variable and --staging-flags, if set. Enforce that '
236 'all optional artifacts are deployed.')
Ryan Cuief91e702013-02-04 12:06:36 -0800237 group.add_option('--staging-flags', default=None, type='gyp_defines',
David Jamesa6e08892013-03-01 13:34:11 -0800238 help='Extra flags to control staging. Valid flags are - %s'
Ryan Cui686ec052013-02-12 16:39:41 -0800239 % ', '.join(chrome_util.STAGING_FLAGS))
Ryan Cuief91e702013-02-04 12:06:36 -0800240
Ryan Cuia56a71e2012-10-18 18:40:35 -0700241 parser.add_option_group(group)
242
243 # Path of an empty directory to stage chrome artifacts to. Defaults to a
244 # temporary directory that is removed when the script finishes. If the path
245 # is specified, then it will not be removed.
246 parser.add_option('--staging-dir', type='path', default=None,
247 help=optparse.SUPPRESS_HELP)
248 # Only prepare the staging directory, and skip deploying to the device.
249 parser.add_option('--staging-only', action='store_true', default=False,
250 help=optparse.SUPPRESS_HELP)
251 # GYP_DEFINES that Chrome was built with. Influences which files are staged
252 # when --build-dir is set. Defaults to reading from the GYP_DEFINES
253 # enviroment variable.
Ryan Cuief91e702013-02-04 12:06:36 -0800254 parser.add_option('--gyp-defines', default=None, type='gyp_defines',
Ryan Cuia56a71e2012-10-18 18:40:35 -0700255 help=optparse.SUPPRESS_HELP)
Ryan Cuie535b172012-10-19 18:25:03 -0700256 return parser
Ryan Cui3045c5d2012-07-13 18:00:33 -0700257
Ryan Cuie535b172012-10-19 18:25:03 -0700258
259def _ParseCommandLine(argv):
260 """Parse args, and run environment-independent checks."""
261 parser = _CreateParser()
Ryan Cui3045c5d2012-07-13 18:00:33 -0700262 (options, args) = parser.parse_args(argv)
263
Ryan Cuia56a71e2012-10-18 18:40:35 -0700264 if not any([options.gs_path, options.local_pkg_path, options.build_dir]):
265 parser.error('Need to specify either --gs-path, --local-pkg-path, or '
Ryan Cuief91e702013-02-04 12:06:36 -0800266 '--build-dir')
Ryan Cuia56a71e2012-10-18 18:40:35 -0700267 if options.build_dir and any([options.gs_path, options.local_pkg_path]):
268 parser.error('Cannot specify both --build_dir and '
269 '--gs-path/--local-pkg-patch')
Ryan Cui686ec052013-02-12 16:39:41 -0800270 if options.build_dir and not options.board:
271 parser.error('--board is required when --build-dir is specified.')
Ryan Cuia56a71e2012-10-18 18:40:35 -0700272 if options.gs_path and options.local_pkg_path:
273 parser.error('Cannot specify both --gs-path and --local-pkg-path')
274 if not (options.staging_only or options.to):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700275 parser.error('Need to specify --to')
Ryan Cuief91e702013-02-04 12:06:36 -0800276 if (options.strict or options.staging_flags) and not options.build_dir:
277 parser.error('--strict and --staging-flags require --build-dir to be '
278 'set.')
279 if options.staging_flags and not options.strict:
David Jamesa6e08892013-03-01 13:34:11 -0800280 parser.error('--staging-flags requires --strict to be set.')
281 if options.sloppy and options.strict:
282 parser.error('Cannot specify both --strict and --sloppy.')
Ryan Cui3045c5d2012-07-13 18:00:33 -0700283 return options, args
284
285
Ryan Cuie535b172012-10-19 18:25:03 -0700286def _PostParseCheck(options, _args):
Ryan Cui3045c5d2012-07-13 18:00:33 -0700287 """Perform some usage validation (after we've parsed the arguments
288
289 Args:
290 options/args: The options/args object returned by optparse
291 """
Ryan Cuia56a71e2012-10-18 18:40:35 -0700292 if options.local_pkg_path and not os.path.isfile(options.local_pkg_path):
293 cros_build_lib.Die('%s is not a file.', options.local_pkg_path)
294
Ryan Cui686ec052013-02-12 16:39:41 -0800295 if options.strict and not options.gyp_defines:
Ryan Cuia56a71e2012-10-18 18:40:35 -0700296 gyp_env = os.getenv('GYP_DEFINES', None)
297 if gyp_env is not None:
298 options.gyp_defines = chrome_util.ProcessGypDefines(gyp_env)
299 logging.info('GYP_DEFINES taken from environment: %s',
300 options.gyp_defines)
301 else:
Ryan Cui686ec052013-02-12 16:39:41 -0800302 cros_build_lib.Die('When --strict is set, the GYP_DEFINES environment '
303 'variable must be set.')
Ryan Cuia56a71e2012-10-18 18:40:35 -0700304
305
Ryan Cui504db722013-01-22 11:48:01 -0800306def _FetchChromePackage(cache_dir, tempdir, gs_path):
Ryan Cuia56a71e2012-10-18 18:40:35 -0700307 """Get the chrome prebuilt tarball from GS.
308
309 Returns: Path to the fetched chrome tarball.
310 """
Ryan Cui4c2d42c2013-02-08 16:22:26 -0800311 gs_ctx = gs.GSContext.Cached(cache_dir, init_boto=True)
312 files = gs_ctx.LS(gs_path).output.splitlines()
313 files = [found for found in files if
314 _UrlBaseName(found).startswith('%s-' % constants.CHROME_PN)]
315 if not files:
316 raise Exception('No chrome package found at %s' % gs_path)
317 elif len(files) > 1:
318 # - Users should provide us with a direct link to either a stripped or
319 # unstripped chrome package.
320 # - In the case of being provided with an archive directory, where both
321 # stripped and unstripped chrome available, use the stripped chrome
322 # package.
323 # - Stripped chrome pkg is chromeos-chrome-<version>.tar.gz
324 # - Unstripped chrome pkg is chromeos-chrome-<version>-unstripped.tar.gz.
325 files = [f for f in files if not 'unstripped' in f]
326 assert len(files) == 1
327 logging.warning('Multiple chrome packages found. Using %s', files[0])
Ryan Cui777ff422012-12-07 13:12:54 -0800328
Ryan Cui4c2d42c2013-02-08 16:22:26 -0800329 filename = _UrlBaseName(files[0])
Ryan Cui4d6b0db2013-02-28 15:13:24 -0800330 logging.info('Fetching %s...', filename)
Ryan Cui4c2d42c2013-02-08 16:22:26 -0800331 gs_ctx.Copy(files[0], tempdir, print_cmd=False)
332 chrome_path = os.path.join(tempdir, filename)
333 assert os.path.exists(chrome_path)
334 return chrome_path
Ryan Cuia56a71e2012-10-18 18:40:35 -0700335
336
337def _PrepareStagingDir(options, tempdir, staging_dir):
338 """Place the necessary files in the staging directory.
339
340 The staging directory is the directory used to rsync the build artifacts over
341 to the device. Only the necessary Chrome build artifacts are put into the
342 staging directory.
343 """
344 if options.build_dir:
Ryan Cuia0215a72013-02-14 16:20:45 -0800345 sdk = cros_chrome_sdk.SDKFetcher(options.cache_dir, options.board)
Ryan Cui686ec052013-02-12 16:39:41 -0800346 components = (sdk.TARGET_TOOLCHAIN_KEY, constants.CHROME_ENV_TAR)
347 with sdk.Prepare(components=components) as ctx:
348 env_path = os.path.join(ctx.key_map[constants.CHROME_ENV_TAR].path,
349 constants.CHROME_ENV_FILE)
350 strip_bin = osutils.SourceEnvironment(env_path, ['STRIP'])['STRIP']
351 strip_bin = os.path.join(ctx.key_map[sdk.TARGET_TOOLCHAIN_KEY].path,
352 'bin', os.path.basename(strip_bin))
353 chrome_util.StageChromeFromBuildDir(
354 staging_dir, options.build_dir, strip_bin, strict=options.strict,
David Jamesa6e08892013-03-01 13:34:11 -0800355 sloppy=options.sloppy, gyp_defines=options.gyp_defines,
356 staging_flags=options.staging_flags)
Ryan Cuia56a71e2012-10-18 18:40:35 -0700357 else:
358 pkg_path = options.local_pkg_path
359 if options.gs_path:
Ryan Cui504db722013-01-22 11:48:01 -0800360 pkg_path = _FetchChromePackage(options.cache_dir, tempdir,
361 options.gs_path)
Ryan Cuia56a71e2012-10-18 18:40:35 -0700362
363 assert pkg_path
Ryan Cui4d6b0db2013-02-28 15:13:24 -0800364 logging.info('Extracting %s...', pkg_path)
Ryan Cuif2d1a582013-02-19 14:08:13 -0800365 osutils.SafeMakedirs(staging_dir)
Ryan Cui4d6b0db2013-02-28 15:13:24 -0800366 cros_build_lib.DebugRunCommand(['tar', '-xpf', pkg_path], cwd=staging_dir)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700367
368
369def main(argv):
370 options, args = _ParseCommandLine(argv)
371 _PostParseCheck(options, args)
372
373 # Set cros_build_lib debug level to hide RunCommand spew.
374 if options.verbose:
Ryan Cuia56a71e2012-10-18 18:40:35 -0700375 logging.getLogger().setLevel(logging.DEBUG)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700376 else:
Ryan Cui4d6b0db2013-02-28 15:13:24 -0800377 logging.getLogger().setLevel(logging.INFO)
Ryan Cui3045c5d2012-07-13 18:00:33 -0700378
Ryan Cuif2d1a582013-02-19 14:08:13 -0800379 with osutils.TempDirContextManager() as tempdir:
380 staging_dir = options.staging_dir
381 if not staging_dir:
382 staging_dir = os.path.join(tempdir, 'chrome')
383 _PrepareStagingDir(options, tempdir, staging_dir)
Ryan Cuia56a71e2012-10-18 18:40:35 -0700384
Ryan Cuif2d1a582013-02-19 14:08:13 -0800385 if options.staging_only:
386 return 0
Ryan Cuia56a71e2012-10-18 18:40:35 -0700387
Ryan Cuif2d1a582013-02-19 14:08:13 -0800388 deploy = DeployChrome(options, tempdir, staging_dir)
389 deploy.Perform()