New upstream release.

This commit is contained in:
Jelmer Vernooij 2021-04-07 00:11:05 +01:00
commit 17b984bcee
No known key found for this signature in database
GPG key ID: 579C160D4C9E23E8
24 changed files with 598 additions and 145 deletions

View file

@ -28,6 +28,7 @@ jobs:
python -m pip install wheel python -m pip install wheel
python -m pip install git+https://salsa.debian.org/apt-team/python-apt python -m pip install git+https://salsa.debian.org/apt-team/python-apt
python -m pip install -e ".[debian]" python -m pip install -e ".[debian]"
python -m pip install testtools
mkdir -p ~/.config/breezy/plugins mkdir -p ~/.config/breezy/plugins
brz branch lp:brz-debian ~/.config/breezy/plugins/debian brz branch lp:brz-debian ~/.config/breezy/plugins/debian
if: "matrix.python-version != 'pypy3' && matrix.os == 'ubuntu-latest'" if: "matrix.python-version != 'pypy3' && matrix.os == 'ubuntu-latest'"

View file

@ -1,6 +1,6 @@
Metadata-Version: 2.1 Metadata-Version: 2.1
Name: ognibuild Name: ognibuild
Version: 0.0.3 Version: 0.0.4
Summary: Detect and run any build system Summary: Detect and run any build system
Home-page: https://jelmer.uk/code/ognibuild Home-page: https://jelmer.uk/code/ognibuild
Maintainer: Jelmer Vernooij Maintainer: Jelmer Vernooij

View file

@ -54,6 +54,8 @@ issues (or lack of support for a particular ecosystem), please file a bug.
- Octave - Octave
- Perl - Perl
- Module::Build::Tiny - Module::Build::Tiny
- Dist::Zilla
- Minilla
- PHP Pear - PHP Pear
- Python - setup.py/setup.cfg/pyproject.toml - Python - setup.py/setup.cfg/pyproject.toml
- R - R

5
debian/changelog vendored
View file

@ -1,3 +1,8 @@
ognibuild (0.0.4-1) UNRELEASED; urgency=low
* New upstream release.
-- Jelmer Vernooij <jelmer@debian.org> Wed, 07 Apr 2021 00:10:53 -0000
ognibuild (0.0.3-1) unstable; urgency=medium ognibuild (0.0.3-1) unstable; urgency=medium
* Add missing dependency on python3-lz4. * Add missing dependency on python3-lz4.

View file

@ -1,6 +1,6 @@
Metadata-Version: 2.1 Metadata-Version: 2.1
Name: ognibuild Name: ognibuild
Version: 0.0.3 Version: 0.0.4
Summary: Detect and run any build system Summary: Detect and run any build system
Home-page: https://jelmer.uk/code/ognibuild Home-page: https://jelmer.uk/code/ognibuild
Maintainer: Jelmer Vernooij Maintainer: Jelmer Vernooij

View file

@ -20,7 +20,7 @@ import os
import stat import stat
__version__ = (0, 0, 3) __version__ = (0, 0, 4)
USER_AGENT = "Ognibuild" USER_AGENT = "Ognibuild"

View file

@ -150,6 +150,10 @@ def main(): # noqa: C901
external_dir, internal_dir = session.setup_from_directory(args.directory) external_dir, internal_dir = session.setup_from_directory(args.directory)
session.chdir(internal_dir) session.chdir(internal_dir)
os.chdir(external_dir) os.chdir(external_dir)
if not session.is_temporary and args.subcommand == 'info':
args.explain = True
if args.resolve == "apt": if args.resolve == "apt":
resolver = AptResolver.from_session(session) resolver = AptResolver.from_session(session)
elif args.resolve == "native": elif args.resolve == "native":
@ -211,7 +215,7 @@ def main(): # noqa: C901
if args.subcommand == "info": if args.subcommand == "info":
from .info import run_info from .info import run_info
run_info(session, buildsystems=bss) run_info(session, buildsystems=bss, fixers=fixers)
except ExplainInstall as e: except ExplainInstall as e:
display_explain_commands(e.commands) display_explain_commands(e.commands)
except (UnidentifiedError, DetailedFailure): except (UnidentifiedError, DetailedFailure):

View file

@ -53,6 +53,7 @@ from buildlog_consultant.common import (
MissingVagueDependency, MissingVagueDependency,
DhAddonLoadFailure, DhAddonLoadFailure,
MissingMavenArtifacts, MissingMavenArtifacts,
MissingIntrospectionTypelib,
GnomeCommonMissing, GnomeCommonMissing,
MissingGnomeCommonDependency, MissingGnomeCommonDependency,
UnknownCertificateAuthority, UnknownCertificateAuthority,
@ -60,7 +61,11 @@ from buildlog_consultant.common import (
MissingLibtool, MissingLibtool,
MissingQt, MissingQt,
MissingX11, MissingX11,
MissingPerlPredeclared,
MissingLatexFile,
MissingCargoCrate,
) )
from buildlog_consultant.apt import UnsatisfiedAptDependencies
from .fix_build import BuildFixer from .fix_build import BuildFixer
from .requirements import ( from .requirements import (
@ -99,6 +104,10 @@ from .requirements import (
X11Requirement, X11Requirement,
LibtoolRequirement, LibtoolRequirement,
VagueDependencyRequirement, VagueDependencyRequirement,
IntrospectionTypelibRequirement,
PerlPreDeclaredRequirement,
LatexPackageRequirement,
CargoCrateRequirement,
) )
from .resolver import UnsatisfiedRequirements from .resolver import UnsatisfiedRequirements
@ -112,6 +121,8 @@ def problem_to_upstream_requirement(problem): # noqa: C901
return PkgConfigRequirement(problem.module, problem.minimum_version) return PkgConfigRequirement(problem.module, problem.minimum_version)
elif isinstance(problem, MissingCHeader): elif isinstance(problem, MissingCHeader):
return CHeaderRequirement(problem.header) return CHeaderRequirement(problem.header)
elif isinstance(problem, MissingIntrospectionTypelib):
return IntrospectionTypelibRequirement(problem.library)
elif isinstance(problem, MissingJavaScriptRuntime): elif isinstance(problem, MissingJavaScriptRuntime):
return JavaScriptRuntimeRequirement() return JavaScriptRuntimeRequirement()
elif isinstance(problem, MissingRubyGem): elif isinstance(problem, MissingRubyGem):
@ -130,6 +141,10 @@ def problem_to_upstream_requirement(problem): # noqa: C901
return NodeModuleRequirement(problem.module) return NodeModuleRequirement(problem.module)
elif isinstance(problem, MissingNodePackage): elif isinstance(problem, MissingNodePackage):
return NodePackageRequirement(problem.package) return NodePackageRequirement(problem.package)
elif isinstance(problem, MissingLatexFile):
if problem.filename.endswith('.sty'):
return LatexPackageRequirement(problem.filename[:-4])
return None
elif isinstance(problem, MissingVagueDependency): elif isinstance(problem, MissingVagueDependency):
return VagueDependencyRequirement(problem.name, minimum_version=problem.minimum_version) return VagueDependencyRequirement(problem.name, minimum_version=problem.minimum_version)
elif isinstance(problem, MissingLibrary): elif isinstance(problem, MissingLibrary):
@ -169,6 +184,11 @@ def problem_to_upstream_requirement(problem): # noqa: C901
return LibtoolRequirement() return LibtoolRequirement()
elif isinstance(problem, UnknownCertificateAuthority): elif isinstance(problem, UnknownCertificateAuthority):
return CertificateAuthorityRequirement(problem.url) return CertificateAuthorityRequirement(problem.url)
elif isinstance(problem, MissingPerlPredeclared):
return PerlPreDeclaredRequirement(problem.name)
elif isinstance(problem, MissingCargoCrate):
# TODO(jelmer): handle problem.requirements
return CargoCrateRequirement(problem.crate)
elif isinstance(problem, MissingSetupPyCommand): elif isinstance(problem, MissingSetupPyCommand):
if problem.command == "test": if problem.command == "test":
return PythonPackageRequirement("setuptools") return PythonPackageRequirement("setuptools")
@ -207,6 +227,9 @@ def problem_to_upstream_requirement(problem): # noqa: C901
python_version=problem.python_version, python_version=problem.python_version,
minimum_version=problem.minimum_version, minimum_version=problem.minimum_version,
) )
elif isinstance(problem, UnsatisfiedAptDependencies):
from .resolver.apt import AptRequirement
return AptRequirement(problem.relations)
else: else:
return None return None

View file

@ -118,6 +118,11 @@ class Pear(BuildSystem):
name = "pear" name = "pear"
PEAR_NAMESPACES = [
"http://pear.php.net/dtd/package-2.0",
"http://pear.php.net/dtd/package-2.1",
]
def __init__(self, path): def __init__(self, path):
self.path = path self.path = path
@ -146,10 +151,7 @@ class Pear(BuildSystem):
try: try:
root = xmlparse_simplify_namespaces( root = xmlparse_simplify_namespaces(
path, path,
[ self.PEAR_NAMESPACES
"http://pear.php.net/dtd/package-2.0",
"http://pear.php.net/dtd/package-2.1",
],
) )
except ET.ParseError as e: except ET.ParseError as e:
logging.warning("Unable to parse package.xml: %s", e) logging.warning("Unable to parse package.xml: %s", e)
@ -173,9 +175,25 @@ class Pear(BuildSystem):
@classmethod @classmethod
def probe(cls, path): def probe(cls, path):
if os.path.exists(os.path.join(path, "package.xml")): package_xml_path = os.path.join(path, "package.xml")
logging.debug("Found package.xml, assuming pear package.") if not os.path.exists(package_xml_path):
return cls(os.path.join(path, "package.xml")) return
import xml.etree.ElementTree as ET
try:
tree = ET.iterparse(package_xml_path)
except ET.ParseError as e:
logging.warning("Unable to parse package.xml: %s", e)
return
if not tree.root:
# No root?
return
for ns in cls.PEAR_NAMESPACES:
if tree.root.tag == '{%s}package' % ns:
logging.debug("Found package.xml with namespace %s, assuming pear package.")
return cls(path)
# run_setup, but setting __name__ # run_setup, but setting __name__
@ -221,7 +239,6 @@ import sys
script_name = %(script_name)s script_name = %(script_name)s
save_argv = sys.argv.copy()
g = {"__file__": script_name, "__name__": "__main__"} g = {"__file__": script_name, "__name__": "__main__"}
try: try:
core._setup_stop_after = "init" core._setup_stop_after = "init"
@ -522,9 +539,6 @@ class SetupPy(BuildSystem):
if os.path.exists(os.path.join(path, "setup.py")): if os.path.exists(os.path.join(path, "setup.py")):
logging.debug("Found setup.py, assuming python project.") logging.debug("Found setup.py, assuming python project.")
return cls(path) return cls(path)
if os.path.exists(os.path.join(path, "setup.cfg")):
logging.debug("Found setup.py, assuming python project.")
return cls(path)
if os.path.exists(os.path.join(path, "pyproject.toml")): if os.path.exists(os.path.join(path, "pyproject.toml")):
logging.debug("Found pyproject.toml, assuming python project.") logging.debug("Found pyproject.toml, assuming python project.")
return cls(path) return cls(path)
@ -713,6 +727,10 @@ class R(BuildSystem):
if "Imports" in description: if "Imports" in description:
for s in parse_list(description["Imports"]): for s in parse_list(description["Imports"]):
yield "build", RPackageRequirement.from_str(s) yield "build", RPackageRequirement.from_str(s)
if "LinkingTo" in description:
for s in parse_list(description["LinkingTo"]):
yield "build", RPackageRequirement.from_str(s)
# TODO(jelmer): Suggests
def get_declared_outputs(self, session, fixers=None): def get_declared_outputs(self, session, fixers=None):
description = self._read_description() description = self._read_description()
@ -978,6 +996,8 @@ class RunTests(BuildSystem):
def _read_cpanfile(session, args, kind, fixers): def _read_cpanfile(session, args, kind, fixers):
for line in run_with_build_fixers(session, ["cpanfile-dump"] + args, fixers): for line in run_with_build_fixers(session, ["cpanfile-dump"] + args, fixers):
line = line.strip()
if line:
yield kind, PerlModuleRequirement(line) yield kind, PerlModuleRequirement(line)
@ -986,6 +1006,26 @@ def _declared_deps_from_cpanfile(session, fixers):
yield from _read_cpanfile(session, ["--test"], "test", fixers) yield from _read_cpanfile(session, ["--test"], "test", fixers)
def _declared_deps_from_meta_yml(f):
# See http://module-build.sourceforge.net/META-spec-v1.4.html for
# the specification of the format.
import ruamel.yaml
import ruamel.yaml.reader
try:
data = ruamel.yaml.load(f, ruamel.yaml.SafeLoader)
except ruamel.yaml.reader.ReaderError as e:
warnings.warn("Unable to parse META.yml: %s" % e)
return
for require in data.get("requires", []):
yield "core", PerlModuleRequirement(require)
for require in data.get("build_requires", []):
yield "build", PerlModuleRequirement(require)
for require in data.get("configure_requires", []):
yield "build", PerlModuleRequirement(require)
# TODO(jelmer): recommends
class Make(BuildSystem): class Make(BuildSystem):
name = "make" name = "make"
@ -1106,19 +1146,8 @@ class Make(BuildSystem):
something = False something = False
# TODO(jelmer): Split out the perl-specific stuff? # TODO(jelmer): Split out the perl-specific stuff?
if os.path.exists(os.path.join(self.path, "META.yml")): if os.path.exists(os.path.join(self.path, "META.yml")):
# See http://module-build.sourceforge.net/META-spec-v1.4.html for
# the specification of the format.
import ruamel.yaml
import ruamel.yaml.reader
with open(os.path.join(self.path, "META.yml"), "rb") as f: with open(os.path.join(self.path, "META.yml"), "rb") as f:
try: yield from _declared_deps_from_meta_yml(f)
data = ruamel.yaml.load(f, ruamel.yaml.SafeLoader)
except ruamel.yaml.reader.ReaderError as e:
warnings.warn("Unable to parse META.yml: %s" % e)
return
for require in data.get("requires", []):
yield "build", PerlModuleRequirement(require)
something = True something = True
if os.path.exists(os.path.join(self.path, "cpanfile")): if os.path.exists(os.path.join(self.path, "cpanfile")):
yield from _declared_deps_from_cpanfile(session, fixers) yield from _declared_deps_from_cpanfile(session, fixers)
@ -1204,6 +1233,7 @@ def _parse_go_mod(f):
while line: while line:
parts = line.strip().split(" ") parts = line.strip().split(" ")
if not parts or parts == [""]: if not parts or parts == [""]:
line = readline()
continue continue
if len(parts) == 2 and parts[1] == "(": if len(parts) == 2 and parts[1] == "(":
line = readline() line = readline()
@ -1393,6 +1423,7 @@ class PerlBuildTiny(BuildSystem):
def __init__(self, path): def __init__(self, path):
self.path = path self.path = path
self.minilla = os.path.exists(os.path.join(self.path, "minil.toml"))
def __repr__(self): def __repr__(self):
return "%s(%r)" % (type(self).__name__, self.path) return "%s(%r)" % (type(self).__name__, self.path)
@ -1402,6 +1433,9 @@ class PerlBuildTiny(BuildSystem):
def test(self, session, resolver, fixers): def test(self, session, resolver, fixers):
self.setup(session, fixers) self.setup(session, fixers)
if self.minilla:
run_with_build_fixers(session, ["minil", "test"], fixers)
else:
run_with_build_fixers(session, ["./Build", "test"], fixers) run_with_build_fixers(session, ["./Build", "test"], fixers)
def build(self, session, resolver, fixers): def build(self, session, resolver, fixers):
@ -1412,10 +1446,46 @@ class PerlBuildTiny(BuildSystem):
self.setup(session, fixers) self.setup(session, fixers)
run_with_build_fixers(session, ["./Build", "clean"], fixers) run_with_build_fixers(session, ["./Build", "clean"], fixers)
def dist(self, session, resolver, fixers, target_directory, quiet=False):
self.setup(session, fixers)
with DistCatcher([session.external_path('.')]) as dc:
if self.minilla:
run_with_build_fixers(session, ["minil", "dist"], fixers)
else:
try:
run_with_build_fixers(session, ["./Build", "dist"], fixers)
except UnidentifiedError as e:
if "Can't find dist packages without a MANIFEST file" in e.lines:
run_with_build_fixers(session, ["./Build", "manifest"], fixers)
run_with_build_fixers(session, ["./Build", "dist"], fixers)
elif "No such action 'dist'" in e.lines:
raise NotImplementedError
else:
raise
return dc.copy_single(target_directory)
def install(self, session, resolver, fixers, install_target): def install(self, session, resolver, fixers, install_target):
self.setup(session, fixers) self.setup(session, fixers)
if self.minilla:
run_with_build_fixers(session, ["minil", "install"], fixers)
else:
run_with_build_fixers(session, ["./Build", "install"], fixers) run_with_build_fixers(session, ["./Build", "install"], fixers)
def get_declared_dependencies(self, session, fixers=None):
self.setup(session, fixers)
try:
run_with_build_fixers(session, ["./Build", "distmeta"], fixers)
except UnidentifiedError as e:
if "No such action 'distmeta'" in e.lines:
pass
else:
raise
try:
with open(os.path.join(self.path, 'META.yml'), 'r') as f:
yield from _declared_deps_from_meta_yml(f)
except FileNotFoundError:
pass
@classmethod @classmethod
def probe(cls, path): def probe(cls, path):
if os.path.exists(os.path.join(path, "Build.PL")): if os.path.exists(os.path.join(path, "Build.PL")):

View file

@ -28,7 +28,7 @@ from .. import DetailedFailure, UnidentifiedError
from ..session import Session, run_with_tee, get_user from ..session import Session, run_with_tee, get_user
from .file_search import ( from .file_search import (
FileSearcher, FileSearcher,
AptCachedContentsFileSearcher, get_apt_contents_file_searcher,
GENERATED_FILE_SEARCHER, GENERATED_FILE_SEARCHER,
get_packages_for_paths, get_packages_for_paths,
) )
@ -41,6 +41,7 @@ def run_apt(
if prefix is None: if prefix is None:
prefix = [] prefix = []
args = prefix = ["apt", "-y"] + args args = prefix = ["apt", "-y"] + args
logging.info('apt: running %r', args)
retcode, lines = run_with_tee(session, args, cwd="/", user="root") retcode, lines = run_with_tee(session, args, cwd="/", user="root")
if retcode == 0: if retcode == 0:
return return
@ -76,21 +77,27 @@ class AptManager(object):
def searchers(self): def searchers(self):
if self._searchers is None: if self._searchers is None:
self._searchers = [ self._searchers = [
AptCachedContentsFileSearcher.from_session(self.session), get_apt_contents_file_searcher(self.session),
GENERATED_FILE_SEARCHER, GENERATED_FILE_SEARCHER,
] ]
return self._searchers return self._searchers
def package_exists(self, package): @property
def apt_cache(self):
if self._apt_cache is None: if self._apt_cache is None:
import apt import apt
self._apt_cache = apt.Cache(rootdir=self.session.location) self._apt_cache = apt.Cache(rootdir=self.session.location)
return package in self._apt_cache return self._apt_cache
def package_exists(self, package):
return package in self.apt_cache
def package_versions(self, package):
return list(self.apt_cache[package].versions)
def get_packages_for_paths(self, paths, regex=False, case_insensitive=False): def get_packages_for_paths(self, paths, regex=False, case_insensitive=False):
logging.debug("Searching for packages containing %r", paths) logging.debug("Searching for packages containing %r", paths)
# TODO(jelmer): Make sure we use whatever is configured in self.session
return get_packages_for_paths( return get_packages_for_paths(
paths, self.searchers(), regex=regex, case_insensitive=case_insensitive paths, self.searchers(), regex=regex, case_insensitive=case_insensitive
) )

View file

@ -16,22 +16,24 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
__all__ = [ __all__ = [
"changes_filename",
"get_build_architecture", "get_build_architecture",
"add_dummy_changelog_entry", "add_dummy_changelog_entry",
"build", "build",
"SbuildFailure", "DetailedDebianBuildFailure",
"UnidentifiedDebianBuildError",
] ]
from datetime import datetime from datetime import datetime
from debmutate.changelog import ChangelogEditor
import logging import logging
import os import os
import re import re
import shlex
import subprocess import subprocess
import sys import sys
from debian.changelog import Changelog from debian.changelog import Changelog
from debmutate.changelog import get_maintainer, format_datetime from debmutate.changelog import get_maintainer
from breezy.mutabletree import MutableTree from breezy.mutabletree import MutableTree
from breezy.plugins.debian.builder import BuildFailedError from breezy.plugins.debian.builder import BuildFailedError
@ -39,13 +41,33 @@ from breezy.tree import Tree
from buildlog_consultant.sbuild import ( from buildlog_consultant.sbuild import (
worker_failure_from_sbuild_log, worker_failure_from_sbuild_log,
SbuildFailure,
) )
from .. import DetailedFailure as DetailedFailure, UnidentifiedError
DEFAULT_BUILDER = "sbuild --no-clean-source" DEFAULT_BUILDER = "sbuild --no-clean-source"
class DetailedDebianBuildFailure(DetailedFailure):
def __init__(self, stage, phase, retcode, argv, error, description):
super(DetailedDebianBuildFailure, self).__init__(retcode, argv, error)
self.stage = stage
self.phase = phase
self.description = description
class UnidentifiedDebianBuildError(UnidentifiedError):
def __init__(self, stage, phase, retcode, argv, lines, description, secondary=None):
super(UnidentifiedDebianBuildError, self).__init__(
retcode, argv, lines, secondary)
self.stage = stage
self.phase = phase
self.description = description
class MissingChangesFile(Exception): class MissingChangesFile(Exception):
"""Expected changes file was not written.""" """Expected changes file was not written."""
@ -53,11 +75,15 @@ class MissingChangesFile(Exception):
self.filename = filename self.filename = filename
def changes_filename(package, version, arch): def find_changes_files(path, package, version):
non_epoch_version = version.upstream_version non_epoch_version = version.upstream_version
if version.debian_version is not None: if version.debian_version is not None:
non_epoch_version += "-%s" % version.debian_version non_epoch_version += "-%s" % version.debian_version
return "%s_%s_%s.changes" % (package, non_epoch_version, arch) c = re.compile('%s_%s_(.*).changes' % (re.escape(package), re.escape(non_epoch_version)))
for entry in os.scandir(path):
m = c.match(entry.name)
if m:
yield m.group(1), entry
def get_build_architecture(): def get_build_architecture():
@ -119,25 +145,16 @@ def add_dummy_changelog_entry(
maintainer = get_maintainer() maintainer = get_maintainer()
if timestamp is None: if timestamp is None:
timestamp = datetime.now() timestamp = datetime.now()
with tree.get_file(path) as f: with ChangelogEditor(tree.abspath(os.path.join(path))) as editor:
cl = Changelog() version = editor[0].version
cl.parse_changelog(f, max_blocks=None, allow_empty_author=True, strict=False)
version = cl[0].version
if version.debian_revision: if version.debian_revision:
version.debian_revision = add_suffix(version.debian_revision, suffix) version.debian_revision = add_suffix(version.debian_revision, suffix)
else: else:
version.upstream_version = add_suffix(version.upstream_version, suffix) version.upstream_version = add_suffix(version.upstream_version, suffix)
cl.new_block( editor.auto_version(version, timestamp=timestamp)
package=cl[0].package, editor.add_entry(
version=version, summary=[message], maintainer=maintainer, timestamp=timestamp, urgency='low')
urgency="low", editor[0].distributions = suite
distributions=suite,
author="%s <%s>" % maintainer,
date=format_datetime(timestamp),
changes=["", " * " + message, ""],
)
cl_str = cl._format(allow_missing_author=True)
tree.put_file_bytes_non_atomic(path, cl_str.encode(cl._encoding))
def get_latest_changelog_version(local_tree, subpath=""): def get_latest_changelog_version(local_tree, subpath=""):
@ -158,7 +175,10 @@ def build(
distribution=None, distribution=None,
subpath="", subpath="",
source_date_epoch=None, source_date_epoch=None,
extra_repositories=None,
): ):
for repo in extra_repositories or []:
build_command += " --extra-repository=" + shlex.quote(repo)
args = [ args = [
sys.executable, sys.executable,
"-m", "-m",
@ -192,8 +212,10 @@ def build_once(
build_command, build_command,
subpath="", subpath="",
source_date_epoch=None, source_date_epoch=None,
extra_repositories=None
): ):
build_log_path = os.path.join(output_directory, "build.log") build_log_path = os.path.join(output_directory, "build.log")
logging.debug("Writing build log to %s", build_log_path)
try: try:
with open(build_log_path, "w") as f: with open(build_log_path, "w") as f:
build( build(
@ -204,21 +226,35 @@ def build_once(
distribution=build_suite, distribution=build_suite,
subpath=subpath, subpath=subpath,
source_date_epoch=source_date_epoch, source_date_epoch=source_date_epoch,
extra_repositories=extra_repositories,
) )
except BuildFailedError: except BuildFailedError as e:
with open(build_log_path, "rb") as f: with open(build_log_path, "rb") as f:
raise worker_failure_from_sbuild_log(f) sbuild_failure = worker_failure_from_sbuild_log(f)
retcode = getattr(e, 'returncode', None)
if sbuild_failure.error:
raise DetailedDebianBuildFailure(
sbuild_failure.stage,
sbuild_failure.phase, retcode,
shlex.split(build_command),
sbuild_failure.error,
sbuild_failure.description)
else:
raise UnidentifiedDebianBuildError(
sbuild_failure.stage,
sbuild_failure.phase,
retcode, shlex.split(build_command),
[], sbuild_failure.description)
(cl_package, cl_version) = get_latest_changelog_version(local_tree, subpath) (cl_package, cl_version) = get_latest_changelog_version(local_tree, subpath)
changes_name = changes_filename(cl_package, cl_version, get_build_architecture()) changes_names = []
changes_path = os.path.join(output_directory, changes_name) for kind, entry in find_changes_files(output_directory, cl_package, cl_version):
if not os.path.exists(changes_path): changes_names.append((entry.name))
raise MissingChangesFile(changes_name) return (changes_names, cl_version)
return (changes_name, cl_version)
def gbp_dch(path): def gbp_dch(path):
subprocess.check_call(["gbp", "dch"], cwd=path) subprocess.check_call(["gbp", "dch", "--ignore-branch"], cwd=path)
def attempt_build( def attempt_build(
@ -230,6 +266,8 @@ def attempt_build(
build_changelog_entry=None, build_changelog_entry=None,
subpath="", subpath="",
source_date_epoch=None, source_date_epoch=None,
run_gbp_dch=False,
extra_repositories=None
): ):
"""Attempt a build, with a custom distribution set. """Attempt a build, with a custom distribution set.
@ -244,6 +282,8 @@ def attempt_build(
source_date_epoch: Source date epoch to set source_date_epoch: Source date epoch to set
Returns: Tuple with (changes_name, cl_version) Returns: Tuple with (changes_name, cl_version)
""" """
if run_gbp_dch and not subpath:
gbp_dch(local_tree.abspath(subpath))
if build_changelog_entry is not None: if build_changelog_entry is not None:
add_dummy_changelog_entry( add_dummy_changelog_entry(
local_tree, subpath, suffix, build_suite, build_changelog_entry local_tree, subpath, suffix, build_suite, build_changelog_entry
@ -255,4 +295,5 @@ def attempt_build(
build_command, build_command,
subpath, subpath,
source_date_epoch=source_date_epoch, source_date_epoch=source_date_epoch,
extra_repositories=extra_repositories,
) )

View file

@ -65,9 +65,9 @@ class BuildDependencyTieBreaker(object):
return None return None
top = max(by_count.items(), key=lambda k: k[1]) top = max(by_count.items(), key=lambda k: k[1])
logging.info( logging.info(
"Breaking tie between %r to %r based on build-depends count", "Breaking tie between [%s] to %s based on build-depends count",
[repr(r) for r in reqs], ', '.join([repr(r.pkg_relation_str()) for r in reqs]),
top[0], repr(top[0].pkg_relation_str()),
) )
return top[0] return top[0]

View file

@ -21,11 +21,13 @@ from datetime import datetime
from debian.deb822 import Release from debian.deb822 import Release
import os import os
import re import re
import subprocess
from typing import Iterator, List from typing import Iterator, List
import logging import logging
from .. import USER_AGENT from .. import USER_AGENT
from ..session import Session
class FileSearcher(object): class FileSearcher(object):
@ -158,7 +160,68 @@ def load_apt_cache_file(url, cache_dir):
raise FileNotFoundError(url) raise FileNotFoundError(url)
class AptCachedContentsFileSearcher(FileSearcher): class AptFileFileSearcher(FileSearcher):
CACHE_IS_EMPTY_PATH = '/usr/share/apt-file/is-cache-empty'
def __init__(self, session: Session):
self.session = session
@classmethod
def has_cache(cls, session: Session) -> bool:
if not os.path.exists(session.external_path(cls.CACHE_IS_EMPTY_PATH)):
return False
try:
session.check_call([cls.CACHE_IS_EMPTY_PATH])
except subprocess.CalledProcessError as e:
if e.returncode == 1:
return True
raise
else:
return False
@classmethod
def from_session(cls, session):
logging.info('Using apt-file to search apt contents')
if not os.path.exists(session.external_path(cls.CACHE_IS_EMPTY_PATH)):
from .apt import AptManager
AptManager.from_session(session).install(['apt-file'])
if not cls.has_cache(session):
session.check_call(['apt-file', 'update'], user='root')
return cls(session)
def search_files(self, path, regex=False, case_insensitive=False):
args = []
if regex:
args.append('-x')
else:
args.append('-F')
if case_insensitive:
args.append('-i')
args.append(path)
try:
output = self.session.check_output(['/usr/bin/apt-file', 'search'] + args)
except subprocess.CalledProcessError as e:
if e.returncode == 1:
# No results
return
if e.returncode == 3:
raise Exception('apt-file cache is empty')
raise
for line in output.splitlines(False):
pkg, path = line.split(b': ')
yield pkg.decode('utf-8')
def get_apt_contents_file_searcher(session):
if AptFileFileSearcher.has_cache(session):
return AptFileFileSearcher.from_session(session)
return RemoteContentsFileSearcher.from_session(session)
class RemoteContentsFileSearcher(FileSearcher):
def __init__(self): def __init__(self):
self._db = {} self._db = {}
@ -268,12 +331,12 @@ class GeneratedFileSearcher(FileSearcher):
with open(path, "r") as f: with open(path, "r") as f:
for line in f: for line in f:
(path, pkg) = line.strip().split(None, 1) (path, pkg) = line.strip().split(None, 1)
self._db[path] = pkg self._db.append(path, pkg)
def search_files( def search_files(
self, path: str, regex: bool = False, case_insensitive: bool = False self, path: str, regex: bool = False, case_insensitive: bool = False
) -> Iterator[str]: ) -> Iterator[str]:
for p, pkg in sorted(self._db.items()): for p, pkg in self._db:
if regex: if regex:
flags = 0 flags = 0
if case_insensitive: if case_insensitive:
@ -290,16 +353,16 @@ class GeneratedFileSearcher(FileSearcher):
# TODO(jelmer): read from a file # TODO(jelmer): read from a file
GENERATED_FILE_SEARCHER = GeneratedFileSearcher( GENERATED_FILE_SEARCHER = GeneratedFileSearcher(
{ [
"/etc/locale.gen": "locales", ("/etc/locale.gen", "locales"),
# Alternative # Alternative
"/usr/bin/rst2html": "python3-docutils", ("/usr/bin/rst2html", "python3-docutils"),
# aclocal is a symlink to aclocal-1.XY # aclocal is a symlink to aclocal-1.XY
"/usr/bin/aclocal": "automake", ("/usr/bin/aclocal", "automake"),
"/usr/bin/automake": "automake", ("/usr/bin/automake", "automake"),
# maven lives in /usr/share # maven lives in /usr/share
"/usr/bin/mvn": "maven", ("/usr/bin/mvn", "maven"),
} ]
) )
@ -322,6 +385,7 @@ def get_packages_for_paths(
def main(argv): def main(argv):
import argparse import argparse
from ..session.plain import PlainSession
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("path", help="Path to search for.", type=str, nargs="*") parser.add_argument("path", help="Path to search for.", type=str, nargs="*")
@ -334,7 +398,7 @@ def main(argv):
else: else:
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
main_searcher = AptCachedContentsFileSearcher() main_searcher = get_apt_contents_file_searcher(PlainSession())
main_searcher.load_local() main_searcher.load_local()
searchers = [main_searcher, GENERATED_FILE_SEARCHER] searchers = [main_searcher, GENERATED_FILE_SEARCHER]

View file

@ -22,6 +22,7 @@ __all__ = [
from functools import partial from functools import partial
import logging import logging
import os import os
import re
import shutil import shutil
import sys import sys
from typing import List, Set, Optional, Type from typing import List, Set, Optional, Type
@ -31,7 +32,7 @@ from debian.deb822 import (
PkgRelation, PkgRelation,
) )
from breezy.commit import PointlessCommit from breezy.commit import PointlessCommit, NullCommitReporter
from breezy.tree import Tree from breezy.tree import Tree
from debmutate.changelog import ChangelogEditor from debmutate.changelog import ChangelogEditor
from debmutate.control import ( from debmutate.control import (
@ -111,9 +112,13 @@ from buildlog_consultant.common import (
MissingPerlFile, MissingPerlFile,
) )
from buildlog_consultant.sbuild import ( from buildlog_consultant.sbuild import (
SbuildFailure, DebcargoUnacceptablePredicate,
) )
from .build import (
DetailedDebianBuildFailure,
UnidentifiedDebianBuildError,
)
from ..buildlog import problem_to_upstream_requirement from ..buildlog import problem_to_upstream_requirement
from ..fix_build import BuildFixer, resolve_error from ..fix_build import BuildFixer, resolve_error
from ..resolver.apt import ( from ..resolver.apt import (
@ -154,7 +159,10 @@ class DebianPackagingContext(object):
cl_path = self.abspath("debian/changelog") cl_path = self.abspath("debian/changelog")
with ChangelogEditor(cl_path) as editor: with ChangelogEditor(cl_path) as editor:
editor.add_entry([summary]) editor.add_entry([summary])
debcommit(self.tree, committer=self.committer, subpath=self.subpath) debcommit(
self.tree, committer=self.committer,
subpath=self.subpath,
reporter=self.commit_reporter)
else: else:
self.tree.commit( self.tree.commit(
message=summary, message=summary,
@ -304,6 +312,8 @@ def python_tie_breaker(tree, subpath, reqs):
return True return True
if pkg.startswith("lib%s-" % python_version): if pkg.startswith("lib%s-" % python_version):
return True return True
if re.match(r'lib%s\.[0-9]-dev' % python_version, pkg):
return True
return False return False
for python_version in targeted: for python_version in targeted:
@ -423,6 +433,13 @@ def fix_missing_makefile_pl(error, phase, context):
return False return False
def coerce_unaccpetable_predicate(error, phase, context):
from debmutate.debcargo import DebcargoEditor
with DebcargoEditor(context.abspath('debian/debcargo.toml')) as editor:
editor['allow_prerelease_deps'] = True
return context.commit('Enable allow_prerelease_deps.')
class SimpleBuildFixer(BuildFixer): class SimpleBuildFixer(BuildFixer):
def __init__(self, packaging_context, problem_cls: Type[Problem], fn): def __init__(self, packaging_context, problem_cls: Type[Problem], fn):
self.context = packaging_context self.context = packaging_context
@ -475,6 +492,7 @@ def versioned_package_fixers(session, packaging_context, apt):
packaging_context, MissingConfigStatusInput, fix_missing_config_status_input packaging_context, MissingConfigStatusInput, fix_missing_config_status_input
), ),
SimpleBuildFixer(packaging_context, MissingPerlFile, fix_missing_makefile_pl), SimpleBuildFixer(packaging_context, MissingPerlFile, fix_missing_makefile_pl),
SimpleBuildFixer(packaging_context, DebcargoUnacceptablePredicate, coerce_unaccpetable_predicate),
] ]
@ -497,6 +515,16 @@ def apt_fixers(apt, packaging_context) -> List[BuildFixer]:
] ]
def default_fixers(local_tree, subpath, apt, committer=None, update_changelog=None):
packaging_context = DebianPackagingContext(
local_tree, subpath, committer, update_changelog,
commit_reporter=NullCommitReporter()
)
return versioned_package_fixers(apt.session, packaging_context, apt) + apt_fixers(
apt, packaging_context
)
def build_incrementally( def build_incrementally(
local_tree, local_tree,
apt, apt,
@ -510,14 +538,14 @@ def build_incrementally(
subpath="", subpath="",
source_date_epoch=None, source_date_epoch=None,
update_changelog=True, update_changelog=True,
extra_repositories=None,
fixers=None
): ):
fixed_errors = [] fixed_errors = []
packaging_context = DebianPackagingContext( if fixers is None:
local_tree, subpath, committer, update_changelog fixers = default_fixers(
) local_tree, subpath, apt, committer=committer,
fixers = versioned_package_fixers(apt.session, packaging_context, apt) + apt_fixers( update_changelog=update_changelog)
apt, packaging_context
)
logging.info("Using fixers: %r", fixers) logging.info("Using fixers: %r", fixers)
while True: while True:
try: try:
@ -530,11 +558,13 @@ def build_incrementally(
build_changelog_entry, build_changelog_entry,
subpath=subpath, subpath=subpath,
source_date_epoch=source_date_epoch, source_date_epoch=source_date_epoch,
run_gbp_dch=(update_changelog is False),
extra_repositories=extra_repositories,
) )
except SbuildFailure as e: except UnidentifiedDebianBuildError:
if e.error is None:
logging.warning("Build failed with unidentified error. Giving up.") logging.warning("Build failed with unidentified error. Giving up.")
raise raise
except DetailedDebianBuildFailure as e:
if e.phase is None: if e.phase is None:
logging.info("No relevant context, not making any changes.") logging.info("No relevant context, not making any changes.")
raise raise
@ -603,6 +633,11 @@ def main(argv=None):
dest="update_changelog", dest="update_changelog",
help="do not update the changelog", help="do not update the changelog",
) )
parser.add_argument(
'--max-iterations',
type=int,
default=DEFAULT_MAX_ITERATIONS,
help='Maximum number of issues to attempt to fix before giving up.')
parser.add_argument( parser.add_argument(
"--update-changelog", "--update-changelog",
action="store_true", action="store_true",
@ -646,7 +681,7 @@ def main(argv=None):
apt = AptManager(session) apt = AptManager(session)
try: try:
(changes_filename, cl_version) = build_incrementally( (changes_filenames, cl_version) = build_incrementally(
tree, tree,
apt, apt,
args.suffix, args.suffix,
@ -656,23 +691,30 @@ def main(argv=None):
None, None,
committer=args.committer, committer=args.committer,
update_changelog=args.update_changelog, update_changelog=args.update_changelog,
max_iterations=args.max_iterations,
) )
except SbuildFailure as e: except DetailedDebianBuildFailure as e:
if e.phase is None: if e.phase is None:
phase = "unknown phase" phase = "unknown phase"
elif len(e.phase) == 1: elif len(e.phase) == 1:
phase = e.phase[0] phase = e.phase[0]
else: else:
phase = "%s (%s)" % (e.phase[0], e.phase[1]) phase = "%s (%s)" % (e.phase[0], e.phase[1])
if e.error:
logging.fatal("Error during %s: %s", phase, e.error) logging.fatal("Error during %s: %s", phase, e.error)
return 1
except UnidentifiedDebianBuildError as e:
if e.phase is None:
phase = "unknown phase"
elif len(e.phase) == 1:
phase = e.phase[0]
else: else:
phase = "%s (%s)" % (e.phase[0], e.phase[1])
logging.fatal("Error during %s: %s", phase, e.description) logging.fatal("Error during %s: %s", phase, e.description)
return 1 return 1
logging.info( logging.info(
'Built %s - changes file at %s.', 'Built %s - changes file at %r.',
os.path.join(output_directory, changes_filename)) cl_version, changes_filenames)
if __name__ == "__main__": if __name__ == "__main__":

View file

@ -51,6 +51,8 @@ def run_dist(session, buildsystems, resolver, fixers, target_directory, quiet=Fa
# e.g. pip caches in ~/.cache # e.g. pip caches in ~/.cache
session.create_home() session.create_home()
logging.info('Using dependency resolver: %s', resolver)
for buildsystem in buildsystems: for buildsystem in buildsystems:
filename = buildsystem.dist( filename = buildsystem.dist(
session, resolver, fixers, target_directory, quiet=quiet session, resolver, fixers, target_directory, quiet=quiet

View file

@ -94,7 +94,8 @@ class DistCatcher(object):
logging.warning( logging.warning(
"Found multiple tarballs %r in %s.", possible_new, directory "Found multiple tarballs %r in %s.", possible_new, directory
) )
return self.files.extend([entry.path for entry in possible_new])
return possible_new[0].name
if len(possible_updated) == 1: if len(possible_updated) == 1:
entry = possible_updated[0] entry = possible_updated[0]

View file

@ -17,7 +17,7 @@
from functools import partial from functools import partial
import logging import logging
from typing import List, Tuple, Callable, Any from typing import List, Tuple, Callable, Any, Optional
from buildlog_consultant import Problem from buildlog_consultant import Problem
from buildlog_consultant.common import ( from buildlog_consultant.common import (
@ -104,8 +104,10 @@ def iterate_with_build_fixers(fixers: List[BuildFixer], cb: Callable[[], Any]):
def run_with_build_fixers( def run_with_build_fixers(
session: Session, args: List[str], fixers: List[BuildFixer], **kwargs session: Session, args: List[str], fixers: Optional[List[BuildFixer]], **kwargs
): ):
if fixers is None:
fixers = []
return iterate_with_build_fixers( return iterate_with_build_fixers(
fixers, partial(run_detecting_problems, session, args, **kwargs) fixers, partial(run_detecting_problems, session, args, **kwargs)
) )

View file

@ -16,12 +16,12 @@
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
def run_info(session, buildsystems): def run_info(session, buildsystems, fixers=None):
for buildsystem in buildsystems: for buildsystem in buildsystems:
print("%r:" % buildsystem) print("%r:" % buildsystem)
deps = {} deps = {}
try: try:
for kind, dep in buildsystem.get_declared_dependencies(session): for kind, dep in buildsystem.get_declared_dependencies(session, fixers=fixers):
deps.setdefault(kind, []).append(dep) deps.setdefault(kind, []).append(dep)
except NotImplementedError: except NotImplementedError:
print( print(
@ -35,7 +35,7 @@ def run_info(session, buildsystems):
print("\t\t\t%s" % dep) print("\t\t\t%s" % dep)
print("") print("")
try: try:
outputs = list(buildsystem.get_declared_outputs(session)) outputs = list(buildsystem.get_declared_outputs(session, fixers=fixers))
except NotImplementedError: except NotImplementedError:
print("\tUnable to detect declared outputs for this type of build system") print("\tUnable to detect declared outputs for this type of build system")
outputs = [] outputs = []

View file

@ -82,6 +82,15 @@ class PythonPackageRequirement(Requirement):
return p.returncode == 0 return p.returncode == 0
class LatexPackageRequirement(Requirement):
def __init__(self, package: str):
self.package = package
def __repr__(self):
return "%s(%r)" % (type(self).__name__, self.package)
class PhpPackageRequirement(Requirement): class PhpPackageRequirement(Requirement):
def __init__( def __init__(
self, self,
@ -192,6 +201,32 @@ class NodePackageRequirement(Requirement):
return "%s(%r)" % (type(self).__name__, self.package) return "%s(%r)" % (type(self).__name__, self.package)
class PerlPreDeclaredRequirement(Requirement):
name: str
# TODO(jelmer): Can we obtain this information elsewhere?
KNOWN_MODULES = {
'auto_set_repository': 'Module::Install::Repository',
'author_tests': 'Module::Install::AuthorTests',
'readme_from': 'Module::Install::ReadmeFromPod',
'catalyst': 'Module::Install::Catalyst',
'githubmeta': 'Module::Install::GithubMeta',
'use_ppport': 'Module::Install::XSUtil',
}
def __init__(self, name):
super(PerlPreDeclaredRequirement, self).__init__("perl-predeclared")
self.name = name
def lookup_module(self):
module = self.KNOWN_MODULES[self.name]
return PerlModuleRequirement(module=module)
def __repr__(self):
return "%s(%r)" % (type(self).__name__, self.name)
class NodeModuleRequirement(Requirement): class NodeModuleRequirement(Requirement):
module: str module: str
@ -246,6 +281,10 @@ class PkgConfigRequirement(Requirement):
self.module = module self.module = module
self.minimum_version = minimum_version self.minimum_version = minimum_version
def __repr__(self):
return "%s(%r, minimum_version=%r)" % (
type(self).__name__, self.module, self.minimum_version)
class PathRequirement(Requirement): class PathRequirement(Requirement):
@ -255,6 +294,9 @@ class PathRequirement(Requirement):
super(PathRequirement, self).__init__("path") super(PathRequirement, self).__init__("path")
self.path = path self.path = path
def __repr__(self):
return "%s(%r)" % (type(self).__name__, self.path)
class CHeaderRequirement(Requirement): class CHeaderRequirement(Requirement):
@ -264,6 +306,9 @@ class CHeaderRequirement(Requirement):
super(CHeaderRequirement, self).__init__("c-header") super(CHeaderRequirement, self).__init__("c-header")
self.header = header self.header = header
def __repr__(self):
return "%s(%r)" % (type(self).__name__, self.header)
class JavaScriptRuntimeRequirement(Requirement): class JavaScriptRuntimeRequirement(Requirement):
def __init__(self): def __init__(self):
@ -587,6 +632,11 @@ class LibtoolRequirement(Requirement):
super(LibtoolRequirement, self).__init__("libtool") super(LibtoolRequirement, self).__init__("libtool")
class IntrospectionTypelibRequirement(Requirement):
def __init__(self, library):
self.library = library
class PythonModuleRequirement(Requirement): class PythonModuleRequirement(Requirement):
module: str module: str
@ -619,3 +669,8 @@ class PythonModuleRequirement(Requirement):
) )
p.communicate() p.communicate()
return p.returncode == 0 return p.returncode == 0
def __repr__(self):
return "%s(%r, python_version=%r, minimum_version=%r)" % (
type(self).__name__, self.module, self.python_version,
self.minimum_version)

View file

@ -16,7 +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 logging
import subprocess import subprocess
from .. import UnidentifiedError
from ..fix_build import run_detecting_problems from ..fix_build import run_detecting_problems
@ -67,7 +69,7 @@ class CPANResolver(Resolver):
continue continue
perlreqs.append(requirement) perlreqs.append(requirement)
if perlreqs: if perlreqs:
yield (self._cmd(perlreqs), [perlreqs]) yield (self._cmd(perlreqs), perlreqs)
def install(self, requirements): def install(self, requirements):
from ..requirements import PerlModuleRequirement from ..requirements import PerlModuleRequirement
@ -88,9 +90,11 @@ class CPANResolver(Resolver):
if not isinstance(requirement, PerlModuleRequirement): if not isinstance(requirement, PerlModuleRequirement):
missing.append(requirement) missing.append(requirement)
continue continue
cmd = self._cmd([requirement])
logging.info("CPAN: running %r", cmd)
run_detecting_problems( run_detecting_problems(
self.session, self.session,
self._cmd([requirement]), cmd,
env=env, env=env,
user=user, user=user,
) )
@ -98,6 +102,73 @@ class CPANResolver(Resolver):
raise UnsatisfiedRequirements(missing) raise UnsatisfiedRequirements(missing)
class TlmgrResolver(Resolver):
def __init__(self, session, repository: str, user_local=False):
self.session = session
self.user_local = user_local
self.repository = repository
def __str__(self):
if self.repository.startswith('http://') or self.repository.startswith('https://'):
return 'tlmgr(%r)' % self.repository
else:
return self.repository
def __repr__(self):
return "%s(%r, %r)" % (
type(self).__name__, self.session, self.repository)
def _cmd(self, reqs):
ret = ["tlmgr", "--repository=%s" % self.repository, "install"]
if self.user_local:
ret.append("--usermode")
ret.extend([req.package for req in reqs])
return ret
def explain(self, requirements):
from ..requirements import LatexPackageRequirement
latexreqs = []
for requirement in requirements:
if not isinstance(requirement, LatexPackageRequirement):
continue
latexreqs.append(requirement)
if latexreqs:
yield (self._cmd(latexreqs), latexreqs)
def install(self, requirements):
from ..requirements import LatexPackageRequirement
if not self.user_local:
user = "root"
else:
user = None
missing = []
for requirement in requirements:
if not isinstance(requirement, LatexPackageRequirement):
missing.append(requirement)
continue
cmd = self._cmd([requirement])
logging.info("tlmgr: running %r", cmd)
try:
run_detecting_problems(self.session, cmd, user=user)
except UnidentifiedError as e:
if "tlmgr: user mode not initialized, please read the documentation!" in e.lines:
self.session.check_call(['tlmgr', 'init-usertree'])
else:
raise
if missing:
raise UnsatisfiedRequirements(missing)
class CTANResolver(TlmgrResolver):
def __init__(self, session, user_local=False):
super(CTANResolver, self).__init__(
session, "ctan", user_local=user_local)
class RResolver(Resolver): class RResolver(Resolver):
def __init__(self, session, repos, user_local=False): def __init__(self, session, repos, user_local=False):
self.session = session self.session = session
@ -142,7 +213,9 @@ class RResolver(Resolver):
if not isinstance(requirement, RPackageRequirement): if not isinstance(requirement, RPackageRequirement):
missing.append(requirement) missing.append(requirement)
continue continue
self.session.check_call(self._cmd(requirement), user=user) cmd = self._cmd(requirement)
logging.info("RResolver(%r): running %r", self.repos, cmd)
run_detecting_problems(self.session, cmd, user=user)
if missing: if missing:
raise UnsatisfiedRequirements(missing) raise UnsatisfiedRequirements(missing)
@ -186,7 +259,9 @@ class OctaveForgeResolver(Resolver):
if not isinstance(requirement, OctavePackageRequirement): if not isinstance(requirement, OctavePackageRequirement):
missing.append(requirement) missing.append(requirement)
continue continue
self.session.check_call(self._cmd(requirement), user=user) cmd = self._cmd(requirement)
logging.info("Octave: running %r", cmd)
run_detecting_problems(self.session, cmd, user=user)
if missing: if missing:
raise UnsatisfiedRequirements(missing) raise UnsatisfiedRequirements(missing)
@ -235,7 +310,9 @@ class HackageResolver(Resolver):
if not isinstance(requirement, HaskellPackageRequirement): if not isinstance(requirement, HaskellPackageRequirement):
missing.append(requirement) missing.append(requirement)
continue continue
self.session.check_call(self._cmd([requirement]), user=user) cmd = self._cmd([requirement])
logging.info("Hackage: running %r", cmd)
run_detecting_problems(self.session, cmd, user=user)
if missing: if missing:
raise UnsatisfiedRequirements(missing) raise UnsatisfiedRequirements(missing)
@ -281,8 +358,10 @@ class PypiResolver(Resolver):
if not isinstance(requirement, PythonPackageRequirement): if not isinstance(requirement, PythonPackageRequirement):
missing.append(requirement) missing.append(requirement)
continue continue
cmd = self._cmd([requirement])
logging.info("pip: running %r", cmd)
try: try:
self.session.check_call(self._cmd([requirement]), user=user) run_detecting_problems(self.session, cmd, user=user)
except subprocess.CalledProcessError: except subprocess.CalledProcessError:
missing.append(requirement) missing.append(requirement)
if missing: if missing:
@ -325,7 +404,9 @@ class GoResolver(Resolver):
if not isinstance(requirement, GoPackageRequirement): if not isinstance(requirement, GoPackageRequirement):
missing.append(requirement) missing.append(requirement)
continue continue
self.session.check_call(["go", "get", requirement.package], env=env) cmd = ["go", "get", requirement.package]
logging.info("go: running %r", cmd)
run_detecting_problems(self.session, cmd, env=env)
if missing: if missing:
raise UnsatisfiedRequirements(missing) raise UnsatisfiedRequirements(missing)
@ -344,6 +425,7 @@ class GoResolver(Resolver):
NPM_COMMAND_PACKAGES = { NPM_COMMAND_PACKAGES = {
"del-cli": "del-cli", "del-cli": "del-cli",
"husky": "husky", "husky": "husky",
"cross-env": "cross-env",
} }
@ -382,13 +464,17 @@ class NpmResolver(Resolver):
requirement = NodePackageRequirement(package) requirement = NodePackageRequirement(package)
if isinstance(requirement, NodeModuleRequirement): if isinstance(requirement, NodeModuleRequirement):
# TODO: Is this legit? # TODO: Is this legit?
requirement = NodePackageRequirement(requirement.module.split("/")[0]) parts = requirement.module.split("/")
if parts[0].startswith('@'):
requirement = NodePackageRequirement('/'.join(parts[:2]))
else:
requirement = NodePackageRequirement(parts[0])
if not isinstance(requirement, NodePackageRequirement): if not isinstance(requirement, NodePackageRequirement):
missing.append(requirement) missing.append(requirement)
continue continue
self.session.check_call( cmd = ["npm", "-g", "install", requirement.package]
["npm", "-g", "install", requirement.package], user=user logging.info("npm: running %r", cmd)
) run_detecting_problems(self.session, cmd, user=user)
if missing: if missing:
raise UnsatisfiedRequirements(missing) raise UnsatisfiedRequirements(missing)
@ -445,6 +531,7 @@ class StackedResolver(Resolver):
NATIVE_RESOLVER_CLS = [ NATIVE_RESOLVER_CLS = [
CPANResolver, CPANResolver,
CTANResolver,
PypiResolver, PypiResolver,
NpmResolver, NpmResolver,
GoResolver, GoResolver,

View file

@ -68,6 +68,8 @@ from ..requirements import (
CertificateAuthorityRequirement, CertificateAuthorityRequirement,
LibtoolRequirement, LibtoolRequirement,
VagueDependencyRequirement, VagueDependencyRequirement,
PerlPreDeclaredRequirement,
IntrospectionTypelibRequirement,
) )
@ -115,6 +117,36 @@ class AptRequirement(Requirement):
return True return True
return False return False
def satisfied_by(self, binaries, version):
def binary_pkg_matches(entry, binary):
# TODO(jelmer): check versions
if entry['name'] == binary['Package']:
return True
for provides_top in PkgRelation.parse_relations(
binary.get('Provides', '')):
for provides in provides_top:
if entry['name'] == provides['name']:
return True
return False
for rel in self.relations:
for entry in rel:
if any(binary_pkg_matches(entry, binary) for binary in binaries):
break
else:
return False
return True
def resolve_perl_predeclared_req(apt_mgr, req):
try:
req = req.lookup_module()
except KeyError:
logging.warning(
'Unable to map predeclared function %s to a perl module', req.name)
return None
return resolve_perl_module_req(apt_mgr, req)
def find_package_names( def find_package_names(
apt_mgr: AptManager, paths: List[str], regex: bool = False, case_insensitive=False apt_mgr: AptManager, paths: List[str], regex: bool = False, case_insensitive=False
@ -282,6 +314,17 @@ def get_package_for_python_module(apt_mgr, module, python_version, specs):
vague_map = { vague_map = {
"the Gnu Scientific Library": "libgsl-dev", "the Gnu Scientific Library": "libgsl-dev",
"the required FreeType library": "libfreetype-dev", "the required FreeType library": "libfreetype-dev",
"the Boost C++ libraries": "libboost-dev",
# TODO(jelmer): Support resolving virtual packages
"PythonLibs": "libpython3-dev",
"ZLIB": "libz3-dev",
"Osmium": "libosmium2-dev",
"glib": "libglib2.0-dev",
# TODO(jelmer): For Python, check minimum_version and map to python 2 or python 3
"Python": "libpython3-dev",
"Lua": "liblua5.4-dev",
} }
@ -289,7 +332,7 @@ def resolve_vague_dep_req(apt_mgr, req):
name = req.name name = req.name
options = [] options = []
if name in vague_map: if name in vague_map:
options.append(AptRequirement.simple(vague_map[name])) options.append(AptRequirement.simple(vague_map[name], minimum_version=req.minimum_version))
for x in req.expand(): for x in req.expand():
options.extend(resolve_requirement_apt(apt_mgr, x)) options.extend(resolve_requirement_apt(apt_mgr, x))
return options return options
@ -393,9 +436,9 @@ def resolve_php_package_req(apt_mgr, req):
def resolve_r_package_req(apt_mgr, req): def resolve_r_package_req(apt_mgr, req):
paths = [ paths = [
posixpath.join("/usr/lib/R/site-library/.*/R/%s$" % re.escape(req.package)) posixpath.join("/usr/lib/R/site-library", req.package, "DESCRIPTION")
] ]
return find_reqs_simple(apt_mgr, paths, regex=True) return find_reqs_simple(apt_mgr, paths, minimum_version=req.minimum_version)
def resolve_node_module_req(apt_mgr, req): def resolve_node_module_req(apt_mgr, req):
@ -485,7 +528,7 @@ def resolve_cmake_file_req(apt_mgr, req):
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" % re.escape(req.deps[0][0]) path = "/var/lib/ghc/package\\.conf\\.d/%s-.*\\.conf" % re.escape(req.package)
return find_reqs_simple(apt_mgr, [path], regex=True) return find_reqs_simple(apt_mgr, [path], regex=True)
@ -620,6 +663,12 @@ def resolve_ca_req(apt_mgr, req):
return [AptRequirement.simple("ca-certificates")] return [AptRequirement.simple("ca-certificates")]
def resolve_introspection_typelib_req(apt_mgr, req):
return find_reqs_simple(
apt_mgr, [r'/usr/lib/.*/girepository-.*/%s-.*\.typelib' % re.escape(req.library)],
regex=True)
def resolve_apt_req(apt_mgr, req): def resolve_apt_req(apt_mgr, req):
# TODO(jelmer): This should be checking whether versions match as well. # TODO(jelmer): This should be checking whether versions match as well.
for package_name in req.package_names(): for package_name in req.package_names():
@ -632,6 +681,7 @@ APT_REQUIREMENT_RESOLVERS = [
(AptRequirement, resolve_apt_req), (AptRequirement, resolve_apt_req),
(BinaryRequirement, resolve_binary_req), (BinaryRequirement, resolve_binary_req),
(VagueDependencyRequirement, resolve_vague_dep_req), (VagueDependencyRequirement, resolve_vague_dep_req),
(PerlPreDeclaredRequirement, resolve_perl_predeclared_req),
(PkgConfigRequirement, resolve_pkg_config_req), (PkgConfigRequirement, resolve_pkg_config_req),
(PathRequirement, resolve_path_req), (PathRequirement, resolve_path_req),
(CHeaderRequirement, resolve_c_header_req), (CHeaderRequirement, resolve_c_header_req),
@ -668,6 +718,7 @@ APT_REQUIREMENT_RESOLVERS = [
(PythonPackageRequirement, resolve_python_package_req), (PythonPackageRequirement, resolve_python_package_req),
(CertificateAuthorityRequirement, resolve_ca_req), (CertificateAuthorityRequirement, resolve_ca_req),
(CargoCrateRequirement, resolve_cargo_crate_req), (CargoCrateRequirement, resolve_cargo_crate_req),
(IntrospectionTypelibRequirement, resolve_introspection_typelib_req),
] ]
@ -683,11 +734,20 @@ def resolve_requirement_apt(apt_mgr, req: Requirement) -> List[AptRequirement]:
raise NotImplementedError(type(req)) raise NotImplementedError(type(req))
def default_tie_breakers(session):
from ..debian.udd import popcon_tie_breaker
from ..debian.build_deps import BuildDependencyTieBreaker
return [
BuildDependencyTieBreaker.from_session(session),
popcon_tie_breaker,
]
class AptResolver(Resolver): class AptResolver(Resolver):
def __init__(self, apt, tie_breakers=None): def __init__(self, apt, tie_breakers=None):
self.apt = apt self.apt = apt
if tie_breakers is None: if tie_breakers is None:
tie_breakers = [] tie_breakers = default_tie_breakers(apt.session)
self.tie_breakers = tie_breakers self.tie_breakers = tie_breakers
def __str__(self): def __str__(self):

View file

@ -185,6 +185,8 @@ class SchrootSession(Session):
self.check_call(["chown", user, home], cwd="/", user="root") self.check_call(["chown", user, home], cwd="/", user="root")
def external_path(self, path: str) -> str: def external_path(self, path: str) -> str:
if os.path.isabs(path):
return os.path.join(self.location, path.lstrip("/"))
if self._cwd is None: if self._cwd is None:
raise ValueError("no cwd set") raise ValueError("no cwd set")
return os.path.join(self.location, os.path.join(self._cwd, path).lstrip("/")) return os.path.join(self.location, os.path.join(self._cwd, path).lstrip("/"))

View file

@ -53,17 +53,12 @@ janitor (0.1-1) UNRELEASED; urgency=medium
) )
self.assertFileEqual( self.assertFileEqual(
"""\ """\
janitor (0.1-1jan+some1) some-fixes; urgency=low janitor (0.1-1jan+some1) some-fixes; urgency=medium
* Initial release. (Closes: #XXXXXX)
* Dummy build. * Dummy build.
-- Jelmer Vernooij <jelmer@debian.org> Sat, 05 Sep 2020 12:35:04 -0000 -- Jelmer Vernooij <jelmer@debian.org> Sat, 05 Sep 2020 12:35:04 -0000
janitor (0.1-1) UNRELEASED; urgency=medium
* Initial release. (Closes: #XXXXXX)
-- Jelmer Vernooij <jelmer@debian.org> Sat, 04 Apr 2020 14:12:13 +0000
""", """,
"debian/changelog", "debian/changelog",
) )
@ -97,17 +92,12 @@ janitor (0.1) UNRELEASED; urgency=medium
) )
self.assertFileEqual( self.assertFileEqual(
"""\ """\
janitor (0.1jan+some1) some-fixes; urgency=low janitor (0.1jan+some1) some-fixes; urgency=medium
* Initial release. (Closes: #XXXXXX)
* Dummy build. * Dummy build.
-- Jelmer Vernooij <jelmer@debian.org> Sat, 05 Sep 2020 12:35:04 -0000 -- Jelmer Vernooij <jelmer@debian.org> Sat, 05 Sep 2020 12:35:04 -0000
janitor (0.1) UNRELEASED; urgency=medium
* Initial release. (Closes: #XXXXXX)
-- Jelmer Vernooij <jelmer@debian.org> Sat, 04 Apr 2020 14:12:13 +0000
""", """,
"debian/changelog", "debian/changelog",
) )
@ -141,17 +131,12 @@ janitor (0.1-1jan+some1) UNRELEASED; urgency=medium
) )
self.assertFileEqual( self.assertFileEqual(
"""\ """\
janitor (0.1-1jan+some2) some-fixes; urgency=low janitor (0.1-1jan+some2) some-fixes; urgency=medium
* Initial release. (Closes: #XXXXXX)
* Dummy build. * Dummy build.
-- Jelmer Vernooij <jelmer@debian.org> Sat, 05 Sep 2020 12:35:04 -0000 -- Jelmer Vernooij <jelmer@debian.org> Sat, 05 Sep 2020 12:35:04 -0000
janitor (0.1-1jan+some1) UNRELEASED; urgency=medium
* Initial release. (Closes: #XXXXXX)
-- Jelmer Vernooij <jelmer@debian.org> Sat, 04 Apr 2020 14:12:13 +0000
""", """,
"debian/changelog", "debian/changelog",
) )

View file

@ -6,12 +6,12 @@ from setuptools import setup
setup(name="ognibuild", setup(name="ognibuild",
description="Detect and run any build system", description="Detect and run any build system",
version="0.0.3", version="0.0.4",
maintainer="Jelmer Vernooij", maintainer="Jelmer Vernooij",
maintainer_email="jelmer@jelmer.uk", maintainer_email="jelmer@jelmer.uk",
license="GNU GPLv2 or later", license="GNU GPLv2 or later",
url="https://jelmer.uk/code/ognibuild", url="https://jelmer.uk/code/ognibuild",
packages=['ognibuild'], packages=['ognibuild', 'ognibuild.tests', 'ognibuild.debian', 'ognibuild.resolver', 'ognibuild.session'],
classifiers=[ classifiers=[
'Development Status :: 4 - Beta', 'Development Status :: 4 - Beta',
'License :: OSI Approved :: ' 'License :: OSI Approved :: '