#!/usr/bin/env python3 """converts RPM spec fixtures into holy recipes and builds the produced manifests.""" import hashlib import io from pathlib import Path import shutil import subprocess import sys import tarfile import tempfile binary = str(Path(sys.argv[1]).resolve()) def run(*args, status=0): result = subprocess.run([binary, *map(str, args)], text=True, capture_output=True, errors="replace") assert result.returncode == status, (args, result.returncode, result.stdout, result.stderr) return result.stdout def read_metadata(artifact): result = subprocess.run(["lz4", "-dc", str(artifact)], capture_output=True) assert result.returncode == 0, (artifact, result.returncode, result.stderr) with tarfile.open(fileobj=io.BytesIO(result.stdout)) as archive: def read(name): return archive.extractfile(name).read().decode() return {name: read(name) for name in ("HOLY/meta", "HOLY/files", "HOLY/deps", "HOLY/origin", "HOLY/transform")}, None def write(path, text): path.parent.mkdir(parents=True, exist_ok=True) path.write_text(text) def source_tree(root, name, version): directory = root / f"{name}-{version}" (directory / "usr/share").mkdir(parents=True) write(directory / "Makefile", """all: bin/%(name)s bin/%(name)s: hello.c \tmkdir -p bin \t$(CC) $(CFLAGS) -o $@ hello.c install: bin/%(name)s \tmkdir -p $(DESTDIR)/usr/bin $(DESTDIR)/usr/lib $(DESTDIR)/usr/include \tcp bin/%(name)s $(DESTDIR)/usr/bin/ \tcp hello.txt $(DESTDIR)/usr/lib/ \tcp hello.h $(DESTDIR)/usr/include/ """ % {"name": name}) write(directory / "hello.c", '#include \nint main(void){puts("hi");return 0;}\n') write(directory / "hello.h", "/* header */\n") write(directory / "hello.txt", "greeting\n") return directory def main(): if not shutil.which("lz4"): print("lz4 required for the rpmspec fixture", file=sys.stderr) return 6 for tool in ("cc", "make", "tar", "patch"): if not shutil.which(tool): print(f"{tool} required for the rpmspec fixture", file=sys.stderr) return 6 with tempfile.TemporaryDirectory() as scratch: root = Path(scratch) source = source_tree(root, "rpm-demo", "1.4.0") archive = root / "rpm-demo-1.4.0.tar.gz" subprocess.run(["tar", "czf", str(archive), "-C", str(root), "rpm-demo-1.4.0"], check=True) digest = hashlib.sha256(archive.read_bytes()).hexdigest() package = root / "rpm-demo" package.mkdir() shutil.copy(archive, package / "rpm-demo-1.4.0.tar.gz") write(package / "greeting.patch", "--- a/hello.c\n+++ b/hello.c\n@@ -1 +1 @@\n" "-#include \n+#include /* patched */\n") write(package / "rpm-demo.spec", """%global build_id 42 Name: rpm-demo Version: 1.4.0 Release: 7%{?dist} Summary: An RPM spec conversion fixture License: MIT URL: https://example.org/rpm-demo Source0: rpm-demo-1.4.0.tar.gz Patch0: greeting.patch BuildArch: noarch BuildRequires: make BuildRequires: gcc >= 11 Requires: bash >= 5.0 Requires: /bin/sh Provides: rpm-demo-extra = %{version} Conflicts: rpm-demo-old Recommends: bash-completion %description The rpm-demo program greets the operator once and then stops. It exists so the converter has a spec to read. %package devel Summary: Development files for %{name} Requires: %{name} = %{version}-%{release} %description devel Headers and the pkg-config fragment for %{name}. %prep %setup -q -n %{name}-%{version} %autopatch -p1 %build make CFLAGS="-O2" %install mkdir -p %{buildroot}/usr/bin %{buildroot}/usr/lib %{buildroot}/usr/include cp bin/rpm-demo %{buildroot}/usr/bin/ cp hello.txt %{buildroot}/usr/lib/ cp hello.h %{buildroot}/usr/include/ %check test -x bin/rpm-demo %files %license COPYING /usr/bin/rpm-demo %files devel /usr/include/hello.h %changelog * Mon Jan 01 2024 Fixture - 1.4.0-7 - The fixture release. """) out = run("convert", package / "rpm-demo.spec", "--source", "fedora", "--output", root / "conv", status=3) assert "converted rpm-demo status review-required" in out recipe = (root / "conv" / "rpm-demo.recipe").read_text() report = (root / "conv" / "conversion").read_text() assert (root / "conv" / "rpm-demo.spec").read_text() == \ (package / "rpm-demo.spec").read_text() assert hashlib.sha256((root / "conv" / "rpm-demo.spec").read_bytes()).hexdigest() in report assert (root / "conv" / "rpm-demo-1.4.0.tar.gz").is_file() # identity, provenance and the recorded sources assert 'name "rpm-demo"' in recipe and 'version "1.4.0"' in recipe assert 'release "7"' in recipe and 'arch "noarch"' in recipe and 'libc any' in recipe assert 'summary "An RPM spec conversion fixture"' in recipe assert 'license "MIT"' in recipe assert 'homepage "https://example.org/rpm-demo"' in recipe assert 'x-source-family rpm' in recipe and 'x-converter rpmspec-1' in recipe assert f'source "rpm-demo-1.4.0.tar.gz" "rpm-demo-1.4.0.tar.gz"' in recipe assert f'source-sha256 "rpm-demo-1.4.0.tar.gz" "{digest}"' in recipe assert 'source "greeting.patch" "greeting.patch"' in recipe # the requirement lists keep their comparison assert 'build-depend "make" "any" "-"' in recipe assert 'build-depend "gcc" "ge" "11"' in recipe assert 'depend "bash" "ge" "5.0"' in recipe assert 'x-provides "rpm-demo-extra"' in recipe assert 'x-suggests "bash-completion"' in recipe assert 'depend "bash-completion"' not in recipe assert 'x-conflicts "rpm-demo-old"' in recipe assert 'depend "rpm-demo" "ge" "5.0"' not in recipe # the sections become the Holy phases assert "step prepare /bin/sh < $RPM_BUILD_ROOT/usr/share/rpm-imported/data """) run("import", package / "rpm-demo.spec", "--source", "fedora", "--format", "rpmspec", "--output", root / "imported", status=0) assert (root / "imported" / "conversion").is_file() assert (root / "imported" / "rpm-imported.recipe").is_file() return 0 if __name__ == "__main__": sys.exit(main())