More work supporting --explain.

This commit is contained in:
Jelmer Vernooij 2021-03-02 15:14:04 +00:00
parent 0fa372afd4
commit fb91d5ca60
5 changed files with 167 additions and 32 deletions

View file

@ -17,16 +17,28 @@
import logging import logging
import os import os
import shlex
import sys import sys
from . import UnidentifiedError from . import UnidentifiedError, DetailedFailure
from .buildlog import InstallFixer, ExplainInstallFixer, ExplainInstall
from .buildsystem import NoBuildToolsFound, detect_buildsystems from .buildsystem import NoBuildToolsFound, detect_buildsystems
from .resolver import ( from .resolver import (
auto_resolver, auto_resolver,
native_resolvers, native_resolvers,
UnsatisfiedRequirements,
) )
from .resolver.apt import AptResolver from .resolver.apt import AptResolver
def display_explain_commands(commands):
logging.info("Run one or more of the following commands:")
for command, reqs in commands:
if isinstance(command, list):
command = shlex.join(command)
logging.info(
' %s (to install %s)', command, ', '.join(map(str, reqs)))
def get_necessary_declared_requirements(resolver, requirements, stages): def get_necessary_declared_requirements(resolver, requirements, stages):
missing = [] missing = []
for stage, req in requirements: for stage, req in requirements:
@ -35,18 +47,19 @@ def get_necessary_declared_requirements(resolver, requirements, stages):
return missing return missing
def install_necessary_declared_requirements(session, resolver, buildsystem, stages): def install_necessary_declared_requirements(session, resolver, buildsystems, stages, explain=False):
relevant = [] relevant = []
try: declared_reqs = []
declared_reqs = list(buildsystem.get_declared_dependencies()) for buildsystem in buildsystems:
except NotImplementedError: try:
logging.warning( declared_reqs.extend(buildsystem.get_declared_dependencies())
"Unable to determine declared dependencies from %s", buildsystem except NotImplementedError:
) logging.warning(
else: "Unable to determine declared dependencies from %r", buildsystem
relevant.extend( )
get_necessary_declared_requirements(resolver, declared_reqs, stages) relevant.extend(
) get_necessary_declared_requirements(resolver, declared_reqs, stages)
)
missing = [] missing = []
for req in relevant: for req in relevant:
try: try:
@ -55,7 +68,13 @@ def install_necessary_declared_requirements(session, resolver, buildsystem, stag
except NotImplementedError: except NotImplementedError:
missing.append(req) missing.append(req)
if missing: if missing:
resolver.install(missing) if explain:
commands = resolver.explain(missing)
if not commands:
raise UnsatisfiedRequirements(missing)
raise ExplainInstall(commands)
else:
resolver.install(missing)
# Types of dependencies: # Types of dependencies:
@ -74,9 +93,11 @@ STAGE_MAP = {
} }
def determine_fixers(session, resolver): def determine_fixers(session, resolver, explain=False):
from .buildlog import InstallFixer if explain:
return [InstallFixer(resolver)] return [ExplainInstallFixer(resolver)]
else:
return [InstallFixer(resolver)]
def main(): # noqa: C901 def main(): # noqa: C901
@ -143,14 +164,19 @@ def main(): # noqa: C901
os.chdir(args.directory) os.chdir(args.directory)
try: try:
bss = list(detect_buildsystems(args.directory)) bss = list(detect_buildsystems(args.directory))
logging.info("Detected buildsystems: %r", bss) logging.info(
if not args.ignore_declared_dependencies and not args.explain: "Detected buildsystems: %s", ', '.join(map(str, bss)))
if not args.ignore_declared_dependencies:
stages = STAGE_MAP[args.subcommand] stages = STAGE_MAP[args.subcommand]
if stages: if stages:
logging.info("Checking that declared requirements are present") logging.info("Checking that declared requirements are present")
for bs in bss: try:
install_necessary_declared_requirements(session, resolver, bs, stages) install_necessary_declared_requirements(
fixers = determine_fixers(session, resolver) session, resolver, bss, stages, explain=args.explain)
except ExplainInstall as e:
display_explain_commands(e.commands)
return 1
fixers = determine_fixers(session, resolver, explain=args.explain)
if args.subcommand == "dist": if args.subcommand == "dist":
from .dist import run_dist from .dist import run_dist
@ -183,7 +209,9 @@ def main(): # noqa: C901
from .info import run_info from .info import run_info
run_info(session, buildsystems=bss) run_info(session, buildsystems=bss)
except UnidentifiedError: except ExplainInstall as e:
display_explain_commands(e.commands)
except (UnidentifiedError, DetailedFailure):
return 1 return 1
except NoBuildToolsFound: except NoBuildToolsFound:
logging.info("No build tools found.") logging.info("No build tools found.")

View file

@ -194,3 +194,37 @@ class InstallFixer(BuildFixer):
except UnsatisfiedRequirements: except UnsatisfiedRequirements:
return False return False
return True return True
class ExplainInstall(Exception):
def __init__(self, commands):
self.commands = commands
class ExplainInstallFixer(BuildFixer):
def __init__(self, resolver):
self.resolver = resolver
def __repr__(self):
return "%s(%r)" % (type(self).__name__, self.resolver)
def __str__(self):
return "upstream requirement install explainer(%s)" % self.resolver
def can_fix(self, error):
req = problem_to_upstream_requirement(error)
return req is not None
def fix(self, error, context):
reqs = problem_to_upstream_requirement(error)
if reqs is None:
return False
if not isinstance(reqs, list):
reqs = [reqs]
explanations = list(self.resolver.explain(reqs))
if not explanations:
return False
raise ExplainInstall(explanations)

View file

@ -33,6 +33,8 @@ class PythonPackageRequirement(Requirement):
self.python_version = python_version self.python_version = python_version
if minimum_version is not None: if minimum_version is not None:
specs = [(">=", minimum_version)] specs = [(">=", minimum_version)]
if specs is None:
specs = []
self.specs = specs self.specs = specs
def __repr__(self): def __repr__(self):

View file

@ -16,6 +16,9 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import subprocess
class UnsatisfiedRequirements(Exception): class UnsatisfiedRequirements(Exception):
def __init__(self, reqs): def __init__(self, reqs):
self.requirements = reqs self.requirements = reqs
@ -45,6 +48,17 @@ class CPANResolver(Resolver):
def __repr__(self): def __repr__(self):
return "%s(%r)" % (type(self).__name__, self.session) return "%s(%r)" % (type(self).__name__, self.session)
def explain(self, requirements):
from ..requirements import PerlModuleRequirement
perlreqs = []
for requirement in requirements:
if not isinstance(requirement, PerlModuleRequirement):
continue
perlreqs.append(requirement)
if perlreqs:
yield (["cpan", "-i"] + [req.module for req in perlreqs], [perlreqs])
def install(self, requirements): def install(self, requirements):
from ..requirements import PerlModuleRequirement from ..requirements import PerlModuleRequirement
@ -61,9 +75,6 @@ class CPANResolver(Resolver):
if missing: if missing:
raise UnsatisfiedRequirements(missing) raise UnsatisfiedRequirements(missing)
def explain(self, requirements):
raise NotImplementedError(self.explain)
class HackageResolver(Resolver): class HackageResolver(Resolver):
def __init__(self, session): def __init__(self, session):
@ -90,7 +101,16 @@ class HackageResolver(Resolver):
raise UnsatisfiedRequirements(missing) raise UnsatisfiedRequirements(missing)
def explain(self, requirements): def explain(self, requirements):
raise NotImplementedError(self.explain) from ..requirements import HaskellPackageRequirement
haskellreqs = []
for requirement in requirements:
if not isinstance(requirement, HaskellPackageRequirement):
continue
haskellreqs.append(requirement)
if haskellreqs:
yield (["cabal", "install"] + [req.package for req in haskellreqs],
haskellreqs)
class PypiResolver(Resolver): class PypiResolver(Resolver):
@ -111,12 +131,25 @@ class PypiResolver(Resolver):
if not isinstance(requirement, PythonPackageRequirement): if not isinstance(requirement, PythonPackageRequirement):
missing.append(requirement) missing.append(requirement)
continue continue
self.session.check_call(["pip", "install", requirement.package]) try:
self.session.check_call(
["pip", "install", requirement.package])
except subprocess.CalledProcessError:
missing.append(requirement)
if missing: if missing:
raise UnsatisfiedRequirements(missing) raise UnsatisfiedRequirements(missing)
def explain(self, requirements): def explain(self, requirements):
raise NotImplementedError(self.explain) from ..requirements import PythonPackageRequirement
pyreqs = []
for requirement in requirements:
if not isinstance(requirement, PythonPackageRequirement):
continue
pyreqs.append(requirement)
if pyreqs:
yield (["pip", "install"] + [req.package for req in pyreqs],
pyreqs)
class GoResolver(Resolver): class GoResolver(Resolver):
@ -143,7 +176,16 @@ class GoResolver(Resolver):
raise UnsatisfiedRequirements(missing) raise UnsatisfiedRequirements(missing)
def explain(self, requirements): def explain(self, requirements):
raise NotImplementedError(self.explain) from ..requirements import GoPackageRequirement
goreqs = []
for requirement in requirements:
if not isinstance(requirement, GoPackageRequirement):
continue
goreqs.append(requirement)
if goreqs:
yield (["go", "get"] + [req.package for req in goreqs],
goreqs)
NPM_COMMAND_PACKAGES = { NPM_COMMAND_PACKAGES = {
@ -179,7 +221,21 @@ class NpmResolver(Resolver):
raise UnsatisfiedRequirements(missing) raise UnsatisfiedRequirements(missing)
def explain(self, requirements): def explain(self, requirements):
raise NotImplementedError(self.explain) from ..requirements import NodePackageRequirement
nodereqs = []
packages = []
for requirement in requirements:
if not isinstance(requirement, NodePackageRequirement):
continue
try:
package = NPM_COMMAND_PACKAGES[requirement.command]
except KeyError:
continue
nodereqs.append(requirement)
packages.append(package)
if nodereqs:
yield (["npm", "-g", "install"] + packages, nodereqs)
class StackedResolver(Resolver): class StackedResolver(Resolver):
@ -192,6 +248,10 @@ class StackedResolver(Resolver):
def __str__(self): def __str__(self):
return "[" + ", ".join(map(str, self.subs)) + "]" return "[" + ", ".join(map(str, self.subs)) + "]"
def explain(self, requirements):
for sub in self.subs:
yield from sub.explain(requirements)
def install(self, requirements): def install(self, requirements):
for sub in self.subs: for sub in self.subs:
try: try:
@ -228,12 +288,14 @@ class ExplainResolver(Resolver):
def auto_resolver(session): def auto_resolver(session):
# TODO(jelmer): if session is SchrootSession or if we're root, use apt # if session is SchrootSession or if we're root, use apt
from .apt import AptResolver from .apt import AptResolver
from ..session.schroot import SchrootSession from ..session.schroot import SchrootSession
user = session.check_output(["echo", "$USER"]).decode().strip() user = session.check_output(["echo", "$USER"]).decode().strip()
resolvers = [] resolvers = []
# TODO(jelmer): Check VIRTUAL_ENV, and prioritize PypiResolver if
# present?
if isinstance(session, SchrootSession) or user == "root": if isinstance(session, SchrootSession) or user == "root":
resolvers.append(AptResolver.from_session(session)) resolvers.append(AptResolver.from_session(session))
resolvers.extend([kls(session) for kls in NATIVE_RESOLVER_CLS]) resolvers.extend([kls(session) for kls in NATIVE_RESOLVER_CLS])

View file

@ -76,6 +76,9 @@ class AptRequirement(Requirement):
def pkg_relation_str(self): def pkg_relation_str(self):
return PkgRelation.str(self.relations) return PkgRelation.str(self.relations)
def __str__(self):
return "apt requirement: %s" % self.pkg_relation_str()
def touches_package(self, package): def touches_package(self, package):
for rel in self.relations: for rel in self.relations:
for entry in rel: for entry in rel:
@ -586,7 +589,13 @@ class AptResolver(Resolver):
raise UnsatisfiedRequirements(still_missing) raise UnsatisfiedRequirements(still_missing)
def explain(self, requirements): def explain(self, requirements):
raise NotImplementedError(self.explain) apt_requirements = []
for r in requirements:
apt_req = self.resolve(r)
if apt_req is not None:
apt_requirements.append((r, apt_req))
if apt_requirements:
yield (["apt", "satisfy"] + [PkgRelation.str(chain(*[r.relations for o, r in apt_requirements]))], [o for o, r in apt_requirements])
def resolve(self, req: Requirement): def resolve(self, req: Requirement):
return resolve_requirement_apt(self.apt, req) return resolve_requirement_apt(self.apt, req)