1 # Copyright (c) 2012 The Chromium OS Authors. All rights reserved.
2 # Use of this source code is governed by a BSD-style license that can be
3 # found in the LICENSE file.
5 """Calculate what workon packages have changed since the last build.
7 A workon package is treated as changed if any of the below are true:
8 1) The package is not installed.
9 2) A file exists in the associated repository which has a newer modification
10 time than the installed package.
11 3) The source ebuild has a newer modification time than the installed package.
14 - We do not look at eclasses. This replicates the existing behavior of the
15 commit queue, which also does not look at eclass changes.
16 - We do not try to fallback to the non-workon package if the local tree is
17 unmodified. This is probably a good thing, since developers who are
18 "working on" a package want to compile it locally.
19 - Portage only stores the time that a package finished building, so we
20 aren't able to detect when users modify source code during builds.
25 import multiprocessing
31 # Python-3 renamed to "queue". We still use Queue to avoid collisions
32 # with naming variables as "queue". Maybe we'll transition at some point.
33 # pylint: disable=F0401
36 from chromite.cbuildbot import constants
37 from chromite.cbuildbot import portage_utilities
38 from chromite.lib import cros_build_lib
39 from chromite.lib import git
40 from chromite.lib import osutils
41 from chromite.lib import parallel
44 class WorkonProjectsMonitor(object):
45 """Class for monitoring the last modification time of workon projects.
48 _tasks: A list of the (project, path) pairs to check.
49 _result_queue: A queue. When GetProjectModificationTimes is called,
50 (project, mtime) tuples are pushed onto the end of this queue.
53 def __init__(self, projects):
54 """Create a new object for checking what projects were modified and when.
57 projects: A list of the project names we are interested in monitoring.
59 manifest = git.ManifestCheckout.Cached(constants.SOURCE_ROOT)
61 for project in set(projects).intersection(manifest.checkouts_by_name):
62 for checkout in manifest.FindCheckouts(project):
63 self._tasks.append((project, checkout.GetPath(absolute=True)))
64 self._result_queue = multiprocessing.Queue(len(self._tasks))
66 def _EnqueueProjectModificationTime(self, project, path):
67 """Calculate the last time that this project was modified, and enqueue it.
70 project: The project to look at.
71 path: The path associated with the specified project.
73 if os.path.isdir(path):
74 self._result_queue.put((project, self._LastModificationTime(path)))
76 def _LastModificationTime(self, path):
77 """Calculate the last time a directory subtree was modified.
80 path: Directory to look at.
82 cmd = 'find . -name .git -prune -o -printf "%T@\n" | sort -nr | head -n1'
83 ret = cros_build_lib.RunCommand(cmd, cwd=path, shell=True, print_cmd=False,
85 return float(ret.output) if ret.output else 0
87 def GetProjectModificationTimes(self):
88 """Get the last modification time of each specified project.
91 A dictionary mapping project names to last modification times.
93 task = self._EnqueueProjectModificationTime
94 parallel.RunTasksInProcessPool(task, self._tasks)
96 # Create a dictionary mapping project names to last modification times.
97 # All of the workon projects are already stored in the queue, so we can
98 # retrieve them all without waiting any longer.
102 project, mtime = self._result_queue.get_nowait()
105 mtimes[project] = mtime
109 class WorkonPackageInfo(object):
110 """Class for getting information about workon packages.
113 cp: The package name (e.g. chromeos-base/power_manager).
114 mtime: The modification time of the installed package.
115 project: The project associated with the installed package.
116 src_ebuild_mtime: The modification time of the source ebuild.
119 def __init__(self, cp, mtime, projects, src_ebuild_mtime):
121 self.pkg_mtime = int(mtime)
122 self.projects = projects
123 self.src_ebuild_mtime = src_ebuild_mtime
126 def ListWorkonPackages(board, host, all_opt=False):
127 """List the packages that are currently being worked on.
130 board: The board to look at. If host is True, this should be set to None.
131 host: Whether to look at workon packages for the host.
132 all_opt: Pass --all to cros_workon. For testing purposes.
134 cmd = [os.path.join(constants.CROSUTILS_DIR, 'cros_workon'), 'list']
135 cmd.extend(['--host'] if host else ['--board', board])
138 result = cros_build_lib.RunCommand(cmd, print_cmd=False, capture_output=True)
139 return result.output.split()
142 def ListWorkonPackagesInfo(board, host):
143 """Find the specified workon packages for the specified board.
146 board: The board to look at. If host is True, this should be set to None.
147 host: Whether to look at workon packages for the host.
150 A list of unique packages being worked on.
152 # Import portage late so that this script can be imported outside the chroot.
153 # pylint: disable=F0401
155 packages = ListWorkonPackages(board, host)
159 install_root = cros_build_lib.GetSysroot(board=board)
160 vdb_path = os.path.join(install_root, portage.const.VDB_PATH)
161 buildroot, both = constants.SOURCE_ROOT, constants.BOTH_OVERLAYS
162 for overlay in portage_utilities.FindOverlays(both, board, buildroot):
163 for filename, projects in portage_utilities.GetWorkonProjectMap(overlay,
165 # chromeos-base/power_manager/power_manager-9999
166 # cp = chromeos-base/power_manager
167 # cpv = chromeos-base/power_manager-9999
168 category, pn, p = portage_utilities.SplitEbuildPath(filename)
169 cp = '%s/%s' % (category, pn)
170 cpv = '%s/%s' % (category, p)
172 # Get the time the package finished building. TODO(build): Teach Portage
173 # to store the time the package started building and use that here.
174 pkg_mtime_file = os.path.join(vdb_path, cpv, 'BUILD_TIME')
176 pkg_mtime = int(osutils.ReadFile(pkg_mtime_file))
177 except EnvironmentError as ex:
178 if ex.errno != errno.ENOENT:
182 # Get the modificaton time of the ebuild in the overlay.
183 src_ebuild_mtime = os.lstat(os.path.join(overlay, filename)).st_mtime
185 # Write info into the results dictionary, overwriting any previous
186 # values. This ensures that overlays override appropriately.
187 results[cp] = WorkonPackageInfo(cp, pkg_mtime, projects, src_ebuild_mtime)
189 return results.values()
192 def ListModifiedWorkonPackages(board, host):
193 """List the workon packages that need to be rebuilt.
196 board: The board to look at. If host is True, this should be set to None.
197 host: Whether to look at workon packages for the host.
199 packages = ListWorkonPackagesInfo(board, host)
202 for info in packages:
203 projects.extend(info.projects)
204 mtimes = WorkonProjectsMonitor(projects).GetProjectModificationTimes()
205 for info in packages:
206 mtime = int(max([mtimes.get(p, 0) for p in info.projects] +
207 [info.src_ebuild_mtime]))
208 if mtime >= info.pkg_mtime:
212 def _ParseArguments(argv):
213 parser = optparse.OptionParser(usage='USAGE: %prog [options]')
215 parser.add_option('--board', default=None,
218 parser.add_option('--host', default=False,
219 dest='host', action='store_true',
220 help='Look at host packages instead of board packages')
222 flags, remaining_arguments = parser.parse_args(argv)
223 if not flags.board and not flags.host:
225 cros_build_lib.Die('--board or --host is required')
226 if flags.board is not None and flags.host:
228 cros_build_lib.Die('--board and --host are mutually exclusive')
229 if remaining_arguments:
231 cros_build_lib.Die('Invalid arguments')
236 logging.getLogger().setLevel(logging.INFO)
237 flags = _ParseArguments(argv)
238 modified = ListModifiedWorkonPackages(flags.board, flags.host)
239 print ' '.join(sorted(modified))