More work on resolvers.

This commit is contained in:
Jelmer Vernooij 2021-02-13 14:50:09 +00:00
parent 7359c07b96
commit ee5a8462f3
No known key found for this signature in database
GPG key ID: 579C160D4C9E23E8
10 changed files with 273 additions and 173 deletions

View file

@ -18,7 +18,6 @@
import os import os
import stat import stat
import sys
class DetailedFailure(Exception): class DetailedFailure(Exception):
@ -44,9 +43,11 @@ def shebang_binary(p):
class UpstreamRequirement(object): class UpstreamRequirement(object):
def __init__(self, family, name): # Name of the family of requirements - e.g. "python-package"
family: str
def __init__(self, family):
self.family = family self.family = family
self.name = name
class UpstreamOutput(object): class UpstreamOutput(object):

View file

@ -25,12 +25,12 @@ from .clean import run_clean
from .dist import run_dist from .dist import run_dist
from .install import run_install from .install import run_install
from .resolver import ( from .resolver import (
AptResolver,
ExplainResolver, ExplainResolver,
AutoResolver, AutoResolver,
NativeResolver, NativeResolver,
MissingDependencies, MissingDependencies,
) )
from .resolver.apt import AptResolver
from .test import run_test from .test import run_test
@ -84,6 +84,7 @@ def main(): # noqa: C901
help="Ignore declared dependencies, follow build errors only", help="Ignore declared dependencies, follow build errors only",
) )
args = parser.parse_args() args = parser.parse_args()
logging.basicConfig(level=logging.INFO)
if args.schroot: if args.schroot:
from .session.schroot import SchrootSession from .session.schroot import SchrootSession

View file

@ -22,7 +22,14 @@ import os
import re import re
import warnings import warnings
from . import shebang_binary, UpstreamRequirement, UpstreamOutput from . import shebang_binary, UpstreamOutput
from .requirements import (
BinaryRequirement,
PythonPackageRequirement,
PerlModuleRequirement,
NodePackageRequirement,
CargoCrateRequirement,
)
from .apt import UnidentifiedError from .apt import UnidentifiedError
from .fix_build import run_with_build_fixer from .fix_build import run_with_build_fixer
@ -66,7 +73,7 @@ class Pear(BuildSystem):
self.path = path self.path = path
def setup(self, resolver): def setup(self, resolver):
resolver.install([UpstreamRequirement("binary", "pear")]) resolver.install([BinaryRequirement("pear")])
def dist(self, session, resolver): def dist(self, session, resolver):
self.setup(resolver) self.setup(resolver)
@ -94,18 +101,13 @@ class SetupPy(BuildSystem):
name = "setup.py" name = "setup.py"
def __init__(self, path): def __init__(self, path):
self.path = path
from distutils.core import run_setup from distutils.core import run_setup
self.result = run_setup(os.path.abspath(path), stop_after="init") self.result = run_setup(os.path.abspath(path), stop_after="init")
def setup(self, resolver): def setup(self, resolver):
resolver.install( resolver.install([PythonPackageRequirement('pip')])
[ with open(self.path, "r") as f:
UpstreamRequirement("python3", "pip"),
UpstreamRequirement("binary", "python3"),
]
)
with open("setup.py", "r") as f:
setup_py_contents = f.read() setup_py_contents = f.read()
try: try:
with open("setup.cfg", "r") as f: with open("setup.cfg", "r") as f:
@ -114,7 +116,7 @@ class SetupPy(BuildSystem):
setup_cfg_contents = "" setup_cfg_contents = ""
if "setuptools" in setup_py_contents: if "setuptools" in setup_py_contents:
logging.info("Reference to setuptools found, installing.") logging.info("Reference to setuptools found, installing.")
resolver.install([UpstreamRequirement("python3", "setuptools")]) resolver.install([PythonPackageRequirement("setuptools")])
if ( if (
"setuptools_scm" in setup_py_contents "setuptools_scm" in setup_py_contents
or "setuptools_scm" in setup_cfg_contents or "setuptools_scm" in setup_cfg_contents
@ -122,9 +124,9 @@ class SetupPy(BuildSystem):
logging.info("Reference to setuptools-scm found, installing.") logging.info("Reference to setuptools-scm found, installing.")
resolver.install( resolver.install(
[ [
UpstreamRequirement("python3", "setuptools-scm"), PythonPackageRequirement("setuptools-scm"),
UpstreamRequirement("binary", "git"), BinaryRequirement("git"),
UpstreamRequirement("binary", "mercurial"), BinaryRequirement("mercurial"),
] ]
) )
@ -150,24 +152,24 @@ class SetupPy(BuildSystem):
interpreter = shebang_binary("setup.py") interpreter = shebang_binary("setup.py")
if interpreter is not None: if interpreter is not None:
if interpreter in ("python3", "python2", "python"): if interpreter in ("python3", "python2", "python"):
resolver.install([UpstreamRequirement("binary", interpreter)]) resolver.install([BinaryRequirement(interpreter)])
else: else:
raise ValueError("Unknown interpreter %r" % interpreter) raise ValueError("Unknown interpreter %r" % interpreter)
run_with_build_fixer(session, ["./setup.py"] + args) run_with_build_fixer(session, ["./setup.py"] + args)
else: else:
# Just assume it's Python 3 # Just assume it's Python 3
resolver.install([UpstreamRequirement("binary", "python3")]) resolver.install([BinaryRequirement("python3")])
run_with_build_fixer(session, ["python3", "./setup.py"] + args) run_with_build_fixer(session, ["python3", "./setup.py"] + args)
def get_declared_dependencies(self): def get_declared_dependencies(self):
for require in self.result.get_requires(): for require in self.result.get_requires():
yield "build", UpstreamRequirement("python3", require) yield "build", PythonPackageRequirement(require)
if self.result.install_requires: if self.result.install_requires:
for require in self.result.install_requires: for require in self.result.install_requires:
yield "install", UpstreamRequirement("python3", require) yield "install", PythonPackageRequirement(require)
if self.result.tests_require: if self.result.tests_require:
for require in self.result.tests_require: for require in self.result.tests_require:
yield "test", UpstreamRequirement("python3", require) yield "test", PythonPackageRequirement(require)
def get_declared_outputs(self): def get_declared_outputs(self):
for script in self.result.scripts or []: for script in self.result.scripts or []:
@ -200,8 +202,8 @@ class PyProject(BuildSystem):
) )
resolver.install( resolver.install(
[ [
UpstreamRequirement("python3", "venv"), PythonPackageRequirement("venv"),
UpstreamRequirement("python3", "pip"), PythonPackageRequirement("pip"),
] ]
) )
session.check_call(["pip3", "install", "poetry"], user="root") session.check_call(["pip3", "install", "poetry"], user="root")
@ -220,8 +222,8 @@ class SetupCfg(BuildSystem):
def setup(self, resolver): def setup(self, resolver):
resolver.install( resolver.install(
[ [
UpstreamRequirement("python3", "pep517"), PythonPackageRequirement("pep517"),
UpstreamRequirement("python3", "pip"), PythonPackageRequirement("pip"),
] ]
) )
@ -244,10 +246,10 @@ class Npm(BuildSystem):
if "devDependencies" in self.package: if "devDependencies" in self.package:
for name, unused_version in self.package["devDependencies"].items(): for name, unused_version in self.package["devDependencies"].items():
# TODO(jelmer): Look at version # TODO(jelmer): Look at version
yield "dev", UpstreamRequirement("npm", name) yield "dev", NodePackageRequirement(name)
def setup(self, resolver): def setup(self, resolver):
resolver.install([UpstreamRequirement("binary", "npm")]) resolver.install([BinaryRequirement("npm")])
def dist(self, session, resolver): def dist(self, session, resolver):
self.setup(resolver) self.setup(resolver)
@ -262,7 +264,7 @@ class Waf(BuildSystem):
self.path = path self.path = path
def setup(self, resolver): def setup(self, resolver):
resolver.install([UpstreamRequirement("binary", "python3")]) resolver.install([BinaryRequirement("python3")])
def dist(self, session, resolver): def dist(self, session, resolver):
self.setup(resolver) self.setup(resolver)
@ -277,7 +279,7 @@ class Gem(BuildSystem):
self.path = path self.path = path
def setup(self, resolver): def setup(self, resolver):
resolver.install([UpstreamRequirement("binary", "gem2deb")]) resolver.install([BinaryRequirement("gem2deb")])
def dist(self, session, resolver): def dist(self, session, resolver):
self.setup(resolver) self.setup(resolver)
@ -314,18 +316,18 @@ class DistInkt(BuildSystem):
def setup(self, resolver): def setup(self, resolver):
resolver.install( resolver.install(
[ [
UpstreamRequirement("perl", "Dist::Inkt"), PerlModuleRequirement("Dist::Inkt"),
] ]
) )
def dist(self, session, resolver): def dist(self, session, resolver):
self.setup(resolver) self.setup(resolver)
if self.name == "dist-inkt": if self.name == "dist-inkt":
resolver.install([UpstreamRequirement("perl-module", self.dist_inkt_class)]) resolver.install([PerlModuleRequirement(self.dist_inkt_class)])
run_with_build_fixer(session, ["distinkt-dist"]) run_with_build_fixer(session, ["distinkt-dist"])
else: else:
# Default to invoking Dist::Zilla # Default to invoking Dist::Zilla
resolver.install([UpstreamRequirement("perl", "Dist::Zilla")]) resolver.install([PerlModuleRequirement("Dist::Zilla")])
run_with_build_fixer(session, ["dzil", "build", "--in", ".."]) run_with_build_fixer(session, ["dzil", "build", "--in", ".."])
@ -335,7 +337,7 @@ class Make(BuildSystem):
def setup(self, session, resolver): def setup(self, session, resolver):
if session.exists("Makefile.PL") and not session.exists("Makefile"): if session.exists("Makefile.PL") and not session.exists("Makefile"):
resolver.install([UpstreamRequirement("binary", "perl")]) resolver.install([BinaryRequirement("perl")])
run_with_build_fixer(session, ["perl", "Makefile.PL"]) run_with_build_fixer(session, ["perl", "Makefile.PL"])
if not session.exists("Makefile") and not session.exists("configure"): if not session.exists("Makefile") and not session.exists("configure"):
@ -357,10 +359,10 @@ class Make(BuildSystem):
elif session.exists("configure.ac") or session.exists("configure.in"): elif session.exists("configure.ac") or session.exists("configure.in"):
resolver.install( resolver.install(
[ [
UpstreamRequirement("binary", "autoconf"), BinaryRequirement("autoconf"),
UpstreamRequirement("binary", "automake"), BinaryRequirement("automake"),
UpstreamRequirement("binary", "gettextize"), BinaryRequirement("gettextize"),
UpstreamRequirement("binary", "libtoolize"), BinaryRequirement("libtoolize"),
] ]
) )
run_with_build_fixer(session, ["autoreconf", "-i"]) run_with_build_fixer(session, ["autoreconf", "-i"])
@ -370,7 +372,7 @@ class Make(BuildSystem):
def dist(self, session, resolver): def dist(self, session, resolver):
self.setup(session, resolver) self.setup(session, resolver)
resolver.install([UpstreamRequirement("binary", "make")]) resolver.install([BinaryRequirement("make")])
try: try:
run_with_build_fixer(session, ["make", "dist"]) run_with_build_fixer(session, ["make", "dist"])
except UnidentifiedError as e: except UnidentifiedError as e:
@ -437,7 +439,7 @@ class Make(BuildSystem):
warnings.warn("Unable to parse META.yml: %s" % e) warnings.warn("Unable to parse META.yml: %s" % e)
return return
for require in data.get("requires", []): for require in data.get("requires", []):
yield "build", UpstreamRequirement("perl", require) yield "build", PerlModuleRequirement(require)
class Cargo(BuildSystem): class Cargo(BuildSystem):
@ -454,7 +456,7 @@ class Cargo(BuildSystem):
if "dependencies" in self.cargo: if "dependencies" in self.cargo:
for name, details in self.cargo["dependencies"].items(): for name, details in self.cargo["dependencies"].items():
# TODO(jelmer): Look at details['features'], details['version'] # TODO(jelmer): Look at details['features'], details['version']
yield "build", UpstreamRequirement("cargo-crate", name) yield "build", CargoCrateRequirement(name)
class Golang(BuildSystem): class Golang(BuildSystem):

View file

@ -21,15 +21,13 @@ __all__ = [
import logging import logging
import os import os
import re
import subprocess import subprocess
import sys import sys
from typing import Iterator, List, Callable, Type, Tuple, Set, Optional from typing import List, Callable, Type, Tuple, Set, Optional
from debian.deb822 import ( from debian.deb822 import (
Deb822, Deb822,
PkgRelation, PkgRelation,
Release,
) )
from debian.changelog import Version from debian.changelog import Version
@ -113,6 +111,11 @@ from buildlog_consultant.sbuild import (
SbuildFailure, SbuildFailure,
) )
from ..apt import AptManager, LocalAptManager
from ..resolver.apt import AptResolver
from ..requirements import BinaryRequirement
from .build import attempt_build
DEFAULT_MAX_ITERATIONS = 10 DEFAULT_MAX_ITERATIONS = 10
@ -128,15 +131,21 @@ class DependencyContext(object):
def __init__( def __init__(
self, self,
tree: MutableTree, tree: MutableTree,
apt: AptManager,
subpath: str = "", subpath: str = "",
committer: Optional[str] = None, committer: Optional[str] = None,
update_changelog: bool = True, update_changelog: bool = True,
): ):
self.tree = tree self.tree = tree
self.apt = apt
self.resolver = AptResolver(apt)
self.subpath = subpath self.subpath = subpath
self.committer = committer self.committer = committer
self.update_changelog = update_changelog self.update_changelog = update_changelog
def resolve_apt(self, req):
return self.resolver.resolve(req)
def add_dependency( def add_dependency(
self, package: str, minimum_version: Optional[Version] = None self, package: str, minimum_version: Optional[Version] = None
) -> bool: ) -> bool:
@ -157,11 +166,11 @@ class BuildDependencyContext(DependencyContext):
class AutopkgtestDependencyContext(DependencyContext): class AutopkgtestDependencyContext(DependencyContext):
def __init__( def __init__(
self, testname, tree, subpath="", committer=None, update_changelog=True self, testname, tree, apt, subpath="", committer=None, update_changelog=True
): ):
self.testname = testname self.testname = testname
super(AutopkgtestDependencyContext, self).__init__( super(AutopkgtestDependencyContext, self).__init__(
tree, subpath, committer, update_changelog tree, apt, subpath, committer, update_changelog
) )
def add_dependency(self, package, minimum_version=None): def add_dependency(self, package, minimum_version=None):
@ -301,27 +310,7 @@ def commit_debian_changes(
return True return True
def get_package_for_paths(paths, regex=False): def get_package_for_python_module(apt, module, python_version):
from .apt import search_apt_file
candidates = set()
for path in paths:
candidates.update(search_apt_file(path, regex=regex))
if candidates:
break
if len(candidates) == 0:
logging.warning("No packages found that contain %r", paths)
return None
if len(candidates) > 1:
logging.warning(
"More than 1 packages found that contain %r: %r", path, candidates
)
# Euhr. Pick the one with the shortest name?
return sorted(candidates, key=len)[0]
else:
return candidates.pop()
def get_package_for_python_module(module, python_version):
if python_version == "python3": if python_version == "python3":
paths = [ paths = [
os.path.join( os.path.join(
@ -374,7 +363,7 @@ def get_package_for_python_module(module, python_version):
] ]
else: else:
raise AssertionError("unknown python version %r" % python_version) raise AssertionError("unknown python version %r" % python_version)
return get_package_for_paths(paths, regex=True) return apt.get_package_for_paths(paths, regex=True)
def targeted_python_versions(tree: Tree) -> Set[str]: def targeted_python_versions(tree: Tree) -> Set[str]:
@ -394,23 +383,8 @@ def targeted_python_versions(tree: Tree) -> Set[str]:
return targeted return targeted
apt_cache = None
def package_exists(package):
global apt_cache
if apt_cache is None:
import apt_pkg
apt_cache = apt_pkg.Cache()
for p in apt_cache.packages:
if p.name == package:
return True
return False
def fix_missing_javascript_runtime(error, context): def fix_missing_javascript_runtime(error, context):
package = get_package_for_paths(["/usr/bin/node", "/usr/bin/duk"], regex=False) package = context.apt.get_package_for_paths(["/usr/bin/node", "/usr/bin/duk"], regex=False)
if package is None: if package is None:
return False return False
return context.add_dependency(package) return context.add_dependency(package)
@ -420,30 +394,30 @@ def fix_missing_python_distribution(error, context): # noqa: C901
targeted = targeted_python_versions(context.tree) targeted = targeted_python_versions(context.tree)
default = not targeted default = not targeted
pypy_pkg = get_package_for_paths( pypy_pkg = context.apt.get_package_for_paths(
["/usr/lib/pypy/dist-packages/%s-.*.egg-info" % error.distribution], regex=True ["/usr/lib/pypy/dist-packages/%s-.*.egg-info" % error.distribution], regex=True
) )
if pypy_pkg is None: if pypy_pkg is None:
pypy_pkg = "pypy-%s" % error.distribution pypy_pkg = "pypy-%s" % error.distribution
if not package_exists(pypy_pkg): if not context.apt.package_exists(pypy_pkg):
pypy_pkg = None pypy_pkg = None
py2_pkg = get_package_for_paths( py2_pkg = context.apt.get_package_for_paths(
["/usr/lib/python2\\.[0-9]/dist-packages/%s-.*.egg-info" % error.distribution], ["/usr/lib/python2\\.[0-9]/dist-packages/%s-.*.egg-info" % error.distribution],
regex=True, regex=True,
) )
if py2_pkg is None: if py2_pkg is None:
py2_pkg = "python-%s" % error.distribution py2_pkg = "python-%s" % error.distribution
if not package_exists(py2_pkg): if not context.apt.package_exists(py2_pkg):
py2_pkg = None py2_pkg = None
py3_pkg = get_package_for_paths( py3_pkg = context.apt.get_package_for_paths(
["/usr/lib/python3/dist-packages/%s-.*.egg-info" % error.distribution], ["/usr/lib/python3/dist-packages/%s-.*.egg-info" % error.distribution],
regex=True, regex=True,
) )
if py3_pkg is None: if py3_pkg is None:
py3_pkg = "python3-%s" % error.distribution py3_pkg = "python3-%s" % error.distribution
if not package_exists(py3_pkg): if not context.apt.package_exists(py3_pkg):
py3_pkg = None py3_pkg = None
extra_build_deps = [] extra_build_deps = []
@ -488,9 +462,9 @@ def fix_missing_python_module(error, context):
targeted = set() targeted = set()
default = not targeted default = not targeted
pypy_pkg = get_package_for_python_module(error.module, "pypy") pypy_pkg = get_package_for_python_module(context.apt, error.module, "pypy")
py2_pkg = get_package_for_python_module(error.module, "python2") py2_pkg = get_package_for_python_module(context.apt, error.module, "python2")
py3_pkg = get_package_for_python_module(error.module, "python3") py3_pkg = get_package_for_python_module(context.apt, error.module, "python3")
extra_build_deps = [] extra_build_deps = []
if error.python_version == 2: if error.python_version == 2:
@ -528,7 +502,7 @@ def fix_missing_python_module(error, context):
def fix_missing_go_package(error, context): def fix_missing_go_package(error, context):
package = get_package_for_paths( package = context.apt.get_package_for_paths(
[os.path.join("/usr/share/gocode/src", error.package, ".*")], regex=True [os.path.join("/usr/share/gocode/src", error.package, ".*")], regex=True
) )
if package is None: if package is None:
@ -537,11 +511,11 @@ def fix_missing_go_package(error, context):
def fix_missing_c_header(error, context): def fix_missing_c_header(error, context):
package = get_package_for_paths( package = context.apt.get_package_for_paths(
[os.path.join("/usr/include", error.header)], regex=False [os.path.join("/usr/include", error.header)], regex=False
) )
if package is None: if package is None:
package = get_package_for_paths( package = context.apt.get_package_for_paths(
[os.path.join("/usr/include", ".*", error.header)], regex=True [os.path.join("/usr/include", ".*", error.header)], regex=True
) )
if package is None: if package is None:
@ -550,11 +524,11 @@ def fix_missing_c_header(error, context):
def fix_missing_pkg_config(error, context): def fix_missing_pkg_config(error, context):
package = get_package_for_paths( package = context.apt.get_package_for_paths(
[os.path.join("/usr/lib/pkgconfig", error.module + ".pc")] [os.path.join("/usr/lib/pkgconfig", error.module + ".pc")]
) )
if package is None: if package is None:
package = get_package_for_paths( package = context.apt.get_package_for_paths(
[os.path.join("/usr/lib", ".*", "pkgconfig", error.module + ".pc")], [os.path.join("/usr/lib", ".*", "pkgconfig", error.module + ".pc")],
regex=True, regex=True,
) )
@ -564,21 +538,12 @@ def fix_missing_pkg_config(error, context):
def fix_missing_command(error, context): def fix_missing_command(error, context):
if os.path.isabs(error.command): package = context.resolve_apt(BinaryRequirement(error.command))
paths = [error.command]
else:
paths = [
os.path.join(dirname, error.command) for dirname in ["/usr/bin", "/bin"]
]
package = get_package_for_paths(paths)
if package is None:
logging.info("No packages found that contain %r", paths)
return False
return context.add_dependency(package) return context.add_dependency(package)
def fix_missing_file(error, context): def fix_missing_file(error, context):
package = get_package_for_paths([error.path]) package = context.apt.get_package_for_paths([error.path])
if package is None: if package is None:
return False return False
return context.add_dependency(package) return context.add_dependency(package)
@ -590,7 +555,7 @@ def fix_missing_sprockets_file(error, context):
else: else:
logging.warning("unable to handle content type %s", error.content_type) logging.warning("unable to handle content type %s", error.content_type)
return False return False
package = get_package_for_paths([path], regex=True) package = context.apt.get_package_for_paths([path], regex=True)
if package is None: if package is None:
return False return False
return context.add_dependency(package) return context.add_dependency(package)
@ -619,7 +584,7 @@ def fix_missing_perl_file(error, context):
paths = [error.filename] paths = [error.filename]
else: else:
paths = [os.path.join(inc, error.filename) for inc in error.inc] paths = [os.path.join(inc, error.filename) for inc in error.inc]
package = get_package_for_paths(paths, regex=False) package = context.apt.get_package_for_paths(paths, regex=False)
if package is None: if package is None:
if getattr(error, "module", None): if getattr(error, "module", None):
logging.warning( logging.warning(
@ -635,17 +600,17 @@ def fix_missing_perl_file(error, context):
return context.add_dependency(package) return context.add_dependency(package)
def get_package_for_node_package(node_package): def get_package_for_node_package(apt, node_package):
paths = [ paths = [
"/usr/share/nodejs/.*/node_modules/%s/package.json" % node_package, "/usr/share/nodejs/.*/node_modules/%s/package.json" % node_package,
"/usr/lib/nodejs/%s/package.json" % node_package, "/usr/lib/nodejs/%s/package.json" % node_package,
"/usr/share/nodejs/%s/package.json" % node_package, "/usr/share/nodejs/%s/package.json" % node_package,
] ]
return get_package_for_paths(paths, regex=True) return apt.get_package_for_paths(paths, regex=True)
def fix_missing_node_module(error, context): def fix_missing_node_module(error, context):
package = get_package_for_node_package(error.module) package = get_package_for_node_package(context.apt, error.module)
if package is None: if package is None:
logging.warning("no node package found for %s.", error.module) logging.warning("no node package found for %s.", error.module)
return False return False
@ -654,7 +619,7 @@ def fix_missing_node_module(error, context):
def fix_missing_dh_addon(error, context): def fix_missing_dh_addon(error, context):
paths = [os.path.join("/usr/share/perl5", error.path)] paths = [os.path.join("/usr/share/perl5", error.path)]
package = get_package_for_paths(paths) package = context.apt.get_package_for_paths(paths)
if package is None: if package is None:
logging.warning("no package for debhelper addon %s", error.name) logging.warning("no package for debhelper addon %s", error.name)
return False return False
@ -667,7 +632,7 @@ def retry_apt_failure(error, context):
def fix_missing_php_class(error, context): def fix_missing_php_class(error, context):
path = "/usr/share/php/%s.php" % error.php_class.replace("\\", "/") path = "/usr/share/php/%s.php" % error.php_class.replace("\\", "/")
package = get_package_for_paths([path]) package = context.apt.get_package_for_paths([path])
if package is None: if package is None:
logging.warning("no package for PHP class %s", error.php_class) logging.warning("no package for PHP class %s", error.php_class)
return False return False
@ -676,7 +641,7 @@ def fix_missing_php_class(error, context):
def fix_missing_jdk_file(error, context): def fix_missing_jdk_file(error, context):
path = error.jdk_path + ".*/" + error.filename path = error.jdk_path + ".*/" + error.filename
package = get_package_for_paths([path], regex=True) package = context.apt.get_package_for_paths([path], regex=True)
if package is None: if package is None:
logging.warning( logging.warning(
"no package found for %s (JDK: %s) - regex %s", "no package found for %s (JDK: %s) - regex %s",
@ -690,7 +655,7 @@ def fix_missing_jdk_file(error, context):
def fix_missing_vala_package(error, context): def fix_missing_vala_package(error, context):
path = "/usr/share/vala-[0-9.]+/vapi/%s.vapi" % error.package path = "/usr/share/vala-[0-9.]+/vapi/%s.vapi" % error.package
package = get_package_for_paths([path], regex=True) package = context.apt.get_package_for_paths([path], regex=True)
if package is None: if package is None:
logging.warning("no file found for package %s - regex %s", error.package, path) logging.warning("no file found for package %s - regex %s", error.package, path)
return False return False
@ -710,7 +675,7 @@ def fix_missing_xml_entity(error, context):
else: else:
return False return False
package = get_package_for_paths([search_path], regex=False) package = context.apt.get_package_for_paths([search_path], regex=False)
if package is None: if package is None:
return False return False
return context.add_dependency(package) return context.add_dependency(package)
@ -723,7 +688,7 @@ def fix_missing_library(error, context):
os.path.join("/usr/lib/lib%s.a$" % error.library), os.path.join("/usr/lib/lib%s.a$" % error.library),
os.path.join("/usr/lib/.*/lib%s.a$" % error.library), os.path.join("/usr/lib/.*/lib%s.a$" % error.library),
] ]
package = get_package_for_paths(paths, regex=True) package = context.apt.get_package_for_paths(paths, regex=True)
if package is None: if package is None:
logging.warning("no package for library %s", error.library) logging.warning("no package for library %s", error.library)
return False return False
@ -737,7 +702,7 @@ def fix_missing_ruby_gem(error, context):
"specifications/%s-.*\\.gemspec" % error.gem "specifications/%s-.*\\.gemspec" % error.gem
) )
] ]
package = get_package_for_paths(paths, regex=True) package = context.apt.get_package_for_paths(paths, regex=True)
if package is None: if package is None:
logging.warning("no package for gem %s", error.gem) logging.warning("no package for gem %s", error.gem)
return False return False
@ -746,7 +711,7 @@ def fix_missing_ruby_gem(error, context):
def fix_missing_ruby_file(error, context): def fix_missing_ruby_file(error, context):
paths = [os.path.join("/usr/lib/ruby/vendor_ruby/%s.rb" % error.filename)] paths = [os.path.join("/usr/lib/ruby/vendor_ruby/%s.rb" % error.filename)]
package = get_package_for_paths(paths) package = context.apt.get_package_for_paths(paths)
if package is not None: if package is not None:
return context.add_dependency(package) return context.add_dependency(package)
paths = [ paths = [
@ -755,7 +720,7 @@ def fix_missing_ruby_file(error, context):
"lib/%s.rb" % error.filename "lib/%s.rb" % error.filename
) )
] ]
package = get_package_for_paths(paths, regex=True) package = context.apt.get_package_for_paths(paths, regex=True)
if package is not None: if package is not None:
return context.add_dependency(package) return context.add_dependency(package)
@ -765,7 +730,7 @@ def fix_missing_ruby_file(error, context):
def fix_missing_r_package(error, context): def fix_missing_r_package(error, context):
paths = [os.path.join("/usr/lib/R/site-library/.*/R/%s$" % error.package)] paths = [os.path.join("/usr/lib/R/site-library/.*/R/%s$" % error.package)]
package = get_package_for_paths(paths, regex=True) package = context.apt.get_package_for_paths(paths, regex=True)
if package is None: if package is None:
logging.warning("no package for R package %s", error.package) logging.warning("no package for R package %s", error.package)
return False return False
@ -781,7 +746,7 @@ def fix_missing_java_class(error, context):
logging.warning("unable to find classpath for %s", error.classname) logging.warning("unable to find classpath for %s", error.classname)
return False return False
logging.info("Classpath for %s: %r", error.classname, classpath) logging.info("Classpath for %s: %r", error.classname, classpath)
package = get_package_for_paths(classpath) package = context.apt.get_package_for_paths(classpath)
if package is None: if package is None:
logging.warning("no package for files in %r", classpath) logging.warning("no package for files in %r", classpath)
return False return False
@ -849,7 +814,7 @@ def fix_missing_maven_artifacts(error, context):
"%s-%s.%s" % (artifact_id, version, kind), "%s-%s.%s" % (artifact_id, version, kind),
) )
] ]
package = get_package_for_paths(paths, regex=regex) package = context.apt.get_package_for_paths(paths, regex=regex)
if package is None: if package is None:
logging.warning("no package for artifact %s", artifact) logging.warning("no package for artifact %s", artifact)
return False return False
@ -862,7 +827,7 @@ def install_gnome_common(error, context):
def install_gnome_common_dep(error, context): def install_gnome_common_dep(error, context):
if error.package == "glib-gettext": if error.package == "glib-gettext":
package = get_package_for_paths(["/usr/bin/glib-gettextize"]) package = context.apt.get_package_for_paths(["/usr/bin/glib-gettextize"])
else: else:
package = None package = None
if package is None: if package is None:
@ -875,7 +840,7 @@ def install_gnome_common_dep(error, context):
def install_xfce_dep(error, context): def install_xfce_dep(error, context):
if error.package == "gtk-doc": if error.package == "gtk-doc":
package = get_package_for_paths(["/usr/bin/gtkdocize"]) package = context.apt.get_package_for_paths(["/usr/bin/gtkdocize"])
else: else:
package = None package = None
if package is None: if package is None:
@ -947,7 +912,7 @@ def fix_missing_autoconf_macro(error, context):
except KeyError: except KeyError:
logging.info("No local m4 file found defining %s", error.macro) logging.info("No local m4 file found defining %s", error.macro)
return False return False
package = get_package_for_paths([path]) package = context.apt.get_package_for_paths([path])
if package is None: if package is None:
logging.warning("no package for macro file %s", path) logging.warning("no package for macro file %s", path)
return False return False
@ -960,7 +925,7 @@ def fix_missing_c_sharp_compiler(error, context):
def fix_missing_haskell_dependencies(error, context): def fix_missing_haskell_dependencies(error, context):
path = "/var/lib/ghc/package.conf.d/%s-.*.conf" % error.deps[0][0] path = "/var/lib/ghc/package.conf.d/%s-.*.conf" % error.deps[0][0]
package = get_package_for_paths([path], regex=True) package = context.apt.get_package_for_paths([path], regex=True)
if package is None: if package is None:
logging.warning("no package for macro file %s", path) logging.warning("no package for macro file %s", path)
return False return False
@ -1033,6 +998,7 @@ def resolve_error(error, context, fixers):
def build_incrementally( def build_incrementally(
local_tree, local_tree,
apt,
suffix, suffix,
build_suite, build_suite,
output_directory, output_directory,
@ -1074,6 +1040,7 @@ def build_incrementally(
if e.context[0] == "build": if e.context[0] == "build":
context = BuildDependencyContext( context = BuildDependencyContext(
local_tree, local_tree,
apt,
subpath=subpath, subpath=subpath,
committer=committer, committer=committer,
update_changelog=update_changelog, update_changelog=update_changelog,
@ -1082,6 +1049,7 @@ def build_incrementally(
context = AutopkgtestDependencyContext( context = AutopkgtestDependencyContext(
e.context[1], e.context[1],
local_tree, local_tree,
apt,
subpath=subpath, subpath=subpath,
committer=committer, committer=committer,
update_changelog=update_changelog, update_changelog=update_changelog,
@ -1154,9 +1122,12 @@ def main(argv=None):
args = parser.parse_args() args = parser.parse_args()
from breezy.workingtree import WorkingTree from breezy.workingtree import WorkingTree
apt = LocalAptManager()
tree = WorkingTree.open(".") tree = WorkingTree.open(".")
build_incrementally( build_incrementally(
tree, tree,
apt,
args.suffix, args.suffix,
args.suite, args.suite,
args.output_directory, args.output_directory,

View file

@ -124,7 +124,7 @@ def create_dist_schroot(
subdir: Optional[str] = None, subdir: Optional[str] = None,
) -> str: ) -> str:
from .buildsystem import detect_buildsystems from .buildsystem import detect_buildsystems
from .resolver import AptResolver from .resolver.apt import AptResolver
if subdir is None: if subdir is None:
subdir = "package" subdir = "package"

View file

@ -22,6 +22,7 @@ from buildlog_consultant.common import (
find_build_failure_description, find_build_failure_description,
Problem, Problem,
MissingPerlModule, MissingPerlModule,
MissingPythonDistribution,
MissingCommand, MissingCommand,
) )
@ -69,10 +70,16 @@ def fix_npm_missing_command(error, context):
return True return True
def fix_python_package_from_pip(error, context):
context.session.check_call(["pip", "install", error.distribution])
return True
GENERIC_INSTALL_FIXERS: List[ GENERIC_INSTALL_FIXERS: List[
Tuple[Type[Problem], Callable[[Problem, DependencyContext], bool]] Tuple[Type[Problem], Callable[[Problem, DependencyContext], bool]]
] = [ ] = [
(MissingPerlModule, fix_perl_module_from_cpan), (MissingPerlModule, fix_perl_module_from_cpan),
(MissingPythonDistribution, fix_python_package_from_pip),
(MissingCommand, fix_npm_missing_command), (MissingCommand, fix_npm_missing_command),
] ]
@ -84,11 +91,12 @@ def run_with_build_fixer(session: Session, args: List[str]):
retcode, lines = run_with_tee(session, args) retcode, lines = run_with_tee(session, args)
if retcode == 0: if retcode == 0:
return return
offset, line, error = find_build_failure_description(lines) match, error = find_build_failure_description(lines)
if error is None: if error is None:
logging.warning("Build failed with unidentified error. Giving up.") logging.warning("Build failed with unidentified error. Giving up.")
if line is not None: if match is not None:
raise UnidentifiedError(retcode, args, lines, secondary=(offset, line)) raise UnidentifiedError(
retcode, args, lines, secondary=(match.lineno, match.line))
raise UnidentifiedError(retcode, args, lines) raise UnidentifiedError(retcode, args, lines)
logging.info("Identified error: %r", error) logging.info("Identified error: %r", error)

64
ognibuild/requirements.py Normal file
View file

@ -0,0 +1,64 @@
#!/usr/bin/python
# Copyright (C) 2019-2020 Jelmer Vernooij <jelmer@jelmer.uk>
# encoding: utf-8
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
from . import UpstreamRequirement
class PythonPackageRequirement(UpstreamRequirement):
package: str
def __init__(self, package):
super(PythonPackageRequirement, self).__init__('python-package')
self.package = package
class BinaryRequirement(UpstreamRequirement):
binary_name: str
def __init__(self, binary_name):
super(BinaryRequirement, self).__init__('binary')
self.binary_name = binary_name
class PerlModuleRequirement(UpstreamRequirement):
module: str
def __init__(self, module):
super(PerlModuleRequirement, self).__init__('perl-module')
self.module = module
class NodePackageRequirement(UpstreamRequirement):
package: str
def __init__(self, package):
super(NodePackageRequirement, self).__init__('npm-package')
self.package = package
class CargoCrateRequirement(UpstreamRequirement):
crate: str
def __init__(self, crate):
super(CargoCrateRequirement, self).__init__('cargo-crate')
self.crate = crate

View file

@ -17,11 +17,13 @@
class MissingDependencies(Exception): class MissingDependencies(Exception):
def __init__(self, reqs): def __init__(self, reqs):
self.requirements = reqs self.requirements = reqs
class Resolver(object): class Resolver(object):
def install(self, requirements): def install(self, requirements):
raise NotImplementedError(self.install) raise NotImplementedError(self.install)
@ -29,43 +31,6 @@ class Resolver(object):
raise NotImplementedError(self.explain) raise NotImplementedError(self.explain)
class AptResolver(Resolver):
def __init__(self, apt):
self.apt = apt
@classmethod
def from_session(cls, session):
from .apt import AptManager
return cls(AptManager(session))
def install(self, requirements):
missing = []
for req in requirements:
pps = list(self._possible_paths(req))
if not pps or not any(self.apt.session.exists(p) for p in pps):
missing.append(req)
if missing:
self.apt.install(list(self.resolve(missing)))
def explain(self, requirements):
raise NotImplementedError(self.explain)
def _possible_paths(self, req):
if req.family == "binary":
yield "/usr/bin/%s" % req.name
else:
return
def resolve(self, requirements):
for req in requirements:
if req.family == "python3":
yield "python3-%s" % req.name
else:
list(self._possible_paths(req))
raise NotImplementedError
class NativeResolver(Resolver): class NativeResolver(Resolver):
def __init__(self, session): def __init__(self, session):
self.session = session self.session = session
@ -94,7 +59,8 @@ class ExplainResolver(Resolver):
class AutoResolver(Resolver): class AutoResolver(Resolver):
"""Automatically find out the most appropriate way to instal dependencies.""" """Automatically find out the most appropriate way to install dependencies.
"""
def __init__(self, session): def __init__(self, session):
self.session = session self.session = session

84
ognibuild/resolver/apt.py Normal file
View file

@ -0,0 +1,84 @@
#!/usr/bin/python3
# Copyright (C) 2020 Jelmer Vernooij <jelmer@jelmer.uk>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
import posixpath
from ..apt import AptManager
from . import Resolver
from ..requirements import (
BinaryRequirement,
PythonPackageRequirement,
)
class NoAptPackage(Exception):
"""No apt package."""
def resolve_binary_req(apt_mgr, req):
if posixpath.isabs(req.binary_name):
paths = [req.binary_name]
else:
paths = [
posixpath.join(dirname, req.binary_name)
for dirname in ["/usr/bin", "/bin"]
]
return apt_mgr.get_package_for_paths(paths)
APT_REQUIREMENT_RESOLVERS = [
(BinaryRequirement, resolve_binary_req),
]
class AptResolver(Resolver):
def __init__(self, apt):
self.apt = apt
@classmethod
def from_session(cls, session):
return cls(AptManager(session))
def install(self, requirements):
missing = []
for req in requirements:
try:
pps = list(req.possible_paths())
except NotImplementedError:
missing.append(req)
else:
if not pps or not any(self.apt.session.exists(p) for p in pps):
missing.append(req)
if missing:
self.apt.install(list(self.resolve(missing)))
def explain(self, requirements):
raise NotImplementedError(self.explain)
def resolve(self, requirements):
for req in requirements:
for rr_class, rr_fn in APT_REQUIREMENT_RESOLVERS:
if isinstance(req, rr_class):
package_name = rr_fn(self.apt, req)
if package_name is None:
raise NoAptPackage()
yield package_name
break
else:
raise NotImplementedError

View file

@ -31,6 +31,7 @@ from buildlog_consultant.common import (
MissingValaPackage, MissingValaPackage,
) )
from ..debian import apt from ..debian import apt
from ..debian.apt import LocalAptManager
from ..debian.fix_build import ( from ..debian.fix_build import (
resolve_error, resolve_error,
VERSIONED_PACKAGE_FIXERS, VERSIONED_PACKAGE_FIXERS,
@ -88,8 +89,10 @@ blah (0.1) UNRELEASED; urgency=medium
yield pkg yield pkg
def resolve(self, error, context=("build",)): def resolve(self, error, context=("build",)):
apt = LocalAptManager()
context = BuildDependencyContext( context = BuildDependencyContext(
self.tree, self.tree,
apt,
subpath="", subpath="",
committer="Janitor <janitor@jelmer.uk>", committer="Janitor <janitor@jelmer.uk>",
update_changelog=True, update_changelog=True,