#!/usr/bin/env python3 """converts Flatpak manifests into holy recipes and builds the produced manifests.""" import hashlib import io import json 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 install: bin/%(name)s \tmkdir -p $(DESTDIR)/usr/bin $(DESTDIR)/usr/share/%(name)s \tcp bin/%(name)s $(DESTDIR)/usr/bin/ \tprintf 'from the template\\n' > $(DESTDIR)/usr/share/%(name)s/greeting """ % {"name": name}) write(directory / "main.c", '#include \nint main(void){printf("hi\\n");return 0;}\n') return directory def archive_of(directory, root): archive = root / f"{directory.name}.tar.gz" subprocess.run(["tar", "czf", str(archive), "-C", str(directory.parent), directory.name], check=True) return archive def convert(directory, file_name=None, name="Holy", status=0, source="flathub", fmt=None): if file_name is None: file_name = sorted(p.name for p in directory.glob("*.json"))[0] args = ["convert", directory / file_name, "--source", source, "--output", directory / "conv"] if fmt: args = ["import", directory / file_name, "--source", source, "--format", fmt, "--output", directory / "conv"] out = run(*args, status=status) return out, (directory / "conv" / f"{name}.recipe").read_text(), \ (directory / "conv" / "conversion").read_text() def main(): if not shutil.which("lz4"): print("lz4 required for the flatpak fixture", file=sys.stderr) return 6 for tool in ("cc", "make", "tar", "patch"): if not shutil.which(tool): print(f"{tool} required for the flatpak fixture", file=sys.stderr) return 6 with tempfile.TemporaryDirectory() as scratch: root = Path(scratch) tree = source_tree(root, "holy-flatpak-demo", "1.0") archive = archive_of(tree, root) digest = hashlib.sha256(archive.read_bytes()).hexdigest() # a manifest that builds a program through a template this converter spells out meson = root / "meson" meson.mkdir(parents=True) write(meson / "org.example.Meson.json", f"""{{ "id": "org.example.Meson", "branch": "stable", "runtime": "org.holy.Platform//45", "runtime-version": "45", "sdk": "org.holy.Sdk//45", "command": "bin/holy-flatpak-demo", "arch": "x86_64", "summary": "A Flatpak manifest built with meson", "url": "https://example.org/meson", "modules": [ {{ "name": "holy-flatpak-demo", "buildsystem": "meson", "build-options": {{ "env": {{ "CC": "cc" }}, "append-path": ["/app/bin"], "cflags": ["-O2"] }}, "sources": [ {{ "type": "archive", "url": "https://example.org/holy-flatpak-demo-1.0.tar.gz", "sha256": "{digest}" }} ] }} ] }} """) out, recipe, report = convert(meson, name="Meson") assert "converted Meson status native" in out assert 'name "Meson"' in recipe and 'arch "x86_64"' in recipe and "libc any" in recipe assert "the machine x86_64 of the manifest or its branch" in report assert 'version "0"' in recipe assert 'summary "A Flatpak manifest built with meson"' in recipe assert 'homepage "https://example.org/meson"' in recipe assert 'x-source-family flatpak' in recipe and 'x-converter flatpak-1' in recipe assert 'x-app-id "org.example.Meson"' in recipe assert 'x-flatpak-branch "stable"' in recipe assert 'x-flatpak-command "bin/holy-flatpak-demo"' in recipe # the sdk builds the modules and the runtime is what the program runs on assert 'build-depend "Sdk" "any" "-"' in recipe assert 'depend "Platform" "any" "-"' in recipe assert "the sdk org.holy.Sdk//45 builds the modules" in report assert "so they are separate requirements" in report # the command is a path under /app, and a Holy payload installs under /usr assert "is a path under /app" in report # the archive is a source with the digest the manifest pins assert 'source "holy-flatpak-demo-0" "https://example.org/holy-flatpak-demo-1.0.tar.gz"' \ in recipe assert f'source-sha256 "holy-flatpak-demo-0" "{digest}"' in recipe # the environment of the module and the meson template spelled out assert 'export CC="cc"' in recipe and 'export CFLAGS="-O2"' in recipe assert 'PATH="/app/bin:$PATH"' in recipe assert 'meson setup build --prefix="$PREFIX" --libdir=lib' in recipe assert 'meson compile -C build -j "$HOLY_JOBS"' in recipe assert 'DESTDIR="$HOLY_DEST" meson install -C build' in recipe assert 'cd "$HOLY_SRC/holy-flatpak-demo-0" || exit 1' in recipe assert "the meson template of module holy-flatpak-demo is replaced" in report assert "status native" in report and "unknown 0" in report assert hashlib.sha256((meson / "org.example.Meson.json").read_bytes()).hexdigest() in report # the same manifest reaches the converter through import --format imported = run("import", meson / "org.example.Meson.json", "--source", "flathub", "--format", "flatpak", "--output", root / "imported") assert (root / "imported" / "Meson.recipe").is_file(), imported # a manifest whose modules build with the other templates, with a patch, an # inline file and a shell source, and with permissions the build cannot keep simple = root / "simple" simple.mkdir(parents=True) patch = source_tree(root, "holy-flatpak-simple", "2.1") write(patch / "greeting.txt", "before\n") write(patch / "version.txt", "after\n") diff = subprocess.run(["diff", "-u", "greeting.txt", "version.txt"], cwd=str(patch), capture_output=True, text=True).stdout write(simple / "greeting.patch", diff.replace("--- greeting.txt", "--- a/greeting.txt") .replace("+++ version.txt", "+++ b/greeting.txt")) simple_archive = archive_of(patch, root) shutil.copy(simple_archive, simple / simple_archive.name) write(simple / "org.example.Simple.json", f"""{{ "id": "org.example.Simple", "runtime": "org.holy.Platform//45", "sdk": "org.holy.Sdk//45", "finish-args": ["--share=network", "--socket=x11"], "cleanup": ["appstream"], "modules": [ {{ "name": "app", "buildsystem": "simple", "sources": [ {{ "type": "archive", "url": "{simple_archive.name}" }}, {{ "type": "patch", "path": "greeting.patch" }}, {{ "type": "shell", "commands": ["printf 'shell source\\n' > shell-ran"] }} ], "build-commands": [ "make", "install -Dm644 greeting.txt /app/share/greeting" ] }}, {{ "name": "extra", "buildsystem": "make", "sources": [ {{ "type": "inline", "contents": "inline body", "dest-filename": "extra.txt" }}, {{ "type": "inline", "dest-filename": "extra.c", "contents": "int main(void) {{ return 0; }}\\n" }}, {{ "type": "inline", "dest-filename": "Makefile", "contents": "all:\\n\\t$(CC) -o extra extra.c\\ninstall: all\\n\\tmkdir -p $(DESTDIR)/usr/bin\\n\\tcp extra $(DESTDIR)/usr/bin/\\n" }} ] }}, {{ "name": "unknown-system", "buildsystem": "gradle" }} ] }} """) out, recipe, report = convert(simple, name="Simple", status=3) # the patch applies before the module builds, in the tree the engine extracted assert 'step prepare /bin/sh <