mirror of
https://github.com/flatcar/scripts.git
synced 2025-11-18 17:12:08 +01:00
Update parallel_emerge to support --force-remote-binary.
This feature is useful if you want to stick to an old binary package of a package you're not working on. BUG=chromium-os:8769 TEST=./parallel_emerge --board=x86-mario -uDNvg chromeos --force-remote-binary=chromeos-chrome --force-remote-binary=libcros Change-Id: I7d3011fa64134158ed848f136bc75e09b0af438e Review URL: http://codereview.chromium.org/4555002
This commit is contained in:
parent
170bde0fd4
commit
6418c50450
215
parallel_emerge
215
parallel_emerge
@ -7,7 +7,7 @@
|
|||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
./parallel_emerge [--board=BOARD] [--workon=PKGS] [--no-workon-deps]
|
./parallel_emerge [--board=BOARD] [--workon=PKGS] [--no-workon-deps]
|
||||||
[emerge args] package"
|
[--force-remote-binary=PKGS] [emerge args] package
|
||||||
|
|
||||||
Basic operation:
|
Basic operation:
|
||||||
Runs 'emerge -p --debug' to display dependencies, and stores a
|
Runs 'emerge -p --debug' to display dependencies, and stores a
|
||||||
@ -84,6 +84,7 @@ from _emerge.Scheduler import Scheduler
|
|||||||
from _emerge.stdout_spinner import stdout_spinner
|
from _emerge.stdout_spinner import stdout_spinner
|
||||||
import portage
|
import portage
|
||||||
import portage.debug
|
import portage.debug
|
||||||
|
import portage.versions
|
||||||
|
|
||||||
|
|
||||||
def Usage():
|
def Usage():
|
||||||
@ -218,7 +219,8 @@ class DepGraphGenerator(object):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
__slots__ = ["board", "emerge", "mandatory_source", "no_workon_deps",
|
__slots__ = ["board", "emerge", "mandatory_source", "no_workon_deps",
|
||||||
"nomerge", "package_db", "rebuild", "show_output"]
|
"nomerge", "package_db", "rebuild", "show_output",
|
||||||
|
"force_remote_binary", "forced_remote_binary_packages"]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.board = None
|
self.board = None
|
||||||
@ -229,6 +231,8 @@ class DepGraphGenerator(object):
|
|||||||
self.package_db = {}
|
self.package_db = {}
|
||||||
self.rebuild = False
|
self.rebuild = False
|
||||||
self.show_output = False
|
self.show_output = False
|
||||||
|
self.force_remote_binary = set()
|
||||||
|
self.forced_remote_binary_packages = set()
|
||||||
|
|
||||||
def ParseParallelEmergeArgs(self, argv):
|
def ParseParallelEmergeArgs(self, argv):
|
||||||
"""Read the parallel emerge arguments from the command-line.
|
"""Read the parallel emerge arguments from the command-line.
|
||||||
@ -251,6 +255,11 @@ class DepGraphGenerator(object):
|
|||||||
workon_str = arg.replace("--workon=", "")
|
workon_str = arg.replace("--workon=", "")
|
||||||
package_list = shlex.split(" ".join(shlex.split(workon_str)))
|
package_list = shlex.split(" ".join(shlex.split(workon_str)))
|
||||||
self.mandatory_source.update(package_list)
|
self.mandatory_source.update(package_list)
|
||||||
|
elif arg.startswith("--force-remote-binary="):
|
||||||
|
force_remote_binary = arg.replace("--force-remote-binary=", "")
|
||||||
|
force_remote_binary = \
|
||||||
|
shlex.split(" ".join(shlex.split(force_remote_binary)))
|
||||||
|
self.force_remote_binary.update(force_remote_binary)
|
||||||
elif arg.startswith("--nomerge="):
|
elif arg.startswith("--nomerge="):
|
||||||
nomerge_str = arg.replace("--nomerge=", "")
|
nomerge_str = arg.replace("--nomerge=", "")
|
||||||
package_list = shlex.split(" ".join(shlex.split(nomerge_str)))
|
package_list = shlex.split(" ".join(shlex.split(nomerge_str)))
|
||||||
@ -460,7 +469,7 @@ class DepGraphGenerator(object):
|
|||||||
cur_iuse, now_use, now_iuse)
|
cur_iuse, now_use, now_iuse)
|
||||||
return not flags
|
return not flags
|
||||||
|
|
||||||
def GenDependencyTree(self):
|
def GenDependencyTree(self, remote_pkgs):
|
||||||
"""Get dependency tree info from emerge.
|
"""Get dependency tree info from emerge.
|
||||||
|
|
||||||
TODO(): Update cros_extract_deps to also use this code.
|
TODO(): Update cros_extract_deps to also use this code.
|
||||||
@ -479,10 +488,7 @@ class DepGraphGenerator(object):
|
|||||||
# --workon and the dependencies have changed.
|
# --workon and the dependencies have changed.
|
||||||
emerge = self.emerge
|
emerge = self.emerge
|
||||||
emerge_opts = emerge.opts.copy()
|
emerge_opts = emerge.opts.copy()
|
||||||
emerge_opts.pop("--getbinpkg", None)
|
if self.mandatory_source or self.rebuild or self.force_remote_binary:
|
||||||
if "--usepkgonly" not in emerge_opts:
|
|
||||||
emerge_opts.pop("--usepkg", None)
|
|
||||||
if self.mandatory_source or self.rebuild:
|
|
||||||
# Enable --emptytree so that we get the full tree, which we need for
|
# Enable --emptytree so that we get the full tree, which we need for
|
||||||
# dependency analysis. By default, with this option, emerge optimizes
|
# dependency analysis. By default, with this option, emerge optimizes
|
||||||
# the graph by removing uninstall instructions from the graph. By
|
# the graph by removing uninstall instructions from the graph. By
|
||||||
@ -491,10 +497,30 @@ class DepGraphGenerator(object):
|
|||||||
emerge_opts["--tree"] = True
|
emerge_opts["--tree"] = True
|
||||||
emerge_opts["--emptytree"] = True
|
emerge_opts["--emptytree"] = True
|
||||||
|
|
||||||
|
# Tell emerge not to worry about use flags yet. We handle those inside
|
||||||
|
# parallel_emerge itself. Further, when we use the --force-remote-binary
|
||||||
|
# flag, we don't emerge to reject a package just because it has different
|
||||||
|
# use flags.
|
||||||
|
emerge_opts.pop("--newuse", None)
|
||||||
|
emerge_opts.pop("--reinstall", None)
|
||||||
|
|
||||||
# Create a list of packages to merge
|
# Create a list of packages to merge
|
||||||
packages = set(emerge.cmdline_packages[:])
|
packages = set(emerge.cmdline_packages[:])
|
||||||
if self.mandatory_source:
|
if self.mandatory_source:
|
||||||
packages.update(self.mandatory_source)
|
packages.update(self.mandatory_source)
|
||||||
|
if self.force_remote_binary:
|
||||||
|
forced_pkgs = {}
|
||||||
|
for pkg in remote_pkgs:
|
||||||
|
category, pkgname, _, _ = portage.catpkgsplit(pkg)
|
||||||
|
full_pkgname = "%s/%s" % (category, pkgname)
|
||||||
|
if (pkgname in self.force_remote_binary or
|
||||||
|
full_pkgname in self.force_remote_binary):
|
||||||
|
forced_pkgs.setdefault(full_pkgname, []).append(pkg)
|
||||||
|
|
||||||
|
for pkgs in forced_pkgs.values():
|
||||||
|
forced_package = portage.versions.best(pkgs)
|
||||||
|
packages.add("=%s" % forced_package)
|
||||||
|
self.forced_remote_binary_packages.add(forced_package)
|
||||||
|
|
||||||
# Tell emerge to be quiet. We print plenty of info ourselves so we don't
|
# Tell emerge to be quiet. We print plenty of info ourselves so we don't
|
||||||
# need any extra output from portage.
|
# need any extra output from portage.
|
||||||
@ -580,9 +606,6 @@ class DepGraphGenerator(object):
|
|||||||
optional = True
|
optional = True
|
||||||
break
|
break
|
||||||
|
|
||||||
# Add the package to our database.
|
|
||||||
self.package_db[str(pkg.cpv)] = pkg
|
|
||||||
|
|
||||||
# Save off info about the package
|
# Save off info about the package
|
||||||
deps_info[str(pkg.cpv)] = {"idx": len(deps_info),
|
deps_info[str(pkg.cpv)] = {"idx": len(deps_info),
|
||||||
"optional": optional}
|
"optional": optional}
|
||||||
@ -611,7 +634,65 @@ class DepGraphGenerator(object):
|
|||||||
print "%s %s (%s)" % (depth, entry, action)
|
print "%s %s (%s)" % (depth, entry, action)
|
||||||
self.PrintTree(deps[entry]["deps"], depth=depth + " ")
|
self.PrintTree(deps[entry]["deps"], depth=depth + " ")
|
||||||
|
|
||||||
def GenDependencyGraph(self, deps_tree, deps_info):
|
def RemotePackageDatabase(self, binhost_url):
|
||||||
|
"""Grab the latest binary package database from the prebuilt server.
|
||||||
|
|
||||||
|
We need to know the modification times of the prebuilt packages so that we
|
||||||
|
know when it is OK to use these packages and when we should rebuild them
|
||||||
|
instead.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
binhost_url: Base URL of remote packages (PORTAGE_BINHOST).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A dict mapping package identifiers to modification times.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not binhost_url:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def retry_urlopen(url, tries=3):
|
||||||
|
"""Open the specified url, retrying if we run into network errors.
|
||||||
|
|
||||||
|
We do not retry for HTTP errors.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
url: The specified url.
|
||||||
|
tries: The number of times to try.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The result of urllib2.urlopen(url).
|
||||||
|
"""
|
||||||
|
for i in range(tries):
|
||||||
|
try:
|
||||||
|
return urllib2.urlopen(url)
|
||||||
|
except urllib2.HTTPError as e:
|
||||||
|
raise
|
||||||
|
except urllib2.URLError as e:
|
||||||
|
if i + 1 == tries:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
print "Cannot GET %s: %s" % (url, e)
|
||||||
|
|
||||||
|
url = os.path.join(binhost_url, "Packages")
|
||||||
|
try:
|
||||||
|
f = retry_urlopen(url)
|
||||||
|
except urllib2.HTTPError as e:
|
||||||
|
if e.code == 404:
|
||||||
|
return {}
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
prebuilt_pkgs = {}
|
||||||
|
for line in f:
|
||||||
|
if line.startswith("CPV: "):
|
||||||
|
pkg = line.replace("CPV: ", "").rstrip()
|
||||||
|
elif line.startswith("MTIME: "):
|
||||||
|
prebuilt_pkgs[pkg] = int(line[:-1].replace("MTIME: ", ""))
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
return prebuilt_pkgs
|
||||||
|
|
||||||
|
def GenDependencyGraph(self, deps_tree, deps_info, remote_pkgs):
|
||||||
"""Generate a doubly linked dependency graph.
|
"""Generate a doubly linked dependency graph.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -660,6 +741,10 @@ class DepGraphGenerator(object):
|
|||||||
# If true, indicates that this package must be installed. We don't care
|
# If true, indicates that this package must be installed. We don't care
|
||||||
# whether it's binary or source, unless the mandatory_source flag is
|
# whether it's binary or source, unless the mandatory_source flag is
|
||||||
# also set.
|
# also set.
|
||||||
|
# - force_remote_binary:
|
||||||
|
# If true, indicates that we want to update to the latest remote prebuilt
|
||||||
|
# of this package. Packages that depend on this package should be built
|
||||||
|
# from source.
|
||||||
#
|
#
|
||||||
deps_map = {}
|
deps_map = {}
|
||||||
|
|
||||||
@ -678,7 +763,8 @@ class DepGraphGenerator(object):
|
|||||||
# Create an entry for the package
|
# Create an entry for the package
|
||||||
action = packages[pkg]["action"]
|
action = packages[pkg]["action"]
|
||||||
default_pkg = {"needs": {}, "provides": set(), "action": action,
|
default_pkg = {"needs": {}, "provides": set(), "action": action,
|
||||||
"mandatory_source": False, "mandatory": False}
|
"mandatory_source": False, "mandatory": False,
|
||||||
|
"force_remote_binary": False}
|
||||||
this_pkg = deps_map.setdefault(pkg, default_pkg)
|
this_pkg = deps_map.setdefault(pkg, default_pkg)
|
||||||
|
|
||||||
# Create entries for dependencies of this package first.
|
# Create entries for dependencies of this package first.
|
||||||
@ -909,64 +995,6 @@ class DepGraphGenerator(object):
|
|||||||
if this_pkg["action"] == "nomerge":
|
if this_pkg["action"] == "nomerge":
|
||||||
this_pkg["action"] = "merge"
|
this_pkg["action"] = "merge"
|
||||||
|
|
||||||
def RemotePackageDatabase(binhost_url):
|
|
||||||
"""Grab the latest binary package database from the prebuilt server.
|
|
||||||
|
|
||||||
We need to know the modification times of the prebuilt packages so that we
|
|
||||||
know when it is OK to use these packages and when we should rebuild them
|
|
||||||
instead.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
binhost_url: Base URL of remote packages (PORTAGE_BINHOST).
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A dict mapping package identifiers to modification times.
|
|
||||||
"""
|
|
||||||
|
|
||||||
if not binhost_url:
|
|
||||||
return {}
|
|
||||||
|
|
||||||
def retry_urlopen(url, tries=3):
|
|
||||||
"""Open the specified url, retrying if we run into network errors.
|
|
||||||
|
|
||||||
We do not retry for HTTP errors.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
url: The specified url.
|
|
||||||
tries: The number of times to try.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The result of urllib2.urlopen(url).
|
|
||||||
"""
|
|
||||||
for i in range(tries):
|
|
||||||
try:
|
|
||||||
return urllib2.urlopen(url)
|
|
||||||
except urllib2.HTTPError as e:
|
|
||||||
raise
|
|
||||||
except urllib2.URLError as e:
|
|
||||||
if i + 1 == tries:
|
|
||||||
raise
|
|
||||||
else:
|
|
||||||
print "Cannot GET %s: %s" % (url, e)
|
|
||||||
|
|
||||||
url = binhost_url + "/Packages"
|
|
||||||
try:
|
|
||||||
f = retry_urlopen(url)
|
|
||||||
except urllib2.HTTPError as e:
|
|
||||||
if e.code == 404:
|
|
||||||
return {}
|
|
||||||
else:
|
|
||||||
raise
|
|
||||||
prebuilt_pkgs = {}
|
|
||||||
for line in f:
|
|
||||||
if line.startswith("CPV: "):
|
|
||||||
pkg = line.replace("CPV: ", "").rstrip()
|
|
||||||
elif line.startswith("MTIME: "):
|
|
||||||
prebuilt_pkgs[pkg] = int(line[:-1].replace("MTIME: ", ""))
|
|
||||||
f.close()
|
|
||||||
|
|
||||||
return prebuilt_pkgs
|
|
||||||
|
|
||||||
def LocalPackageDatabase():
|
def LocalPackageDatabase():
|
||||||
"""Get the modification times of the packages in the local database.
|
"""Get the modification times of the packages in the local database.
|
||||||
|
|
||||||
@ -1019,7 +1047,7 @@ class DepGraphGenerator(object):
|
|||||||
"""
|
"""
|
||||||
if pkg in cache:
|
if pkg in cache:
|
||||||
return cache[pkg]
|
return cache[pkg]
|
||||||
if pkg not in pkg_db:
|
if pkg not in pkg_db and pkg not in self.forced_remote_binary_packages:
|
||||||
cache[pkg] = False
|
cache[pkg] = False
|
||||||
else:
|
else:
|
||||||
cache[pkg] = True
|
cache[pkg] = True
|
||||||
@ -1081,7 +1109,7 @@ class DepGraphGenerator(object):
|
|||||||
else:
|
else:
|
||||||
MergeChildren(pkg, "mandatory_source")
|
MergeChildren(pkg, "mandatory_source")
|
||||||
|
|
||||||
def UsePrebuiltPackages():
|
def UsePrebuiltPackages(remote_pkgs):
|
||||||
"""Update packages that can use prebuilts to do so."""
|
"""Update packages that can use prebuilts to do so."""
|
||||||
start = time.time()
|
start = time.time()
|
||||||
|
|
||||||
@ -1099,13 +1127,18 @@ class DepGraphGenerator(object):
|
|||||||
|
|
||||||
# Build list of prebuilt packages
|
# Build list of prebuilt packages
|
||||||
for pkg, info in deps_map.iteritems():
|
for pkg, info in deps_map.iteritems():
|
||||||
if info and not info["mandatory_source"] and info["action"] == "merge":
|
if info and info["action"] == "merge":
|
||||||
|
if (not info["force_remote_binary"] and info["mandatory_source"] or
|
||||||
|
"--usepkgonly" not in emerge.opts and pkg not in remote_pkgs):
|
||||||
|
continue
|
||||||
|
|
||||||
db_keys = list(bindb._aux_cache_keys)
|
db_keys = list(bindb._aux_cache_keys)
|
||||||
try:
|
try:
|
||||||
db_vals = bindb.aux_get(pkg, db_keys + ["MTIME"])
|
db_vals = bindb.aux_get(pkg, db_keys + ["MTIME"])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
# No binary package
|
# No binary package
|
||||||
continue
|
continue
|
||||||
|
|
||||||
mtime = int(db_vals.pop() or 0)
|
mtime = int(db_vals.pop() or 0)
|
||||||
metadata = zip(db_keys, db_vals)
|
metadata = zip(db_keys, db_vals)
|
||||||
db_pkg = Package(built=True, cpv=pkg, installed=False,
|
db_pkg = Package(built=True, cpv=pkg, installed=False,
|
||||||
@ -1116,14 +1149,15 @@ class DepGraphGenerator(object):
|
|||||||
|
|
||||||
# Calculate what packages need to be rebuilt due to changes in use flags.
|
# Calculate what packages need to be rebuilt due to changes in use flags.
|
||||||
for pkg, db_pkg in prebuilt_pkgs.iteritems():
|
for pkg, db_pkg in prebuilt_pkgs.iteritems():
|
||||||
db_pkg_src = self.package_db[pkg]
|
db_pkg_src = self.package_db.get(pkg)
|
||||||
if not self.CheckUseFlags(pkgsettings, db_pkg, db_pkg_src):
|
if db_pkg_src and not self.CheckUseFlags(pkgsettings, db_pkg,
|
||||||
|
db_pkg_src):
|
||||||
MergeChildren(pkg, "mandatory_source")
|
MergeChildren(pkg, "mandatory_source")
|
||||||
|
|
||||||
# Convert eligible packages to binaries.
|
# Convert eligible packages to binaries.
|
||||||
for pkg, info in deps_map.iteritems():
|
for pkg, info in deps_map.iteritems():
|
||||||
if (info and not info["mandatory_source"] and
|
if info and info["action"] == "merge" and pkg in prebuilt_pkgs:
|
||||||
info["action"] == "merge" and pkg in prebuilt_pkgs):
|
if not info["mandatory_source"] or info["force_remote_binary"]:
|
||||||
self.package_db[pkg] = prebuilt_pkgs[pkg]
|
self.package_db[pkg] = prebuilt_pkgs[pkg]
|
||||||
|
|
||||||
seconds = time.time() - start
|
seconds = time.time() - start
|
||||||
@ -1154,6 +1188,18 @@ class DepGraphGenerator(object):
|
|||||||
BuildFinalPackageSet()
|
BuildFinalPackageSet()
|
||||||
AddSecretDeps()
|
AddSecretDeps()
|
||||||
|
|
||||||
|
# Mark that we want to use remote binaries only for a particular package.
|
||||||
|
vardb = emerge.depgraph._frozen_config.trees[root]["vartree"].dbapi
|
||||||
|
for pkg in self.force_remote_binary:
|
||||||
|
for db_pkg in final_db.match_pkgs(pkg):
|
||||||
|
match = deps_map.get(str(db_pkg.cpv))
|
||||||
|
if match:
|
||||||
|
match["force_remote_binary"] = True
|
||||||
|
|
||||||
|
rebuild_blacklist.add(str(db_pkg.cpv))
|
||||||
|
if not vardb.match_pkgs(db_pkg.cpv):
|
||||||
|
MergeChildren(str(db_pkg.cpv), "mandatory")
|
||||||
|
|
||||||
if self.no_workon_deps:
|
if self.no_workon_deps:
|
||||||
for pkg in self.mandatory_source.copy():
|
for pkg in self.mandatory_source.copy():
|
||||||
for db_pkg in final_db.match_pkgs(pkg):
|
for db_pkg in final_db.match_pkgs(pkg):
|
||||||
@ -1166,10 +1212,6 @@ class DepGraphGenerator(object):
|
|||||||
cycles = FindCycles()
|
cycles = FindCycles()
|
||||||
if self.rebuild:
|
if self.rebuild:
|
||||||
local_pkgs = LocalPackageDatabase()
|
local_pkgs = LocalPackageDatabase()
|
||||||
remote_pkgs = {}
|
|
||||||
if "--getbinpkg" in emerge.opts:
|
|
||||||
binhost = emerge.settings["PORTAGE_BINHOST"]
|
|
||||||
remote_pkgs = RemotePackageDatabase(binhost)
|
|
||||||
AutoRebuildDeps(local_pkgs, remote_pkgs, cycles)
|
AutoRebuildDeps(local_pkgs, remote_pkgs, cycles)
|
||||||
|
|
||||||
# We need to remove installed packages so that we can use the dependency
|
# We need to remove installed packages so that we can use the dependency
|
||||||
@ -1180,7 +1222,7 @@ class DepGraphGenerator(object):
|
|||||||
SanitizeTree()
|
SanitizeTree()
|
||||||
if deps_map:
|
if deps_map:
|
||||||
if "--usepkg" in emerge.opts:
|
if "--usepkg" in emerge.opts:
|
||||||
UsePrebuiltPackages()
|
UsePrebuiltPackages(remote_pkgs)
|
||||||
AddRemainingPackages()
|
AddRemainingPackages()
|
||||||
return deps_map
|
return deps_map
|
||||||
|
|
||||||
@ -1734,13 +1776,18 @@ def main():
|
|||||||
print " Skipping package %s on %s" % (nomerge_packages,
|
print " Skipping package %s on %s" % (nomerge_packages,
|
||||||
deps.board or "root")
|
deps.board or "root")
|
||||||
|
|
||||||
deps_tree, deps_info = deps.GenDependencyTree()
|
remote_pkgs = {}
|
||||||
|
if "--getbinpkg" in emerge.opts:
|
||||||
|
binhost = emerge.settings["PORTAGE_BINHOST"]
|
||||||
|
remote_pkgs = deps.RemotePackageDatabase(binhost)
|
||||||
|
|
||||||
|
deps_tree, deps_info = deps.GenDependencyTree(remote_pkgs)
|
||||||
|
|
||||||
# You want me to be verbose? I'll give you two trees! Twice as much value.
|
# You want me to be verbose? I'll give you two trees! Twice as much value.
|
||||||
if "--tree" in emerge.opts and "--verbose" in emerge.opts:
|
if "--tree" in emerge.opts and "--verbose" in emerge.opts:
|
||||||
deps.PrintTree(deps_tree)
|
deps.PrintTree(deps_tree)
|
||||||
|
|
||||||
deps_graph = deps.GenDependencyGraph(deps_tree, deps_info)
|
deps_graph = deps.GenDependencyGraph(deps_tree, deps_info, remote_pkgs)
|
||||||
|
|
||||||
# OK, time to print out our progress so far.
|
# OK, time to print out our progress so far.
|
||||||
deps.PrintInstallPlan(deps_graph)
|
deps.PrintInstallPlan(deps_graph)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user