blob: c2f21570d15f70cbd010f45985ad88ea23c2658c [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',
Aviv Keshet3cc4e9e2013-04-24 10:47:23 -070038 'chromeos-base/autotest-tests-ltp',
39 'chromeos-base/autotest-tests-ownershipapi']
Aviv Keshet787ffcd2013-04-08 15:14:56 -070040
Aviv Keshetc73cfc32013-06-14 16:18:53 -070041IGNORE_SUBDIRS = ['ExternalSource',
42 'logs',
43 'results',
44 'site-packages']
45
Aviv Keshet787ffcd2013-04-08 15:14:56 -070046# Data structure describing a single rsync filesystem change.
47#
48# change_description: An 11 character string, the rsync change description
49# for the particular file.
50# absolute_path: The absolute path of the created or modified file.
51ItemizedChange = namedtuple('ItemizedChange', ['change_description',
52 'absolute_path'])
53
54
55# Data structure describing the rsync new/modified files or directories.
56#
57# new_files: A list of ItemizedChange objects for new files.
58# modified_files: A list of ItemizedChange objects for modified files.
59# new_directories: A list of ItemizedChange objects for new directories.
60ItemizedChangeReport = namedtuple('ItemizedChangeReport',
61 ['new_files', 'modified_files',
62 'new_directories'])
63
Aviv Keshet84bdfc52013-06-04 13:19:38 -070064class PortagePackageAPIError(Exception):
65 """Exception thrown when unable to retrieve a portage package API."""
66
Aviv Keshet787ffcd2013-04-08 15:14:56 -070067
Aviv Keshetc73cfc32013-06-14 16:18:53 -070068
69def GetNewestFileTime(path, ignore_subdirs=[]):
70 #pylint: disable-msg=W0102
71 """Recursively determine the newest file modification time.
72
73 Arguments:
74 path: The absolute path of the directory to recursively search.
75 ignore_subdirs: list of names of subdirectores of given path, to be
76 ignored by recursive search. Useful as a speed
77 optimization, to ignore directories full of many
78 files.
79
80 Returns:
81 The modification time of the most recently modified file recursively
82 contained within the specified directory. Returned as seconds since
83 Jan. 1, 1970, 00:00 GMT, with fractional part (floating point number).
84 """
85 command = ['find', path]
86 for ignore in ignore_subdirs:
87 command.extend(['-path', os.path.join(path, ignore), '-prune', '-o'])
88 command.extend(['-printf', r'%T@\n'])
89
90 command_result = cros_build_lib.RunCommandCaptureOutput(command,
91 error_code_ok=True)
92 float_times = [float(str_time) for str_time in
93 command_result.output.split('\n')
94 if str_time != '']
95
96 return max(float_times)
97
98
Aviv Keshet75d65962013-04-17 16:15:23 -070099def GetStalePackageNames(change_list, autotest_sysroot):
Aviv Keshete7b20192013-04-24 14:05:53 -0700100 """Given a rsync change report, returns the names of stale test packages.
Aviv Keshet75d65962013-04-17 16:15:23 -0700101
102 This function pulls out test package names for client-side tests, stored
103 within the client/site_tests directory tree, that had any files added or
104 modified and for whom any existing bzipped test packages may now be stale.
105
106 Arguments:
107 change_list: A list of ItemizedChange objects corresponding to changed
108 or modified files.
109 autotest_sysroot: Absolute path of autotest in the sysroot,
110 e.g. '/build/lumpy/usr/local/autotest'
111
112 Returns:
113 A list of test package names, eg ['factory_Leds', 'login_UserPolicyKeys'].
114 May contain duplicate entries if multiple files within a test directory
115 were modified.
116 """
117 exp = os.path.abspath(autotest_sysroot) + r'/client/site_tests/(.*?)/.*'
118 matches = [re.match(exp, change.absolute_path) for change in change_list]
119 return [match.group(1) for match in matches if match]
120
121
Aviv Keshet787ffcd2013-04-08 15:14:56 -0700122def ItemizeChangesFromRsyncOutput(rsync_output, destination_path):
123 """Convert the output of an rsync with `-i` to a ItemizedChangeReport object.
124
125 Arguments:
126 rsync_output: String stdout of rsync command that was run with `-i` option.
127 destination_path: String absolute path of the destination directory for the
128 rsync operations. This argument is necessary because
129 rsync's output only gives the relative path of
130 touched/added files.
131
132 Returns:
133 ItemizedChangeReport object giving the absolute paths of files that were
134 created or modified by rsync.
135 """
136 modified_matches = re.findall(r'([.>]f[^+]{9}) (.*)', rsync_output)
137 new_matches = re.findall(r'(>f\+{9}) (.*)', rsync_output)
138 new_symlink_matches = re.findall(r'(cL\+{9}) (.*) -> .*', rsync_output)
139 new_dir_matches = re.findall(r'(cd\+{9}) (.*)', rsync_output)
140
141 absolute_modified = [ItemizedChange(c, os.path.join(destination_path, f))
142 for (c, f) in modified_matches]
143
144 # Note: new symlinks are treated as new files.
145 absolute_new = [ItemizedChange(c, os.path.join(destination_path, f))
146 for (c, f) in new_matches + new_symlink_matches]
147
148 absolute_new_dir = [ItemizedChange(c, os.path.join(destination_path, f))
149 for (c, f) in new_dir_matches]
150
151 return ItemizedChangeReport(new_files=absolute_new,
152 modified_files=absolute_modified,
153 new_directories=absolute_new_dir)
154
155
Aviv Keshete00caeb2013-04-17 14:03:25 -0700156def GetPackageAPI(portage_root, package_cp):
Aviv Keshete7b20192013-04-24 14:05:53 -0700157 """Gets portage API handles for the given package.
Aviv Keshet940c17f2013-04-11 18:41:42 -0700158
159 Arguments:
Aviv Keshete00caeb2013-04-17 14:03:25 -0700160 portage_root: Root directory of portage tree. Eg '/' or '/build/lumpy'
161 package_cp: A string similar to 'chromeos-base/autotest-tests'.
162
163 Returns:
164 Returns (package, vartree) tuple, where
165 package is of type portage.dbapi.vartree.dblink
166 vartree is of type portage.dbapi.vartree.vartree
Aviv Keshet940c17f2013-04-11 18:41:42 -0700167 """
168 if portage_root is None:
Aviv Keshete7b20192013-04-24 14:05:53 -0700169 # pylint: disable-msg=E1101
170 portage_root = portage.root
Aviv Keshet940c17f2013-04-11 18:41:42 -0700171 # Ensure that portage_root ends with trailing slash.
172 portage_root = os.path.join(portage_root, '')
173
Aviv Keshete7b20192013-04-24 14:05:53 -0700174 # Create a vartree object corresponding to portage_root.
Aviv Keshet940c17f2013-04-11 18:41:42 -0700175 trees = portage.create_trees(portage_root, portage_root)
176 vartree = trees[portage_root]['vartree']
177
Aviv Keshete7b20192013-04-24 14:05:53 -0700178 # List the matching installed packages in cpv format.
Aviv Keshet940c17f2013-04-11 18:41:42 -0700179 matching_packages = vartree.dbapi.cp_list(package_cp)
180
181 if not matching_packages:
Aviv Keshet84bdfc52013-06-04 13:19:38 -0700182 raise PortagePackageAPIError('No matching package for %s in portage_root '
183 '%s' % (package_cp, portage_root))
Aviv Keshet940c17f2013-04-11 18:41:42 -0700184
185 if len(matching_packages) > 1:
Aviv Keshet84bdfc52013-06-04 13:19:38 -0700186 raise PortagePackageAPIError('Too many matching packages for %s in '
187 'portage_root %s' % (package_cp,
188 portage_root))
Aviv Keshet940c17f2013-04-11 18:41:42 -0700189
Aviv Keshete7b20192013-04-24 14:05:53 -0700190 # Convert string match to package dblink.
Aviv Keshet940c17f2013-04-11 18:41:42 -0700191 package_cpv = matching_packages[0]
192 package_split = portage_utilities.SplitCPV(package_cpv)
Aviv Keshete7b20192013-04-24 14:05:53 -0700193 # pylint: disable-msg=E1101
194 package = portage.dblink(package_split.category,
Aviv Keshet940c17f2013-04-11 18:41:42 -0700195 package_split.pv, settings=vartree.settings,
196 vartree=vartree)
197
Aviv Keshete00caeb2013-04-17 14:03:25 -0700198 return package, vartree
199
200
201def DowngradePackageVersion(portage_root, package_cp,
202 downgrade_to_version='0'):
Aviv Keshete7b20192013-04-24 14:05:53 -0700203 """Downgrade the specified portage package version.
Aviv Keshete00caeb2013-04-17 14:03:25 -0700204
205 Arguments:
206 portage_root: Root directory of portage tree. Eg '/' or '/build/lumpy'
207 package_cp: A string similar to 'chromeos-base/autotest-tests'.
208 downgrade_to_version: String version to downgrade to. Default: '0'
209
210 Returns:
Aviv Keshet557e6882013-04-25 13:26:09 -0700211 True on success. False on failure (nonzero return code from `mv` command).
Aviv Keshete00caeb2013-04-17 14:03:25 -0700212 """
Aviv Keshet84bdfc52013-06-04 13:19:38 -0700213 try:
214 package, _ = GetPackageAPI(portage_root, package_cp)
215 except PortagePackageAPIError:
216 # Unable to fetch a corresponding portage package API for this
217 # package_cp (either no such package, or name ambigious and matches).
218 # So, just fail out.
219 return False
Aviv Keshete00caeb2013-04-17 14:03:25 -0700220
221 source_directory = package.dbdir
222 destination_path = os.path.join(
223 package.dbroot, package_cp + '-' + downgrade_to_version)
224 if os.path.abspath(source_directory) == os.path.abspath(destination_path):
Aviv Keshet557e6882013-04-25 13:26:09 -0700225 return True
Aviv Keshete00caeb2013-04-17 14:03:25 -0700226 command = ['mv', source_directory, destination_path]
Aviv Keshet557e6882013-04-25 13:26:09 -0700227 code = cros_build_lib.SudoRunCommand(command, error_code_ok=True).returncode
228 return code == 0
Aviv Keshete00caeb2013-04-17 14:03:25 -0700229
230
Aviv Keshete7b20192013-04-24 14:05:53 -0700231def UpdatePackageContents(change_report, package_cp, portage_root=None):
232 """Add newly created files/directors to package contents.
Aviv Keshete00caeb2013-04-17 14:03:25 -0700233
234 Given an ItemizedChangeReport, add the newly created files and directories
235 to the CONTENTS of an installed portage package, such that these files are
236 considered owned by that package.
237
238 Arguments:
239 changereport: ItemizedChangeReport object for the changes to be
240 made to the package.
241 package_cp: A string similar to 'chromeos-base/autotest-tests' giving
242 the package category and name of the package to be altered.
243 portage_root: Portage root path, corresponding to the board that
244 we are working on. Defaults to '/'
245 """
246 package, vartree = GetPackageAPI(portage_root, package_cp)
247
Aviv Keshete7b20192013-04-24 14:05:53 -0700248 # Append new contents to package contents dictionary.
Aviv Keshet940c17f2013-04-11 18:41:42 -0700249 contents = package.getcontents().copy()
250 for _, filename in change_report.new_files:
251 contents.setdefault(filename, (u'obj', '0', '0'))
252 for _, dirname in change_report.new_directories:
Aviv Keshete7b20192013-04-24 14:05:53 -0700253 # Strip trailing slashes if present.
254 contents.setdefault(dirname.rstrip('/'), (u'dir',))
Aviv Keshet940c17f2013-04-11 18:41:42 -0700255
Aviv Keshete7b20192013-04-24 14:05:53 -0700256 # Write new contents dictionary to file.
Aviv Keshet940c17f2013-04-11 18:41:42 -0700257 vartree.dbapi.writeContentsToContentsFile(package, contents)
258
259
Aviv Keshet19276752013-05-16 11:12:23 -0700260def RemoveBzipPackages(autotest_sysroot):
261 """Remove all bzipped test/dep/profiler packages from sysroot autotest.
Aviv Keshet75d65962013-04-17 16:15:23 -0700262
263 Arguments:
Aviv Keshet75d65962013-04-17 16:15:23 -0700264 autotest_sysroot: Absolute path of autotest in the sysroot,
265 e.g. '/build/lumpy/usr/local/autotest'
266 """
Aviv Keshet19276752013-05-16 11:12:23 -0700267 osutils.RmDir(os.path.join(autotest_sysroot, 'packages'),
268 ignore_missing=True)
269 osutils.SafeUnlink(os.path.join(autotest_sysroot, 'packages.checksum'))
Aviv Keshet75d65962013-04-17 16:15:23 -0700270
271
Aviv Keshetb1238c32013-04-01 11:42:13 -0700272def RsyncQuickmerge(source_path, sysroot_autotest_path,
273 include_pattern_file=None, pretend=False,
Aviv Keshet60968ec2013-04-11 18:44:14 -0700274 overwrite=False):
Aviv Keshetb1238c32013-04-01 11:42:13 -0700275 """Run rsync quickmerge command, with specified arguments.
Aviv Keshete7b20192013-04-24 14:05:53 -0700276
Aviv Keshetb1238c32013-04-01 11:42:13 -0700277 Command will take form `rsync -a [options] --exclude=**.pyc
278 --exclude=**.pyo
279 [optional --include-from argument]
280 --exclude=* [source_path] [sysroot_autotest_path]`
281
282 Arguments:
283 pretend: True to use the '-n' option to rsync, to perform dry run.
284 overwrite: True to omit '-u' option, overwrite all files in sysroot,
285 not just older files.
Aviv Keshet557e6882013-04-25 13:26:09 -0700286
287 Returns:
288 The cros_build_lib.CommandResult object resulting from the rsync command.
Aviv Keshetb1238c32013-04-01 11:42:13 -0700289 """
290 command = ['rsync', '-a']
291
292 if pretend:
293 command += ['-n']
294
295 if not overwrite:
296 command += ['-u']
297
Aviv Keshet60968ec2013-04-11 18:44:14 -0700298 command += ['-i']
Aviv Keshetb1238c32013-04-01 11:42:13 -0700299
300 command += ['--exclude=**.pyc']
301 command += ['--exclude=**.pyo']
302
Aviv Keshet787ffcd2013-04-08 15:14:56 -0700303 # Exclude files with a specific substring in their name, because
304 # they create an ambiguous itemized report. (see unit test file for details)
305 command += ['--exclude=** -> *']
306
Aviv Keshetb1238c32013-04-01 11:42:13 -0700307 if include_pattern_file:
308 command += ['--include-from=%s' % include_pattern_file]
309
310 command += ['--exclude=*']
311
312 command += [source_path, sysroot_autotest_path]
313
Aviv Keshet60968ec2013-04-11 18:44:14 -0700314 return cros_build_lib.SudoRunCommand(command, redirect_stdout=True)
Aviv Keshetb1238c32013-04-01 11:42:13 -0700315
316
317def ParseArguments(argv):
318 """Parse command line arguments
319
320 Returns: parsed arguments.
321 """
322 parser = argparse.ArgumentParser(description='Perform a fast approximation '
323 'to emerge-$board autotest-all, by '
324 'rsyncing source tree to sysroot.')
325
Aviv Keshet0a366a02013-07-18 10:52:04 -0700326
327 default_board = cros_build_lib.GetDefaultBoard()
328 parser.add_argument('--board', metavar='BOARD', default=default_board,
329 help='Board to perform quickmerge for. Default: ' +
330 (default_board or 'Not configured.'))
Aviv Keshetb1238c32013-04-01 11:42:13 -0700331 parser.add_argument('--pretend', action='store_true',
332 help='Dry run only, do not modify sysroot autotest.')
333 parser.add_argument('--overwrite', action='store_true',
334 help='Overwrite existing files even if newer.')
Aviv Keshetc73cfc32013-06-14 16:18:53 -0700335 parser.add_argument('--force', action='store_true',
336 help='Do not check whether destination tree is newer '
337 'than source tree, always perform quickmerge.')
Aviv Keshete00caeb2013-04-17 14:03:25 -0700338 parser.add_argument('--verbose', action='store_true',
339 help='Print detailed change report.')
Aviv Keshetb1238c32013-04-01 11:42:13 -0700340
341 return parser.parse_args(argv)
342
343
344def main(argv):
345 cros_build_lib.AssertInsideChroot()
346
347 args = ParseArguments(argv)
348
Aviv Keshete7b20192013-04-24 14:05:53 -0700349 if os.geteuid() != 0:
Aviv Keshet940c17f2013-04-11 18:41:42 -0700350 try:
351 cros_build_lib.SudoRunCommand([sys.executable] + sys.argv)
352 except cros_build_lib.RunCommandError:
353 return 1
354 return 0
355
Aviv Keshetb1238c32013-04-01 11:42:13 -0700356 if not args.board:
Aviv Keshete00caeb2013-04-17 14:03:25 -0700357 print 'No board specified. Aborting.'
Aviv Keshetb1238c32013-04-01 11:42:13 -0700358 return 1
359
360 manifest = git.ManifestCheckout.Cached(constants.SOURCE_ROOT)
361 source_path = manifest.GetProjectPath(AUTOTEST_PROJECT_NAME, absolute=True)
362 source_path = os.path.join(source_path, '')
363
364 script_path = os.path.dirname(__file__)
365 include_pattern_file = os.path.join(script_path, INCLUDE_PATTERNS_FILENAME)
366
367 # TODO: Determine the following string programatically.
Aviv Keshet940c17f2013-04-11 18:41:42 -0700368 sysroot_path = os.path.join('/build', args.board, '')
369 sysroot_autotest_path = os.path.join(sysroot_path, 'usr', 'local',
Aviv Keshetb1238c32013-04-01 11:42:13 -0700370 'autotest', '')
371
Aviv Keshetc73cfc32013-06-14 16:18:53 -0700372 if not args.force:
373 newest_dest_time = GetNewestFileTime(sysroot_autotest_path, IGNORE_SUBDIRS)
374 newest_source_time = GetNewestFileTime(source_path, IGNORE_SUBDIRS)
375 if newest_dest_time >= newest_source_time:
376 logging.info('The sysroot appears to be newer than the source tree, '
377 'doing nothing and exiting now.')
378 return 0
379
Aviv Keshet60968ec2013-04-11 18:44:14 -0700380 rsync_output = RsyncQuickmerge(source_path, sysroot_autotest_path,
Aviv Keshete7b20192013-04-24 14:05:53 -0700381 include_pattern_file, args.pretend,
382 args.overwrite)
Aviv Keshetb1238c32013-04-01 11:42:13 -0700383
Aviv Keshete00caeb2013-04-17 14:03:25 -0700384 if args.verbose:
385 logging.info(rsync_output.output)
386
Aviv Keshet60968ec2013-04-11 18:44:14 -0700387 change_report = ItemizeChangesFromRsyncOutput(rsync_output.output,
388 sysroot_autotest_path)
389
Aviv Keshet940c17f2013-04-11 18:41:42 -0700390 if not args.pretend:
Aviv Keshetc73cfc32013-06-14 16:18:53 -0700391 logging.info('Updating portage database.')
Aviv Keshet5f3cf722013-05-09 17:35:25 -0700392 UpdatePackageContents(change_report, AUTOTEST_EBUILD,
Aviv Keshet940c17f2013-04-11 18:41:42 -0700393 sysroot_path)
Aviv Keshet3cc4e9e2013-04-24 10:47:23 -0700394 for ebuild in DOWNGRADE_EBUILDS:
Aviv Keshet557e6882013-04-25 13:26:09 -0700395 if not DowngradePackageVersion(sysroot_path, ebuild):
Aviv Keshet3cc4e9e2013-04-24 10:47:23 -0700396 logging.warning('Unable to downgrade package %s version number.',
Aviv Keshete7b20192013-04-24 14:05:53 -0700397 ebuild)
Aviv Keshet19276752013-05-16 11:12:23 -0700398 RemoveBzipPackages(sysroot_autotest_path)
Aviv Keshetb1238c32013-04-01 11:42:13 -0700399
Aviv Keshetc73cfc32013-06-14 16:18:53 -0700400 sentinel_filename = os.path.join(sysroot_autotest_path,
401 '.quickmerge_sentinel')
402 cros_build_lib.RunCommand(['touch', sentinel_filename])
403
Aviv Keshet940c17f2013-04-11 18:41:42 -0700404 if args.pretend:
Aviv Keshete00caeb2013-04-17 14:03:25 -0700405 logging.info('The following message is pretend only. No filesystem '
Aviv Keshete7b20192013-04-24 14:05:53 -0700406 'changes made.')
Aviv Keshete00caeb2013-04-17 14:03:25 -0700407 logging.info('Quickmerge complete. Created or modified %s files.',
Aviv Keshete7b20192013-04-24 14:05:53 -0700408 len(change_report.new_files) +
409 len(change_report.modified_files))
Aviv Keshete00caeb2013-04-17 14:03:25 -0700410
411 return 0