Index: parallel_emerge
|
diff --git a/parallel_emerge b/parallel_emerge
|
index 82cac8846f69f3d5652f401cee3b3f2c62d4ee91..60f5eba3c57cc5249e42d32501a732efa1280063 100755
|
--- a/parallel_emerge
|
+++ b/parallel_emerge
|
@@ -6,7 +6,8 @@
|
"""Program to run emerge in parallel, for significant speedup.
|
|
Usage:
|
- ./parallel_emerge --board=BOARD [emerge args] package
|
+ ./parallel_emerge [--board=BOARD] [--workon=PKGS] [--no-workon-deps]
|
+ [emerge args] package"
|
|
Basic operation:
|
Runs 'emerge -p --debug' to display dependencies, and stores a
|
@@ -44,11 +45,25 @@ import subprocess
|
import sys
|
import tempfile
|
import time
|
+import _emerge.main
|
|
|
def Usage():
|
+ """Print usage."""
|
print "Usage:"
|
- print " ./parallel_emerge --board=BOARD --jobs=JOBS [emerge args] package"
|
+ print " ./parallel_emerge [--board=BOARD] [--workon=PKGS] [--no-workon-deps]"
|
+ print " [emerge args] package"
|
+ print
|
+ print "Packages specified as workon packages are always built from source."
|
+ print "Unless --no-workon-deps is specified, packages that depend on these"
|
+ print "packages are also built from source."
|
+ print
|
+ print "The --workon argument is mainly useful when you want to build and"
|
+ print "install packages that you are working on unconditionally, but do not"
|
+ print "to have to rev the package to indicate you want to build it from"
|
+ print "source. The build_packages script will automatically supply the"
|
+ print "workon argument to emerge, ensuring that packages selected using"
|
+ print "cros-workon are rebuilt."
|
sys.exit(1)
|
|
|
@@ -56,12 +71,6 @@ def Usage():
|
# but will prevent the package from installing.
|
secret_deps = {}
|
|
-# Globals: package we are building, board we are targeting,
|
-# emerge args we are passing through.
|
-PACKAGE = None
|
-EMERGE_ARGS = ""
|
-BOARD = None
|
-
|
# Runtime flags. TODO(): Maybe make these command-line options or
|
# environment variables.
|
VERBOSE = False
|
@@ -75,12 +84,8 @@ def ParseArgs(argv):
|
"""Set global vars based on command line.
|
|
We need to be compatible with emerge arg format.
|
- We scrape --board=XXX and --jobs=XXX, and distinguish between args
|
- and package names.
|
- TODO(): Robustify argument processing, as it's possible to
|
- pass in many two argument parameters that are difficult
|
- to programmatically identify, although we don't currently
|
- use any besides --with-bdeps <y|n>.
|
+ We scrape arguments that are specific to parallel_emerge, and pass through
|
+ the rest directly to emerge.
|
Args:
|
argv: arguments list
|
Returns:
|
@@ -88,37 +93,28 @@ def ParseArgs(argv):
|
"""
|
if VERBOSE:
|
print argv
|
- board_arg = None
|
- jobs_arg = 0
|
- package_args = []
|
- emerge_passthru_args = ""
|
+ workon_set = set()
|
+ myopts = {}
|
+ myopts["workon"] = workon_set
|
+ emerge_args = []
|
for arg in argv[1:]:
|
- # Specifically match "--board=" and "--jobs=".
|
+ # Specifically match arguments that are specific to parallel_emerge, and
|
+ # pass through the rest.
|
if arg.startswith("--board="):
|
- board_arg = arg.replace("--board=", "")
|
- elif arg.startswith("--jobs="):
|
- try:
|
- jobs_arg = int(arg.replace("--jobs=", ""))
|
- except ValueError:
|
- print "Unrecognized argument:", arg
|
- Usage()
|
- sys.exit(1)
|
- elif arg.startswith("-") or arg == "y" or arg == "n":
|
- # Not a package name, so pass through to emerge.
|
- emerge_passthru_args = emerge_passthru_args + " " + arg
|
+ myopts["board"] = arg.replace("--board=", "")
|
+ elif arg.startswith("--workon="):
|
+ workon_str = arg.replace("--workon=", "")
|
+ workon_set.update(shlex.split(" ".join(shlex.split(workon_str))))
|
+ elif arg == "--no-workon-deps":
|
+ myopts["no-workon-deps"] = True
|
else:
|
- package_args.append(arg)
|
-
|
- if not package_args and not emerge_passthru_args:
|
- Usage()
|
- sys.exit(1)
|
+ # Not a package name, so pass through to emerge.
|
+ emerge_args.append(arg)
|
|
- # Default to lots of jobs
|
- if jobs_arg <= 0:
|
- jobs_arg = 256
|
+ emerge_action, emerge_opts, emerge_files = _emerge.main.parse_opts(
|
+ emerge_args)
|
|
- # Set globals.
|
- return " ".join(package_args), emerge_passthru_args, board_arg, jobs_arg
|
+ return myopts, emerge_action, emerge_opts, emerge_files
|
|
|
def EmergeCommand():
|
@@ -130,9 +126,15 @@ def EmergeCommand():
|
string containing emerge command.
|
"""
|
emerge = "emerge"
|
- if BOARD:
|
- emerge += "-" + BOARD
|
- return emerge + " " + EMERGE_ARGS
|
+ if "board" in OPTS:
|
+ emerge += "-" + OPTS["board"]
|
+ cmd = [emerge]
|
+ for key, val in EMERGE_OPTS.items():
|
+ if val is True:
|
+ cmd.append(key)
|
+ else:
|
+ cmd.extend([key, str(val)])
|
+ return " ".join(cmd)
|
|
|
def GetDepsFromPortage(package):
|
@@ -147,7 +149,10 @@ def GetDepsFromPortage(package):
|
Text output of emerge -p --debug, which can be processed elsewhere.
|
"""
|
print "Calculating deps for package %s" % package
|
- cmdline = EmergeCommand() + " -p --debug --color=n " + package
|
+ cmdline = (EmergeCommand() + " -p --debug --color=n --with-bdeps=y " +
|
+ "--selective=n " + package)
|
+ if OPTS["workon"]:
|
+ cmdline += " " + " ".join(OPTS["workon"])
|
print "+ %s" % cmdline
|
|
# Store output in a temp file as it is too big for a unix pipe.
|
@@ -155,11 +160,11 @@ def GetDepsFromPortage(package):
|
stdout_buffer = tempfile.TemporaryFile()
|
# Launch the subprocess.
|
start = time.time()
|
- depsproc = subprocess.Popen(shlex.split(cmdline), stderr=stderr_buffer,
|
+ depsproc = subprocess.Popen(shlex.split(str(cmdline)), stderr=stderr_buffer,
|
stdout=stdout_buffer, bufsize=64*1024)
|
depsproc.wait()
|
seconds = time.time() - start
|
- print "Deps calculated in %d:%04.1fs" % (seconds / 60, seconds % 60)
|
+ print "Deps calculated in %dm%.1fs" % (seconds / 60, seconds % 60)
|
stderr_buffer.seek(0)
|
stderr_raw = stderr_buffer.read()
|
info_start = stderr_raw.find("digraph")
|
@@ -259,6 +264,10 @@ def DepsToTree(lines):
|
updatedep[fullpkg].setdefault("action", doins)
|
# Add the type of dep.
|
updatedep[fullpkg].setdefault("deptype", deptype)
|
+ # Add the long name of the package
|
+ updatedep[fullpkg].setdefault("pkgpath", "%s/%s" % (pkgdir, pkgname))
|
+ # Add the short name of the package
|
+ updatedep[fullpkg].setdefault("pkgname", pkgname)
|
|
# Drop any stack entries below our depth.
|
deps_stack = deps_stack[0:depth]
|
@@ -283,6 +292,8 @@ def DepsToTree(lines):
|
# Add the type of dep.
|
updatedep[pkgname].setdefault("action", "world")
|
updatedep[pkgname].setdefault("deptype", "normal")
|
+ updatedep[pkgname].setdefault("pkgpath", None)
|
+ updatedep[pkgname].setdefault("pkgname", None)
|
|
# Drop any obsolete stack entries.
|
deps_stack = deps_stack[0:depth]
|
@@ -297,12 +308,14 @@ def DepsToTree(lines):
|
uninstall = False
|
if oldversion and (desc.find("U") != -1 or desc.find("D") != -1):
|
uninstall = True
|
+ replace = desc.find("R") != -1
|
fullpkg = "%s/%s-%s" % (pkgdir, pkgname, version)
|
deps_info[fullpkg] = {"idx": len(deps_info),
|
"pkgdir": pkgdir,
|
"pkgname": pkgname,
|
"oldversion": oldversion,
|
- "uninstall": uninstall}
|
+ "uninstall": uninstall,
|
+ "replace": replace}
|
else:
|
# Is this a package that failed to match our huge regex?
|
m = re_failed.match(line)
|
@@ -328,17 +341,19 @@ def PrintTree(deps, depth=""):
|
PrintTree(deps[entry]["deps"], depth=depth + " ")
|
|
|
-def GenDependencyGraph(deps_tree, deps_info):
|
+def GenDependencyGraph(deps_tree, deps_info, package_names):
|
"""Generate a doubly linked dependency graph.
|
|
Args:
|
deps_tree: Dependency tree structure.
|
deps_info: More details on the dependencies.
|
+ package_names: Names of packages to add to the world file.
|
Returns:
|
Deps graph in the form of a dict of packages, with each package
|
specifying a "needs" list and "provides" list.
|
"""
|
deps_map = {}
|
+ pkgpaths = {}
|
|
def ReverseTree(packages):
|
"""Convert tree to digraph.
|
@@ -352,8 +367,13 @@ def GenDependencyGraph(deps_tree, deps_info):
|
"""
|
for pkg in packages:
|
action = packages[pkg]["action"]
|
+ pkgpath = packages[pkg]["pkgpath"]
|
+ pkgname = packages[pkg]["pkgname"]
|
+ pkgpaths[pkgpath] = pkg
|
+ pkgpaths[pkgname] = pkg
|
this_pkg = deps_map.setdefault(
|
- pkg, {"needs": set(), "provides": set(), "action": "nomerge"})
|
+ pkg, {"needs": {}, "provides": set(), "action": "nomerge",
|
+ "workon": False, "cmdline": False})
|
if action != "nomerge":
|
this_pkg["action"] = action
|
this_pkg["deps_info"] = deps_info.get(pkg)
|
@@ -363,14 +383,25 @@ def GenDependencyGraph(deps_tree, deps_info):
|
dep_type = dep_item["deptype"]
|
if dep_type != "(runtime_post)":
|
dep_pkg["provides"].add(pkg)
|
- this_pkg["needs"].add(dep)
|
+ this_pkg["needs"][dep] = dep_type
|
|
def RemoveInstalledPackages():
|
"""Remove installed packages, propagating dependencies."""
|
|
+ if "--selective" in EMERGE_OPTS:
|
+ selective = EMERGE_OPTS["--selective"] != "n"
|
+ else:
|
+ selective = "--noreplace" in EMERGE_OPTS or "--update" in EMERGE_OPTS
|
rm_pkgs = set(deps_map.keys()) - set(deps_info.keys())
|
+ for pkg, info in deps_info.items():
|
+ if selective and not deps_map[pkg]["workon"] and info["replace"]:
|
+ rm_pkgs.add(pkg)
|
for pkg in rm_pkgs:
|
this_pkg = deps_map[pkg]
|
+ if this_pkg["cmdline"] and "--oneshot" not in EMERGE_OPTS:
|
+ # If "cmdline" is set, this is a world update that was passed on the
|
+ # command-line. Keep these unless we're in --oneshot mode.
|
+ continue
|
needs = this_pkg["needs"]
|
provides = this_pkg["provides"]
|
for dep in needs:
|
@@ -381,47 +412,52 @@ def GenDependencyGraph(deps_tree, deps_info):
|
for target in provides:
|
target_needs = deps_map[target]["needs"]
|
target_needs.update(needs)
|
- target_needs.discard(pkg)
|
- target_needs.discard(target)
|
+ if pkg in target_needs:
|
+ del target_needs[pkg]
|
+ if target in target_needs:
|
+ del target_needs[target]
|
del deps_map[pkg]
|
|
- def SanitizeDep(basedep, currdep, oldstack, limit):
|
+ def SanitizeDep(basedep, currdep, visited, cycle):
|
"""Search for circular deps between basedep and currdep, then recurse.
|
|
Args:
|
basedep: Original dependency, top of stack.
|
currdep: Bottom of our current recursion, bottom of stack.
|
- oldstack: Current dependency chain.
|
- limit: How many more levels of recusion to go through, max.
|
+ visited: Nodes visited so far.
|
+ cycle: Array where cycle of circular dependencies should be stored.
|
TODO(): Break RDEPEND preferentially.
|
Returns:
|
True iff circular dependencies are found.
|
"""
|
- if limit == 0:
|
- return
|
- for dep in deps_map[currdep]["needs"]:
|
- stack = oldstack + [dep]
|
- if basedep in deps_map[dep]["needs"] or dep == basedep:
|
- if dep != basedep:
|
- stack += [basedep]
|
- print "Remove cyclic dependency from:"
|
- for i in xrange(0, len(stack) - 1):
|
- print " %s -> %s " % (stack[i], stack[i+1])
|
- return True
|
- if dep not in oldstack and SanitizeDep(basedep, dep, stack, limit - 1):
|
- return True
|
- return
|
+ if currdep not in visited:
|
+ visited.add(currdep)
|
+ for dep in deps_map[currdep]["needs"]:
|
+ if dep == basedep or SanitizeDep(basedep, dep, visited, cycle):
|
+ cycle.insert(0, dep)
|
+ return True
|
+ return False
|
|
def SanitizeTree():
|
- """Remove circular dependencies up to cycle length 32."""
|
+ """Remove circular dependencies."""
|
start = time.time()
|
for basedep in deps_map:
|
- for dep in deps_map[basedep]["needs"].copy():
|
- if deps_info[basedep]["idx"] <= deps_info[dep]["idx"]:
|
- if SanitizeDep(basedep, dep, [basedep, dep], 31):
|
- print "Breaking", basedep, " -> ", dep
|
- deps_map[basedep]["needs"].remove(dep)
|
- deps_map[dep]["provides"].remove(basedep)
|
+ this_pkg = deps_map[basedep]
|
+ if this_pkg["action"] == "world":
|
+ # world file updates can't be involved in cycles,
|
+ # and they don't have deps_info, so skip them.
|
+ continue
|
+ for dep in this_pkg["needs"].copy():
|
+ cycle = []
|
+ if (deps_info[basedep]["idx"] <= deps_info[dep]["idx"] and
|
+ SanitizeDep(basedep, dep, set(), cycle)):
|
+ cycle[:0] = [basedep, dep]
|
+ print "Breaking cycle:"
|
+ for i in range(len(cycle) - 1):
|
+ deptype = deps_map[cycle[i]]["needs"][cycle[i+1]]
|
+ print " %s -> %s %s" % (cycle[i], cycle[i+1], deptype)
|
+ del this_pkg["needs"][dep]
|
+ deps_map[dep]["provides"].remove(basedep)
|
seconds = time.time() - start
|
print "Tree sanitized in %d:%04.1fs" % (seconds / 60, seconds % 60)
|
|
@@ -443,8 +479,49 @@ def GenDependencyGraph(deps_tree, deps_info):
|
deps_map[needed_pkg]["provides"].add(bad_pkg)
|
deps_map[bad_pkg]["needs"].add(needed_pkg)
|
|
+ def WorkOnChildren(pkg):
|
+ """Mark this package and all packages it provides as workon packages."""
|
+
|
+ this_pkg = deps_map[pkg]
|
+ if this_pkg["workon"]:
|
+ return False
|
+
|
+ this_pkg["workon"] = True
|
+ updated = False
|
+ for w in this_pkg["provides"]:
|
+ if WorkOnChildren(w):
|
+ updated = True
|
+
|
+ if this_pkg["action"] == "nomerge":
|
+ pkgpath = deps_tree[pkg]["pkgpath"]
|
+ if pkgpath is not None:
|
+ OPTS["workon"].add(pkgpath)
|
+ updated = True
|
+
|
+ return updated
|
+
|
ReverseTree(deps_tree)
|
AddSecretDeps()
|
+
|
+ if "no-workon-deps" in OPTS:
|
+ for pkgpath in OPTS["workon"].copy():
|
+ pkg = pkgpaths[pkgpath]
|
+ deps_map[pkg]["workon"] = True
|
+ else:
|
+ mergelist_updated = False
|
+ for pkgpath in OPTS["workon"].copy():
|
+ pkg = pkgpaths[pkgpath]
|
+ if WorkOnChildren(pkg):
|
+ mergelist_updated = True
|
+ if mergelist_updated:
|
+ print "List of packages to merge updated. Recalculate dependencies..."
|
+ return None
|
+
|
+ for pkgpath in package_names:
|
+ dep_pkg = deps_map.get("original-%s" % pkgpath)
|
+ if dep_pkg and len(dep_pkg["needs"]) == 1:
|
+ dep_pkg["cmdline"] = True
|
+
|
RemoveInstalledPackages()
|
SanitizeTree()
|
return deps_map
|
@@ -477,17 +554,17 @@ class EmergeQueue(object):
|
self._failed = {}
|
|
def _LoadAvg(self):
|
- loads = open('/proc/loadavg', 'r').readline().split()[:3]
|
- return ' '.join(loads)
|
+ loads = open("/proc/loadavg", "r").readline().split()[:3]
|
+ return " ".join(loads)
|
|
def _Status(self):
|
"""Print status."""
|
seconds = time.time() - GLOBAL_START
|
- print "Pending %s, Ready %s, Running %s, Retrying %s, Total %s " \
|
- "[Time %dm%ds Load %s]" % (
|
- len(self._deps_map), len(self._emerge_queue),
|
- len(self._jobs), len(self._retry_queue), self._total_jobs,
|
- seconds / 60, seconds % 60, self._LoadAvg())
|
+ line = ("Pending %s, Ready %s, Running %s, Retrying %s, Total %s "
|
+ "[Time %dm%ds Load %s]")
|
+ print line % (len(self._deps_map), len(self._emerge_queue),
|
+ len(self._jobs), len(self._retry_queue), self._total_jobs,
|
+ seconds / 60, seconds % 60, self._LoadAvg())
|
|
def _LaunchOneEmerge(self, target):
|
"""Run emerge --nodeps to do a single package install.
|
@@ -504,20 +581,35 @@ class EmergeQueue(object):
|
# "original-" signifies one of the packages we originally requested.
|
# Since we have explicitly installed the versioned package as a dep of
|
# this, we only need to tag in "world" that we are done with this
|
- # install request. "--select -n" indicates an addition to "world"
|
- # without an actual install.
|
+ # install request.
|
+ # --nodeps: Ignore dependencies -- we handle them internally.
|
+ # --noreplace: Don't replace or upgrade any packages. (In this case, the
|
+ # package is already installed, so we are just updating the
|
+ # world file.)
|
+ # --selective: Make sure that --noreplace sticks even if --selective=n is
|
+ # specified by the user on the command-line.
|
+ # NOTE: If the user specifies --oneshot on the command-line, this command
|
+ # will do nothing. That is desired, since the user requested not to
|
+ # update the world file.
|
newtarget = target.replace("original-", "")
|
- cmdline = EmergeCommand() + " --nodeps --select --noreplace " + newtarget
|
+ cmdline = (EmergeCommand() + " --nodeps --selective --noreplace " +
|
+ newtarget)
|
else:
|
# This package is a dependency of something we specifically
|
# requested. Therefore we should install it but not allow it
|
- # in the "world" file, which represents explicit intalls.
|
- # "--oneshot" here will prevent it from being tagged in world.
|
- cmdline = EmergeCommand() + " --nodeps --oneshot =" + target
|
- deps_info = self._deps_map[target]["deps_info"]
|
+ # in the "world" file, which represents explicit installs.
|
+ # --oneshot" here will prevent it from being tagged in world.
|
+ cmdline = EmergeCommand() + " --nodeps --oneshot "
|
+ this_pkg = self._deps_map[target]
|
+ if this_pkg["workon"]:
|
+ # --usepkg=n --getbinpkg=n: Build from source
|
+ # --selective=n: Re-emerge even if package is already installed.
|
+ cmdline += "--usepkg=n --getbinpkg=n --selective=n "
|
+ cmdline += "=" + target
|
+ deps_info = this_pkg["deps_info"]
|
if deps_info["uninstall"]:
|
package = "%(pkgdir)s/%(pkgname)s-%(oldversion)s" % deps_info
|
- cmdline += " && %s -1C =%s" % (EmergeCommand(), package)
|
+ cmdline += " && %s -C =%s" % (EmergeCommand(), package)
|
|
print "+ %s" % cmdline
|
|
@@ -543,7 +635,7 @@ class EmergeQueue(object):
|
def _Finish(self, target):
|
"""Mark a target as completed and unblock dependecies."""
|
for dep in self._deps_map[target]["provides"]:
|
- self._deps_map[dep]["needs"].remove(target)
|
+ del self._deps_map[dep]["needs"][target]
|
if not self._deps_map[dep]["needs"]:
|
if VERBOSE:
|
print "Unblocking %s" % dep
|
@@ -563,9 +655,10 @@ class EmergeQueue(object):
|
dependency graph to merge.
|
"""
|
secs = 0
|
+ max_jobs = EMERGE_OPTS.get("--jobs", 256)
|
while self._deps_map:
|
# If we have packages that are ready, kick them off.
|
- if self._emerge_queue and len(self._jobs) < JOBS:
|
+ if self._emerge_queue and len(self._jobs) < max_jobs:
|
target = self._emerge_queue.pop(0)
|
action = self._deps_map[target]["action"]
|
# We maintain a tree of all deps, if this doesn't need
|
@@ -653,25 +746,43 @@ class EmergeQueue(object):
|
|
|
# Main control code.
|
-PACKAGE, EMERGE_ARGS, BOARD, JOBS = ParseArgs(sys.argv)
|
+OPTS, EMERGE_ACTION, EMERGE_OPTS, EMERGE_FILES = ParseArgs(sys.argv)
|
|
-if not PACKAGE:
|
- # No packages. Pass straight through to emerge.
|
- # Allows users to just type ./parallel_emerge --depclean
|
+if EMERGE_ACTION is not None:
|
+ # Pass action arguments straight through to emerge
|
+ EMERGE_OPTS["--%s" % EMERGE_ACTION] = True
|
sys.exit(os.system(EmergeCommand()))
|
+elif not EMERGE_FILES:
|
+ Usage()
|
+ sys.exit(1)
|
|
print "Starting fast-emerge."
|
-print " Building package %s on %s (%s)" % (PACKAGE, EMERGE_ARGS, BOARD)
|
-print "Running emerge to generate deps"
|
-deps_output = GetDepsFromPortage(PACKAGE)
|
-print "Processing emerge output"
|
-dependency_tree, dependency_info = DepsToTree(deps_output)
|
-if VERBOSE:
|
- print "Print tree"
|
- PrintTree(dependency_tree)
|
+print " Building package %s on %s" % (" ".join(EMERGE_FILES),
|
+ OPTS.get("board", "root"))
|
|
-print "Generate dependency graph."
|
-dependency_graph = GenDependencyGraph(dependency_tree, dependency_info)
|
+# If the user supplied the --workon option, we may have to run emerge twice
|
+# to generate a dependency ordering for packages that depend on the workon
|
+# packages.
|
+for it in range(2):
|
+ print "Running emerge to generate deps"
|
+ deps_output = GetDepsFromPortage(" ".join(EMERGE_FILES))
|
+
|
+ print "Processing emerge output"
|
+ dependency_tree, dependency_info = DepsToTree(deps_output)
|
+
|
+ if VERBOSE:
|
+ print "Print tree"
|
+ PrintTree(dependency_tree)
|
+
|
+ print "Generate dependency graph."
|
+ dependency_graph = GenDependencyGraph(dependency_tree, dependency_info,
|
+ EMERGE_FILES)
|
+
|
+ if dependency_graph is not None:
|
+ break
|
+else:
|
+ print "Can't crack cycle"
|
+ sys.exit(1)
|
|
if VERBOSE:
|
PrintDepsMap(dependency_graph)
|
|