#!/usr/bin/env python3 """converts Pacstall pacscripts 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): contents = subprocess.run(["lz4", "-dc", str(artifact)], check=True, capture_output=True).stdout with tarfile.open(fileobj=io.BytesIO(contents)) as archive: def read(name): return archive.extractfile(name).read().decode() names = sorted(member.name for member in archive.getmembers()) return {name: read(name) for name in ("HOLY/meta", "HOLY/files", "HOLY/deps", "HOLY/provides", "HOLY/hooks", "HOLY/origin", "HOLY/transform")}, names 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.mkdir(parents=True) write(directory / "Makefile", """CFLAGS ?= -O2 -pipe all: bin/%(name)s bin/%(name)s: main.c \tmkdir -p bin \t$(CC) $(CFLAGS) -o $@ main.c clean: \trm -rf bin check: bin/%(name)s \t./bin/%(name)s install: bin/%(name)s \tmkdir -p $(DESTDIR)/usr/bin $(DESTDIR)/usr/share/%(name)s $(DESTDIR)/etc \tcp bin/%(name)s $(DESTDIR)/usr/bin/ \tprintf 'greeting\\n' > $(DESTDIR)/usr/share/%(name)s/greeting \tprintf 'enabled = 1\\n' > $(DESTDIR)/etc/%(name)s.conf """ % {"name": name}) write(directory / "main.c", '#include \nint main(void){printf("hi\\n");return 0;}\n') write(directory / "README", f"{name} documentation\n") return directory def convert(package, file_name, name=None, status=0, source="pacstall"): out = run("convert", package / f"{file_name}.pacscript", "--source", source, "--output", package / "conv", status=status) name = name or file_name return out, (package / "conv" / f"{name}.recipe").read_text(), \ (package / "conv" / "conversion").read_text() def beside(directory, archive): """a local source travels with the pacscript that names it""" directory.mkdir(parents=True, exist_ok=True) shutil.copy(archive, directory / archive.name) def main(): if not shutil.which("lz4"): print("lz4 required for the pacstall fixture", file=sys.stderr) return 6 for tool in ("cc", "make", "tar"): if not shutil.which(tool): print(f"{tool} required for the pacstall fixture", file=sys.stderr) return 6 with tempfile.TemporaryDirectory() as scratch: root = Path(scratch) package = root / "pacstall-demo" package.mkdir(parents=True) source_tree(package, "holy-pacstall-demo", "1.2.3") archive = package / "holy-pacstall-demo-1.2.3.tar.gz" subprocess.run(["tar", "czf", str(archive), "-C", str(package), "holy-pacstall-demo-1.2.3"], check=True) digest = hashlib.sha256(archive.read_bytes()).hexdigest() write(package / "holy-pacstall-demo.pacscript", f"""# Maintainer: Fixture pkgname="holy-pacstall-demo" pkgver="1.2.3" pkgrel="2" pkgdesc="Pacstall conversion fixture" arch=("amd64") url="https://example.org/holy-pacstall-demo" license=("MIT") maintainer=("Fixture ") depends=("libc6" "zlib1g>=1.2") makedepends=("gcc" "make") optdepends=("python3: scripting") backup=("/etc/holy-pacstall-demo.conf") source=("holy-pacstall-demo-$pkgver.tar.gz") sha256sums=("{digest}") prepare() {{ make -C "$srcdir/holy-pacstall-demo-$pkgver" clean }} build() {{ make -C "$srcdir/holy-pacstall-demo-$pkgver" -j"$NCPU" }} check() {{ make -C "$srcdir/holy-pacstall-demo-$pkgver" check }} package() {{ make -C "$srcdir/holy-pacstall-demo-$pkgver" DESTDIR="$pkgdir" install }} """) out, recipe, report = convert(package, "holy-pacstall-demo") assert "converted holy-pacstall-demo status native" in out assert str(package / "conv" / "holy-pacstall-demo.recipe") in out # the identity, the provenance and the metadata a pacscript writes assert 'name "holy-pacstall-demo"' in recipe and 'version "1.2.3"' in recipe assert 'release "2"' in recipe and 'arch "x86_64"' in recipe assert 'libc any' in recipe assert 'summary "Pacstall conversion fixture"' in recipe assert 'homepage "https://example.org/holy-pacstall-demo"' in recipe assert 'x-source-family pacstall' in recipe and 'x-converter pacstall-1' in recipe assert 'x-license "MIT"' in recipe assert 'x-maintainer "Fixture "' in recipe assert 'output "holy-pacstall-demo" runtime' in recipe assert 'config "etc/holy-pacstall-demo.conf"' in recipe assert 'x-optdepend "python3" "scripting"' in recipe assert 'depend "libc6" "any" "-"' in recipe assert 'depend "zlib1g" "ge" "1.2"' in recipe assert 'build-depend "gcc" "any" "-"' in recipe assert "carried depends" in report and "carried makedepends" in report assert "status native" in report and "unknown 0" in report assert hashlib.sha256((package / "holy-pacstall-demo.pacscript").read_bytes()).hexdigest() \ in report # the local source travels with the recipe and is verified there assert 'source "holy-pacstall-demo-1.2.3.tar.gz" "holy-pacstall-demo-1.2.3.tar.gz"' in recipe assert f'source-sha256 "holy-pacstall-demo-1.2.3.tar.gz" "{digest}"' in recipe assert (package / "conv" / "holy-pacstall-demo-1.2.3.tar.gz").is_file() assert "carried the local source holy-pacstall-demo-1.2.3.tar.gz" in report # the body of each phase keeps its own shell behind the Pacstall prologue for phase, body in (("prepare", "make -C"), ("build", '-j"$NCPU"'), ("check", '$pkgver" check'), ("package", 'DESTDIR="$pkgdir"')): assert f"step {phase} /bin/sh < /dev/null; then meson setup "$KVER" "$homedir" fi } package() { printf 'x\n' > "${pkgdir}/usr/bin/holy-pacstall-reported" } """) out, recipe, report = convert(report_case, "holy-pacstall-reported", status=3) assert 'name "holy-pacstall-reported"' in recipe assert 'x-epoch "2"' in recipe assert "the epoch 2 is preserved and dropped" in report assert "arch amd64 is one machine of a list" in report assert "arch arm64 names a machine Holy does not carry" in report assert 'arch "arm64"' in recipe assert 'x-conflicts "nothing"' not in recipe # a git address, a missing digest and a plain http address are all reported assert "is a git address" in report assert "has no sha256sums entry" in report assert "is a plain http address" in report assert "the sha256sums list holds 3 entries for 3 sources" not in report assert "the source_amd64 list names a machine or a distribution" in report assert "the sha256sums_amd64 list names a machine or a distribution" in report pacscript = str(report_case / "holy-pacstall-reported.pacscript") assert f"the group one|two at {pacscript}:13 offers alternatives" in report assert 'depend "one" "any" "-"' in recipe assert 'depend "two" "any" "-"' not in recipe assert 'depend "helper-pac" "any" "-"' in recipe assert "names a package of the same pacstall repository" in report assert "incompatible steers a Pacstall run" in report assert "external_connection steers a Pacstall run" in report assert "the md5sums list pins a digest a Holy source cannot use" in report assert "noextract steers a Pacstall run" in report assert 'config "etc/holy-pacstall-reported.conf" mutable' in recipe assert "asks for the old file to be removed on an upgrade" in report assert 'config "etc/holy-pacstall-reported.state"' in recipe assert 'x-optdepend "python3"' in recipe assert "the arch_note key is a Pacstall setting this converter reports" in report assert "the give_this key is a Pacstall setting this converter reports" in report assert "the prepare body reads KVER, which is the running kernel" in report assert "the prepare body reads homedir, which is the home directory" in report assert f"conditional block {pacscript}:15 if [[" in report assert f"conditional assignment pacdeps {pacscript}:16" in report # a machine that Holy does not carry is written as it stands foreign = root / "foreign" beside(foreign, archive) write(foreign / "holy-pacstall-foreign.pacscript", """pkgname="holy-pacstall-foreign" pkgver="1" pkgdesc="A pacscript for a machine Holy does not carry" arch=("riscv64") source=("holy-pacstall-demo-1.2.3.tar.gz") sha256sums=("0000000000000000000000000000000000000000000000000000000000000000") """) out, recipe, report = convert(foreign, "holy-pacstall-foreign", status=3) assert 'arch "riscv64"' in recipe assert "arch riscv64 names a machine Holy does not carry" in report assert "the pacscript names no source" not in report # a local source that is not beside the pacscript is reported missing = root / "missing" write(missing / "holy-pacstall-missing.pacscript", """pkgname="holy-pacstall-missing" pkgver="1" pkgdesc="A pacscript whose local source is absent" arch=("any") source=("absent.tar.gz") sha256sums=("0000000000000000000000000000000000000000000000000000000000000000") """) out, recipe, report = convert(missing, "holy-pacstall-missing", status=3) assert "is not beside the pacscript" in report assert 'source "absent.tar.gz"' not in recipe # a gives a body installs under becomes the package name when it is computed computed = root / "computed" beside(computed, archive) write(computed / "holy-pacstall-computed.pacscript", """pkgname="holy-pacstall-computed" pkgver="1" pkgdesc="A pacscript with a computed gives" arch=("any") gives="libicu${pkgver:0:2}" source=("holy-pacstall-demo-1.2.3.tar.gz") sha256sums=("0000000000000000000000000000000000000000000000000000000000000000") package() { make -C "$srcdir" DESTDIR="$pkgdir" install } """) out, recipe, report = convert(computed, "holy-pacstall-computed") # the body installs under the package name, since the expansion needs the # shell to pick the first characters of a version assert 'gives="holy-pacstall-computed"' in recipe # a pacscript that cannot state its identity write(computed / "no-name.pacscript", """pkgver="1" pkgdesc="No name here" """) run("convert", computed / "no-name.pacscript", "--source", "pacstall", "--output", root / "nope", status=2) write(computed / "computed-name.pacscript", """pkgname="holy-${_nowhere}" pkgver="1" pkgdesc="A name no assignment holds" """) run("convert", computed / "computed-name.pacscript", "--source", "pacstall", "--output", root / "nope", status=2) write(computed / "tilde-version.pacscript", """pkgname="holy-tilde" pkgver="1.0~rc1" pkgdesc="A version a recipe cannot hold" arch=("any") """) run("convert", computed / "tilde-version.pacscript", "--source", "pacstall", "--output", root / "nope", status=2) write(computed / "split-no-base.pacscript", """pkgname=( "one" "two" ) pkgver="1" pkgdesc="A list of names with no pkgbase" """) run("convert", computed / "split-no-base.pacscript", "--source", "pacstall", "--output", root / "nope", status=2) write(computed / "unterminated.pacscript", """pkgname="holy-unterminated" pkgver="1" pkgdesc="A body that never closes" package() { printf 'x\\n' > "$pkgdir/x" """) run("convert", computed / "unterminated.pacscript", "--source", "pacstall", "--output", root / "nope", status=2) # unusable arguments run("convert", package / "holy-pacstall-demo.pacscript", "--source", "pacstall", status=2) run("convert", package / "holy-pacstall-demo.pacscript", "--source", "local", "--output", root / "nope", status=2) run("convert", package / "holy-pacstall-demo.pacscript", "--source", "pacstall:other", "--output", root / "nope", status=2) run("convert", root / "absent" / "holy-pacstall-demo.pacscript", "--source", "pacstall", "--output", root / "nope", status=6) # import takes the same conversion path imported = root / "imported-source" beside(imported, archive) write(imported / "holy-pacstall-import.pacscript", f"""pkgname="holy-pacstall-import" pkgver="1.0" pkgdesc="An import fixture" arch=("any") source=("holy-pacstall-demo-1.2.3.tar.gz") sha256sums=("{digest}") depends=("libc6") """) run("import", imported / "holy-pacstall-import.pacscript", "--source", "pacstall", "--format", "pacstall", "--output", root / "imported") assert (root / "imported" / "holy-pacstall-import.recipe").is_file() assert (root / "imported" / "conversion").is_file() assert 'depend "libc6"' in (root / "imported" / "holy-pacstall-import.recipe").read_text() return 0 if __name__ == "__main__": sys.exit(main())