blob: 9cbf8964f7afe298f869ddfb3c54f3aa74301047 [file] [log] [blame]
# Copyright 2012 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Calculate what workon packages have changed since the last build.
A workon package is treated as changed if any of the below are true:
1) The package is not installed.
2) A file exists in the associated repository which has a newer modification
time than the installed package.
3) The source ebuild has a newer modification time than the installed package.
Some caveats:
- We do not look at eclasses. This replicates the existing behavior of the
commit queue, which also does not look at eclass changes.
- We do not try to fall back to the non-workon package if the local tree is
unmodified. This is probably a good thing, since developers who are
"working on" a package want to compile it locally.
- Portage only stores the time that a package finished building, so we
aren't able to detect when users modify source code during builds.
"""
import errno
import logging
import multiprocessing
import os
import queue as Queue
from chromite.lib import build_target_lib
from chromite.lib import commandline
from chromite.lib import constants
from chromite.lib import cros_build_lib
from chromite.lib import git
from chromite.lib import osutils
from chromite.lib import parallel
from chromite.lib import portage_util
from chromite.lib import sysroot_lib
from chromite.lib import workon_helper
class ModificationTimeMonitor:
"""Base class for monitoring last modification time of paths.
This takes a list of (keys, path) pairs and finds the latest mtime of an
object within each of the path's subtrees, populating a map from keys to
mtimes. Note that a key may be associated with multiple paths, in which case
the latest mtime among them will be returned.
Attributes:
_tasks: A list of (key, path) pairs to check.
_result_queue: A queue populated with corresponding (key, mtime) pairs.
"""
def __init__(self, key_path_pairs) -> None:
self._tasks = list(key_path_pairs)
self._result_queue = multiprocessing.Queue(len(self._tasks))
def _EnqueueModificationTime(self, key, path) -> None:
"""Calculate the last modification time of |path| and enqueue it."""
if os.path.isdir(path):
self._result_queue.put((key, self._LastModificationTime(path)))
def _LastModificationTime(self, path):
"""Returns the latest modification time for anything under |path|."""
cmd = (
'find . -name .git -prune -o -printf "%T@\n" | sort -nr | head -n1'
)
ret = cros_build_lib.run(
cmd, cwd=path, shell=True, print_cmd=False, capture_output=True
)
return float(ret.stdout) if ret.stdout else 0
def GetModificationTimes(self):
"""Get the latest modification time for each of the queued keys."""
parallel.RunTasksInProcessPool(
self._EnqueueModificationTime, self._tasks
)
mtimes = {}
try:
while True:
key, mtime = self._result_queue.get_nowait()
mtimes[key] = max((mtimes.get(key, 0), mtime))
except Queue.Empty:
return mtimes
class WorkonPackageInfo:
"""Class for getting information about workon packages.
Attributes:
cp: The package name (e.g. chromeos-base/power_manager).
pkg_mtime: The modification time of the installed package.
projects: The project(s) associated with the package.
src_ebuild_mtime: The modification time of the source ebuild.
"""
def __init__(self, cp, mtime, projects, src_ebuild_mtime) -> None:
self.cp = cp
self.pkg_mtime = int(mtime)
self.projects = projects
self.src_ebuild_mtime = src_ebuild_mtime
def ListWorkonPackages(sysroot, all_opt=False):
"""List the packages that are currently being worked on.
Args:
sysroot: sysroot_lib.Sysroot object.
all_opt: Pass --all to cros_workon. For testing purposes.
"""
helper = workon_helper.WorkonHelper(sysroot.path)
return helper.ListAtoms(use_all=all_opt)
def ListWorkonPackagesInfo(sysroot):
"""Find the specified workon packages for the specified board.
Args:
sysroot: sysroot_lib.Sysroot object.
Returns:
A list of WorkonPackageInfo objects for unique packages being worked on.
"""
packages = ListWorkonPackages(sysroot)
if not packages:
return []
results = {}
if sysroot.path == "/":
overlays = portage_util.FindOverlays(constants.BOTH_OVERLAYS, None)
else:
overlays = sysroot.portdir_overlay
vdb_path = os.path.join(sysroot.path, portage_util.VDB_PATH)
for overlay in overlays:
for filename, projects in portage_util.GetWorkonProjectMap(
overlay, packages
):
# chromeos-base/power_manager/power_manager-9999
# cp = chromeos-base/power_manager
# cpv = chromeos-base/power_manager-9999
category, pn, p = portage_util.SplitEbuildPath(filename)
cp = "%s/%s" % (category, pn)
cpv = "%s/%s" % (category, p)
# Get the time the package finished building. TODO(build): Teach
# Portage to store the time the package started building and use
# that here.
pkg_mtime_file = os.path.join(vdb_path, cpv, "BUILD_TIME")
try:
pkg_mtime = int(osutils.ReadFile(pkg_mtime_file))
except EnvironmentError as ex:
if ex.errno != errno.ENOENT:
raise
pkg_mtime = 0
# Get the modification time of the ebuild in the overlay.
src_ebuild_mtime = os.lstat(
os.path.join(overlay, filename)
).st_mtime
# Write info into the results dictionary, overwriting any previous
# values. This ensures that overlays override appropriately.
results[cp] = WorkonPackageInfo(
cp, pkg_mtime, projects, src_ebuild_mtime
)
return list(results.values())
def WorkonProjectsMonitor(projects):
"""Returns a monitor for project modification times."""
# TODO(garnold) In order for the mtime monitor to be as accurate as
# possible, this only needs to enqueue the checkout(s) relevant for the
# task at hand, e.g. the specific ebuild we want to emerge. In general, the
# CROS_WORKON_LOCALNAME variable in workon ebuilds defines the source path
# uniquely and can be used for this purpose.
project_path_pairs = []
manifest = git.ManifestCheckout.Cached(constants.SOURCE_ROOT)
for project in set(projects).intersection(manifest.checkouts_by_name):
for checkout in manifest.FindCheckouts(project):
project_path_pairs.append(
(project, checkout.GetPath(absolute=True))
)
return ModificationTimeMonitor(project_path_pairs)
def ListModifiedWorkonPackages(sysroot):
"""List the workon packages that need to be rebuilt.
Args:
sysroot: sysroot_lib.Sysroot object.
"""
packages = ListWorkonPackagesInfo(sysroot)
if not packages:
return
# Get mtimes for all projects and source paths associated with our packages.
all_projects = [p for info in packages for p in info.projects]
project_mtimes = WorkonProjectsMonitor(all_projects).GetModificationTimes()
for info in packages:
mtime = int(
max(
[project_mtimes.get(p, 0) for p in info.projects]
+ [info.src_ebuild_mtime]
)
)
if mtime >= info.pkg_mtime:
yield info.cp
def _ParseArguments(argv):
parser = commandline.ArgumentParser(description=__doc__)
target = parser.add_mutually_exclusive_group(required=True)
target.add_argument("--board", help="Board name")
target.add_argument(
"--host",
default=False,
action="store_true",
help="Look at host packages instead of board packages",
)
target.add_argument("--sysroot", help="Sysroot path.")
flags = parser.parse_args(argv)
flags.Freeze()
return flags
def main(argv) -> None:
commandline.RunInsideChroot()
logging.getLogger().setLevel(logging.INFO)
flags = _ParseArguments(argv)
if flags.board:
sysroot = build_target_lib.get_default_sysroot_path(flags.board)
elif flags.host:
sysroot = "/"
else:
sysroot = flags.sysroot
modified = ListModifiedWorkonPackages(sysroot_lib.Sysroot(sysroot))
print(" ".join(sorted(modified)))