diff --git a/ognibuild/buildsystem.py b/ognibuild/buildsystem.py index 71e44c5..82d4992 100644 --- a/ognibuild/buildsystem.py +++ b/ognibuild/buildsystem.py @@ -40,6 +40,7 @@ from .requirements import ( CargoCrateRequirement, RPackageRequirement, OctavePackageRequirement, + PhpPackageRequirement, ) from .fix_build import run_with_build_fixers from .session import which @@ -98,6 +99,16 @@ class BuildSystem(object): return None +def xmlparse_simplify_namespaces(path, namespaces): + import xml.etree.ElementTree as ET + namespaces = ['{%s}' % ns for ns in namespaces] + tree = ET.iterparse(path) + for _, el in tree: + for namespace in namespaces: + el.tag = el.tag.replace(namespace, '') + return tree.root + + class Pear(BuildSystem): name = "pear" @@ -130,6 +141,32 @@ class Pear(BuildSystem): self.setup(resolver) run_with_build_fixers(session, ["pear", "install", self.path], fixers) + def get_declared_dependencies(self, session, fixers=None): + path = os.path.join(self.path, "package.xml") + import xml.etree.ElementTree as ET + try: + root = xmlparse_simplify_namespaces(path, [ + 'http://pear.php.net/dtd/package-2.0', + 'http://pear.php.net/dtd/package-2.1']) + except ET.ParseError as e: + logging.warning('Unable to parse package.xml: %s', e) + return + assert root.tag == 'package', 'root tag is %r' % root.tag + dependencies_tag = root.find('dependencies') + if dependencies_tag is not None: + required_tag = root.find('dependencies') + if required_tag is not None: + for package_tag in root.findall('package'): + name = package_tag.find('name').text + min_tag = package_tag.find('min') + max_tag = package_tag.find('max') + channel_tag = package_tag.find('channel') + yield "core", PhpPackageRequirement( + name, + channel=(channel_tag.text if channel_tag else None), + min_version=(min_tag.text if min_tag else None), + max_version=(max_tag.text if max_tag else None)) + @classmethod def probe(cls, path): if os.path.exists(os.path.join(path, "package.xml")): @@ -1214,6 +1251,23 @@ class Cabal(BuildSystem): return cls(os.path.join(path, "Setup.hs")) +class Composer(BuildSystem): + + name = "composer" + + def __init__(self, path): + self.path = path + + def __repr__(self): + return "%s(%r)" % (type(self).__name__, self.path) + + @classmethod + def probe(cls, path): + if os.path.exists(os.path.join(path, "composer.json")): + logging.debug("Found composer.json, assuming composer package.") + return cls(path) + + class PerlBuildTiny(BuildSystem): name = "perl-build-tiny" @@ -1254,7 +1308,7 @@ BUILDSYSTEM_CLSES = [ Pear, SetupPy, Npm, Waf, Cargo, Meson, Cabal, Gradle, Maven, DistZilla, Gem, PerlBuildTiny, Golang, R, Octave, # Make is intentionally at the end of the list. - Make, RunTests] + Make, Composer, RunTests] def scan_buildsystems(path): diff --git a/ognibuild/fix_build.py b/ognibuild/fix_build.py index 6bfc593..3853b26 100644 --- a/ognibuild/fix_build.py +++ b/ognibuild/fix_build.py @@ -69,21 +69,34 @@ def run_detecting_problems(session: Session, args: List[str], **kwargs): def run_with_build_fixers(session: Session, args: List[str], fixers: List[BuildFixer], **kwargs): fixed_errors = [] while True: + to_resolve = [] try: run_detecting_problems(session, args, **kwargs) except DetailedFailure as e: + to_resolve.append(e) + else: + return + while to_resolve: + e = to_resolve.pop(-1) logging.info("Identified error: %r", e.error) if e.error in fixed_errors: logging.warning( "Failed to resolve error %r, it persisted. Giving up.", e.error ) - raise DetailedFailure(e.retcode, args, e.error) - if not resolve_error(e.error, None, fixers=fixers): - logging.warning("Failed to find resolution for error %r. Giving up.", e.error) - raise DetailedFailure(e.retcode, args, e.error) - fixed_errors.append(e.error) - else: - return + raise e + try: + if not resolve_error(e.error, None, fixers=fixers): + logging.warning("Failed to find resolution for error %r. Giving up.", e.error) + raise e + except DetailedFailure as n: + logging.info('New error %r while resolving %r', n, e) + if n in to_resolve: + raise + to_resolve.append(e) + to_resolve.append(n) + else: + fixed_errors.append(e.error) + def resolve_error(error, phase, fixers): diff --git a/ognibuild/requirements.py b/ognibuild/requirements.py index 1e37a4d..cb83fbd 100644 --- a/ognibuild/requirements.py +++ b/ognibuild/requirements.py @@ -80,6 +80,22 @@ class PythonPackageRequirement(Requirement): return p.returncode == 0 +class PhpPackageRequirement(Requirement): + + def __init__(self, package: str, channel: Optional[str] = None, + min_version: Optional[str] = None, + max_version: Optional[str] = None): + self.package = package + self.channel = channel + self.min_version = min_version + self.max_version = max_version + + def __repr__(self): + return "%s(%r, %r, %r, %r)" % ( + type(self).__name__, self.package, self.channel, + self.min_version, self.max_version) + + class BinaryRequirement(Requirement): binary_name: str diff --git a/ognibuild/resolver/apt.py b/ognibuild/resolver/apt.py index 6b6f1d2..39ceb7c 100644 --- a/ognibuild/resolver/apt.py +++ b/ognibuild/resolver/apt.py @@ -41,6 +41,7 @@ from ..requirements import ( GoPackageRequirement, DhAddonRequirement, PhpClassRequirement, + PhpPackageRequirement, RPackageRequirement, NodeModuleRequirement, NodePackageRequirement, @@ -315,6 +316,10 @@ def resolve_php_class_req(apt_mgr, req): return find_reqs_simple(apt_mgr, [path]) +def resolve_php_package_req(apt_mgr, req): + return [AptRequirement.simple('php-%s' % req.package, minimum_version=req.min_version)] + + def resolve_r_package_req(apt_mgr, req): paths = [posixpath.join("/usr/lib/R/site-library/.*/R/%s$" % re.escape(req.package))] return find_reqs_simple(apt_mgr, paths, regex=True) @@ -560,6 +565,7 @@ APT_REQUIREMENT_RESOLVERS = [ (GoPackageRequirement, resolve_go_package_req), (DhAddonRequirement, resolve_dh_addon_req), (PhpClassRequirement, resolve_php_class_req), + (PhpPackageRequirement, resolve_php_package_req), (RPackageRequirement, resolve_r_package_req), (NodeModuleRequirement, resolve_node_module_req), (NodePackageRequirement, resolve_node_package_req),