More fixes.

This commit is contained in:
Jelmer Vernooij 2021-02-26 03:19:33 +00:00
parent aa2a3e47fa
commit 6b30479b97
No known key found for this signature in database
GPG key ID: 579C160D4C9E23E8
9 changed files with 499 additions and 320 deletions

View file

@ -23,7 +23,7 @@ from .buildsystem import NoBuildToolsFound, detect_buildsystems
from .resolver import ( from .resolver import (
ExplainResolver, ExplainResolver,
AutoResolver, AutoResolver,
NativeResolver, native_resolvers,
MissingDependencies, MissingDependencies,
) )
from .resolver.apt import AptResolver from .resolver.apt import AptResolver
@ -91,6 +91,9 @@ def main(): # noqa: C901
'--user', action='store_true', help='Install in local-user directories.') '--user', action='store_true', help='Install in local-user directories.')
args = parser.parse_args() args = parser.parse_args()
if not args.subcommand:
parser.print_usage()
return 1
if args.verbose: if args.verbose:
logging.basicConfig(level=logging.DEBUG) logging.basicConfig(level=logging.DEBUG)
else: else:
@ -109,7 +112,7 @@ def main(): # noqa: C901
elif args.resolve == "explain": elif args.resolve == "explain":
resolver = ExplainResolver.from_session(session) resolver = ExplainResolver.from_session(session)
elif args.resolve == "native": elif args.resolve == "native":
resolver = NativeResolver.from_session(session) resolver = native_resolvers(session)
elif args.resolver == "auto": elif args.resolver == "auto":
resolver = AutoResolver.from_session(session) resolver = AutoResolver.from_session(session)
os.chdir(args.directory) os.chdir(args.directory)
@ -149,10 +152,10 @@ def main(): # noqa: C901
except MissingDependencies as e: except MissingDependencies as e:
for req in e.requirements: for req in e.requirements:
logging.info("Missing dependency (%s:%s)", logging.info("Missing dependency (%s:%s)",
req.family, req.name) req.family, req.package)
for resolver in [ for resolver in [
AptResolver.from_session(session), AptResolver.from_session(session),
NativeResolver.from_session(session), native_resolvers(session),
]: ]:
logging.info(" %s", resolver.explain([req])) logging.info(" %s", resolver.explain([req]))
return 2 return 2

192
ognibuild/buildlog.py Normal file
View file

@ -0,0 +1,192 @@
#!/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
"""Convert problems found in the buildlog to upstream requirements.
"""
import logging
from buildlog_consultant.common import (
MissingConfigStatusInput,
MissingPythonModule,
MissingPythonDistribution,
MissingCHeader,
MissingPkgConfig,
MissingCommand,
MissingFile,
MissingJavaScriptRuntime,
MissingSprocketsFile,
MissingGoPackage,
MissingPerlFile,
MissingPerlModule,
MissingXmlEntity,
MissingJDKFile,
MissingNodeModule,
MissingPhpClass,
MissingRubyGem,
MissingLibrary,
MissingJavaClass,
MissingCSharpCompiler,
MissingConfigure,
MissingAutomakeInput,
MissingRPackage,
MissingRubyFile,
MissingAutoconfMacro,
MissingValaPackage,
MissingXfceDependency,
MissingHaskellDependencies,
NeedPgBuildExtUpdateControl,
DhAddonLoadFailure,
MissingMavenArtifacts,
GnomeCommonMissing,
MissingGnomeCommonDependency,
)
from .fix_build import BuildFixer
from .requirements import (
BinaryRequirement,
PathRequirement,
PkgConfigRequirement,
CHeaderRequirement,
JavaScriptRuntimeRequirement,
ValaPackageRequirement,
RubyGemRequirement,
GoPackageRequirement,
DhAddonRequirement,
PhpClassRequirement,
RPackageRequirement,
NodePackageRequirement,
LibraryRequirement,
RubyFileRequirement,
XmlEntityRequirement,
SprocketsFileRequirement,
JavaClassRequirement,
HaskellPackageRequirement,
MavenArtifactRequirement,
GnomeCommonRequirement,
JDKFileRequirement,
PerlModuleRequirement,
PerlFileRequirement,
AutoconfMacroRequirement,
PythonModuleRequirement,
PythonPackageRequirement,
)
def problem_to_upstream_requirement(problem):
if isinstance(problem, MissingFile):
return PathRequirement(problem.path)
elif isinstance(problem, MissingCommand):
return BinaryRequirement(problem.command)
elif isinstance(problem, MissingPkgConfig):
return PkgConfigRequirement(
problem.module, problem.minimum_version)
elif isinstance(problem, MissingCHeader):
return CHeaderRequirement(problem.header)
elif isinstance(problem, MissingJavaScriptRuntime):
return JavaScriptRuntimeRequirement()
elif isinstance(problem, MissingRubyGem):
return RubyGemRequirement(problem.gem, problem.version)
elif isinstance(problem, MissingValaPackage):
return ValaPackageRequirement(problem.package)
elif isinstance(problem, MissingGoPackage):
return GoPackageRequirement(problem.package)
elif isinstance(problem, DhAddonLoadFailure):
return DhAddonRequirement(problem.path)
elif isinstance(problem, MissingPhpClass):
return PhpClassRequirement(problem.php_class)
elif isinstance(problem, MissingRPackage):
return RPackageRequirement(problem.package, problem.minimum_version)
elif isinstance(problem, MissingNodeModule):
return NodePackageRequirement(problem.module)
elif isinstance(problem, MissingLibrary):
return LibraryRequirement(problem.library)
elif isinstance(problem, MissingRubyFile):
return RubyFileRequirement(problem.filename)
elif isinstance(problem, MissingXmlEntity):
return XmlEntityRequirement(problem.url)
elif isinstance(problem, MissingSprocketsFile):
return SprocketsFileRequirement(problem.content_type, problem.name)
elif isinstance(problem, MissingJavaClass):
return JavaClassRequirement(problem.classname)
elif isinstance(problem, MissingHaskellDependencies):
# TODO(jelmer): Create multiple HaskellPackageRequirement objects?
return HaskellPackageRequirement(problem.package)
elif isinstance(problem, MissingMavenArtifacts):
# TODO(jelmer): Create multiple MavenArtifactRequirement objects?
return MavenArtifactRequirement(problem.artifacts)
elif isinstance(problem, MissingCSharpCompiler):
return BinaryRequirement('msc')
elif isinstance(problem, GnomeCommonMissing):
return GnomeCommonRequirement()
elif isinstance(problem, MissingJDKFile):
return JDKFileRequirement(problem.jdk_path, problem.filename)
elif isinstance(problem, MissingGnomeCommonDependency):
if problem.package == "glib-gettext":
return BinaryRequirement('glib-gettextize')
else:
logging.warning(
"No known command for gnome-common dependency %s",
problem.package)
return None
elif isinstance(problem, MissingXfceDependency):
if problem.package == "gtk-doc":
return BinaryRequirement("gtkdocize")
else:
logging.warning(
"No known command for xfce dependency %s",
problem.package)
return None
elif isinstance(problem, MissingPerlModule):
return PerlModuleRequirement(
module=problem.module,
filename=problem.filename,
inc=problem.inc)
elif isinstance(problem, MissingPerlFile):
return PerlFileRequirement(filename=problem.filename)
elif isinstance(problem, MissingAutoconfMacro):
return AutoconfMacroRequirement(problem.macro)
elif isinstance(problem, MissingPythonModule):
return PythonModuleRequirement(
problem.module,
python_version=problem.python_version,
minimum_version=problem.minimum_version)
elif isinstance(problem, MissingPythonDistribution):
return PythonPackageRequirement(
problem.module,
python_version=problem.python_version,
minimum_version=problem.minimum_version)
else:
return None
class UpstreamRequirementFixer(BuildFixer):
def __init__(self, resolver):
self.resolver = resolver
def can_fix(self, error):
req = problem_to_upstream_requirement(error)
return req is not None
def fix(self, error, context):
req = problem_to_upstream_requirement(error)
if req is None:
return False
package = self.resolver.resolve(req)
return context.add_dependency(package)

View file

@ -182,17 +182,19 @@ class SetupPy(BuildSystem):
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", PythonPackageRequirement(require) yield "build", PythonPackageRequirement(require)
if self.result.install_requires: # Not present for distutils-only packages
if getattr(self.result, 'install_requires', []):
for require in self.result.install_requires: for require in self.result.install_requires:
yield "install", PythonPackageRequirement(require) yield "install", PythonPackageRequirement(require)
if self.result.tests_require: # Not present for distutils-only packages
if getattr(self.result, 'tests_require', []):
for require in self.result.tests_require: for require in self.result.tests_require:
yield "test", PythonPackageRequirement(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 []:
yield UpstreamOutput("binary", os.path.basename(script)) yield UpstreamOutput("binary", os.path.basename(script))
entry_points = self.result.entry_points or {} entry_points = getattr(self.result, 'entry_points', None) or {}
for script in entry_points.get("console_scripts", []): for script in entry_points.get("console_scripts", []):
yield UpstreamOutput("binary", script.split("=")[0]) yield UpstreamOutput("binary", script.split("=")[0])
for package in self.result.packages or []: for package in self.result.packages or []:

View file

@ -70,14 +70,9 @@ class AptManager(object):
def package_exists(self, package): def package_exists(self, package):
if self._apt_cache is None: if self._apt_cache is None:
import apt_pkg import apt
self._apt_cache = apt.Cache(rootdir=self.session.location)
# TODO(jelmer): Load from self.session return package in self._apt_cache
self._apt_cache = apt_pkg.Cache()
for p in self._apt_cache.packages:
if p.name == package:
return True
return False
def get_package_for_paths(self, paths, regex=False): def get_package_for_paths(self, paths, regex=False):
logging.debug('Searching for packages containing %r', paths) logging.debug('Searching for packages containing %r', paths)
@ -121,9 +116,11 @@ class RemoteAptContentsFileSearcher(FileSearcher):
def from_session(cls, session): def from_session(cls, session):
logging.info('Loading apt contents information') logging.info('Loading apt contents information')
# TODO(jelmer): what about sources.list.d? # TODO(jelmer): what about sources.list.d?
with open(os.path.join(session.location, 'etc/apt/sources.list'), 'r') as f: from aptsources.sourceslist import SourcesList
return cls.from_repositories( sl = SourcesList()
f.readlines(), sl.load(os.path.join(session.location, 'etc/apt/sources.list'))
return cls.from_sources_list(
sl,
cache_dir=os.path.join(session.location, 'var/lib/apt/lists')) cache_dir=os.path.join(session.location, 'var/lib/apt/lists'))
def __setitem__(self, path, package): def __setitem__(self, path, package):
@ -174,32 +171,31 @@ class RemoteAptContentsFileSearcher(FileSearcher):
self.load_url(url) self.load_url(url)
except ContentsFileNotFound: except ContentsFileNotFound:
if mandatory: if mandatory:
raise logging.warning(
'Unable to fetch contents file %s', url)
else:
logging.debug( logging.debug(
'Unable to fetch optional contents file %s', url) 'Unable to fetch optional contents file %s', url)
return self return self
@classmethod @classmethod
def from_repositories(cls, sources, cache_dir=None): def from_sources_list(cls, sl, cache_dir=None):
# TODO(jelmer): Use aptsources.sourceslist.SourcesList # TODO(jelmer): Use aptsources.sourceslist.SourcesList
from .build import get_build_architecture from .build import get_build_architecture
# TODO(jelmer): Verify signatures, etc. # TODO(jelmer): Verify signatures, etc.
urls = [] urls = []
arches = [(get_build_architecture(), True), ("all", False)] arches = [(get_build_architecture(), True), ("all", False)]
for source in sources: for source in sl.list:
if not source.strip(): if source.invalid or source.disabled:
continue continue
if source.strip().startswith('#'): if source.type == 'deb-src':
continue continue
parts = source.split(" ") if source.type != 'deb':
if parts[0] == "deb-src":
continue
if parts[0] != "deb":
logging.warning("Invalid line in sources: %r", source) logging.warning("Invalid line in sources: %r", source)
continue continue
base_url = parts[1].strip().rstrip("/") base_url = source.uri.rstrip('/')
name = parts[2].strip() name = source.dist.rstrip('/')
components = [c.strip() for c in parts[3:]] components = source.comps
if components: if components:
dists_url = base_url + "/dists" dists_url = base_url + "/dists"
else: else:

View file

@ -63,81 +63,28 @@ from debmutate._rules import (
from breezy.plugins.debian.changelog import debcommit from breezy.plugins.debian.changelog import debcommit
from buildlog_consultant import Problem from buildlog_consultant import Problem
from buildlog_consultant.common import (
MissingConfigStatusInput,
MissingPythonModule,
MissingPythonDistribution,
MissingCHeader,
MissingPkgConfig,
MissingCommand,
MissingFile,
MissingJavaScriptRuntime,
MissingSprocketsFile,
MissingGoPackage,
MissingPerlFile,
MissingPerlModule,
MissingXmlEntity,
MissingJDKFile,
MissingNodeModule,
MissingPhpClass,
MissingRubyGem,
MissingLibrary,
MissingJavaClass,
MissingCSharpCompiler,
MissingConfigure,
MissingAutomakeInput,
MissingRPackage,
MissingRubyFile,
MissingAutoconfMacro,
MissingValaPackage,
MissingXfceDependency,
MissingHaskellDependencies,
NeedPgBuildExtUpdateControl,
DhAddonLoadFailure,
MissingMavenArtifacts,
GnomeCommonMissing,
MissingGnomeCommonDependency,
)
from buildlog_consultant.apt import ( from buildlog_consultant.apt import (
AptFetchFailure, AptFetchFailure,
) )
from buildlog_consultant.common import (
MissingConfigStatusInput,
MissingAutomakeInput,
MissingConfigure,
NeedPgBuildExtUpdateControl,
MissingPythonModule,
MissingPythonDistribution,
MissingPerlFile,
)
from buildlog_consultant.sbuild import ( from buildlog_consultant.sbuild import (
SbuildFailure, SbuildFailure,
) )
from ..fix_build import BuildFixer, SimpleBuildFixer, resolve_error, DependencyContext from ..fix_build import BuildFixer, resolve_error, DependencyContext
from ..buildlog import UpstreamRequirementFixer
from ..resolver.apt import ( from ..resolver.apt import (
NoAptPackage, NoAptPackage,
get_package_for_python_module, get_package_for_python_module,
) )
from ..requirements import (
BinaryRequirement,
PathRequirement,
PkgConfigRequirement,
CHeaderRequirement,
JavaScriptRuntimeRequirement,
ValaPackageRequirement,
RubyGemRequirement,
GoPackageRequirement,
DhAddonRequirement,
PhpClassRequirement,
RPackageRequirement,
NodePackageRequirement,
LibraryRequirement,
RubyFileRequirement,
XmlEntityRequirement,
SprocketsFileRequirement,
JavaClassRequirement,
HaskellPackageRequirement,
MavenArtifactRequirement,
GnomeCommonRequirement,
JDKFileRequirement,
PerlModuleRequirement,
PerlFileRequirement,
AutoconfMacroRequirement,
PythonModuleRequirement,
PythonPackageRequirement,
)
from .build import attempt_build, DEFAULT_BUILDER from .build import attempt_build, DEFAULT_BUILDER
@ -437,111 +384,6 @@ def fix_missing_python_module(error, context):
return True return True
def problem_to_upstream_requirement(problem):
if isinstance(problem, MissingFile):
return PathRequirement(problem.path)
elif isinstance(problem, MissingCommand):
return BinaryRequirement(problem.command)
elif isinstance(problem, MissingPkgConfig):
return PkgConfigRequirement(
problem.module, problem.minimum_version)
elif isinstance(problem, MissingCHeader):
return CHeaderRequirement(problem.header)
elif isinstance(problem, MissingJavaScriptRuntime):
return JavaScriptRuntimeRequirement()
elif isinstance(problem, MissingRubyGem):
return RubyGemRequirement(problem.gem, problem.version)
elif isinstance(problem, MissingValaPackage):
return ValaPackageRequirement(problem.package)
elif isinstance(problem, MissingGoPackage):
return GoPackageRequirement(problem.package)
elif isinstance(problem, DhAddonLoadFailure):
return DhAddonRequirement(problem.path)
elif isinstance(problem, MissingPhpClass):
return PhpClassRequirement(problem.php_class)
elif isinstance(problem, MissingRPackage):
return RPackageRequirement(problem.package, problem.minimum_version)
elif isinstance(problem, MissingNodeModule):
return NodePackageRequirement(problem.module)
elif isinstance(problem, MissingLibrary):
return LibraryRequirement(problem.library)
elif isinstance(problem, MissingRubyFile):
return RubyFileRequirement(problem.filename)
elif isinstance(problem, MissingXmlEntity):
return XmlEntityRequirement(problem.url)
elif isinstance(problem, MissingSprocketsFile):
return SprocketsFileRequirement(problem.content_type, problem.name)
elif isinstance(problem, MissingJavaClass):
return JavaClassRequirement(problem.classname)
elif isinstance(problem, MissingHaskellDependencies):
# TODO(jelmer): Create multiple HaskellPackageRequirement objects?
return HaskellPackageRequirement(problem.package)
elif isinstance(problem, MissingMavenArtifacts):
# TODO(jelmer): Create multiple MavenArtifactRequirement objects?
return MavenArtifactRequirement(problem.artifacts)
elif isinstance(problem, MissingCSharpCompiler):
return BinaryRequirement('msc')
elif isinstance(problem, GnomeCommonMissing):
return GnomeCommonRequirement()
elif isinstance(problem, MissingJDKFile):
return JDKFileRequirement(problem.jdk_path, problem.filename)
elif isinstance(problem, MissingGnomeCommonDependency):
if problem.package == "glib-gettext":
return BinaryRequirement('glib-gettextize')
else:
logging.warning(
"No known command for gnome-common dependency %s",
problem.package)
return None
elif isinstance(problem, MissingXfceDependency):
if problem.package == "gtk-doc":
return BinaryRequirement("gtkdocize")
else:
logging.warning(
"No known command for xfce dependency %s",
problem.package)
return None
elif isinstance(problem, MissingPerlModule):
return PerlModuleRequirement(
module=problem.module,
filename=problem.filename,
inc=problem.inc)
elif isinstance(problem, MissingPerlFile):
return PerlFileRequirement(filename=problem.filename)
elif isinstance(problem, MissingAutoconfMacro):
return AutoconfMacroRequirement(problem.macro)
elif isinstance(problem, MissingPythonModule):
return PythonModuleRequirement(
problem.module,
python_version=problem.python_version,
minimum_version=problem.minimum_version)
elif isinstance(problem, MissingPythonDistribution):
return PythonPackageRequirement(
problem.module,
python_version=problem.python_version,
minimum_version=problem.minimum_version)
else:
return None
class UpstreamRequirementFixer(BuildFixer):
def can_fix(self, error):
req = problem_to_upstream_requirement(error)
return req is not None
def fix(self, error, context):
req = problem_to_upstream_requirement(error)
if req is None:
return False
try:
package = context.resolver.resolve(req)
except NoAptPackage:
return False
return context.add_dependency(package)
def retry_apt_failure(error, context): def retry_apt_failure(error, context):
return True return True
@ -635,26 +477,39 @@ def fix_missing_makefile_pl(error, context):
return False return False
VERSIONED_PACKAGE_FIXERS: List[BuildFixer] = [ class SimpleBuildFixer(BuildFixer):
def __init__(self, problem_cls, fn):
self._problem_cls = problem_cls
self._fn = fn
def can_fix(self, problem):
return isinstance(problem, self._problem_cls)
def _fix(self, problem, context):
return self._fn(problem, context)
def versioned_package_fixers():
return [
SimpleBuildFixer( SimpleBuildFixer(
NeedPgBuildExtUpdateControl, run_pgbuildext_updatecontrol), NeedPgBuildExtUpdateControl, run_pgbuildext_updatecontrol),
SimpleBuildFixer(MissingConfigure, fix_missing_configure), SimpleBuildFixer(MissingConfigure, fix_missing_configure),
SimpleBuildFixer(MissingAutomakeInput, fix_missing_automake_input), SimpleBuildFixer(MissingAutomakeInput, fix_missing_automake_input),
SimpleBuildFixer(MissingConfigStatusInput, fix_missing_config_status_input), SimpleBuildFixer(MissingConfigStatusInput, fix_missing_config_status_input),
] SimpleBuildFixer(MissingPerlFile, fix_missing_makefile_pl),
]
APT_FIXERS: List[BuildFixer] = [ def apt_fixers(apt) -> List[BuildFixer]:
from ..resolver.apt import AptResolver
resolver = AptResolver(apt)
return [
SimpleBuildFixer(MissingPythonModule, fix_missing_python_module), SimpleBuildFixer(MissingPythonModule, fix_missing_python_module),
SimpleBuildFixer(MissingPythonDistribution, fix_missing_python_distribution), SimpleBuildFixer(MissingPythonDistribution, fix_missing_python_distribution),
SimpleBuildFixer(AptFetchFailure, retry_apt_failure), SimpleBuildFixer(AptFetchFailure, retry_apt_failure),
UpstreamRequirementFixer(), UpstreamRequirementFixer(resolver),
] ]
GENERIC_FIXERS: List[BuildFixer] = [
SimpleBuildFixer(MissingPerlFile, fix_missing_makefile_pl),
]
def build_incrementally( def build_incrementally(
@ -720,7 +575,7 @@ def build_incrementally(
raise raise
try: try:
if not resolve_error( if not resolve_error(
e.error, context, VERSIONED_PACKAGE_FIXERS + APT_FIXERS + GENERIC_FIXERS e.error, context, versioned_package_fixers() + apt_fixers(apt)
): ):
logging.warning("Failed to resolve error %r. Giving up.", e.error) logging.warning("Failed to resolve error %r. Giving up.", e.error)
raise raise

View file

@ -47,19 +47,6 @@ class BuildFixer(object):
return self._fix(problem, context) return self._fix(problem, context)
class SimpleBuildFixer(BuildFixer):
def __init__(self, problem_cls, fn):
self._problem_cls = problem_cls
self._fn = fn
def can_fix(self, problem):
return isinstance(problem, self._problem_cls)
def _fix(self, problem, context):
return self._fn(problem, context)
class DependencyContext(object): class DependencyContext(object):
def __init__( def __init__(
self, self,
@ -71,8 +58,6 @@ class DependencyContext(object):
): ):
self.tree = tree self.tree = tree
self.apt = apt self.apt = apt
from .resolver.apt import AptResolver
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
@ -94,47 +79,23 @@ class SchrootDependencyContext(DependencyContext):
return True return True
def fix_perl_module_from_cpan(error, context): def generic_install_fixers(session):
# TODO(jelmer): Specify -T to skip tests? from .buildlog import UpstreamRequirementFixer
context.session.check_call( from .resolver import CPANResolver, PypiResolver, NpmResolver
["cpan", "-i", error.module], user="root", env={"PERL_MM_USE_DEFAULT": "1"} return [
) UpstreamRequirementFixer(CPANResolver(session)),
return True UpstreamRequirementFixer(PypiResolver(session)),
UpstreamRequirementFixer(NpmResolver(session)),
]
NPM_COMMAND_PACKAGES = {
"del-cli": "del-cli",
}
def fix_npm_missing_command(error, context):
try:
package = NPM_COMMAND_PACKAGES[error.command]
except KeyError:
return False
context.session.check_call(["npm", "-g", "install", package])
return True
def fix_python_package_from_pip(error, context):
context.session.check_call(["pip", "install", error.distribution])
return True
GENERIC_INSTALL_FIXERS: List[BuildFixer] = [
SimpleBuildFixer(MissingPerlModule, fix_perl_module_from_cpan),
SimpleBuildFixer(MissingPythonDistribution, fix_python_package_from_pip),
SimpleBuildFixer(MissingCommand, fix_npm_missing_command),
]
def run_with_build_fixer( def run_with_build_fixer(
session: Session, args: List[str], session: Session, args: List[str],
fixers: Optional[List[BuildFixer]] = None): fixers: Optional[List[BuildFixer]] = None):
if fixers is None: if fixers is None:
from .debian.fix_build import APT_FIXERS from .debian.fix_build import apt_fixers
fixers = GENERIC_INSTALL_FIXERS + APT_FIXERS from .resolver.apt import AptResolver
fixers = generic_install_fixers(session) + apt_fixers(AptResolver.from_session(session))
logging.info("Running %r", args) logging.info("Running %r", args)
fixed_errors = [] fixed_errors = []
while True: while True:

View file

@ -34,20 +34,109 @@ class Resolver(object):
raise NotImplementedError(self.met) raise NotImplementedError(self.met)
class NativeResolver(Resolver): class CPANResolver(object):
def __init__(self, session): def __init__(self, session):
self.session = session self.session = session
@classmethod
def from_session(cls, session):
return cls(session)
def install(self, requirements): def install(self, requirements):
raise NotImplementedError(self.install) from ..requirements import PerlModuleRequirement
missing = []
for requirement in requirements:
if not isinstance(requirement, PerlModuleRequirement):
missing.append(requirement)
continue
# TODO(jelmer): Specify -T to skip tests?
self.session.check_call(
["cpan", "-i", requirement.module],
user="root", env={"PERL_MM_USE_DEFAULT": "1"}
)
if missing:
raise MissingDependencies(missing)
def explain(self, requirements): def explain(self, requirements):
raise NotImplementedError(self.explain) raise NotImplementedError(self.explain)
def met(self, requirement):
raise NotImplementedError(self.met)
class PypiResolver(object):
def __init__(self, session):
self.session = session
def install(self, requirements):
from ..requirements import PythonPackageRequirement
missing = []
for requirement in requirements:
if not isinstance(requirement, PythonPackageRequirement):
missing.append(requirement)
continue
self.session.check_call(["pip", "install", requirement.package])
if missing:
raise MissingDependencies(missing)
def explain(self, requirements):
raise NotImplementedError(self.explain)
def met(self, requirement):
raise NotImplementedError(self.met)
NPM_COMMAND_PACKAGES = {
"del-cli": "del-cli",
}
class NpmResolver(object):
def __init__(self, session):
self.session = session
def install(self, requirements):
from ..requirements import NodePackageRequirement
missing = []
for requirement in requirements:
if not isinstance(requirement, NodePackageRequirement):
missing.append(requirement)
continue
try:
package = NPM_COMMAND_PACKAGES[requirement.command]
except KeyError:
missing.append(requirement)
continue
self.session.check_call(["npm", "-g", "install", package])
if missing:
raise MissingDependencies(missing)
def explain(self, requirements):
raise NotImplementedError(self.explain)
def met(self, requirement):
raise NotImplementedError(self.met)
class StackedResolver(Resolver):
def __init__(self, subs):
self.subs = subs
def install(self, requirements):
for sub in self.subs:
try:
sub.install(requirements)
except MissingDependencies as e:
requirements = e.requirements
else:
return
def native_resolvers(session):
return StackedResolver([
CPANResolver(session),
PypiResolver(session),
NpmResolver(session)])
class ExplainResolver(Resolver): class ExplainResolver(Resolver):
def __init__(self, session): def __init__(self, session):

View file

@ -21,7 +21,7 @@ import posixpath
from ..debian.apt import AptManager from ..debian.apt import AptManager
from . import Resolver from . import Resolver, MissingDependencies
from ..requirements import ( from ..requirements import (
BinaryRequirement, BinaryRequirement,
CHeaderRequirement, CHeaderRequirement,
@ -57,24 +57,35 @@ class NoAptPackage(Exception):
"""No apt package.""" """No apt package."""
class AptRequirement(object):
def __init__(self, package, minimum_version=None):
self.package = package
self.minimum_version = minimum_version
def get_package_for_python_package(apt_mgr, package, python_version, minimum_version=None): def get_package_for_python_package(apt_mgr, package, python_version, minimum_version=None):
if python_version == "pypy": if python_version == "pypy":
return apt_mgr.get_package_for_paths( pkg_name = apt_mgr.get_package_for_paths(
["/usr/lib/pypy/dist-packages/%s-.*.egg-info/PKG-INFO" % package], ["/usr/lib/pypy/dist-packages/%s-.*.egg-info/PKG-INFO" % package],
regex=True) regex=True)
elif python_version == "cpython2": elif python_version == "cpython2":
return apt_mgr.get_package_for_paths( pkg_name = apt_mgr.get_package_for_paths(
["/usr/lib/python2\\.[0-9]/dist-packages/%s-.*.egg-info/PKG-INFO" % package], ["/usr/lib/python2\\.[0-9]/dist-packages/%s-.*.egg-info/PKG-INFO" % package],
regex=True) regex=True)
elif python_version == "cpython3": elif python_version == "cpython3":
return apt_mgr.get_package_for_paths( pkg_name = apt_mgr.get_package_for_paths(
["/usr/lib/python3/dist-packages/%s-.*.egg-info/PKG-INFO" % package], ["/usr/lib/python3/dist-packages/%s-.*.egg-info/PKG-INFO" % package],
regex=True) regex=True)
else: else:
raise NotImplementedError raise NotImplementedError
# TODO(jelmer): Dealing with epoch, etc?
if pkg_name is not None:
return AptRequirement(pkg_name, minimum_version)
return None
def get_package_for_python_module(apt_mgr, module, python_version): def get_package_for_python_module(apt_mgr, module, python_version, minimum_version):
if python_version == "python3": if python_version == "python3":
paths = [ paths = [
posixpath.join( posixpath.join(
@ -127,7 +138,10 @@ def get_package_for_python_module(apt_mgr, module, python_version):
] ]
else: else:
raise AssertionError("unknown python version %r" % python_version) raise AssertionError("unknown python version %r" % python_version)
return apt_mgr.get_package_for_paths(paths, regex=True) pkg_name = apt_mgr.get_package_for_paths(paths, regex=True)
if pkg_name is not None:
return AptRequirement(pkg_name, minimum_version=minimum_version)
return None
def resolve_binary_req(apt_mgr, req): def resolve_binary_req(apt_mgr, req):
@ -138,7 +152,10 @@ def resolve_binary_req(apt_mgr, req):
posixpath.join(dirname, req.binary_name) posixpath.join(dirname, req.binary_name)
for dirname in ["/usr/bin", "/bin"] for dirname in ["/usr/bin", "/bin"]
] ]
return apt_mgr.get_package_for_paths(paths) pkg_name = apt_mgr.get_package_for_paths(paths)
if pkg_name is not None:
return AptRequirement(pkg_name)
return None
def resolve_pkg_config_req(apt_mgr, req): def resolve_pkg_config_req(apt_mgr, req):
@ -151,11 +168,16 @@ def resolve_pkg_config_req(apt_mgr, req):
[posixpath.join("/usr/lib", ".*", "pkgconfig", req.module + ".pc")], [posixpath.join("/usr/lib", ".*", "pkgconfig", req.module + ".pc")],
regex=True, regex=True,
minimum_version=req.minimum_version) minimum_version=req.minimum_version)
return package if package is not None:
return AptRequirement(package)
return None
def resolve_path_req(apt_mgr, req): def resolve_path_req(apt_mgr, req):
return apt_mgr.get_package_for_paths([req.path]) package = apt_mgr.get_package_for_paths([req.path])
if package is not None:
return AptRequirement(package)
return None
def resolve_c_header_req(apt_mgr, req): def resolve_c_header_req(apt_mgr, req):
@ -166,17 +188,25 @@ def resolve_c_header_req(apt_mgr, req):
package = apt_mgr.get_package_for_paths( package = apt_mgr.get_package_for_paths(
[posixpath.join("/usr/include", ".*", req.header)], regex=True [posixpath.join("/usr/include", ".*", req.header)], regex=True
) )
return package if package is None:
return None
return AptRequirement(package)
def resolve_js_runtime_req(apt_mgr, req): def resolve_js_runtime_req(apt_mgr, req):
return apt_mgr.get_package_for_paths( package = apt_mgr.get_package_for_paths(
["/usr/bin/node", "/usr/bin/duk"], regex=False) ["/usr/bin/node", "/usr/bin/duk"], regex=False)
if package is not None:
return AptRequirement(package)
return None
def resolve_vala_package_req(apt_mgr, req): def resolve_vala_package_req(apt_mgr, req):
path = "/usr/share/vala-[0-9.]+/vapi/%s.vapi" % req.package path = "/usr/share/vala-[0-9.]+/vapi/%s.vapi" % req.package
return apt_mgr.get_package_for_paths([path], regex=True) package = apt_mgr.get_package_for_paths([path], regex=True)
if package is not None:
return AptRequirement(package)
return None
def resolve_ruby_gem_req(apt_mgr, req): def resolve_ruby_gem_req(apt_mgr, req):
@ -186,30 +216,45 @@ def resolve_ruby_gem_req(apt_mgr, req):
"specifications/%s-.*\\.gemspec" % req.gem "specifications/%s-.*\\.gemspec" % req.gem
) )
] ]
return apt_mgr.get_package_for_paths( package = apt_mgr.get_package_for_paths(
paths, regex=True, minimum_version=req.minimum_version) paths, regex=True)
if package is not None:
return AptRequirement(package, minimum_version=req.minimum_version)
return None
def resolve_go_package_req(apt_mgr, req): def resolve_go_package_req(apt_mgr, req):
return apt_mgr.get_package_for_paths( package = apt_mgr.get_package_for_paths(
[posixpath.join("/usr/share/gocode/src", req.package, ".*")], [posixpath.join("/usr/share/gocode/src", req.package, ".*")],
regex=True regex=True
) )
if package is not None:
return AptRequirement(package)
return None
def resolve_dh_addon_req(apt_mgr, req): def resolve_dh_addon_req(apt_mgr, req):
paths = [posixpath.join("/usr/share/perl5", req.path)] paths = [posixpath.join("/usr/share/perl5", req.path)]
return apt_mgr.get_package_for_paths(paths) package = apt_mgr.get_package_for_paths(paths)
if package is not None:
return AptRequirement(package)
return None
def resolve_php_class_req(apt_mgr, req): def resolve_php_class_req(apt_mgr, req):
path = "/usr/share/php/%s.php" % req.php_class.replace("\\", "/") path = "/usr/share/php/%s.php" % req.php_class.replace("\\", "/")
return apt_mgr.get_package_for_paths([path]) package = apt_mgr.get_package_for_paths([path])
if package is not None:
return AptRequirement(package)
return None
def resolve_r_package_req(apt_mgr, req): def resolve_r_package_req(apt_mgr, req):
paths = [posixpath.join("/usr/lib/R/site-library/.*/R/%s$" % req.package)] paths = [posixpath.join("/usr/lib/R/site-library/.*/R/%s$" % req.package)]
return apt_mgr.get_package_for_paths(paths, regex=True) package = apt_mgr.get_package_for_paths(paths, regex=True)
if package is not None:
return AptRequirement(package)
return None
def resolve_node_package_req(apt_mgr, req): def resolve_node_package_req(apt_mgr, req):
@ -218,7 +263,10 @@ def resolve_node_package_req(apt_mgr, req):
"/usr/lib/nodejs/%s/package.json" % req.package, "/usr/lib/nodejs/%s/package.json" % req.package,
"/usr/share/nodejs/%s/package.json" % req.package, "/usr/share/nodejs/%s/package.json" % req.package,
] ]
return apt_mgr.get_package_for_paths(paths, regex=True) pkg_name = apt_mgr.get_package_for_paths(paths, regex=True)
if pkg_name is not None:
return AptRequirement(pkg_name)
return None
def resolve_library_req(apt_mgr, req): def resolve_library_req(apt_mgr, req):
@ -228,21 +276,27 @@ def resolve_library_req(apt_mgr, req):
posixpath.join("/usr/lib/lib%s.a$" % req.library), posixpath.join("/usr/lib/lib%s.a$" % req.library),
posixpath.join("/usr/lib/.*/lib%s.a$" % req.library), posixpath.join("/usr/lib/.*/lib%s.a$" % req.library),
] ]
return apt_mgr.get_package_for_paths(paths, regex=True) pkg_name = apt_mgr.get_package_for_paths(paths, regex=True)
if pkg_name is not None:
return AptRequirement(pkg_name)
return None
def resolve_ruby_file_req(apt_mgr, req): def resolve_ruby_file_req(apt_mgr, req):
paths = [posixpath.join("/usr/lib/ruby/vendor_ruby/%s.rb" % req.filename)] paths = [posixpath.join("/usr/lib/ruby/vendor_ruby/%s.rb" % req.filename)]
package = apt_mgr.get_package_for_paths(paths) package = apt_mgr.get_package_for_paths(paths)
if package is not None: if package is not None:
return package return AptRequirement(package)
paths = [ paths = [
posixpath.join( posixpath.join(
r"/usr/share/rubygems-integration/all/gems/([^/]+)/" r"/usr/share/rubygems-integration/all/gems/([^/]+)/"
"lib/%s.rb" % req.filename "lib/%s.rb" % req.filename
) )
] ]
return apt_mgr.get_package_for_paths(paths, regex=True) pkg_name = apt_mgr.get_package_for_paths(paths, regex=True)
if pkg_name is not None:
return AptRequirement(pkg_name)
return None
def resolve_xml_entity_req(apt_mgr, req): def resolve_xml_entity_req(apt_mgr, req):
@ -258,7 +312,10 @@ def resolve_xml_entity_req(apt_mgr, req):
else: else:
return None return None
return apt_mgr.get_package_for_paths([search_path], regex=False) pkg_name = apt_mgr.get_package_for_paths([search_path], regex=False)
if pkg_name is not None:
return AptRequirement(pkg_name)
return None
def resolve_sprockets_file_req(apt_mgr, req): def resolve_sprockets_file_req(apt_mgr, req):
@ -267,7 +324,10 @@ def resolve_sprockets_file_req(apt_mgr, req):
else: else:
logging.warning("unable to handle content type %s", req.content_type) logging.warning("unable to handle content type %s", req.content_type)
return None return None
return apt_mgr.get_package_for_paths([path], regex=True) pkg_name = apt_mgr.get_package_for_paths([path], regex=True)
if pkg_name is not None:
return AptRequirement(pkg_name)
return None
def resolve_java_class_req(apt_mgr, req): def resolve_java_class_req(apt_mgr, req):
@ -285,12 +345,15 @@ def resolve_java_class_req(apt_mgr, req):
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 None return None
return package return AptRequirement(package)
def resolve_haskell_package_req(apt_mgr, req): def resolve_haskell_package_req(apt_mgr, req):
path = "/var/lib/ghc/package.conf.d/%s-.*.conf" % req.deps[0][0] path = "/var/lib/ghc/package.conf.d/%s-.*.conf" % req.deps[0][0]
return apt_mgr.get_package_for_paths([path], regex=True) pkg_name = apt_mgr.get_package_for_paths([path], regex=True)
if pkg_name is not None:
return AptRequirement(pkg_name)
return None
def resolve_maven_artifact_req(apt_mgr, req): def resolve_maven_artifact_req(apt_mgr, req):
@ -319,16 +382,22 @@ def resolve_maven_artifact_req(apt_mgr, req):
"%s-%s.%s" % (artifact_id, version, kind), "%s-%s.%s" % (artifact_id, version, kind),
) )
] ]
return apt_mgr.get_package_for_paths(paths, regex=regex) pkg_name = apt_mgr.get_package_for_paths(paths, regex=regex)
if pkg_name is not None:
return AptRequirement(pkg_name)
return None
def resolve_gnome_common_req(apt_mgr, req): def resolve_gnome_common_req(apt_mgr, req):
return 'gnome-common' return AptRequirement('gnome-common')
def resolve_jdk_file_req(apt_mgr, req): def resolve_jdk_file_req(apt_mgr, req):
path = req.jdk_path + ".*/" + req.filename path = req.jdk_path + ".*/" + req.filename
return apt_mgr.get_package_for_paths([path], regex=True) pkg_name = apt_mgr.get_package_for_paths([path], regex=True)
if pkg_name is not None:
return AptRequirement(pkg_name)
return None
def resolve_perl_module_req(apt_mgr, req): def resolve_perl_module_req(apt_mgr, req):
@ -344,11 +413,17 @@ def resolve_perl_module_req(apt_mgr, req):
paths = [req.filename] paths = [req.filename]
else: else:
paths = [posixpath.join(inc, req.filename) for inc in req.inc] paths = [posixpath.join(inc, req.filename) for inc in req.inc]
return apt_mgr.get_package_for_paths(paths, regex=False) pkg_name = apt_mgr.get_package_for_paths(paths, regex=False)
if pkg_name is not None:
return AptRequirement(pkg_name)
return None
def resolve_perl_file_req(apt_mgr, req): def resolve_perl_file_req(apt_mgr, req):
return apt_mgr.get_package_for_paths([req.filename], regex=False) pkg_name = apt_mgr.get_package_for_paths([req.filename], regex=False)
if pkg_name is not None:
return AptRequirement(pkg_name)
return None
def _find_aclocal_fun(macro): def _find_aclocal_fun(macro):
@ -370,7 +445,10 @@ def resolve_autoconf_macro_req(apt_mgr, req):
except KeyError: except KeyError:
logging.info("No local m4 file found defining %s", req.macro) logging.info("No local m4 file found defining %s", req.macro)
return None return None
return apt_mgr.get_package_for_paths([path]) pkg_name = apt_mgr.get_package_for_paths([path])
if pkg_name is not None:
return AptRequirement(pkg_name)
return None
def resolve_python_module_req(apt_mgr, req): def resolve_python_module_req(apt_mgr, req):
@ -421,14 +499,7 @@ APT_REQUIREMENT_RESOLVERS = [
] ]
class AptRequirement(object): def resolve_requirement_apt(apt_mgr, req: UpstreamRequirement) -> AptRequirement:
def __init__(self, package, minimum_version=None):
self.package = package
self.minimum_version = minimum_version
def resolve_requirement_apt(apt_mgr, req: UpstreamRequirement):
for rr_class, rr_fn in APT_REQUIREMENT_RESOLVERS: for rr_class, rr_fn in APT_REQUIREMENT_RESOLVERS:
if isinstance(req, rr_class): if isinstance(req, rr_class):
deb_req = rr_fn(apt_mgr, req) deb_req = rr_fn(apt_mgr, req)
@ -456,7 +527,17 @@ class AptResolver(Resolver):
except NotImplementedError: except NotImplementedError:
missing.append(req) missing.append(req)
if missing: if missing:
self.apt.install([self.resolve(m) for m in missing]) still_missing = []
apt_requirements = []
for m in missing:
try:
apt_requirements.append(self.resolve(m))
except NoAptPackage:
still_missing.append(m)
self.apt.install(
[req.package for req in apt_requirements])
if still_missing:
raise MissingDependencies(still_missing)
def explain(self, requirements): def explain(self, requirements):
raise NotImplementedError(self.explain) raise NotImplementedError(self.explain)

View file

@ -34,8 +34,8 @@ from ..debian import apt
from ..debian.apt import AptManager from ..debian.apt import AptManager
from ..debian.fix_build import ( from ..debian.fix_build import (
resolve_error, resolve_error,
VERSIONED_PACKAGE_FIXERS, versioned_package_fixers,
APT_FIXERS, apt_fixers,
BuildDependencyContext, BuildDependencyContext,
) )
from breezy.tests import TestCaseWithTransport from breezy.tests import TestCaseWithTransport
@ -95,10 +95,10 @@ blah (0.1) UNRELEASED; urgency=medium
self.tree, self.tree,
apt, apt,
subpath="", subpath="",
committer="Janitor <janitor@jelmer.uk>", committer="ognibuild <ognibuild@jelmer.uk>",
update_changelog=True, update_changelog=True,
) )
return resolve_error(error, context, VERSIONED_PACKAGE_FIXERS + APT_FIXERS) return resolve_error(error, context, versioned_package_fixers() + apt_fixers(apt))
def get_build_deps(self): def get_build_deps(self):
with open(self.tree.abspath("debian/control"), "r") as f: with open(self.tree.abspath("debian/control"), "r") as f: