blob: b1d01b6525dd8f2790ee8a19184cb10ffc06018c [file] [log] [blame]
Aviv Keshetb1238c32013-04-01 11:42:13 -07001#!/usr/bin/python
2
3# Copyright (c) 2013 The Chromium OS Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6
7
8"""
9Simple script to be run inside the chroot. Used as a fast approximation of
10emerge-$board autotest-all, by simply rsync'ing changes from trunk to sysroot.
11"""
12
Aviv Keshete7b20192013-04-24 14:05:53 -070013import argparse
Aviv Keshete00caeb2013-04-17 14:03:25 -070014import logging
Aviv Keshetb1238c32013-04-01 11:42:13 -070015import os
Aviv Keshet787ffcd2013-04-08 15:14:56 -070016import re
Aviv Keshetb1238c32013-04-01 11:42:13 -070017import sys
Aviv Keshet787ffcd2013-04-08 15:14:56 -070018from collections import namedtuple
19
Aviv Keshetb1238c32013-04-01 11:42:13 -070020from chromite.buildbot import constants
Aviv Keshet940c17f2013-04-11 18:41:42 -070021from chromite.buildbot import portage_utilities
Aviv Keshetb1238c32013-04-01 11:42:13 -070022from chromite.lib import cros_build_lib
23from chromite.lib import git
Aviv Keshet557e6882013-04-25 13:26:09 -070024from chromite.lib import osutils
Aviv Keshetb1238c32013-04-01 11:42:13 -070025
Aviv Keshetb1238c32013-04-01 11:42:13 -070026
Aviv Keshet940c17f2013-04-11 18:41:42 -070027if cros_build_lib.IsInsideChroot():
28 # Only import portage after we've checked that we're inside the chroot.
29 import portage
30
Aviv Keshetb1238c32013-04-01 11:42:13 -070031INCLUDE_PATTERNS_FILENAME = 'autotest-quickmerge-includepatterns'
32AUTOTEST_PROJECT_NAME = 'chromiumos/third_party/autotest'
Aviv Keshet5f3cf722013-05-09 17:35:25 -070033AUTOTEST_EBUILD = 'chromeos-base/autotest'
Aviv Keshet3cc4e9e2013-04-24 10:47:23 -070034DOWNGRADE_EBUILDS = ['chromeos-base/autotest',
35 'chromeos-base/autotest-tests',
36 'chromeos-base/autotest-chrome',
37 'chromeos-base/autotest-factory',
38 'chromeos-base/autotest-telemetry',
39 'chromeos-base/autotest-tests-ltp',
40 'chromeos-base/autotest-tests-ownershipapi']
Aviv Keshet787ffcd2013-04-08 15:14:56 -070041
42# Data structure describing a single rsync filesystem change.
43#
44# change_description: An 11 character string, the rsync change description
45# for the particular file.
46# absolute_path: The absolute path of the created or modified file.
47ItemizedChange = namedtuple('ItemizedChange', ['change_description',
48 'absolute_path'])
49
50
51# Data structure describing the rsync new/modified files or directories.
52#
53# new_files: A list of ItemizedChange objects for new files.
54# modified_files: A list of ItemizedChange objects for modified files.
55# new_directories: A list of ItemizedChange objects for new directories.
56ItemizedChangeReport = namedtuple('ItemizedChangeReport',
57 ['new_files', 'modified_files',
58 'new_directories'])
59
60
Aviv Keshet75d65962013-04-17 16:15:23 -070061def GetStalePackageNames(change_list, autotest_sysroot):
Aviv Keshete7b20192013-04-24 14:05:53 -070062 """Given a rsync change report, returns the names of stale test packages.
Aviv Keshet75d65962013-04-17 16:15:23 -070063
64 This function pulls out test package names for client-side tests, stored
65 within the client/site_tests directory tree, that had any files added or
66 modified and for whom any existing bzipped test packages may now be stale.
67
68 Arguments:
69 change_list: A list of ItemizedChange objects corresponding to changed
70 or modified files.
71 autotest_sysroot: Absolute path of autotest in the sysroot,
72 e.g. '/build/lumpy/usr/local/autotest'
73
74 Returns:
75 A list of test package names, eg ['factory_Leds', 'login_UserPolicyKeys'].
76 May contain duplicate entries if multiple files within a test directory
77 were modified.
78 """
79 exp = os.path.abspath(autotest_sysroot) + r'/client/site_tests/(.*?)/.*'
80 matches = [re.match(exp, change.absolute_path) for change in change_list]
81 return [match.group(1) for match in matches if match]
82
83
Aviv Keshet787ffcd2013-04-08 15:14:56 -070084def ItemizeChangesFromRsyncOutput(rsync_output, destination_path):
85 """Convert the output of an rsync with `-i` to a ItemizedChangeReport object.
86
87 Arguments:
88 rsync_output: String stdout of rsync command that was run with `-i` option.
89 destination_path: String absolute path of the destination directory for the
90 rsync operations. This argument is necessary because
91 rsync's output only gives the relative path of
92 touched/added files.
93
94 Returns:
95 ItemizedChangeReport object giving the absolute paths of files that were
96 created or modified by rsync.
97 """
98 modified_matches = re.findall(r'([.>]f[^+]{9}) (.*)', rsync_output)
99 new_matches = re.findall(r'(>f\+{9}) (.*)', rsync_output)
100 new_symlink_matches = re.findall(r'(cL\+{9}) (.*) -> .*', rsync_output)
101 new_dir_matches = re.findall(r'(cd\+{9}) (.*)', rsync_output)
102
103 absolute_modified = [ItemizedChange(c, os.path.join(destination_path, f))
104 for (c, f) in modified_matches]
105
106 # Note: new symlinks are treated as new files.
107 absolute_new = [ItemizedChange(c, os.path.join(destination_path, f))
108 for (c, f) in new_matches + new_symlink_matches]
109
110 absolute_new_dir = [ItemizedChange(c, os.path.join(destination_path, f))
111 for (c, f) in new_dir_matches]
112
113 return ItemizedChangeReport(new_files=absolute_new,
114 modified_files=absolute_modified,
115 new_directories=absolute_new_dir)
116
117
Aviv Keshete00caeb2013-04-17 14:03:25 -0700118def GetPackageAPI(portage_root, package_cp):
Aviv Keshete7b20192013-04-24 14:05:53 -0700119 """Gets portage API handles for the given package.
Aviv Keshet940c17f2013-04-11 18:41:42 -0700120
121 Arguments:
Aviv Keshete00caeb2013-04-17 14:03:25 -0700122 portage_root: Root directory of portage tree. Eg '/' or '/build/lumpy'
123 package_cp: A string similar to 'chromeos-base/autotest-tests'.
124
125 Returns:
126 Returns (package, vartree) tuple, where
127 package is of type portage.dbapi.vartree.dblink
128 vartree is of type portage.dbapi.vartree.vartree
Aviv Keshet940c17f2013-04-11 18:41:42 -0700129 """
130 if portage_root is None:
Aviv Keshete7b20192013-04-24 14:05:53 -0700131 # pylint: disable-msg=E1101
132 portage_root = portage.root
Aviv Keshet940c17f2013-04-11 18:41:42 -0700133 # Ensure that portage_root ends with trailing slash.
134 portage_root = os.path.join(portage_root, '')
135
Aviv Keshete7b20192013-04-24 14:05:53 -0700136 # Create a vartree object corresponding to portage_root.
Aviv Keshet940c17f2013-04-11 18:41:42 -0700137 trees = portage.create_trees(portage_root, portage_root)
138 vartree = trees[portage_root]['vartree']
139
Aviv Keshete7b20192013-04-24 14:05:53 -0700140 # List the matching installed packages in cpv format.
Aviv Keshet940c17f2013-04-11 18:41:42 -0700141 matching_packages = vartree.dbapi.cp_list(package_cp)
142
143 if not matching_packages:
144 raise ValueError('No matching package for %s in portage_root %s' % (
Aviv Keshete7b20192013-04-24 14:05:53 -0700145 package_cp, portage_root))
Aviv Keshet940c17f2013-04-11 18:41:42 -0700146
147 if len(matching_packages) > 1:
148 raise ValueError('Too many matching packages for %s in portage_root '
Aviv Keshete7b20192013-04-24 14:05:53 -0700149 '%s' % (package_cp, portage_root))
Aviv Keshet940c17f2013-04-11 18:41:42 -0700150
Aviv Keshete7b20192013-04-24 14:05:53 -0700151 # Convert string match to package dblink.
Aviv Keshet940c17f2013-04-11 18:41:42 -0700152 package_cpv = matching_packages[0]
153 package_split = portage_utilities.SplitCPV(package_cpv)
Aviv Keshete7b20192013-04-24 14:05:53 -0700154 # pylint: disable-msg=E1101
155 package = portage.dblink(package_split.category,
Aviv Keshet940c17f2013-04-11 18:41:42 -0700156 package_split.pv, settings=vartree.settings,
157 vartree=vartree)
158
Aviv Keshete00caeb2013-04-17 14:03:25 -0700159 return package, vartree
160
161
162def DowngradePackageVersion(portage_root, package_cp,
163 downgrade_to_version='0'):
Aviv Keshete7b20192013-04-24 14:05:53 -0700164 """Downgrade the specified portage package version.
Aviv Keshete00caeb2013-04-17 14:03:25 -0700165
166 Arguments:
167 portage_root: Root directory of portage tree. Eg '/' or '/build/lumpy'
168 package_cp: A string similar to 'chromeos-base/autotest-tests'.
169 downgrade_to_version: String version to downgrade to. Default: '0'
170
171 Returns:
Aviv Keshet557e6882013-04-25 13:26:09 -0700172 True on success. False on failure (nonzero return code from `mv` command).
Aviv Keshete00caeb2013-04-17 14:03:25 -0700173 """
174 package, _ = GetPackageAPI(portage_root, package_cp)
175
176 source_directory = package.dbdir
177 destination_path = os.path.join(
178 package.dbroot, package_cp + '-' + downgrade_to_version)
179 if os.path.abspath(source_directory) == os.path.abspath(destination_path):
Aviv Keshet557e6882013-04-25 13:26:09 -0700180 return True
Aviv Keshete00caeb2013-04-17 14:03:25 -0700181 command = ['mv', source_directory, destination_path]
Aviv Keshet557e6882013-04-25 13:26:09 -0700182 code = cros_build_lib.SudoRunCommand(command, error_code_ok=True).returncode
183 return code == 0
Aviv Keshete00caeb2013-04-17 14:03:25 -0700184
185
Aviv Keshete7b20192013-04-24 14:05:53 -0700186def UpdatePackageContents(change_report, package_cp, portage_root=None):
187 """Add newly created files/directors to package contents.
Aviv Keshete00caeb2013-04-17 14:03:25 -0700188
189 Given an ItemizedChangeReport, add the newly created files and directories
190 to the CONTENTS of an installed portage package, such that these files are
191 considered owned by that package.
192
193 Arguments:
194 changereport: ItemizedChangeReport object for the changes to be
195 made to the package.
196 package_cp: A string similar to 'chromeos-base/autotest-tests' giving
197 the package category and name of the package to be altered.
198 portage_root: Portage root path, corresponding to the board that
199 we are working on. Defaults to '/'
200 """
201 package, vartree = GetPackageAPI(portage_root, package_cp)
202
Aviv Keshete7b20192013-04-24 14:05:53 -0700203 # Append new contents to package contents dictionary.
Aviv Keshet940c17f2013-04-11 18:41:42 -0700204 contents = package.getcontents().copy()
205 for _, filename in change_report.new_files:
206 contents.setdefault(filename, (u'obj', '0', '0'))
207 for _, dirname in change_report.new_directories:
Aviv Keshete7b20192013-04-24 14:05:53 -0700208 # Strip trailing slashes if present.
209 contents.setdefault(dirname.rstrip('/'), (u'dir',))
Aviv Keshet940c17f2013-04-11 18:41:42 -0700210
Aviv Keshete7b20192013-04-24 14:05:53 -0700211 # Write new contents dictionary to file.
Aviv Keshet940c17f2013-04-11 18:41:42 -0700212 vartree.dbapi.writeContentsToContentsFile(package, contents)
213
214
Aviv Keshet75d65962013-04-17 16:15:23 -0700215def RemoveTestPackages(stale_packages, autotest_sysroot):
Aviv Keshete7b20192013-04-24 14:05:53 -0700216 """Remove bzipped test packages from sysroot.
Aviv Keshet75d65962013-04-17 16:15:23 -0700217
218 Arguments:
219 stale_packages: List of test packages names to be removed.
220 e.g. ['factory_Leds', 'login_UserPolicyKeys']
221 autotest_sysroot: Absolute path of autotest in the sysroot,
222 e.g. '/build/lumpy/usr/local/autotest'
223 """
224 for package in set(stale_packages):
225 package_filename = 'test-' + package + '.tar.bz2'
226 package_file_fullpath = os.path.join(autotest_sysroot, 'packages',
Aviv Keshete7b20192013-04-24 14:05:53 -0700227 package_filename)
Aviv Keshet557e6882013-04-25 13:26:09 -0700228 if osutils.SafeUnlink(package_file_fullpath):
Aviv Keshet75d65962013-04-17 16:15:23 -0700229 logging.info('Removed stale %s', package_file_fullpath)
Aviv Keshet75d65962013-04-17 16:15:23 -0700230
231
Aviv Keshetb1238c32013-04-01 11:42:13 -0700232def RsyncQuickmerge(source_path, sysroot_autotest_path,
233 include_pattern_file=None, pretend=False,
Aviv Keshet60968ec2013-04-11 18:44:14 -0700234 overwrite=False):
Aviv Keshetb1238c32013-04-01 11:42:13 -0700235 """Run rsync quickmerge command, with specified arguments.
Aviv Keshete7b20192013-04-24 14:05:53 -0700236
Aviv Keshetb1238c32013-04-01 11:42:13 -0700237 Command will take form `rsync -a [options] --exclude=**.pyc
238 --exclude=**.pyo
239 [optional --include-from argument]
240 --exclude=* [source_path] [sysroot_autotest_path]`
241
242 Arguments:
243 pretend: True to use the '-n' option to rsync, to perform dry run.
244 overwrite: True to omit '-u' option, overwrite all files in sysroot,
245 not just older files.
Aviv Keshet557e6882013-04-25 13:26:09 -0700246
247 Returns:
248 The cros_build_lib.CommandResult object resulting from the rsync command.
Aviv Keshetb1238c32013-04-01 11:42:13 -0700249 """
250 command = ['rsync', '-a']
251
252 if pretend:
253 command += ['-n']
254
255 if not overwrite:
256 command += ['-u']
257
Aviv Keshet60968ec2013-04-11 18:44:14 -0700258 command += ['-i']
Aviv Keshetb1238c32013-04-01 11:42:13 -0700259
260 command += ['--exclude=**.pyc']
261 command += ['--exclude=**.pyo']
262
Aviv Keshet787ffcd2013-04-08 15:14:56 -0700263 # Exclude files with a specific substring in their name, because
264 # they create an ambiguous itemized report. (see unit test file for details)
265 command += ['--exclude=** -> *']
266
Aviv Keshetb1238c32013-04-01 11:42:13 -0700267 if include_pattern_file:
268 command += ['--include-from=%s' % include_pattern_file]
269
270 command += ['--exclude=*']
271
272 command += [source_path, sysroot_autotest_path]
273
Aviv Keshet60968ec2013-04-11 18:44:14 -0700274 return cros_build_lib.SudoRunCommand(command, redirect_stdout=True)
Aviv Keshetb1238c32013-04-01 11:42:13 -0700275
276
277def ParseArguments(argv):
278 """Parse command line arguments
279
280 Returns: parsed arguments.
281 """
282 parser = argparse.ArgumentParser(description='Perform a fast approximation '
283 'to emerge-$board autotest-all, by '
284 'rsyncing source tree to sysroot.')
285
286 parser.add_argument('--board', metavar='BOARD', default=None, required=True)
287 parser.add_argument('--pretend', action='store_true',
288 help='Dry run only, do not modify sysroot autotest.')
289 parser.add_argument('--overwrite', action='store_true',
290 help='Overwrite existing files even if newer.')
Aviv Keshete00caeb2013-04-17 14:03:25 -0700291 parser.add_argument('--verbose', action='store_true',
292 help='Print detailed change report.')
Aviv Keshetb1238c32013-04-01 11:42:13 -0700293
294 return parser.parse_args(argv)
295
296
297def main(argv):
298 cros_build_lib.AssertInsideChroot()
299
300 args = ParseArguments(argv)
301
Aviv Keshete7b20192013-04-24 14:05:53 -0700302 if os.geteuid() != 0:
Aviv Keshet940c17f2013-04-11 18:41:42 -0700303 try:
304 cros_build_lib.SudoRunCommand([sys.executable] + sys.argv)
305 except cros_build_lib.RunCommandError:
306 return 1
307 return 0
308
Aviv Keshetb1238c32013-04-01 11:42:13 -0700309 if not args.board:
Aviv Keshete00caeb2013-04-17 14:03:25 -0700310 print 'No board specified. Aborting.'
Aviv Keshetb1238c32013-04-01 11:42:13 -0700311 return 1
312
313 manifest = git.ManifestCheckout.Cached(constants.SOURCE_ROOT)
314 source_path = manifest.GetProjectPath(AUTOTEST_PROJECT_NAME, absolute=True)
315 source_path = os.path.join(source_path, '')
316
317 script_path = os.path.dirname(__file__)
318 include_pattern_file = os.path.join(script_path, INCLUDE_PATTERNS_FILENAME)
319
320 # TODO: Determine the following string programatically.
Aviv Keshet940c17f2013-04-11 18:41:42 -0700321 sysroot_path = os.path.join('/build', args.board, '')
322 sysroot_autotest_path = os.path.join(sysroot_path, 'usr', 'local',
Aviv Keshetb1238c32013-04-01 11:42:13 -0700323 'autotest', '')
324
Aviv Keshet60968ec2013-04-11 18:44:14 -0700325 rsync_output = RsyncQuickmerge(source_path, sysroot_autotest_path,
Aviv Keshete7b20192013-04-24 14:05:53 -0700326 include_pattern_file, args.pretend,
327 args.overwrite)
Aviv Keshetb1238c32013-04-01 11:42:13 -0700328
Aviv Keshete00caeb2013-04-17 14:03:25 -0700329 if args.verbose:
330 logging.info(rsync_output.output)
331
Aviv Keshet60968ec2013-04-11 18:44:14 -0700332 change_report = ItemizeChangesFromRsyncOutput(rsync_output.output,
333 sysroot_autotest_path)
334
Aviv Keshet940c17f2013-04-11 18:41:42 -0700335 if not args.pretend:
Aviv Keshet5f3cf722013-05-09 17:35:25 -0700336 UpdatePackageContents(change_report, AUTOTEST_EBUILD,
Aviv Keshet940c17f2013-04-11 18:41:42 -0700337 sysroot_path)
Aviv Keshet3cc4e9e2013-04-24 10:47:23 -0700338 for ebuild in DOWNGRADE_EBUILDS:
Aviv Keshet557e6882013-04-25 13:26:09 -0700339 if not DowngradePackageVersion(sysroot_path, ebuild):
Aviv Keshet3cc4e9e2013-04-24 10:47:23 -0700340 logging.warning('Unable to downgrade package %s version number.',
Aviv Keshete7b20192013-04-24 14:05:53 -0700341 ebuild)
Aviv Keshet75d65962013-04-17 16:15:23 -0700342 stale_packages = GetStalePackageNames(
343 change_report.new_files + change_report.modified_files,
344 sysroot_autotest_path)
345 RemoveTestPackages(stale_packages, sysroot_autotest_path)
Aviv Keshetc87f4122013-05-01 13:40:00 -0700346 osutils.SafeUnlink(os.path.join(sysroot_autotest_path, 'packages.checksum'))
347 osutils.SafeUnlink(os.path.join(sysroot_autotest_path, 'packages',
348 'packages.checksum'))
Aviv Keshetb1238c32013-04-01 11:42:13 -0700349
Aviv Keshet940c17f2013-04-11 18:41:42 -0700350 if args.pretend:
Aviv Keshete00caeb2013-04-17 14:03:25 -0700351 logging.info('The following message is pretend only. No filesystem '
Aviv Keshete7b20192013-04-24 14:05:53 -0700352 'changes made.')
Aviv Keshete00caeb2013-04-17 14:03:25 -0700353 logging.info('Quickmerge complete. Created or modified %s files.',
Aviv Keshete7b20192013-04-24 14:05:53 -0700354 len(change_report.new_files) +
355 len(change_report.modified_files))
Aviv Keshete00caeb2013-04-17 14:03:25 -0700356
357 return 0