1 # Copyright (c) 2014 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.
5 """cros deploy: Deploy the packages onto the target device."""
11 from chromite import cros
12 from chromite.buildbot import portage_utilities
13 from chromite.lib import cros_build_lib
14 from chromite.lib import remote_access
17 @cros.CommandDecorator('deploy')
18 class DeployCommand(cros.CrosCommand):
19 """Deploy the requested packages to the target device.
21 This command assumes the requested packages are already built in the
22 chroot. This command needs to run inside the chroot for inspecting
23 the installed packages.
25 Note: If the rootfs on your device is read-only, this command
26 remounts it as read-write. If the rootfs verification is enabled on
27 your device, this command disables it.
32 cros deploy device power_manager cherrypy
33 cros deploy device /path/to/package
35 To uninstall packages:
36 cros deploy --unmerge cherrypy
38 For more information of cros build usage:
42 DEVICE_BASE_DIR = '/usr/local/tmp/cros-deploy'
43 # This is defined in src/platform/dev/builder.py
44 STRIPPED_PACKAGES_DIR = 'stripped-packages'
46 # Override base class property to enable stats upload.
49 def __init__(self, options):
50 """Initializes DeployCommand."""
51 cros.CrosCommand.__init__(self, options)
54 self.clean_binpkg = True
55 self.ssh_hostname = None
57 self.ssh_username = None
58 self.ssh_private_key = None
59 # The installation root of packages.
63 def AddParser(cls, parser):
65 super(cls, DeployCommand).AddParser(parser)
67 'device', help='IP[:port] address of the target device.')
69 'packages', help='Packages to install. You can specify '
70 '[category/]package[:slot] or the path to the binary package.',
73 '--board', default=None, help='The board to use. By default it is '
74 'automatically detected. You can override the detected board with '
77 '--no-strip', dest='strip', action='store_false', default=True,
78 help='Do not run strip_package to filter out preset paths in the '
79 'package. Stripping removes debug symbol files and reduces the size '
80 'of the package significantly. Defaults to always strip.')
82 '--unmerge', dest='emerge', action='store_false', default=True,
83 help='Unmerge requested packages.')
85 '--root', default='/',
86 help="Package installation root, e.g. '/' or '/usr/local'"
89 '--no-clean-binpkg', dest='clean_binpkg', action='store_false',
90 default=True, help='Do not clean outdated binary packages. '
91 ' Defaults to always clean.')
93 '--emerge-args', default=None,
94 help='Extra arguments to pass to emerge.')
96 '--private-key', type='path', default=None,
97 help='SSH identify file (private key).')
99 def GetLatestPackage(self, board, pkg):
100 """Returns the path to the latest |pkg| for |board|."""
101 sysroot = cros_build_lib.GetSysroot(board=board)
102 matches = portage_utilities.FindPackageNameMatches(
105 raise ValueError('Package %s is not installed!' % pkg)
109 # Ask user to pick among multiple matches.
110 idx = cros_build_lib.GetChoice(
111 'Multiple matches found for %s: ' % pkg,
112 [os.path.join(x.category, x.pv) for x in matches])
118 cros_build_lib.RunCommand(
119 ['strip_package', '--board', board,
120 os.path.join(cpv.category, '%s' % (cpv.pv))])
121 packages_dir = self.STRIPPED_PACKAGES_DIR
122 except cros_build_lib.RunCommandError:
123 logging.error('Cannot strip package %s', pkg)
126 return portage_utilities.GetBinaryPackagePath(
127 cpv.category, cpv.package, cpv.version, sysroot=sysroot,
128 packages_dir=packages_dir)
130 def _Emerge(self, device, board, pkg, root, extra_args=None):
131 """Copies |pkg| to |device| and emerges it.
134 device: A ChromiumOSDevice object.
135 board: The board to use for retrieving |pkg|.
137 root: The installation root of |pkg|.
138 extra_args: Extra arguments to pass to emerge.
140 if os.path.isfile(pkg):
143 latest_pkg = self.GetLatestPackage(board, pkg)
146 cros_build_lib.Die('Missing package %s.' % pkg)
148 pkgroot = os.path.join(device.work_dir, 'packages')
149 pkg_name = os.path.basename(latest_pkg)
150 pkg_dirname = os.path.basename(os.path.dirname(latest_pkg))
151 pkg_dir = os.path.join(pkgroot, pkg_dirname)
152 device.RunCommand(['mkdir', '-p', pkg_dir], remote_sudo=True)
154 logging.info('Copying %s to device...', latest_pkg)
155 device.CopyToDevice(latest_pkg, pkg_dir, remote_sudo=True)
157 portage_tmpdir = os.path.join(device.work_dir, 'portage-tmp')
158 device.RunCommand(['mkdir', '-p', portage_tmpdir], remote_sudo=True)
159 logging.info('Use portage temp dir %s', portage_tmpdir)
161 logging.info('Installing %s...', latest_pkg)
162 pkg_path = os.path.join(pkg_dir, pkg_name)
164 # We set PORTAGE_CONFIGROOT to '/usr/local' because by default all
165 # chromeos-base packages will be skipped due to the configuration
166 # in /etc/protage/make.profile/package.provided. However, there is
167 # a known bug that /usr/local/etc/portage is not setup properly
168 # (crbug.com/312041). This does not affect `cros deploy` because
169 # we do not use the preset PKGDIR.
171 'FEATURES': '-sandbox',
173 'PORTAGE_CONFIGROOT': '/usr/local',
174 'PORTAGE_TMPDIR': portage_tmpdir,
175 'PORTDIR': device.work_dir,
176 'CONFIG_PROTECT': '-*',
178 cmd = ['emerge', '--usepkg', pkg_path]
179 cmd.append('--root=%s' % root)
181 cmd.append(extra_args)
184 # Always showing the emerge output for clarity.
185 device.RunCommand(cmd, extra_env=extra_env, remote_sudo=True,
186 capture_output=False, debug_level=logging.INFO)
188 logging.error('Failed to emerge package %s', pkg)
191 logging.info('%s has been installed.', pkg)
193 # Free up the space for other packages.
194 device.RunCommand(['rm', '-rf', portage_tmpdir, pkg_dir],
195 error_code_ok=True, remote_sudo=True)
197 def _Unmerge(self, device, pkg, root):
198 """Unmerges |pkg| on |device|.
201 device: A RemoteDevice object.
203 root: The installation root of |pkg|.
205 logging.info('Unmerging %s...', pkg)
206 cmd = ['qmerge', '--yes']
207 # Check if qmerge is available on the device. If not, use emerge.
208 if device.RunCommand(
209 ['qmerge', '--version'], error_code_ok=True).returncode != 0:
212 cmd.extend(['--unmerge', pkg, '--root=%s' % root])
214 # Always showing the qmerge/emerge output for clarity.
215 device.RunCommand(cmd, capture_output=False, remote_sudo=True,
216 debug_level=logging.INFO)
218 logging.error('Failed to unmerge package %s', pkg)
221 logging.info('%s has been uninstalled.', pkg)
223 def _IsPathWritable(self, device, path):
224 """Returns True if |path| on |device| is writable."""
225 tmp_file = os.path.join(path, 'tmp.cros_flash')
226 result = device.RunCommand(['touch', tmp_file], remote_sudo=True,
227 error_code_ok=True, capture_output=True)
229 if result.returncode != 0:
232 device.RunCommand(['rm', tmp_file], error_code_ok=True, remote_sudo=True)
236 def _ReadOptions(self):
237 """Processes options and set variables."""
238 self.emerge = self.options.emerge
239 self.strip = self.options.strip
240 self.clean_binpkg = self.options.clean_binpkg
241 self.root = self.options.root
242 device = self.options.device
243 # pylint: disable=E1101
244 if urlparse.urlparse(device).scheme == '':
245 # For backward compatibility, prepend ssh:// ourselves.
246 device = 'ssh://%s' % device
248 parsed = urlparse.urlparse(device)
250 if parsed.scheme == 'ssh':
251 self.ssh_hostname = parsed.hostname
252 self.ssh_username = parsed.username
253 self.ssh_port = parsed.port
254 self.ssh_private_key = self.options.private_key
256 cros_build_lib.Die('Does not support device %s' % self.options.device)
259 """Run cros deploy."""
260 cros_build_lib.AssertInsideChroot()
263 with remote_access.ChromiumOSDeviceHandler(
264 self.ssh_hostname, port=self.ssh_port, username=self.ssh_username,
265 private_key=self.ssh_private_key,
266 base_dir=self.DEVICE_BASE_DIR) as device:
267 board = cros_build_lib.GetBoard(device_board=device.board,
268 override_board=self.options.board)
269 logging.info('Board is %s', board)
271 if self.clean_binpkg:
272 logging.info('Cleaning outdated binary packages for %s', board)
273 portage_utilities.CleanOutdatedBinaryPackages(board)
275 if not self._IsPathWritable(device, self.root):
276 # Only remounts rootfs if the given root is not writable.
277 if not device.MountRootfsReadWrite():
278 cros_build_lib.Die('Cannot remount rootfs as read-write. Exiting.')
280 for pkg in self.options.packages:
282 self._Emerge(device, board, pkg, self.root,
283 extra_args=self.options.emerge_args)
285 self._Unmerge(device, pkg, self.root)
287 except (Exception, KeyboardInterrupt) as e:
289 logging.error('Cros Deploy terminated before completing!')
290 if self.options.debug:
293 logging.info('Cros Deploy completed successfully.')