blob: a9fc19ad8eae1d22d4fcd759ff8a8231fba19bce [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 Keshete00caeb2013-04-17 14:03:25 -070013import logging
Aviv Keshetb1238c32013-04-01 11:42:13 -070014import os
Aviv Keshet787ffcd2013-04-08 15:14:56 -070015import re
Aviv Keshetb1238c32013-04-01 11:42:13 -070016import sys
Aviv Keshet787ffcd2013-04-08 15:14:56 -070017from collections import namedtuple
18
Aviv Keshetb1238c32013-04-01 11:42:13 -070019from chromite.buildbot import constants
Aviv Keshet940c17f2013-04-11 18:41:42 -070020from chromite.buildbot import portage_utilities
Aviv Keshetb1238c32013-04-01 11:42:13 -070021from chromite.lib import cros_build_lib
22from chromite.lib import git
23
24import argparse
25
Aviv Keshet940c17f2013-04-11 18:41:42 -070026if cros_build_lib.IsInsideChroot():
27 # Only import portage after we've checked that we're inside the chroot.
28 import portage
29
Aviv Keshetb1238c32013-04-01 11:42:13 -070030INCLUDE_PATTERNS_FILENAME = 'autotest-quickmerge-includepatterns'
31AUTOTEST_PROJECT_NAME = 'chromiumos/third_party/autotest'
Aviv Keshet940c17f2013-04-11 18:41:42 -070032AUTOTEST_TESTS_EBUILD = 'chromeos-base/autotest-tests'
Aviv Keshetb1238c32013-04-01 11:42:13 -070033
Aviv Keshet787ffcd2013-04-08 15:14:56 -070034
35# Data structure describing a single rsync filesystem change.
36#
37# change_description: An 11 character string, the rsync change description
38# for the particular file.
39# absolute_path: The absolute path of the created or modified file.
40ItemizedChange = namedtuple('ItemizedChange', ['change_description',
41 'absolute_path'])
42
43
44# Data structure describing the rsync new/modified files or directories.
45#
46# new_files: A list of ItemizedChange objects for new files.
47# modified_files: A list of ItemizedChange objects for modified files.
48# new_directories: A list of ItemizedChange objects for new directories.
49ItemizedChangeReport = namedtuple('ItemizedChangeReport',
50 ['new_files', 'modified_files',
51 'new_directories'])
52
53
Aviv Keshet75d65962013-04-17 16:15:23 -070054def GetStalePackageNames(change_list, autotest_sysroot):
55 """
56 Given a rsync change report, returns the names of stale test packages.
57
58 This function pulls out test package names for client-side tests, stored
59 within the client/site_tests directory tree, that had any files added or
60 modified and for whom any existing bzipped test packages may now be stale.
61
62 Arguments:
63 change_list: A list of ItemizedChange objects corresponding to changed
64 or modified files.
65 autotest_sysroot: Absolute path of autotest in the sysroot,
66 e.g. '/build/lumpy/usr/local/autotest'
67
68 Returns:
69 A list of test package names, eg ['factory_Leds', 'login_UserPolicyKeys'].
70 May contain duplicate entries if multiple files within a test directory
71 were modified.
72 """
73 exp = os.path.abspath(autotest_sysroot) + r'/client/site_tests/(.*?)/.*'
74 matches = [re.match(exp, change.absolute_path) for change in change_list]
75 return [match.group(1) for match in matches if match]
76
77
Aviv Keshet787ffcd2013-04-08 15:14:56 -070078def ItemizeChangesFromRsyncOutput(rsync_output, destination_path):
79 """Convert the output of an rsync with `-i` to a ItemizedChangeReport object.
80
81 Arguments:
82 rsync_output: String stdout of rsync command that was run with `-i` option.
83 destination_path: String absolute path of the destination directory for the
84 rsync operations. This argument is necessary because
85 rsync's output only gives the relative path of
86 touched/added files.
87
88 Returns:
89 ItemizedChangeReport object giving the absolute paths of files that were
90 created or modified by rsync.
91 """
92 modified_matches = re.findall(r'([.>]f[^+]{9}) (.*)', rsync_output)
93 new_matches = re.findall(r'(>f\+{9}) (.*)', rsync_output)
94 new_symlink_matches = re.findall(r'(cL\+{9}) (.*) -> .*', rsync_output)
95 new_dir_matches = re.findall(r'(cd\+{9}) (.*)', rsync_output)
96
97 absolute_modified = [ItemizedChange(c, os.path.join(destination_path, f))
98 for (c, f) in modified_matches]
99
100 # Note: new symlinks are treated as new files.
101 absolute_new = [ItemizedChange(c, os.path.join(destination_path, f))
102 for (c, f) in new_matches + new_symlink_matches]
103
104 absolute_new_dir = [ItemizedChange(c, os.path.join(destination_path, f))
105 for (c, f) in new_dir_matches]
106
107 return ItemizedChangeReport(new_files=absolute_new,
108 modified_files=absolute_modified,
109 new_directories=absolute_new_dir)
110
111
Aviv Keshete00caeb2013-04-17 14:03:25 -0700112def GetPackageAPI(portage_root, package_cp):
Aviv Keshet940c17f2013-04-11 18:41:42 -0700113 """
Aviv Keshete00caeb2013-04-17 14:03:25 -0700114 Gets portage API handles for the given package.
Aviv Keshet940c17f2013-04-11 18:41:42 -0700115
116 Arguments:
Aviv Keshete00caeb2013-04-17 14:03:25 -0700117 portage_root: Root directory of portage tree. Eg '/' or '/build/lumpy'
118 package_cp: A string similar to 'chromeos-base/autotest-tests'.
119
120 Returns:
121 Returns (package, vartree) tuple, where
122 package is of type portage.dbapi.vartree.dblink
123 vartree is of type portage.dbapi.vartree.vartree
Aviv Keshet940c17f2013-04-11 18:41:42 -0700124 """
125 if portage_root is None:
126 portage_root = portage.root # pylint: disable-msg=E1101
127 # Ensure that portage_root ends with trailing slash.
128 portage_root = os.path.join(portage_root, '')
129
130 # Create vartree object corresponding to portage_root
131 trees = portage.create_trees(portage_root, portage_root)
132 vartree = trees[portage_root]['vartree']
133
134 # List matching installed packages in cpv format
135 matching_packages = vartree.dbapi.cp_list(package_cp)
136
137 if not matching_packages:
138 raise ValueError('No matching package for %s in portage_root %s' % (
139 package_cp, portage_root))
140
141 if len(matching_packages) > 1:
142 raise ValueError('Too many matching packages for %s in portage_root '
143 '%s' % (package_cp, portage_root))
144
145 # Convert string match to package dblink
146 package_cpv = matching_packages[0]
147 package_split = portage_utilities.SplitCPV(package_cpv)
148 package = portage.dblink(package_split.category, # pylint: disable-msg=E1101
149 package_split.pv, settings=vartree.settings,
150 vartree=vartree)
151
Aviv Keshete00caeb2013-04-17 14:03:25 -0700152 return package, vartree
153
154
155def DowngradePackageVersion(portage_root, package_cp,
156 downgrade_to_version='0'):
157 """
158 Downgrade the specified portage package version.
159
160 Arguments:
161 portage_root: Root directory of portage tree. Eg '/' or '/build/lumpy'
162 package_cp: A string similar to 'chromeos-base/autotest-tests'.
163 downgrade_to_version: String version to downgrade to. Default: '0'
164
165 Returns:
166 Returns the return value of the `mv` command used to perform operation.
167 """
168 package, _ = GetPackageAPI(portage_root, package_cp)
169
170 source_directory = package.dbdir
171 destination_path = os.path.join(
172 package.dbroot, package_cp + '-' + downgrade_to_version)
173 if os.path.abspath(source_directory) == os.path.abspath(destination_path):
174 return 0
175 command = ['mv', source_directory, destination_path]
176 return cros_build_lib.SudoRunCommand(command).returncode
177
178
179def UpdatePackageContents(change_report, package_cp,
180 portage_root=None):
181 """
182 Add newly created files/directors to package contents.
183
184 Given an ItemizedChangeReport, add the newly created files and directories
185 to the CONTENTS of an installed portage package, such that these files are
186 considered owned by that package.
187
188 Arguments:
189 changereport: ItemizedChangeReport object for the changes to be
190 made to the package.
191 package_cp: A string similar to 'chromeos-base/autotest-tests' giving
192 the package category and name of the package to be altered.
193 portage_root: Portage root path, corresponding to the board that
194 we are working on. Defaults to '/'
195 """
196 package, vartree = GetPackageAPI(portage_root, package_cp)
197
Aviv Keshet940c17f2013-04-11 18:41:42 -0700198 # Append new contents to package contents dictionary
199 contents = package.getcontents().copy()
200 for _, filename in change_report.new_files:
201 contents.setdefault(filename, (u'obj', '0', '0'))
202 for _, dirname in change_report.new_directories:
203 # String trailing slashes if present.
204 dirname = dirname.rstrip('/')
205 contents.setdefault(dirname, (u'dir',))
206
207 # Write new contents dictionary to file
208 vartree.dbapi.writeContentsToContentsFile(package, contents)
209
210
Aviv Keshet75d65962013-04-17 16:15:23 -0700211def RemoveTestPackages(stale_packages, autotest_sysroot):
212 """
213 Remove bzipped test packages from sysroot.
214
215 Arguments:
216 stale_packages: List of test packages names to be removed.
217 e.g. ['factory_Leds', 'login_UserPolicyKeys']
218 autotest_sysroot: Absolute path of autotest in the sysroot,
219 e.g. '/build/lumpy/usr/local/autotest'
220 """
221 for package in set(stale_packages):
222 package_filename = 'test-' + package + '.tar.bz2'
223 package_file_fullpath = os.path.join(autotest_sysroot, 'packages',
224 package_filename)
225 try:
226 os.remove(package_file_fullpath)
227 logging.info('Removed stale %s', package_file_fullpath)
228 except OSError as err:
229 # Suppress no-such-file exceptions. Raise all others.
230 if err.errno != 2:
231 raise
232
233
Aviv Keshetb1238c32013-04-01 11:42:13 -0700234def RsyncQuickmerge(source_path, sysroot_autotest_path,
235 include_pattern_file=None, pretend=False,
Aviv Keshet60968ec2013-04-11 18:44:14 -0700236 overwrite=False):
Aviv Keshetb1238c32013-04-01 11:42:13 -0700237 """Run rsync quickmerge command, with specified arguments.
238 Command will take form `rsync -a [options] --exclude=**.pyc
239 --exclude=**.pyo
240 [optional --include-from argument]
241 --exclude=* [source_path] [sysroot_autotest_path]`
242
243 Arguments:
244 pretend: True to use the '-n' option to rsync, to perform dry run.
245 overwrite: True to omit '-u' option, overwrite all files in sysroot,
246 not just older files.
Aviv Keshetb1238c32013-04-01 11:42:13 -0700247 """
248 command = ['rsync', '-a']
249
250 if pretend:
251 command += ['-n']
252
253 if not overwrite:
254 command += ['-u']
255
Aviv Keshet60968ec2013-04-11 18:44:14 -0700256 command += ['-i']
Aviv Keshetb1238c32013-04-01 11:42:13 -0700257
258 command += ['--exclude=**.pyc']
259 command += ['--exclude=**.pyo']
260
Aviv Keshet787ffcd2013-04-08 15:14:56 -0700261 # Exclude files with a specific substring in their name, because
262 # they create an ambiguous itemized report. (see unit test file for details)
263 command += ['--exclude=** -> *']
264
Aviv Keshetb1238c32013-04-01 11:42:13 -0700265 if include_pattern_file:
266 command += ['--include-from=%s' % include_pattern_file]
267
268 command += ['--exclude=*']
269
270 command += [source_path, sysroot_autotest_path]
271
Aviv Keshet60968ec2013-04-11 18:44:14 -0700272 return cros_build_lib.SudoRunCommand(command, redirect_stdout=True)
Aviv Keshetb1238c32013-04-01 11:42:13 -0700273
274
275def ParseArguments(argv):
276 """Parse command line arguments
277
278 Returns: parsed arguments.
279 """
280 parser = argparse.ArgumentParser(description='Perform a fast approximation '
281 'to emerge-$board autotest-all, by '
282 'rsyncing source tree to sysroot.')
283
284 parser.add_argument('--board', metavar='BOARD', default=None, required=True)
285 parser.add_argument('--pretend', action='store_true',
286 help='Dry run only, do not modify sysroot autotest.')
287 parser.add_argument('--overwrite', action='store_true',
288 help='Overwrite existing files even if newer.')
Aviv Keshete00caeb2013-04-17 14:03:25 -0700289 parser.add_argument('--verbose', action='store_true',
290 help='Print detailed change report.')
Aviv Keshetb1238c32013-04-01 11:42:13 -0700291
292 return parser.parse_args(argv)
293
294
295def main(argv):
296 cros_build_lib.AssertInsideChroot()
297
298 args = ParseArguments(argv)
299
Aviv Keshet940c17f2013-04-11 18:41:42 -0700300 if not os.geteuid()==0:
301 try:
302 cros_build_lib.SudoRunCommand([sys.executable] + sys.argv)
303 except cros_build_lib.RunCommandError:
304 return 1
305 return 0
306
Aviv Keshetb1238c32013-04-01 11:42:13 -0700307 if not args.board:
Aviv Keshete00caeb2013-04-17 14:03:25 -0700308 print 'No board specified. Aborting.'
Aviv Keshetb1238c32013-04-01 11:42:13 -0700309 return 1
310
311 manifest = git.ManifestCheckout.Cached(constants.SOURCE_ROOT)
312 source_path = manifest.GetProjectPath(AUTOTEST_PROJECT_NAME, absolute=True)
313 source_path = os.path.join(source_path, '')
314
315 script_path = os.path.dirname(__file__)
316 include_pattern_file = os.path.join(script_path, INCLUDE_PATTERNS_FILENAME)
317
318 # TODO: Determine the following string programatically.
Aviv Keshet940c17f2013-04-11 18:41:42 -0700319 sysroot_path = os.path.join('/build', args.board, '')
320 sysroot_autotest_path = os.path.join(sysroot_path, 'usr', 'local',
Aviv Keshetb1238c32013-04-01 11:42:13 -0700321 'autotest', '')
322
Aviv Keshet60968ec2013-04-11 18:44:14 -0700323 rsync_output = RsyncQuickmerge(source_path, sysroot_autotest_path,
324 include_pattern_file, args.pretend, args.overwrite)
Aviv Keshetb1238c32013-04-01 11:42:13 -0700325
Aviv Keshete00caeb2013-04-17 14:03:25 -0700326 if args.verbose:
327 logging.info(rsync_output.output)
328
Aviv Keshet60968ec2013-04-11 18:44:14 -0700329 change_report = ItemizeChangesFromRsyncOutput(rsync_output.output,
330 sysroot_autotest_path)
331
Aviv Keshet940c17f2013-04-11 18:41:42 -0700332 if not args.pretend:
333 UpdatePackageContents(change_report, AUTOTEST_TESTS_EBUILD,
334 sysroot_path)
Aviv Keshete00caeb2013-04-17 14:03:25 -0700335 if DowngradePackageVersion(sysroot_path, AUTOTEST_TESTS_EBUILD) != 0:
336 logging.warning('Unable to downgrade package %s version number.',
337 AUTOTEST_TESTS_EBUILD)
Aviv Keshet75d65962013-04-17 16:15:23 -0700338 stale_packages = GetStalePackageNames(
339 change_report.new_files + change_report.modified_files,
340 sysroot_autotest_path)
341 RemoveTestPackages(stale_packages, sysroot_autotest_path)
Aviv Keshetb1238c32013-04-01 11:42:13 -0700342
Aviv Keshet940c17f2013-04-11 18:41:42 -0700343 if args.pretend:
Aviv Keshete00caeb2013-04-17 14:03:25 -0700344 logging.info('The following message is pretend only. No filesystem '
345 'changes made.')
346 logging.info('Quickmerge complete. Created or modified %s files.',
347 len(change_report.new_files) + len(change_report.modified_files))
348
349 return 0
350
Aviv Keshetb1238c32013-04-01 11:42:13 -0700351
352if __name__ == '__main__':
353 sys.exit(main(sys.argv))