blob: db6d5d71f670ad2d1c84a429f9c2259ea4bbdc37 [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
54def ItemizeChangesFromRsyncOutput(rsync_output, destination_path):
55 """Convert the output of an rsync with `-i` to a ItemizedChangeReport object.
56
57 Arguments:
58 rsync_output: String stdout of rsync command that was run with `-i` option.
59 destination_path: String absolute path of the destination directory for the
60 rsync operations. This argument is necessary because
61 rsync's output only gives the relative path of
62 touched/added files.
63
64 Returns:
65 ItemizedChangeReport object giving the absolute paths of files that were
66 created or modified by rsync.
67 """
68 modified_matches = re.findall(r'([.>]f[^+]{9}) (.*)', rsync_output)
69 new_matches = re.findall(r'(>f\+{9}) (.*)', rsync_output)
70 new_symlink_matches = re.findall(r'(cL\+{9}) (.*) -> .*', rsync_output)
71 new_dir_matches = re.findall(r'(cd\+{9}) (.*)', rsync_output)
72
73 absolute_modified = [ItemizedChange(c, os.path.join(destination_path, f))
74 for (c, f) in modified_matches]
75
76 # Note: new symlinks are treated as new files.
77 absolute_new = [ItemizedChange(c, os.path.join(destination_path, f))
78 for (c, f) in new_matches + new_symlink_matches]
79
80 absolute_new_dir = [ItemizedChange(c, os.path.join(destination_path, f))
81 for (c, f) in new_dir_matches]
82
83 return ItemizedChangeReport(new_files=absolute_new,
84 modified_files=absolute_modified,
85 new_directories=absolute_new_dir)
86
87
Aviv Keshete00caeb2013-04-17 14:03:25 -070088def GetPackageAPI(portage_root, package_cp):
Aviv Keshet940c17f2013-04-11 18:41:42 -070089 """
Aviv Keshete00caeb2013-04-17 14:03:25 -070090 Gets portage API handles for the given package.
Aviv Keshet940c17f2013-04-11 18:41:42 -070091
92 Arguments:
Aviv Keshete00caeb2013-04-17 14:03:25 -070093 portage_root: Root directory of portage tree. Eg '/' or '/build/lumpy'
94 package_cp: A string similar to 'chromeos-base/autotest-tests'.
95
96 Returns:
97 Returns (package, vartree) tuple, where
98 package is of type portage.dbapi.vartree.dblink
99 vartree is of type portage.dbapi.vartree.vartree
Aviv Keshet940c17f2013-04-11 18:41:42 -0700100 """
101 if portage_root is None:
102 portage_root = portage.root # pylint: disable-msg=E1101
103 # Ensure that portage_root ends with trailing slash.
104 portage_root = os.path.join(portage_root, '')
105
106 # Create vartree object corresponding to portage_root
107 trees = portage.create_trees(portage_root, portage_root)
108 vartree = trees[portage_root]['vartree']
109
110 # List matching installed packages in cpv format
111 matching_packages = vartree.dbapi.cp_list(package_cp)
112
113 if not matching_packages:
114 raise ValueError('No matching package for %s in portage_root %s' % (
115 package_cp, portage_root))
116
117 if len(matching_packages) > 1:
118 raise ValueError('Too many matching packages for %s in portage_root '
119 '%s' % (package_cp, portage_root))
120
121 # Convert string match to package dblink
122 package_cpv = matching_packages[0]
123 package_split = portage_utilities.SplitCPV(package_cpv)
124 package = portage.dblink(package_split.category, # pylint: disable-msg=E1101
125 package_split.pv, settings=vartree.settings,
126 vartree=vartree)
127
Aviv Keshete00caeb2013-04-17 14:03:25 -0700128 return package, vartree
129
130
131def DowngradePackageVersion(portage_root, package_cp,
132 downgrade_to_version='0'):
133 """
134 Downgrade the specified portage package version.
135
136 Arguments:
137 portage_root: Root directory of portage tree. Eg '/' or '/build/lumpy'
138 package_cp: A string similar to 'chromeos-base/autotest-tests'.
139 downgrade_to_version: String version to downgrade to. Default: '0'
140
141 Returns:
142 Returns the return value of the `mv` command used to perform operation.
143 """
144 package, _ = GetPackageAPI(portage_root, package_cp)
145
146 source_directory = package.dbdir
147 destination_path = os.path.join(
148 package.dbroot, package_cp + '-' + downgrade_to_version)
149 if os.path.abspath(source_directory) == os.path.abspath(destination_path):
150 return 0
151 command = ['mv', source_directory, destination_path]
152 return cros_build_lib.SudoRunCommand(command).returncode
153
154
155def UpdatePackageContents(change_report, package_cp,
156 portage_root=None):
157 """
158 Add newly created files/directors to package contents.
159
160 Given an ItemizedChangeReport, add the newly created files and directories
161 to the CONTENTS of an installed portage package, such that these files are
162 considered owned by that package.
163
164 Arguments:
165 changereport: ItemizedChangeReport object for the changes to be
166 made to the package.
167 package_cp: A string similar to 'chromeos-base/autotest-tests' giving
168 the package category and name of the package to be altered.
169 portage_root: Portage root path, corresponding to the board that
170 we are working on. Defaults to '/'
171 """
172 package, vartree = GetPackageAPI(portage_root, package_cp)
173
Aviv Keshet940c17f2013-04-11 18:41:42 -0700174 # Append new contents to package contents dictionary
175 contents = package.getcontents().copy()
176 for _, filename in change_report.new_files:
177 contents.setdefault(filename, (u'obj', '0', '0'))
178 for _, dirname in change_report.new_directories:
179 # String trailing slashes if present.
180 dirname = dirname.rstrip('/')
181 contents.setdefault(dirname, (u'dir',))
182
183 # Write new contents dictionary to file
184 vartree.dbapi.writeContentsToContentsFile(package, contents)
185
186
Aviv Keshetb1238c32013-04-01 11:42:13 -0700187def RsyncQuickmerge(source_path, sysroot_autotest_path,
188 include_pattern_file=None, pretend=False,
Aviv Keshet60968ec2013-04-11 18:44:14 -0700189 overwrite=False):
Aviv Keshetb1238c32013-04-01 11:42:13 -0700190 """Run rsync quickmerge command, with specified arguments.
191 Command will take form `rsync -a [options] --exclude=**.pyc
192 --exclude=**.pyo
193 [optional --include-from argument]
194 --exclude=* [source_path] [sysroot_autotest_path]`
195
196 Arguments:
197 pretend: True to use the '-n' option to rsync, to perform dry run.
198 overwrite: True to omit '-u' option, overwrite all files in sysroot,
199 not just older files.
Aviv Keshetb1238c32013-04-01 11:42:13 -0700200 """
201 command = ['rsync', '-a']
202
203 if pretend:
204 command += ['-n']
205
206 if not overwrite:
207 command += ['-u']
208
Aviv Keshet60968ec2013-04-11 18:44:14 -0700209 command += ['-i']
Aviv Keshetb1238c32013-04-01 11:42:13 -0700210
211 command += ['--exclude=**.pyc']
212 command += ['--exclude=**.pyo']
213
Aviv Keshet787ffcd2013-04-08 15:14:56 -0700214 # Exclude files with a specific substring in their name, because
215 # they create an ambiguous itemized report. (see unit test file for details)
216 command += ['--exclude=** -> *']
217
Aviv Keshetb1238c32013-04-01 11:42:13 -0700218 if include_pattern_file:
219 command += ['--include-from=%s' % include_pattern_file]
220
221 command += ['--exclude=*']
222
223 command += [source_path, sysroot_autotest_path]
224
Aviv Keshet60968ec2013-04-11 18:44:14 -0700225 return cros_build_lib.SudoRunCommand(command, redirect_stdout=True)
Aviv Keshetb1238c32013-04-01 11:42:13 -0700226
227
228def ParseArguments(argv):
229 """Parse command line arguments
230
231 Returns: parsed arguments.
232 """
233 parser = argparse.ArgumentParser(description='Perform a fast approximation '
234 'to emerge-$board autotest-all, by '
235 'rsyncing source tree to sysroot.')
236
237 parser.add_argument('--board', metavar='BOARD', default=None, required=True)
238 parser.add_argument('--pretend', action='store_true',
239 help='Dry run only, do not modify sysroot autotest.')
240 parser.add_argument('--overwrite', action='store_true',
241 help='Overwrite existing files even if newer.')
Aviv Keshete00caeb2013-04-17 14:03:25 -0700242 parser.add_argument('--verbose', action='store_true',
243 help='Print detailed change report.')
Aviv Keshetb1238c32013-04-01 11:42:13 -0700244
245 return parser.parse_args(argv)
246
247
248def main(argv):
249 cros_build_lib.AssertInsideChroot()
250
251 args = ParseArguments(argv)
252
Aviv Keshet940c17f2013-04-11 18:41:42 -0700253 if not os.geteuid()==0:
254 try:
255 cros_build_lib.SudoRunCommand([sys.executable] + sys.argv)
256 except cros_build_lib.RunCommandError:
257 return 1
258 return 0
259
Aviv Keshetb1238c32013-04-01 11:42:13 -0700260 if not args.board:
Aviv Keshete00caeb2013-04-17 14:03:25 -0700261 print 'No board specified. Aborting.'
Aviv Keshetb1238c32013-04-01 11:42:13 -0700262 return 1
263
264 manifest = git.ManifestCheckout.Cached(constants.SOURCE_ROOT)
265 source_path = manifest.GetProjectPath(AUTOTEST_PROJECT_NAME, absolute=True)
266 source_path = os.path.join(source_path, '')
267
268 script_path = os.path.dirname(__file__)
269 include_pattern_file = os.path.join(script_path, INCLUDE_PATTERNS_FILENAME)
270
271 # TODO: Determine the following string programatically.
Aviv Keshet940c17f2013-04-11 18:41:42 -0700272 sysroot_path = os.path.join('/build', args.board, '')
273 sysroot_autotest_path = os.path.join(sysroot_path, 'usr', 'local',
Aviv Keshetb1238c32013-04-01 11:42:13 -0700274 'autotest', '')
275
Aviv Keshet60968ec2013-04-11 18:44:14 -0700276 rsync_output = RsyncQuickmerge(source_path, sysroot_autotest_path,
277 include_pattern_file, args.pretend, args.overwrite)
Aviv Keshetb1238c32013-04-01 11:42:13 -0700278
Aviv Keshete00caeb2013-04-17 14:03:25 -0700279 if args.verbose:
280 logging.info(rsync_output.output)
281
Aviv Keshet60968ec2013-04-11 18:44:14 -0700282 change_report = ItemizeChangesFromRsyncOutput(rsync_output.output,
283 sysroot_autotest_path)
284
Aviv Keshet940c17f2013-04-11 18:41:42 -0700285 if not args.pretend:
286 UpdatePackageContents(change_report, AUTOTEST_TESTS_EBUILD,
287 sysroot_path)
Aviv Keshete00caeb2013-04-17 14:03:25 -0700288 if DowngradePackageVersion(sysroot_path, AUTOTEST_TESTS_EBUILD) != 0:
289 logging.warning('Unable to downgrade package %s version number.',
290 AUTOTEST_TESTS_EBUILD)
Aviv Keshetb1238c32013-04-01 11:42:13 -0700291
Aviv Keshet940c17f2013-04-11 18:41:42 -0700292 if args.pretend:
Aviv Keshete00caeb2013-04-17 14:03:25 -0700293 logging.info('The following message is pretend only. No filesystem '
294 'changes made.')
295 logging.info('Quickmerge complete. Created or modified %s files.',
296 len(change_report.new_files) + len(change_report.modified_files))
297
298 return 0
299
Aviv Keshetb1238c32013-04-01 11:42:13 -0700300
301if __name__ == '__main__':
302 sys.exit(main(sys.argv))