Blame tools/mpp-depsolve.py

Packit a20ca0
#!/usr/bin/python3
Packit a20ca0
Packit a20ca0
"""Manifest-Pre-Processor - Depsolving
Packit a20ca0
Packit a20ca0
This manifest-pre-processor consumes a manifest on stdin, processes it, and
Packit a20ca0
produces the resulting manifest on stdout.
Packit a20ca0
Packit a20ca0
This tool adjusts the `org.osbuild.rpm` stage. It consumes the `mpp-depsolve`
Packit a20ca0
option and produces a package-list and source-entries.
Packit a20ca0
Packit Service 4f7b0c
It supports version "1" and version "2" of the manifest description format.
Packit Service 4f7b0c
Packit Service 4f7b0c
The parameters for this pre-processor, version "1", look like this:
Packit a20ca0
Packit a20ca0
```
Packit a20ca0
...
Packit a20ca0
    {
Packit a20ca0
      "name": "org.osbuild.rpm",
Packit a20ca0
      ...
Packit a20ca0
      "options": {
Packit a20ca0
        ...
Packit a20ca0
        "mpp-depsolve": {
Packit a20ca0
          "architecture": "x86_64",
Packit a20ca0
          "module-platform-id": "f32",
Packit a20ca0
          "baseurl": "http://mirrors.kernel.org/fedora/releases/32/Everything/x86_64/os",
Packit a20ca0
          "repos": [
Packit a20ca0
            {
Packit a20ca0
              "id": "default",
Packit a20ca0
              "metalink": "https://mirrors.fedoraproject.org/metalink?repo=fedora-32&arch=$basearch"
Packit a20ca0
            }
Packit a20ca0
          ],
Packit a20ca0
          "packages": [
Packit a20ca0
            "@core",
Packit a20ca0
            "dracut-config-generic",
Packit a20ca0
            "grub2-pc",
Packit a20ca0
            "kernel"
Packit a20ca0
          ],
Packit a20ca0
          "excludes": [
Packit a20ca0
            (optional excludes)
Packit a20ca0
          ]
Packit a20ca0
        }
Packit a20ca0
      }
Packit a20ca0
    }
Packit a20ca0
...
Packit a20ca0
```
Packit Service 4f7b0c
Packit Service 4f7b0c
The parameters for this pre-processor, version "2", look like this:
Packit Service 4f7b0c
Packit Service 4f7b0c
```
Packit Service 4f7b0c
...
Packit Service 4f7b0c
    {
Packit Service 4f7b0c
      "name": "org.osbuild.rpm",
Packit Service 4f7b0c
      ...
Packit Service 4f7b0c
      "inputs": {
Packit Service 4f7b0c
        packages: {
Packit Service 4f7b0c
          "mpp-depsolve": {
Packit Service 4f7b0c
              see above
Packit Service 4f7b0c
          }
Packit Service 4f7b0c
        }
Packit Service 4f7b0c
      }
Packit Service 4f7b0c
    }
Packit Service 4f7b0c
...
Packit Service 4f7b0c
```
Packit a20ca0
"""
Packit a20ca0
Packit a20ca0
import argparse
Packit a20ca0
import contextlib
Packit a20ca0
import json
Packit a20ca0
import os
Packit a20ca0
import sys
Packit a20ca0
import tempfile
Packit Service 2d981f
import urllib.parse
Packit a20ca0
Packit a20ca0
import dnf
Packit a20ca0
import hawkey
Packit a20ca0
Packit a20ca0
Packit a20ca0
class State:
Packit a20ca0
    dnf_cache = None                    # DNF Cache Directory
Packit a20ca0
Packit a20ca0
    manifest = None                     # Input/Working Manifest
Packit a20ca0
    manifest_urls = None                # Link to sources URL dict
Packit a20ca0
    manifest_todo = []                  # Array of links to RPM stages
Packit a20ca0
Packit a20ca0
Packit a20ca0
def _dnf_repo(conf, desc):
Packit a20ca0
    repo = dnf.repo.Repo(desc["id"], conf)
Packit Service 2d981f
    if "baseurl" in desc:
Packit Service 2d981f
        repo.baseurl = desc["baseurl"]
Packit Service 2d981f
    elif "metalink" in desc:
Packit Service 2d981f
        repo.metalink = desc["metalink"]
Packit Service 2d981f
    elif "mirrorlist" in desc:
Packit Service 2d981f
        repo.metalink = desc["mirrorlist"]
Packit Service 2d981f
    else:
Packit Service 2d981f
        raise ValueError("repo description does not contain baseurl, metalink, or mirrorlist keys")
Packit a20ca0
    return repo
Packit a20ca0
Packit a20ca0
Packit a20ca0
def _dnf_base(repos, module_platform_id, persistdir, cachedir, arch):
Packit a20ca0
    base = dnf.Base()
Packit Service 2d981f
    if cachedir:
Packit Service 2d981f
        base.conf.cachedir = cachedir
Packit a20ca0
    base.conf.config_file_path = "/dev/null"
Packit a20ca0
    base.conf.module_platform_id = module_platform_id
Packit a20ca0
    base.conf.persistdir = persistdir
Packit a20ca0
    base.conf.substitutions['arch'] = arch
Packit a20ca0
    base.conf.substitutions['basearch'] = dnf.rpm.basearch(arch)
Packit a20ca0
Packit a20ca0
    for repo in repos:
Packit a20ca0
        base.repos.add(_dnf_repo(base.conf, repo))
Packit a20ca0
Packit a20ca0
    base.fill_sack(load_system_repo=False)
Packit a20ca0
    return base
Packit a20ca0
Packit a20ca0
Packit a20ca0
def _dnf_resolve(state, mpp_depsolve):
Packit a20ca0
    deps = []
Packit a20ca0
Packit a20ca0
    arch = mpp_depsolve["architecture"]
Packit a20ca0
    mpid = mpp_depsolve["module-platform-id"]
Packit a20ca0
    repos = mpp_depsolve.get("repos", [])
Packit a20ca0
    packages = mpp_depsolve.get("packages", [])
Packit a20ca0
    excludes = mpp_depsolve.get("excludes", [])
Packit Service 4f7b0c
    baseurl = mpp_depsolve.get("baseurl")
Packit Service 4f7b0c
Packit Service 4f7b0c
    baseurls = {
Packit Service 4f7b0c
        repo["id"]: repo.get("baseurl", baseurl) for repo in repos
Packit Service 4f7b0c
    }
Packit a20ca0
Packit a20ca0
    if len(packages) > 0:
Packit a20ca0
        with tempfile.TemporaryDirectory() as persistdir:
Packit a20ca0
            base = _dnf_base(repos, mpid, persistdir, state.dnf_cache, arch)
Packit a20ca0
            base.install_specs(packages, exclude=excludes)
Packit a20ca0
            base.resolve()
Packit a20ca0
Packit a20ca0
            for tsi in base.transaction:
Packit a20ca0
                if tsi.action not in dnf.transaction.FORWARD_ACTIONS:
Packit a20ca0
                    continue
Packit a20ca0
Packit a20ca0
                checksum_type = hawkey.chksum_name(tsi.pkg.chksum[0])
Packit a20ca0
                checksum_hex = tsi.pkg.chksum[1].hex()
Packit Service 4f7b0c
Packit Service 4f7b0c
                path = tsi.pkg.relativepath
Packit Service 4f7b0c
                base = baseurls[tsi.pkg.reponame]
Packit Service 4f7b0c
                # dep["path"] often starts with a "/", even though it's meant to be
Packit Service 4f7b0c
                # relative to `baseurl`. Strip any leading slashes, but ensure there's
Packit Service 4f7b0c
                # exactly one between `baseurl` and the path.
Packit Service 4f7b0c
                url = urllib.parse.urljoin(base + "/", path.lstrip("/"))
Packit Service 4f7b0c
Packit a20ca0
                pkg = {
Packit a20ca0
                    "checksum": f"{checksum_type}:{checksum_hex}",
Packit a20ca0
                    "name": tsi.pkg.name,
Packit Service 4f7b0c
                    "url": url,
Packit a20ca0
                }
Packit a20ca0
                deps.append(pkg)
Packit a20ca0
Packit a20ca0
    return deps
Packit a20ca0
Packit a20ca0
Packit a20ca0
def _manifest_enter(manifest, key, default):
Packit a20ca0
    if key not in manifest:
Packit a20ca0
        manifest[key] = default
Packit a20ca0
    return manifest[key]
Packit a20ca0
Packit a20ca0
Packit Service 4f7b0c
def _manifest_parse_v1(state, data):
Packit a20ca0
    manifest = data
Packit a20ca0
Packit a20ca0
    # Resolve "sources"."org.osbuild.files"."url".
Packit a20ca0
    manifest_sources = _manifest_enter(manifest, "sources", {})
Packit a20ca0
    manifest_files = _manifest_enter(manifest_sources, "org.osbuild.files", {})
Packit a20ca0
    manifest_urls = _manifest_enter(manifest_files, "urls", {})
Packit a20ca0
Packit a20ca0
    # Resolve "pipeline"."stages".
Packit a20ca0
    manifest_pipeline = _manifest_enter(manifest, "pipeline", {})
Packit a20ca0
    manifest_stages = _manifest_enter(manifest_pipeline, "stages", [])
Packit a20ca0
Packit a20ca0
    # Collect all stages of interest in `manifest_todo`.
Packit a20ca0
    manifest_todo = []
Packit a20ca0
    for stage in manifest_stages:
Packit a20ca0
        if stage.get("name", "") != "org.osbuild.rpm":
Packit a20ca0
            continue
Packit a20ca0
Packit a20ca0
        stage_options = _manifest_enter(stage, "options", {})
Packit a20ca0
        if "mpp-depsolve" not in stage_options:
Packit a20ca0
            continue
Packit a20ca0
Packit a20ca0
        manifest_todo.append(stage)
Packit a20ca0
Packit a20ca0
    # Remember links of interest.
Packit a20ca0
    state.manifest = manifest
Packit a20ca0
    state.manifest_urls = manifest_urls
Packit a20ca0
    state.manifest_todo = manifest_todo
Packit a20ca0
Packit a20ca0
Packit Service 4f7b0c
def _manifest_process_v1(state, stage):
Packit a20ca0
    options = _manifest_enter(stage, "options", {})
Packit a20ca0
    options_mpp = _manifest_enter(options, "mpp-depsolve", {})
Packit a20ca0
    options_packages = _manifest_enter(options, "packages", [])
Packit a20ca0
Packit a20ca0
    del(options["mpp-depsolve"])
Packit a20ca0
Packit a20ca0
    deps = _dnf_resolve(state, options_mpp)
Packit a20ca0
    for dep in deps:
Packit a20ca0
        options_packages.append(dep["checksum"])
Packit Service 4f7b0c
        state.manifest_urls[dep["checksum"]] = dep["url"]
Packit Service 4f7b0c
Packit Service 4f7b0c
Packit Service 4f7b0c
def _manifest_depsolve_v1(state, src):
Packit Service 4f7b0c
    _manifest_parse_v1(state, src)
Packit Service 4f7b0c
Packit Service 4f7b0c
    for stage in state.manifest_todo:
Packit Service 4f7b0c
        _manifest_process_v1(state, stage)
Packit Service 4f7b0c
Packit Service 4f7b0c
Packit Service 4f7b0c
def _manifest_parse_v2(state, manifest):
Packit Service 4f7b0c
    todo = []
Packit Service 4f7b0c
Packit Service 4f7b0c
    for pipeline in manifest.get("pipelines", {}):
Packit Service 4f7b0c
        for stage in pipeline.get("stages", []):
Packit Service 4f7b0c
            if stage["type"] != "org.osbuild.rpm":
Packit Service 4f7b0c
                continue
Packit Service 4f7b0c
Packit Service 4f7b0c
            inputs = _manifest_enter(stage, "inputs", {})
Packit Service 4f7b0c
            packages = _manifest_enter(inputs, "packages", {})
Packit Service 4f7b0c
Packit Service 4f7b0c
            if "mpp-depsolve" not in packages:
Packit Service 4f7b0c
                continue
Packit Service 4f7b0c
Packit Service 4f7b0c
            todo.append(packages)
Packit Service 4f7b0c
Packit Service 4f7b0c
    sources = _manifest_enter(manifest, "sources", {})
Packit Service 4f7b0c
    files = _manifest_enter(sources, "org.osbuild.curl", {})
Packit Service 4f7b0c
    urls = _manifest_enter(files, "items", {})
Packit Service 4f7b0c
Packit Service 4f7b0c
    state.manifest = manifest
Packit Service 4f7b0c
    state.manifest_todo = todo
Packit Service 4f7b0c
    state.manifest_urls = urls
Packit Service 4f7b0c
Packit Service 4f7b0c
Packit Service 4f7b0c
def _manifest_process_v2(state, ip):
Packit Service 4f7b0c
    urls = state.manifest_urls
Packit Service 4f7b0c
    refs = _manifest_enter(ip, "references", {})
Packit Service 4f7b0c
Packit Service 4f7b0c
    mpp = ip["mpp-depsolve"]
Packit Service 4f7b0c
Packit Service 4f7b0c
    deps = _dnf_resolve(state, mpp)
Packit Service 4f7b0c
Packit Service 4f7b0c
    for dep in deps:
Packit Service 4f7b0c
        checksum = dep["checksum"]
Packit Service 4f7b0c
        refs[checksum] = {}
Packit Service 4f7b0c
        urls[checksum] = dep["url"]
Packit Service 4f7b0c
Packit Service 4f7b0c
    del ip["mpp-depsolve"]
Packit Service 4f7b0c
Packit Service 4f7b0c
Packit Service 4f7b0c
def _manifest_depsolve_v2(state, src):
Packit Service 4f7b0c
    _manifest_parse_v2(state, src)
Packit Service 4f7b0c
Packit Service 4f7b0c
    for todo in state.manifest_todo:
Packit Service 4f7b0c
        _manifest_process_v2(state, todo)
Packit a20ca0
Packit a20ca0
Packit a20ca0
def _main_args(argv):
Packit a20ca0
    parser = argparse.ArgumentParser(description="Generate Test Manifests")
Packit a20ca0
Packit a20ca0
    parser.add_argument(
Packit a20ca0
        "--dnf-cache",
Packit a20ca0
        metavar="PATH",
Packit a20ca0
        type=os.path.abspath,
Packit a20ca0
        default=None,
Packit a20ca0
        help="Path to DNF cache-directory to use",
Packit a20ca0
    )
Packit a20ca0
Packit a20ca0
    return parser.parse_args(argv[1:])
Packit a20ca0
Packit a20ca0
Packit a20ca0
@contextlib.contextmanager
Packit a20ca0
def _main_state(args):
Packit a20ca0
    state = State()
Packit Service 2d981f
    if args.dnf_cache:
Packit Service 2d981f
        state.dnf_cache = args.dnf_cache
Packit Service 2d981f
    yield state
Packit a20ca0
Packit a20ca0
Packit a20ca0
def _main_process(state):
Packit a20ca0
    src = json.load(sys.stdin)
Packit Service 4f7b0c
    version = src.get("version", "1")
Packit Service 4f7b0c
    if version == "1":
Packit Service 4f7b0c
        _manifest_depsolve_v1(state, src)
Packit Service 4f7b0c
    elif version == "2":
Packit Service 4f7b0c
        _manifest_depsolve_v2(state, src)
Packit Service 4f7b0c
    else:
Packit Service 4f7b0c
        print(f"Unknown manifest version {version}", file=sys.stderr)
Packit Service 4f7b0c
        return 1
Packit a20ca0
Packit a20ca0
    json.dump(state.manifest, sys.stdout, indent=2)
Packit a20ca0
    sys.stdout.write("\n")
Packit Service 4f7b0c
    return 0
Packit a20ca0
Packit a20ca0
Packit a20ca0
def main() -> int:
Packit a20ca0
    args = _main_args(sys.argv)
Packit a20ca0
    with _main_state(args) as state:
Packit a20ca0
        _main_process(state)
Packit a20ca0
Packit a20ca0
    return 0
Packit a20ca0
Packit a20ca0
Packit a20ca0
if __name__ == "__main__":
Packit a20ca0
    sys.exit(main())