From bef387848f63d7c91278fd85a6ed4a6014a9749b Mon Sep 17 00:00:00 2001 From: Achilleas Koutsou Date: Tue, 30 Nov 2021 18:33:34 +0100 Subject: [PATCH] osbuild-mpp: Add support for a pacman resolver This introduces a new dependency resolver to osbuild-mpp for Arch Linux which uses the pacman package manager. The used solver is determined by the `solver` field in the `mpp-depsolve` object inside the manifest file, if it does not exists it falls back to the DepSolver for dnf/rpm. Co-Authored-By: Jelle van der Waa --- tools/osbuild-mpp | 118 ++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 103 insertions(+), 15 deletions(-) diff --git a/tools/osbuild-mpp b/tools/osbuild-mpp index 1d8d21ae..b607e156 100755 --- a/tools/osbuild-mpp +++ b/tools/osbuild-mpp @@ -74,6 +74,7 @@ The parameters for this pre-processor, version "1", look like this: "mpp-depsolve": { "architecture": "x86_64", "module-platform-id": "f32", + "solver": "dnf", "baseurl": "http://mirrors.kernel.org/fedora/releases/32/Everything/x86_64/os", "repos": [ { @@ -353,6 +354,73 @@ class PkgInfo: return self.nevra +class PacmanSolver(): + + def __init__(self, cachedir, persistdir): + self._cachedir = cachedir or "/tmp/pacsolve" + self._persistdir = persistdir + + def setup_root(self): + root = self._cachedir + os.makedirs(root, exist_ok=True) + os.makedirs(os.path.join(root, "var", "lib", "pacman"), exist_ok=True) + os.makedirs(os.path.join(root, "etc"), exist_ok=True) + + def reset(self, arch, _, module_platform_id, ignore_weak_deps): + self.setup_root() + cfg = f""" +[options] +Architecture = {arch} +CheckSpace +SigLevel = Required DatabaseOptional +LocalFileSigLevel = Optional +""" + cfgpath = os.path.join(self._cachedir, "etc", "pacman.conf") + with open(cfgpath, "w", encoding="utf-8") as cfgfile: + cfgfile.write(cfg) + + def add_repo(self, desc, _): + rid = desc["id"] + url = desc["baseurl"] + cfgpath = os.path.join(self._cachedir, "etc", "pacman.conf") + with open(cfgpath, "a", encoding="utf-8") as cfgfile: + cfgfile.write("\n") + cfgfile.write(f"[{rid}]\n") + cfgfile.write(f"Server = {url}\n") + + def _pacman(self, *args): + return subprocess.check_output(["pacman", *args], encoding="utf-8") + + def resolve(self, packages, _): + self._pacman("-Sy", "--root", self._cachedir, "--config", os.path.join(self._cachedir, "etc", "pacman.conf")) + res = self._pacman("-S", "--print", "--print-format", r'{"url": "%l", "version": "%v", "name": "%n"},', + "--sysroot", self._cachedir, *packages) + res = "[" + res.strip().rstrip(",") + "]" + data = json.loads(res) + packages = [] + for pkg in data: + pkginfo = self._pacman("-Sii", "--sysroot", self._cachedir, pkg["name"]) + pkgdata = self.parse_pkg_info(pkginfo) + p = PkgInfo( + "sha256:"+pkgdata["SHA-256 Sum"], + pkg["name"], + pkg["version"], + pkgdata["Architecture"], + ) + p.url = pkg["url"] + packages.append(p) + return packages + + @staticmethod + def parse_pkg_info(info): + lines = info.split("\n") + + def parse_line(l): + k, v = l.split(":", maxsplit=1) + return k.strip(), v.strip() + return dict([parse_line(line) for line in lines if ":" in line]) + + class DepSolver: def __init__(self, cachedir, persistdir): self.cachedir = cachedir @@ -491,6 +559,23 @@ class DepSolver: return deps +class DepSolverFactory(): + + def __init__(self, cachedir, persistdir): + self._cachedir = cachedir + self._persistdir = persistdir + self._solvers = {} + + def get_depsolver(self, solver): + if solver not in self._solvers: + if solver == "alpm": + klass = PacmanSolver + else: + klass = DepSolver + self._solvers[solver] = klass(self._cachedir, self._persistdir) + return self._solvers[solver] + + class Partition: def __init__(self, uid: str = None, @@ -757,12 +842,13 @@ class ManifestFile: with f: return ManifestFile.load_from_fd(f, fullpath, self.overrides, self.vars, self.searchdirs) - def depsolve(self, solver: DepSolver, desc: Dict): + def depsolve(self, solver_factory: DepSolverFactory, desc: Dict): repos = desc.get("repos", []) packages = desc.get("packages", []) excludes = desc.get("excludes", []) baseurl = desc.get("baseurl") arch = desc.get("architecture") + solver = solver_factory.get_depsolver(desc.get("solver", "dnf")) if not packages: return [] @@ -979,8 +1065,8 @@ class ManifestFileV1(ManifestFile): self._process_import(current) current = current.get("pipeline", {}).get("build") - def _process_depsolve(self, solver, stage, pipeline_name): - if stage.get("name", "") != "org.osbuild.rpm": + def _process_depsolve(self, solver_factory, stage, pipeline_name): + if stage.get("name", "") not in ("org.osbuild.pacman", "org.osbuild.rpm"): return options = stage.get("options") if not options: @@ -994,12 +1080,12 @@ class ManifestFileV1(ManifestFile): packages = element_enter(options, "packages", []) - deps = self.depsolve(solver, mpp) + deps = self.depsolve(solver_factory, mpp) checksums = self.add_packages(deps, pipeline_name) packages += checksums - def process_depsolves(self, solver, pipeline=None, depth=0): + def process_depsolves(self, solver_factory, pipeline=None, depth=0): if pipeline is None: pipeline = self.pipeline @@ -1012,11 +1098,11 @@ class ManifestFileV1(ManifestFile): stages = element_enter(pipeline, "stages", []) for stage in stages: - self._process_depsolve(solver, stage, pipeline_name) + self._process_depsolve(solver_factory, stage, pipeline_name) build = pipeline.get("build") if build: if "pipeline" in build: - self.process_depsolves(solver, build["pipeline"], depth+1) + self.process_depsolves(solver_factory, build["pipeline"], depth+1) def process_embed_files(self): "Embedding files is not supported for v1 manifests" @@ -1080,8 +1166,8 @@ class ManifestFileV2(ManifestFile): for pipeline in old_pipelines: self.pipelines.extend(self._process_import(pipeline)) - def _process_depsolve(self, solver, stage, pipeline_name): - if stage.get("type", "") != "org.osbuild.rpm": + def _process_depsolve(self, solver_factory, stage, pipeline_name): + if stage.get("type", "") not in ("org.osbuild.pacman", "org.osbuild.rpm"): return inputs = element_enter(stage, "inputs", {}) packages = element_enter(inputs, "packages", {}) @@ -1094,18 +1180,18 @@ class ManifestFileV2(ManifestFile): refs = element_enter(packages, "references", {}) - deps = self.depsolve(solver, mpp) + deps = self.depsolve(solver_factory, mpp) checksums = self.add_packages(deps, pipeline_name) for checksum in checksums: refs[checksum] = {} - def process_depsolves(self, solver): + def process_depsolves(self, solver_factory): for pipeline in self.pipelines: name = pipeline.get("name", "") stages = element_enter(pipeline, "stages", []) for stage in stages: - self._process_depsolve(solver, stage, name) + self._process_depsolve(solver_factory, stage, name) def process_embed_files(self): @@ -1164,11 +1250,13 @@ class ManifestFileV2(ManifestFile): def main(): parser = argparse.ArgumentParser(description="Manifest pre processor") parser.add_argument( + "--cache", "--dnf-cache", + dest="cachedir", metavar="PATH", type=os.path.abspath, default=None, - help="Path to DNF cache-directory to use", + help="Path to package cache-directory to use", ) parser.add_argument( "-I", "--import-dir", @@ -1223,8 +1311,8 @@ def main(): m.process_embed_files() with tempfile.TemporaryDirectory() as persistdir: - solver = DepSolver(args.dnf_cache, persistdir) - m.process_depsolves(solver) + solver_factory = DepSolverFactory(args.cachedir, persistdir) + m.process_depsolves(solver_factory) m.process_format()