blob: 685f71d7126cea9f943313027946fecb2a5b2d6b [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
Aviv Keshetc73cfc32013-06-14 16:18:53 -070042IGNORE_SUBDIRS = ['ExternalSource',
43 'logs',
44 'results',
45 'site-packages']
46
Aviv Keshet787ffcd2013-04-08 15:14:56 -070047# Data structure describing a single rsync filesystem change.
48#
49# change_description: An 11 character string, the rsync change description
50# for the particular file.
51# absolute_path: The absolute path of the created or modified file.
52ItemizedChange = namedtuple('ItemizedChange', ['change_description',
53 'absolute_path'])
54
55
56# Data structure describing the rsync new/modified files or directories.
57#
58# new_files: A list of ItemizedChange objects for new files.
59# modified_files: A list of ItemizedChange objects for modified files.
60# new_directories: A list of ItemizedChange objects for new directories.
61ItemizedChangeReport = namedtuple('ItemizedChangeReport',
62 ['new_files', 'modified_files',
63 'new_directories'])
64
Aviv Keshet84bdfc52013-06-04 13:19:38 -070065class PortagePackageAPIError(Exception):
66 """Exception thrown when unable to retrieve a portage package API."""
67
Aviv Keshet787ffcd2013-04-08 15:14:56 -070068
Aviv Keshetc73cfc32013-06-14 16:18:53 -070069
70def GetNewestFileTime(path, ignore_subdirs=[]):
71 #pylint: disable-msg=W0102
72 """Recursively determine the newest file modification time.
73
74 Arguments:
75 path: The absolute path of the directory to recursively search.
76 ignore_subdirs: list of names of subdirectores of given path, to be
77 ignored by recursive search. Useful as a speed
78 optimization, to ignore directories full of many
79 files.
80
81 Returns:
82 The modification time of the most recently modified file recursively
83 contained within the specified directory. Returned as seconds since
84 Jan. 1, 1970, 00:00 GMT, with fractional part (floating point number).
85 """
86 command = ['find', path]
87 for ignore in ignore_subdirs:
88 command.extend(['-path', os.path.join(path, ignore), '-prune', '-o'])
89 command.extend(['-printf', r'%T@\n'])
90
91 command_result = cros_build_lib.RunCommandCaptureOutput(command,
92 error_code_ok=True)
93 float_times = [float(str_time) for str_time in
94 command_result.output.split('\n')
95 if str_time != '']
96
97 return max(float_times)
98
99
Aviv Keshet75d65962013-04-17 16:15:23 -0700100def GetStalePackageNames(change_list, autotest_sysroot):
Aviv Keshete7b20192013-04-24 14:05:53 -0700101 """Given a rsync change report, returns the names of stale test packages.
Aviv Keshet75d65962013-04-17 16:15:23 -0700102
103 This function pulls out test package names for client-side tests, stored
104 within the client/site_tests directory tree, that had any files added or
105 modified and for whom any existing bzipped test packages may now be stale.
106
107 Arguments:
108 change_list: A list of ItemizedChange objects corresponding to changed
109 or modified files.
110 autotest_sysroot: Absolute path of autotest in the sysroot,
111 e.g. '/build/lumpy/usr/local/autotest'
112
113 Returns:
114 A list of test package names, eg ['factory_Leds', 'login_UserPolicyKeys'].
115 May contain duplicate entries if multiple files within a test directory
116 were modified.
117 """
118 exp = os.path.abspath(autotest_sysroot) + r'/client/site_tests/(.*?)/.*'
119 matches = [re.match(exp, change.absolute_path) for change in change_list]
120 return [match.group(1) for match in matches if match]
121
122
Aviv Keshet787ffcd2013-04-08 15:14:56 -0700123def ItemizeChangesFromRsyncOutput(rsync_output, destination_path):
124 """Convert the output of an rsync with `-i` to a ItemizedChangeReport object.
125
126 Arguments:
127 rsync_output: String stdout of rsync command that was run with `-i` option.
128 destination_path: String absolute path of the destination directory for the
129 rsync operations. This argument is necessary because
130 rsync's output only gives the relative path of
131 touched/added files.
132
133 Returns:
134 ItemizedChangeReport object giving the absolute paths of files that were
135 created or modified by rsync.
136 """
137 modified_matches = re.findall(r'([.>]f[^+]{9}) (.*)', rsync_output)
138 new_matches = re.findall(r'(>f\+{9}) (.*)', rsync_output)
139 new_symlink_matches = re.findall(r'(cL\+{9}) (.*) -> .*', rsync_output)
140 new_dir_matches = re.findall(r'(cd\+{9}) (.*)', rsync_output)
141
142 absolute_modified = [ItemizedChange(c, os.path.join(destination_path, f))
143 for (c, f) in modified_matches]
144
145 # Note: new symlinks are treated as new files.
146 absolute_new = [ItemizedChange(c, os.path.join(destination_path, f))
147 for (c, f) in new_matches + new_symlink_matches]
148
149 absolute_new_dir = [ItemizedChange(c, os.path.join(destination_path, f))
150 for (c, f) in new_dir_matches]
151
152 return ItemizedChangeReport(new_files=absolute_new,
153 modified_files=absolute_modified,
154 new_directories=absolute_new_dir)
155
156
Aviv Keshete00caeb2013-04-17 14:03:25 -0700157def GetPackageAPI(portage_root, package_cp):
Aviv Keshete7b20192013-04-24 14:05:53 -0700158 """Gets portage API handles for the given package.
Aviv Keshet940c17f2013-04-11 18:41:42 -0700159
160 Arguments:
Aviv Keshete00caeb2013-04-17 14:03:25 -0700161 portage_root: Root directory of portage tree. Eg '/' or '/build/lumpy'
162 package_cp: A string similar to 'chromeos-base/autotest-tests'.
163
164 Returns:
165 Returns (package, vartree) tuple, where
166 package is of type portage.dbapi.vartree.dblink
167 vartree is of type portage.dbapi.vartree.vartree
Aviv Keshet940c17f2013-04-11 18:41:42 -0700168 """
169 if portage_root is None:
Aviv Keshete7b20192013-04-24 14:05:53 -0700170 # pylint: disable-msg=E1101
171 portage_root = portage.root
Aviv Keshet940c17f2013-04-11 18:41:42 -0700172 # Ensure that portage_root ends with trailing slash.
173 portage_root = os.path.join(portage_root, '')
174
Aviv Keshete7b20192013-04-24 14:05:53 -0700175 # Create a vartree object corresponding to portage_root.
Aviv Keshet940c17f2013-04-11 18:41:42 -0700176 trees = portage.create_trees(portage_root, portage_root)
177 vartree = trees[portage_root]['vartree']
178
Aviv Keshete7b20192013-04-24 14:05:53 -0700179 # List the matching installed packages in cpv format.
Aviv Keshet940c17f2013-04-11 18:41:42 -0700180 matching_packages = vartree.dbapi.cp_list(package_cp)
181
182 if not matching_packages:
Aviv Keshet84bdfc52013-06-04 13:19:38 -0700183 raise PortagePackageAPIError('No matching package for %s in portage_root '
184 '%s' % (package_cp, portage_root))
Aviv Keshet940c17f2013-04-11 18:41:42 -0700185
186 if len(matching_packages) > 1:
Aviv Keshet84bdfc52013-06-04 13:19:38 -0700187 raise PortagePackageAPIError('Too many matching packages for %s in '
188 'portage_root %s' % (package_cp,
189 portage_root))
Aviv Keshet940c17f2013-04-11 18:41:42 -0700190
Aviv Keshete7b20192013-04-24 14:05:53 -0700191 # Convert string match to package dblink.
Aviv Keshet940c17f2013-04-11 18:41:42 -0700192 package_cpv = matching_packages[0]
193 package_split = portage_utilities.SplitCPV(package_cpv)
Aviv Keshete7b20192013-04-24 14:05:53 -0700194 # pylint: disable-msg=E1101
195 package = portage.dblink(package_split.category,
Aviv Keshet940c17f2013-04-11 18:41:42 -0700196 package_split.pv, settings=vartree.settings,
197 vartree=vartree)
198
Aviv Keshete00caeb2013-04-17 14:03:25 -0700199 return package, vartree
200
201
202def DowngradePackageVersion(portage_root, package_cp,
203 downgrade_to_version='0'):
Aviv Keshete7b20192013-04-24 14:05:53 -0700204 """Downgrade the specified portage package version.
Aviv Keshete00caeb2013-04-17 14:03:25 -0700205
206 Arguments:
207 portage_root: Root directory of portage tree. Eg '/' or '/build/lumpy'
208 package_cp: A string similar to 'chromeos-base/autotest-tests'.
209 downgrade_to_version: String version to downgrade to. Default: '0'
210
211 Returns:
Aviv Keshet557e6882013-04-25 13:26:09 -0700212 True on success. False on failure (nonzero return code from `mv` command).
Aviv Keshete00caeb2013-04-17 14:03:25 -0700213 """
Aviv Keshet84bdfc52013-06-04 13:19:38 -0700214 try:
215 package, _ = GetPackageAPI(portage_root, package_cp)
216 except PortagePackageAPIError:
217 # Unable to fetch a corresponding portage package API for this
218 # package_cp (either no such package, or name ambigious and matches).
219 # So, just fail out.
220 return False
Aviv Keshete00caeb2013-04-17 14:03:25 -0700221
222 source_directory = package.dbdir
223 destination_path = os.path.join(
224 package.dbroot, package_cp + '-' + downgrade_to_version)
225 if os.path.abspath(source_directory) == os.path.abspath(destination_path):
Aviv Keshet557e6882013-04-25 13:26:09 -0700226 return True
Aviv Keshete00caeb2013-04-17 14:03:25 -0700227 command = ['mv', source_directory, destination_path]
Aviv Keshet557e6882013-04-25 13:26:09 -0700228 code = cros_build_lib.SudoRunCommand(command, error_code_ok=True).returncode
229 return code == 0
Aviv Keshete00caeb2013-04-17 14:03:25 -0700230
231
Aviv Keshete7b20192013-04-24 14:05:53 -0700232def UpdatePackageContents(change_report, package_cp, portage_root=None):
233 """Add newly created files/directors to package contents.
Aviv Keshete00caeb2013-04-17 14:03:25 -0700234
235 Given an ItemizedChangeReport, add the newly created files and directories
236 to the CONTENTS of an installed portage package, such that these files are
237 considered owned by that package.
238
239 Arguments:
240 changereport: ItemizedChangeReport object for the changes to be
241 made to the package.
242 package_cp: A string similar to 'chromeos-base/autotest-tests' giving
243 the package category and name of the package to be altered.
244 portage_root: Portage root path, corresponding to the board that
245 we are working on. Defaults to '/'
246 """
247 package, vartree = GetPackageAPI(portage_root, package_cp)
248
Aviv Keshete7b20192013-04-24 14:05:53 -0700249 # Append new contents to package contents dictionary.
Aviv Keshet940c17f2013-04-11 18:41:42 -0700250 contents = package.getcontents().copy()
251 for _, filename in change_report.new_files:
252 contents.setdefault(filename, (u'obj', '0', '0'))
253 for _, dirname in change_report.new_directories:
Aviv Keshete7b20192013-04-24 14:05:53 -0700254 # Strip trailing slashes if present.
255 contents.setdefault(dirname.rstrip('/'), (u'dir',))
Aviv Keshet940c17f2013-04-11 18:41:42 -0700256
Aviv Keshete7b20192013-04-24 14:05:53 -0700257 # Write new contents dictionary to file.
Aviv Keshet940c17f2013-04-11 18:41:42 -0700258 vartree.dbapi.writeContentsToContentsFile(package, contents)
259
260
Aviv Keshet19276752013-05-16 11:12:23 -0700261def RemoveBzipPackages(autotest_sysroot):
262 """Remove all bzipped test/dep/profiler packages from sysroot autotest.
Aviv Keshet75d65962013-04-17 16:15:23 -0700263
264 Arguments:
Aviv Keshet75d65962013-04-17 16:15:23 -0700265 autotest_sysroot: Absolute path of autotest in the sysroot,
266 e.g. '/build/lumpy/usr/local/autotest'
267 """
Aviv Keshet19276752013-05-16 11:12:23 -0700268 osutils.RmDir(os.path.join(autotest_sysroot, 'packages'),
269 ignore_missing=True)
270 osutils.SafeUnlink(os.path.join(autotest_sysroot, 'packages.checksum'))
Aviv Keshet75d65962013-04-17 16:15:23 -0700271
272
Aviv Keshetb1238c32013-04-01 11:42:13 -0700273def RsyncQuickmerge(source_path, sysroot_autotest_path,
274 include_pattern_file=None, pretend=False,
Aviv Keshet60968ec2013-04-11 18:44:14 -0700275 overwrite=False):
Aviv Keshetb1238c32013-04-01 11:42:13 -0700276 """Run rsync quickmerge command, with specified arguments.
Aviv Keshete7b20192013-04-24 14:05:53 -0700277
Aviv Keshetb1238c32013-04-01 11:42:13 -0700278 Command will take form `rsync -a [options] --exclude=**.pyc
279 --exclude=**.pyo
280 [optional --include-from argument]
281 --exclude=* [source_path] [sysroot_autotest_path]`
282
283 Arguments:
284 pretend: True to use the '-n' option to rsync, to perform dry run.
285 overwrite: True to omit '-u' option, overwrite all files in sysroot,
286 not just older files.
Aviv Keshet557e6882013-04-25 13:26:09 -0700287
288 Returns:
289 The cros_build_lib.CommandResult object resulting from the rsync command.
Aviv Keshetb1238c32013-04-01 11:42:13 -0700290 """
291 command = ['rsync', '-a']
292
293 if pretend:
294 command += ['-n']
295
296 if not overwrite:
297 command += ['-u']
298
Aviv Keshet60968ec2013-04-11 18:44:14 -0700299 command += ['-i']
Aviv Keshetb1238c32013-04-01 11:42:13 -0700300
301 command += ['--exclude=**.pyc']
302 command += ['--exclude=**.pyo']
303
Aviv Keshet787ffcd2013-04-08 15:14:56 -0700304 # Exclude files with a specific substring in their name, because
305 # they create an ambiguous itemized report. (see unit test file for details)
306 command += ['--exclude=** -> *']
307
Aviv Keshetb1238c32013-04-01 11:42:13 -0700308 if include_pattern_file:
309 command += ['--include-from=%s' % include_pattern_file]
310
311 command += ['--exclude=*']
312
313 command += [source_path, sysroot_autotest_path]
314
Aviv Keshet60968ec2013-04-11 18:44:14 -0700315 return cros_build_lib.SudoRunCommand(command, redirect_stdout=True)
Aviv Keshetb1238c32013-04-01 11:42:13 -0700316
317
318def ParseArguments(argv):
319 """Parse command line arguments
320
321 Returns: parsed arguments.
322 """
323 parser = argparse.ArgumentParser(description='Perform a fast approximation '
324 'to emerge-$board autotest-all, by '
325 'rsyncing source tree to sysroot.')
326
Aviv Keshet0a366a02013-07-18 10:52:04 -0700327
328 default_board = cros_build_lib.GetDefaultBoard()
329 parser.add_argument('--board', metavar='BOARD', default=default_board,
330 help='Board to perform quickmerge for. Default: ' +
331 (default_board or 'Not configured.'))
Aviv Keshetb1238c32013-04-01 11:42:13 -0700332 parser.add_argument('--pretend', action='store_true',
333 help='Dry run only, do not modify sysroot autotest.')
334 parser.add_argument('--overwrite', action='store_true',
335 help='Overwrite existing files even if newer.')
Aviv Keshetc73cfc32013-06-14 16:18:53 -0700336 parser.add_argument('--force', action='store_true',
337 help='Do not check whether destination tree is newer '
338 'than source tree, always perform quickmerge.')
Aviv Keshete00caeb2013-04-17 14:03:25 -0700339 parser.add_argument('--verbose', action='store_true',
340 help='Print detailed change report.')
Aviv Keshetb1238c32013-04-01 11:42:13 -0700341
342 return parser.parse_args(argv)
343
344
345def main(argv):
346 cros_build_lib.AssertInsideChroot()
347
348 args = ParseArguments(argv)
349
Aviv Keshete7b20192013-04-24 14:05:53 -0700350 if os.geteuid() != 0:
Aviv Keshet940c17f2013-04-11 18:41:42 -0700351 try:
352 cros_build_lib.SudoRunCommand([sys.executable] + sys.argv)
353 except cros_build_lib.RunCommandError:
354 return 1
355 return 0
356
Aviv Keshetb1238c32013-04-01 11:42:13 -0700357 if not args.board:
Aviv Keshete00caeb2013-04-17 14:03:25 -0700358 print 'No board specified. Aborting.'
Aviv Keshetb1238c32013-04-01 11:42:13 -0700359 return 1
360
361 manifest = git.ManifestCheckout.Cached(constants.SOURCE_ROOT)
362 source_path = manifest.GetProjectPath(AUTOTEST_PROJECT_NAME, absolute=True)
363 source_path = os.path.join(source_path, '')
364
365 script_path = os.path.dirname(__file__)
366 include_pattern_file = os.path.join(script_path, INCLUDE_PATTERNS_FILENAME)
367
368 # TODO: Determine the following string programatically.
Aviv Keshet940c17f2013-04-11 18:41:42 -0700369 sysroot_path = os.path.join('/build', args.board, '')
370 sysroot_autotest_path = os.path.join(sysroot_path, 'usr', 'local',
Aviv Keshetb1238c32013-04-01 11:42:13 -0700371 'autotest', '')
372
Aviv Keshetc73cfc32013-06-14 16:18:53 -0700373 if not args.force:
374 newest_dest_time = GetNewestFileTime(sysroot_autotest_path, IGNORE_SUBDIRS)
375 newest_source_time = GetNewestFileTime(source_path, IGNORE_SUBDIRS)
376 if newest_dest_time >= newest_source_time:
377 logging.info('The sysroot appears to be newer than the source tree, '
378 'doing nothing and exiting now.')
379 return 0
380
Aviv Keshet60968ec2013-04-11 18:44:14 -0700381 rsync_output = RsyncQuickmerge(source_path, sysroot_autotest_path,
Aviv Keshete7b20192013-04-24 14:05:53 -0700382 include_pattern_file, args.pretend,
383 args.overwrite)
Aviv Keshetb1238c32013-04-01 11:42:13 -0700384
Aviv Keshete00caeb2013-04-17 14:03:25 -0700385 if args.verbose:
386 logging.info(rsync_output.output)
387
Aviv Keshet60968ec2013-04-11 18:44:14 -0700388 change_report = ItemizeChangesFromRsyncOutput(rsync_output.output,
389 sysroot_autotest_path)
390
Aviv Keshet940c17f2013-04-11 18:41:42 -0700391 if not args.pretend:
Aviv Keshetc73cfc32013-06-14 16:18:53 -0700392 logging.info('Updating portage database.')
Aviv Keshet5f3cf722013-05-09 17:35:25 -0700393 UpdatePackageContents(change_report, AUTOTEST_EBUILD,
Aviv Keshet940c17f2013-04-11 18:41:42 -0700394 sysroot_path)
Aviv Keshet3cc4e9e2013-04-24 10:47:23 -0700395 for ebuild in DOWNGRADE_EBUILDS:
Aviv Keshet557e6882013-04-25 13:26:09 -0700396 if not DowngradePackageVersion(sysroot_path, ebuild):
Aviv Keshet3cc4e9e2013-04-24 10:47:23 -0700397 logging.warning('Unable to downgrade package %s version number.',
Aviv Keshete7b20192013-04-24 14:05:53 -0700398 ebuild)
Aviv Keshet19276752013-05-16 11:12:23 -0700399 RemoveBzipPackages(sysroot_autotest_path)
Aviv Keshetb1238c32013-04-01 11:42:13 -0700400
Aviv Keshetc73cfc32013-06-14 16:18:53 -0700401 sentinel_filename = os.path.join(sysroot_autotest_path,
402 '.quickmerge_sentinel')
403 cros_build_lib.RunCommand(['touch', sentinel_filename])
404
Aviv Keshet940c17f2013-04-11 18:41:42 -0700405 if args.pretend:
Aviv Keshete00caeb2013-04-17 14:03:25 -0700406 logging.info('The following message is pretend only. No filesystem '
Aviv Keshete7b20192013-04-24 14:05:53 -0700407 'changes made.')
Aviv Keshete00caeb2013-04-17 14:03:25 -0700408 logging.info('Quickmerge complete. Created or modified %s files.',
Aviv Keshete7b20192013-04-24 14:05:53 -0700409 len(change_report.new_files) +
410 len(change_report.modified_files))
Aviv Keshete00caeb2013-04-17 14:03:25 -0700411
412 return 0