git mirror - github.com/owenewans/holy - branch master
clone: https://src.holypkg.eu/holy/

file tests/void.py

#!/usr/bin/env python3
"""converts Void template fixtures into holy recipes and builds the produced manifests."""
import hashlib
import io
import os
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 / "etc").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
check: bin/%(name)s
\t./bin/%(name)s
install: bin/%(name)s
\tmkdir -p $(DESTDIR)/usr/bin $(DESTDIR)/usr/include $(DESTDIR)/usr/lib/pkgconfig
\tcp bin/%(name)s $(DESTDIR)/usr/bin/
\tcp main.h $(DESTDIR)/usr/include/
\tcp %(name)s.pc $(DESTDIR)/usr/lib/pkgconfig/
\tprintf 'greeting\\n' > $(DESTDIR)/usr/share-greeting
""" % {"name": name})
    write(directory / "main.c", '#include <stdio.h>\nint main(void){printf("hi\\n");return 0;}\n')
    write(directory / "main.h", "/* header */\n")
    write(directory / f"{name}.pc", "Name: %s\n" % name)
    write(directory / "README", f"{name} documentation\n")
    write(directory / "LICENSE", "fixture license text\n")
    return directory


def main():
    if not shutil.which("lz4"):
        print("lz4 required for void fixture", file=sys.stderr)
        return 6
    for tool in ("gcc", "make", "tar", "patch"):
        if not shutil.which(tool):
            print(f"{tool} required for void fixture", file=sys.stderr)
            return 6
    with tempfile.TemporaryDirectory() as scratch:
        root = Path(scratch)
        source = source_tree(root, "void-demo", "1.4.2")
        archive = root / "void-demo-1.4.2.tar.gz"
        subprocess.run(["tar", "czf", str(archive), "-C", str(root), "void-demo-1.4.2"],
                       check=True)
        digest = hashlib.sha256(archive.read_bytes()).hexdigest()

        # a template with its own build steps, a patches directory and two subpackages
        package = root / "void-demo"
        package.mkdir()
        write(package / "template", f"""# Template file for 'void-demo'
pkgname=void-demo
version=1.4.2
revision=3
build_style=gnu-makefile
short_desc="Void template conversion fixture"
maintainer="Fixture <fixture@example.org>"
license="MIT"
homepage="https://example.org/void-demo"
changelog="https://example.org/void-demo/CHANGES"
distfiles="void-demo-${{version}}.tar.gz"
checksum={digest}
makedepends="gcc make patch"
hostmakedepends="pkgconf"
checkdepends="tar"
depends="glibc>=2.38"
conf_files="/etc/void-demo.conf"
make_build_args="CC=gcc"

do_build() {{
\tmake ${{makejobs}} PREFIX=/usr
}}

do_check() {{
\tmake check
}}

do_install() {{
\tinstall -d "$DESTDIR/etc"
\tmake PREFIX=/usr DESTDIR="$DESTDIR" install
\tprintf 'enabled = 1\\n' > "$DESTDIR/etc/void-demo.conf"
\tvlicense LICENSE
}}

void-demo-devel_package() {{
\tshort_desc+=" - development files"
\tdepends="${{sourcepkg}}>=${{version}}_${{revision}}"
\tpkg_install() {{
\t\tvmove usr/include
\t\tvmove usr/lib/pkgconfig
\t}}
}}

void-demo-doc_package() {{
\tpkg_install() {{
\t\tvmove usr/share-greeting
\t}}
}}
""")
        write(package / "patches" / "0001-greeting.patch",
              "--- a/main.c\n+++ b/main.c\n@@ -1 +1 @@\n"
              '-#include <stdio.h>\n+#include <stdio.h> /* patched */\n')
        write(package / "patches" / "0001-greeting.patch.args", "-Np1\n")
        shutil.copy(archive, package / "void-demo-1.4.2.tar.gz")

        out = run("convert", package / "template", "--source", "voidpkgs",
                  "--output", root / "conv", status=3)
        assert "converted void-demo status review-required" in out
        recipe = (root / "conv" / "void-demo.recipe").read_text()
        report = (root / "conv" / "conversion").read_text()
        assert (root / "conv" / "template").read_text() == (package / "template").read_text()
        assert hashlib.sha256((root / "conv" / "template").read_bytes()).hexdigest() in report
        assert (root / "conv" / "patches.tar").is_file()

        # identity, dependencies and provenance are carried
        assert 'name "void-demo"' in recipe and 'version "1.4.2"' in recipe
        assert 'release "3"' in recipe and 'arch any' in recipe and 'libc any' in recipe
        assert 'license "MIT"' in recipe
        assert 'x-source-family xbps' in recipe and 'x-converter voidsrc-1' in recipe
        assert 'x-build-style "gnu-makefile"' in recipe
        assert 'x-maintainer "Fixture <fixture@example.org>"' in recipe
        # a local distfile travels with the recipe, so the build needs no network
        assert 'source "void-demo-1.4.2.tar.gz" "void-demo-1.4.2.tar.gz"' in recipe
        assert f'source-sha256 "void-demo-1.4.2.tar.gz" "{digest}"' in recipe
        assert (root / "conv" / "void-demo-1.4.2.tar.gz").is_file()
        assert 'build-depend "gcc" "any" "-"' in recipe
        assert 'build-depend "pkgconf" "any" "-"' in recipe
        assert 'build-depend "patch" "any" "-"' in recipe
        assert 'depend "glibc" "ge" "2.38"' in recipe
        assert 'config "etc/void-demo.conf"' in recipe
        assert 'output "void-demo" runtime' in recipe
        assert 'output "void-demo-devel" runtime' in recipe
        assert 'output "void-demo-doc" runtime' in recipe
        assert 'split-step void-demo-devel split /bin/bash' in recipe
        assert 'split-step void-demo-doc split /bin/bash' in recipe
        # the xbps-src working variables are rebuilt from the exported Holy paths
        assert 'DESTDIR="$HOLY_DEST"' in recipe
        assert 'PKGDESTDIR="$HOLY_DEST"' in recipe
        assert 'PKGDESTDIR="$HOLY_SPLIT_DEST"' in recipe
        assert 'vmove usr/include' in recipe
        assert 'vinstall() {' in recipe
        # the patch directory arrives as one staged archive
        assert 'source "patches" "patches.tar"' in recipe
        assert "patch -s $patch_args" in recipe

        # the report names what was carried, preserved and what stayed unknown
        assert "status review-required" in report
        assert "preserved do_build template:" in report
        assert "preserved do_install template:" in report
        assert "preserved subpackage void-demo-devel template:" in report
        assert "preserved depends depends=" in report
        assert "helper common/build-style/gnu-makefile.sh" in report
        assert "helper v* helpers carried" in report
        assert "semantic-change distfiles are extracted into HOLY_SRC" in report

        # the converted recipe builds through the normal engine
        run("build", root / "conv" / "void-demo.recipe", "--output", root / "built", "--yes")
        library, _ = read_metadata(root / "built" / "void-demo--x86_64--glibc.holy")
        data, _ = read_metadata(root / "built" / "void-demo--noarch--nolibc.holy")
        devel, _ = read_metadata(root / "built" / "void-demo-devel--noarch--nolibc.holy")
        docs, _ = read_metadata(root / "built" / "void-demo-doc--noarch--nolibc.holy")
        assert 'arch "x86_64"' in library["HOLY/meta"]
        assert 'libc "glibc"' in library["HOLY/meta"]
        assert 'x-source-family "xbps"' in library["HOLY/meta"]
        assert 'x-build-style "gnu-makefile"' in library["HOLY/meta"]
        assert "usr/bin/void-demo" in library["HOLY/files"]
        # a file without ELF lands in the noarch output and keeps its config flag
        assert "etc/void-demo.conf" in data["HOLY/files"]
        assert "config" in data["HOLY/files"]
        assert "usr/share/licenses/void-demo/LICENSE" in data["HOLY/files"]
        assert "usr/include/main.h" in devel["HOLY/files"]
        assert "usr/lib/pkgconfig/void-demo.pc" in devel["HOLY/files"]
        assert "usr/share-greeting" in docs["HOLY/files"]
        assert "usr/bin/void-demo" not in devel["HOLY/files"]
        assert "etc/void-demo.conf" not in devel["HOLY/files"]
        assert "usr/include/main.h" not in docs["HOLY/files"]
        assert digest in library["HOLY/origin"]
        assert "gcc" in library["HOLY/transform"]
        assert '"usr/bin/void-demo" "holy-recipe"' in library["HOLY/deps"]

        # unreviewed steps still need a decision
        run("build", root / "conv" / "void-demo.recipe", "--output", root / "unreviewed",
            "--noninteractive", status=3)

        # a build style with no template step of its own reports the missing phase
        write(package / "template", """# Template file for 'void-style'
pkgname=void-style
version=2.0
revision=1
build_style=gnu-configure
short_desc="Build style fixture"
maintainer="Fixture <fixture@example.org>"
license="MIT"
distfiles="https://example.org/void-style-2.0.tar.xz"
checksum="0000000000000000000000000000000000000000000000000000000000000000"
""")
        out = run("convert", package / "template", "--source", "voidpkgs",
                  "--output", root / "style", status=3)
        assert "converted void-style status review-required" in out
        report = (root / "style" / "conversion").read_text()
        assert "helper common/build-style/gnu-configure.sh supplies the configure step" in report
        assert "helper common/build-style/gnu-configure.sh supplies the package step" in report
        assert "helper common/build-style/gnu-configure.sh supplies the build step" in report

        # a template with no build steps at all is native
        write(package / "template", """# Template file for 'void-data'
pkgname=void-data
version=1
revision=1
short_desc="Data only fixture"
maintainer="Fixture <fixture@example.org>"
license="MIT"
distfiles="void-data-1.tar.gz"
checksum=SKIP
metapackage=no

do_install() {
\tvmkdir usr/share/void-data
\tprintf 'data\\n' > "$PKGDESTDIR/usr/share/void-data/data"
}
""")
        write(package / "void-data-1.tar.gz", "not a real archive")
        run("convert", package / "template", "--source", "voidpkgs", "--output", root / "data",
            status=3)
        recipe = (root / "data" / "void-data.recipe").read_text()
        report = (root / "data" / "conversion").read_text()
        assert (root / "data" / "void-data-1.tar.gz").is_file()
        assert 'output "void-data" runtime' in recipe
        assert "unknown source void-data-1.tar.gz has no sha256 checksum" in report
        assert "semantic-change local distfile void-data-1.tar.gz copied" in report

        # build options are fixed to build_options_default and reported
        write(package / "template", """# Template file for 'void-options'
pkgname=void-options
version=3
revision=1
short_desc="Build option fixture"
maintainer="Fixture <fixture@example.org>"
license="MIT"
build_options="zlib docs"
build_options_default="zlib"
configure_args="--with-zlib=$(vopt_with zlib) --with-docs=$(vopt_with docs)"

do_install() {
\tprintf 'option\\n' > "$PKGDESTDIR/option"
}
""")
        out = run("convert", package / "template", "--source", "voidpkgs",
                  "--output", root / "options", status=3)
        assert "converted void-options status review-required" in out
        recipe = (root / "options" / "void-options.recipe").read_text()
        report = (root / "options" / "conversion").read_text()
        assert 'configure_args="--with-zlib=--with-zlib --with-docs=--without-docs"' in recipe
        assert "vopt_with" not in recipe
        assert "semantic-change build options fixed to build_options_default" in report
        assert 'x-build_options "zlib docs"' in recipe

        # a conditional block, an unknown helper and a site constant are reported
        write(package / "template", """# Template file for 'void-review'
pkgname=void-review
version=1
revision=1
short_desc="Review fixture"
maintainer="Fixture <fixture@example.org>"
license="MIT"
distfiles="${PYPI_SITE}/void-review-1.tar.gz"
checksum=0000000000000000000000000000000000000000000000000000000000000000

do_install() {
\tvsed -i Makefile -e 's/a/b/'
\tmsg_warn 'incomplete'
}

if [ "$CHROOT_READY" ]; then
void-review-extra_package() {
\tpkg_install() {
\t\tvmove usr/share/void-review
\t}
}
fi
""")
        out = run("convert", package / "template", "--source", "voidpkgs",
                  "--output", root / "review", status=3)
        assert "converted void-review status review-required" in out
        report = (root / "review" / "conversion").read_text()
        assert "unknown helper vsed is not carried in do_install" in report
        assert "unknown helper msg_warn is not carried in do_install" in report
        assert "unknown conditional block template:17 if [ \"$CHROOT_READY\" ]; then" in report
        assert "unknown distfile ${PYPI_SITE}/void-review-1.tar.gz uses an expansion" in report
        assert "unknown subpackage void-review-extra is defined in a conditional block" in report

        # an INSTALL file becomes one hook and its missing ACTION is reported
        write(package / "template", """# Template file for 'void-hook'
pkgname=void-hook
version=1
revision=1
short_desc="Hook fixture"
maintainer="Fixture <fixture@example.org>"
license="MIT"

do_install() {
\tvmkdir usr/bin
\tcp hook.sh "$PKGDESTDIR/usr/bin/hook.sh"
}
""")
        write(package / "INSTALL", 'case "$ACTION" in\n  post) msg_system "installed";;\nesac\n')
        run("convert", package / "template", "--source", "voidpkgs", "--output", root / "hook",
            status=3)
        recipe = (root / "hook" / "void-hook.recipe").read_text()
        report = (root / "hook" / "conversion").read_text()
        assert (root / "hook" / "INSTALL").is_file()
        assert 'hook-install /bin/bash "usr/share/holy/void-hook/INSTALL"' in recipe
        assert 'source "INSTALL" "INSTALL"' in recipe
        assert "preserved hook INSTALL" in report
        assert "semantic-change INSTALL runs with ACTION unset" in report
        assert 'cp "$HOLY_SRC/$hook_source" "$HOLY_DEST/$hook_source"' in recipe
        assert "usr/share/holy/void-hook" in recipe
        # import takes the same conversion path
        run("import", package / "template", "--source", "voidpkgs", "--format", "void",
            "--output", root / "imported", status=3)
        assert (root / "imported" / "void-hook.recipe").is_file()

        # malformed text, a missing identity and unusable arguments
        write(package / "template", "pkgname=broken\nversion=1\nrevision=1\ndo_build() {\n  true\n")
        run("convert", package / "template", "--source", "voidpkgs",
            "--output", root / "bad", status=2)
        write(package / "template", "pkgname=broken\nversion=$(date +%s)\nrevision=1\n")
        run("convert", package / "template", "--source", "voidpkgs",
            "--output", root / "dynamic", status=2)
        write(package / "template", "pkgname=broken\nrevision=1\n")
        run("convert", package / "template", "--source", "voidpkgs",
            "--output", root / "missing-version", status=2)
        run("convert", package / "template", "--source", "voidpkgs", status=2)
        run("convert", package / "template", "--source", "local",
            "--output", root / "nope", status=2)
        run("convert", package / "template", "--source", "void:extra",
            "--output", root / "nope", status=2)
        run("convert", root / "missing" / "template", "--source", "voidpkgs",
            "--output", root / "nope", status=6)
        run("convert", package / "template", "--source", "voidpkgs",
            "--output", root / "imported", "--format", "void", status=2)
    return 0


if __name__ == "__main__":
    sys.exit(main())