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

file tests/slackbuild.py

#!/usr/bin/env python3
"""converts SlackBuild scripts 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/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 / "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
\tcp bin/%(name)s $(DESTDIR)/usr/bin/
\tcp hello.txt $(DESTDIR)/usr/lib/
""" % {"name": name})
    write(directory / "hello.c", '#include <stdio.h>\nint main(void){puts("hi");return 0;}\n')
    write(directory / "hello.txt", "greeting\n")
    write(directory / "COPYING", "fixture licence text\n")
    return directory


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

        package = root / "sb-demo"
        package.mkdir()
        shutil.copy(archive, package / "sb-demo-3.2.1.tar.gz")
        write(package / "sb-demo.SlackBuild", """#!/bin/bash

# Slackware build script for sb-demo

cd $(dirname $0) ; CWD=$(pwd)

PRGNAM=sb-demo
VERSION=${VERSION:-3.2.1}
BUILD=${BUILD:-1}
TAG=${TAG:-_SBo}
PKGTYPE=${PKGTYPE:-tgz}

if [ -z "$ARCH" ]; then
  case "$( uname -m )" in
    i?86) ARCH=i586 ;;
    arm*) ARCH=arm ;;
       *) ARCH=$( uname -m ) ;;
  esac
fi

if [ ! -z "${PRINT_PACKAGE_NAME}" ]; then
  echo "$PRGNAM-$VERSION-$ARCH-$BUILD$TAG.$PKGTYPE"
  exit 0
fi

TMP=${TMP:-/tmp/SBo}
PKG=$TMP/package-$PRGNAM
OUTPUT=${OUTPUT:-/tmp}

if [ "$ARCH" = "x86_64" ]; then
  SLKCFLAGS="-O2 -fPIC"
  LIBDIRSUFFIX="64"
else
  SLKCFLAGS="-O2"
  LIBDIRSUFFIX=""
fi

set -e

rm -rf $PKG
mkdir -p $TMP $PKG $OUTPUT
cd $TMP

rm -rf $PRGNAM-$VERSION
tar xvf $CWD/$PRGNAM-$VERSION.tar.gz
cd $PRGNAM-$VERSION

patch --verbose -p1 -i $CWD/greeting.patch

make $MAKEFLAGS CFLAGS="$SLKCFLAGS"
make install DESTDIR=$PKG

find $PKG -print0 | xargs -0 file | grep -e "executable" | grep ELF \\
  | cut -f 1 -d : | xargs strip --strip-unneeded 2> /dev/null || true

mkdir -p $PKG/usr/doc/$PRGNAM-$VERSION
cp -a COPYING $PKG/usr/doc/$PRGNAM-$VERSION
cat $CWD/$PRGNAM.SlackBuild > $PKG/usr/doc/$PRGNAM-$VERSION/$PRGNAM.SlackBuild

mkdir -p $PKG/install
cat $CWD/slack-desc > $PKG/install/slack-desc
cat $CWD/doinst.sh > $PKG/install/doinst.sh

cd $PKG
/sbin/makepkg -l y -c n $OUTPUT/$PRGNAM-$VERSION-$ARCH-$BUILD$TAG.$PKGTYPE
""")
        write(package / "greeting.patch",
              "--- a/hello.c\n+++ b/hello.c\n@@ -1 +1 @@\n"
              '-#include <stdio.h>\n+#include <stdio.h> /* patched */\n')
        write(package / "slack-desc", """# HOW TO EDIT THIS FILE:
# Line up the first '|' above the ':' following the base package name.

    |-----handy-ruler------------------------------------------------------|
sb-demo: sb-demo (a SlackBuild fixture)
sb-demo:
sb-demo: The sb-demo program greets the operator once and then stops.
sb-demo: It exists so the converter has a linear script to read.
sb-demo:
sb-demo: homepage: https://example.org/sb-demo
sb-demo: requires: gcc make
sb-demo:
sb-demo:
sb-demo:
""")
        write(package / "info", f"""PRGNAM="sb-demo"
VERSION="3.2.1"
HOMEPAGE="https://example.org/sb-demo"
DOWNLOAD="https://example.org/sb-demo-{digest}.tar.gz"
MD5SUM="00000000000000000000000000000000"
REQUIRES=""
MAINTAINER="Fixture <fixture@example.org>"
EMAIL="fixture@example.org"
""")
        write(package / "doinst.sh", 'if [ -x /usr/bin/sb-demo ]; then\n'
              '  /usr/bin/sb-demo >/dev/null\nfi\n')

        out = run("convert", package / "sb-demo.SlackBuild", "--source", "slackbuilds",
                  "--output", root / "conv", status=3)
        assert "converted sb-demo status review-required" in out
        recipe = (root / "conv" / "sb-demo.recipe").read_text()
        report = (root / "conv" / "conversion").read_text()
        assert (root / "conv" / "sb-demo.SlackBuild").read_text() == \
            (package / "sb-demo.SlackBuild").read_text()
        assert hashlib.sha256((root / "conv" / "sb-demo.SlackBuild").read_bytes()).hexdigest() \
            in report
        assert (root / "conv" / "sb-demo-3.2.1.tar.gz").is_file()
        assert (root / "conv" / "doinst.sh").is_file()

        # identity, provenance and the local source are carried
        assert 'name "sb-demo"' in recipe and 'version "3.2.1"' in recipe
        assert 'release "1"' in recipe and 'arch any' in recipe and 'libc any' in recipe
        assert 'x-source-family slackware' in recipe and 'x-converter slackbuild-1' in recipe
        assert 'x-tag "_SBo"' in recipe and 'x-pkgtype "tgz"' in recipe
        assert "sb-demo (a SlackBuild fixture)" in recipe
        assert "stops." in recipe
        assert 'homepage "https://example.org/sb-demo"' in recipe
        assert 'depend "gcc" "any" "-"' in recipe
        assert 'depend "make" "any" "-"' in recipe
        assert f'source "sb-demo-3.2.1.tar.gz" "sb-demo-3.2.1.tar.gz"' in recipe
        assert f'source-sha256 "sb-demo-3.2.1.tar.gz" "{digest}"' in recipe
        # the slackbuild variables are rebuilt from the exported Holy paths
        assert 'PRGNAM=sb-demo' in recipe and 'VERSION=3.2.1' in recipe and 'BUILD=1' in recipe
        assert 'ARCH="$HOLY_ARCH"' in recipe
        assert 'CWD="$HOLY_SRC"' in recipe and 'TMP="$HOLY_SRC"' in recipe
        assert 'PKG="$HOLY_DEST"' in recipe and 'DESTDIR="$HOLY_DEST"' in recipe
        # the body keeps its own shell, and the packaging call is left out
        assert "make install DESTDIR=$PKG" in recipe
        assert "cd $PKG" not in recipe
        assert "/sbin/makepkg" not in recipe
        assert 'hook-install /bin/sh "usr/share/holy/slackbuild/doinst.sh"' in recipe

        # the report names what was carried, what changed and what stayed unknown
        assert "status review-required" in report
        assert "carried PRGNAM 7" in report
        assert "carried VERSION 8" in report
        assert "preserved PKGTYPE 11" in report
        assert "carried slack-desc summary" in report
        assert "carried source sb-demo-3.2.1.tar.gz" in report
        assert "preserved hook doinst.sh" in report
        assert "unknown the slackbuild strip pass is not reproduced" in report
        assert "unknown the script rewrites ownership" not in report
        assert "preserved info DOWNLOAD" in report
        assert "unknown the md5sum this format pins" in report
        assert "semantic-change the machine the script detects from uname -m becomes $HOLY_ARCH" \
            in report
        assert "semantic-change the engine fetches and unpacks the archive" in report
        assert "semantic-change the cd $PKG and /sbin/makepkg lines are left out" in report
        assert "semantic-change the $PKG/install tree is slackbuild packaging metadata" in report
        assert "carried local source greeting.patch" in report
        assert "carried local source slack-desc" in report

        # the converted recipe builds through the normal engine
        run("build", root / "conv" / "sb-demo.recipe", "--output", root / "built", "--yes")
        # the payload decides the group, so the fixture reads the artifacts it produced
        built = sorted(path.name for path in (root / "built").glob("*.holy"))
        assert built == ["sb-demo--noarch--nolibc.holy", "sb-demo--x86_64--glibc.holy"]
        program, _ = read_metadata(root / "built" / "sb-demo--x86_64--glibc.holy")
        data, _ = read_metadata(root / "built" / "sb-demo--noarch--nolibc.holy")
        assert 'name "sb-demo"' in program["HOLY/meta"]
        assert 'x-source-family "slackware"' in program["HOLY/meta"]
        assert digest in program["HOLY/origin"]
        assert "usr/bin/sb-demo" in program["HOLY/files"]
        assert "usr/bin/sb-demo" not in data["HOLY/files"]
        assert "usr/lib/hello.txt" in data["HOLY/files"]
        assert "usr/doc/sb-demo-3.2.1/COPYING" in data["HOLY/files"]
        assert "usr/share/holy/slackbuild/doinst.sh" in data["HOLY/files"]
        assert 'hook postinstall "/bin/sh" "usr/share/holy/slackbuild/doinst.sh"' \
            in data["HOLY/hooks"]

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

        # a computed identity, a missing archive and a script with no body
        write(package / "sb-demo.SlackBuild", """PRGNAM=sb-demo
VERSION=$(echo 1.0 | tr -d x)
BUILD=1
set -e
make
/sbin/makepkg x
""")
        run("convert", package / "sb-demo.SlackBuild", "--source", "slackbuilds",
            "--output", root / "computed", status=2)
        write(package / "sb-demo.SlackBuild", """PRGNAM=sb-demo
BUILD=1
set -e
make
/sbin/makepkg x
""")
        run("convert", package / "sb-demo.SlackBuild", "--source", "slackbuilds",
            "--output", root / "noversion", status=2)
        write(package / "sb-demo.SlackBuild", """PRGNAM=sb-demo
VERSION=1.0
BUILD=1
set -e
/sbin/makepkg x
""")
        run("convert", package / "sb-demo.SlackBuild", "--source", "slackbuilds",
            "--output", root / "nobody", status=3)
        report = (root / "nobody" / "conversion").read_text()
        assert "unknown the script has no build body between set -e and makepkg" in report
        assert "unknown source sb-demo-1.0.tar.gz is not beside the script" in report

        # a doinst.sh the script installs but that is not beside it
        write(package / "sb-demo.SlackBuild", """PRGNAM=sb-demo
VERSION=1.0
BUILD=1
set -e
tar xvf $CWD/sb-demo-1.0.tar.gz
cp $CWD/doinst.sh $PKG/install/
cd $PKG
/sbin/makepkg x
""")
        (package / "doinst.sh").unlink()
        run("convert", package / "sb-demo.SlackBuild", "--source", "slackbuilds",
            "--output", root / "nohook", status=3)
        report = (root / "nohook" / "conversion").read_text()
        assert "unknown the script installs doinst.sh but no such file is beside it" in report

        # malformed text and unusable arguments
        run("convert", package / "sb-demo.SlackBuild", "--source", "slackbuilds", status=2)
        run("convert", package / "sb-demo.SlackBuild", "--source", "local",
            "--output", root / "nope", status=2)
        run("convert", package / "sb-demo.SlackBuild", "--source", "slackbuilds:extra",
            "--output", root / "nope", status=2)
        run("convert", root / "missing" / "sb-demo.SlackBuild", "--source", "slackbuilds",
            "--output", root / "nope", status=6)
        # import takes the same conversion path
        write(package / "sb-demo.SlackBuild", """PRGNAM=sb-imported
VERSION=1
BUILD=1
set -e
mkdir -p $PKG/usr/share/sb-imported
printf 'data\\n' > $PKG/usr/share/sb-imported/data
cd $PKG
/sbin/makepkg x
""")
        run("import", package / "sb-demo.SlackBuild", "--source", "slackbuilds",
            "--format", "slackbuild", "--output", root / "imported", status=3)
        assert (root / "imported" / "conversion").is_file()
        assert (root / "imported" / "sb-imported.recipe").is_file()
    return 0


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