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

file tests/scoop.py

#!/usr/bin/env python3
"""converts Scoop manifests into native packages and installs what they carry."""
import hashlib
import json
from pathlib import Path
import shutil
import subprocess
import sys
import tempfile
import zipfile


binary = str(Path(sys.argv[1]).resolve())


def call(*args, status=0):
    result = subprocess.run([binary, *map(str, args)], capture_output=True, text=True)
    assert result.returncode == status, (args, result.returncode, result.stdout, result.stderr)
    return result.stdout


def manifest(directory, name, entries, overrides=None):
    """a manifest beside the artifact it names, with the digest the manifest pins"""
    archive = directory / f"{name}-fixture.zip"
    with zipfile.ZipFile(archive, "w") as opened:
        for entry, body in entries.items():
            opened.writestr(entry, body)
    body = {
        "version": "2.4.1",
        "description": "A Scoop conversion fixture",
        "homepage": "https://example.org/fixture",
        "license": "MIT",
        "url": f"https://example.org/{archive.name}",
        "hash": hashlib.sha256(archive.read_bytes()).hexdigest(),
        "depends": "holyruntime/1.0 other/2.3",
        "notes": ["reads its configuration from the working directory"],
        "checkver": "https://example.org/fixture/version",
        "autoupdate": {"url": "https://example.org/fixture-{{version}}.zip"},
        "env_add_path": ["."],
        "env_set": {"FIXTURE_HOME": "$dir"},
        "persist": ["config"],
        "installer": {"script": ["$dir\\fixture.exe", "Write-Host done"]},
    }
    if overrides:
        body.update(overrides)
    (directory / f"{name}.json").write_text(json.dumps(body, indent=2))
    return archive


def main():
    with tempfile.TemporaryDirectory() as scratch:
        root = Path(scratch)
        bucket = root / "bucket"
        bucket.mkdir()
        manifest(bucket, "holyscoop", {
            "holyscoop-2.4.1/holyscoop.exe": "MZ a program for another operating system\n",
            "holyscoop-2.4.1/README.txt": "holyscoop reads its configuration\n",
        }, {"extract_dir": "holyscoop", "bin": "holyscoop.exe"})

        # the artifact is verified against the digest the manifest pins, and carried
        out = root / "converted"
        report = call("import", bucket / "holyscoop.json", "--source", "bucket", "--format",
                      "scoop", "--output", out, status=3)
        artifact = out / "holyscoop--noarch--nolibc.holy"
        assert "imported holyscoop--noarch--nolibc.holy artifact" in report, report
        assert artifact.is_file(), sorted(p.name for p in out.iterdir())
        call("verify", "local:" + str(artifact))
        call("info", "local:" + str(artifact))

        identity = call("info", "local:" + str(artifact))
        assert "name holyscoop\n" in identity and "version 2.4.1\n" in identity, identity
        assert "arch noarch\n" in identity and "libc nolibc\n" in identity, identity

        manifest_text = call("manifest", "local:" + str(artifact))
        for path in ("usr/lib/holy/private/holyscoop/holyscoop/holyscoop.exe",
                     "usr/lib/holy/private/holyscoop/holyscoop/README.txt"):
            assert path in manifest_text, path

        # the manifest travels whole, so an installed copy can be compared with it
        assert (out / "original").read_bytes() == (bucket / "holyscoop.json").read_bytes()
        package = (out / "package").read_text()
        assert "format holy-scoop-package-1\n" in package
        assert "status review-required\n" in package
        assert "artifact https://example.org/holyscoop-fixture.zip" in package
        assert "verified against the digest the manifest pins" in package
        assert "artifact-placed the payload carries 2 files" in package
        assert "extract-dir holyscoop is the directory the manifest places" in package
        assert "program holyscoop.exe is in the payload" in package
        # nothing runs, and no runtime this manager does not provide is promised
        assert "nothing runs the artifact" in package
        assert "a package cannot promise a Wine" in package
        assert "so no such requirement is recorded" in package
        # the installer and the Windows integration are dropped with a count
        assert "installer 1 PowerShell installer keys are dropped" in package
        assert "integration 3 Windows integration keys are dropped" in package
        assert "update 2 keys that check or rewrite the upstream version" in package
        # a bucket dependency is a requirement, since a target has to find it
        requirements = call("requirements", "local:" + str(artifact))
        assert 'require "scoop-depend-0" "holyscoop" "package" "holyruntime"' in requirements
        assert 'require "scoop-depend-1" "holyscoop" "package" "other"' in requirements
        assert "depends 2 apps of the same bucket" in package

        # the payload installs as ordinary files and nothing executes
        target = root / "target"
        (target / "usr/bin").mkdir(parents=True)
        call("db", "init", "--root", target)
        call("cache", "stage", "local:" + str(artifact), "--root", target)
        # the two bucket apps have no provider here, so the set names what is missing
        digest = hashlib.sha256(artifact.read_bytes()).hexdigest()
        refused = subprocess.run([binary, "db", "plan-set", digest, "--root", str(target)],
                                 capture_output=True, text=True)
        assert refused.returncode == 4, refused
        assert "unresolved requirement scoop-depend-0" in refused.stderr, refused.stderr
        assert not (target / "usr/lib").exists()

        # a manifest whose apps are absent is refused, and a fake provider is not
        # invented to satisfy them
        runtimes = root / "runtimes"
        for name in ("holyruntime", "other"):
            tree = runtimes / name
            (tree / "HOLY").mkdir(parents=True)
            (tree / "DATA/usr/lib" / name).mkdir(parents=True)
            (tree / "DATA/usr/lib" / name / "note").write_text(f"{name} fixture\n")
            (tree / "HOLY/meta").write_text(
                "format holy-package-1\nname %s\nversion 1\nrelease 1\nos linux\n"
                "arch noarch\nlibc nolibc\n" % name)
            for field in ("deps", "provides", "hooks", "origin", "transform"):
                (tree / "HOLY" / field).write_text("")
            # the generator reads the tree it walks, so its output lands beside it
            call("manifest", "generate", tree, "--output", root / (name + "-files"))
            (tree / "HOLY/files").write_text((root / (name + "-files")).read_text())
            call("pack", tree, "--output", root / (name + ".holy"))
            call("cache", "stage", "local:" + str(root / (name + ".holy")), "--root", target)
        digests = [digest]
        for name in ("holyruntime", "other"):
            digests.append(hashlib.sha256((root / (name + ".holy")).read_bytes()).hexdigest())
        plan = call("db", "plan-set", *digests, "--root", target)
        plan_sha = [line.split()[10] for line in plan.splitlines()
                    if line.startswith("plan-set ")][0]
        applied = call("db", "apply-set", plan_sha, *digests, "--root", target)
        assert "artifacts 3" in applied, applied
        program = (target / "usr/lib/holy/private/holyscoop/holyscoop/holyscoop.exe")
        assert program.read_text() == "MZ a program for another operating system\n"
        assert (target / "usr/lib/holyruntime/note").read_text() == "holyruntime fixture\n"
        # nothing in the payload is executable on this system, and no launcher exists
        assert not (target / "usr/bin/holyscoop").exists()
        assert not program.stat().st_mode & 0o111

        # an artifact whose digest the manifest does not pin is refused
        wrong = root / "wrong"
        wrong.mkdir()
        manifest(wrong, "holyscoopbad", {"bad/bad.exe": "MZ\n"})
        body = json.loads((wrong / "holyscoopbad.json").read_text())
        body["hash"] = "0" * 64
        (wrong / "holyscoopbad.json").write_text(json.dumps(body))
        result = subprocess.run([binary, "import", wrong / "holyscoopbad.json", "--source",
                                 "bucket", "--format", "scoop", "--output", root / "wrong-out"],
                                capture_output=True, text=True)
        assert result.returncode == 6, result
        assert "the manifest pins" in result.stderr, result.stderr
        assert not list((root / "wrong-out").glob("*.holy"))

        # an artifact that is not beside the manifest is a missing capability
        absent = root / "absent"
        absent.mkdir()
        manifest(absent, "holyscoopsparse", {"x/y.exe": "MZ\n"})
        (absent / "holyscoopsparse-fixture.zip").unlink()
        result = subprocess.run([binary, "import", absent / "holyscoopsparse.json", "--source",
                                 "bucket", "--format", "scoop", "--output", root / "absent-out"],
                                capture_output=True, text=True)
        assert result.returncode == 6, result
        assert "is not beside the manifest" in result.stderr, result.stderr

        # a manifest with no digest, no url or no version is refused
        for field, value in (("hash", None), ("url", None), ("version", None)):
            case = root / ("case-" + field)
            case.mkdir()
            manifest(case, "holyscoopcase", {"c/c.exe": "MZ\n"})
            body = json.loads((case / "holyscoopcase.json").read_text())
            if value is None:
                del body[field]
            (case / "holyscoopcase.json").write_text(json.dumps(body))
            result = subprocess.run([binary, "import", case / "holyscoopcase.json", "--source",
                                     "bucket", "--format", "scoop", "--output",
                                     root / (case.name + "-out")],
                                    capture_output=True, text=True)
            assert result.returncode == 2, (field, result)
            assert "holyscoopcase" not in result.stderr

        # an artifact with an escaping path is refused rather than placed
        escape = root / "escape"
        escape.mkdir()
        manifest(escape, "holyscoopescape", {"../outside.exe": "MZ\n"})
        result = subprocess.run([binary, "import", escape / "holyscoopescape.json", "--source",
                                 "bucket", "--format", "scoop", "--output",
                                 root / "escape-out"], capture_output=True, text=True)
        assert result.returncode == 3, result
        assert "cannot place" in result.stderr, result.stderr

        # an artifact that is a plain file travels as one payload entry
        plain = root / "plain"
        plain.mkdir()
        archive = plain / "holyscoopplain-fixture.bin"
        archive.write_bytes(b"MZ a single file artifact\n")
        (plain / "holyscoopplain.json").write_text(json.dumps({
            "version": "1.0",
            "url": f"https://example.org/{archive.name}",
            "hash": hashlib.sha256(archive.read_bytes()).hexdigest(),
        }, indent=2))
        call("import", plain / "holyscoopplain.json", "--source", "bucket", "--format", "scoop",
             "--output", root / "plain-out", status=3)
        plain_manifest = call("manifest", "local:" + str(root /
                                                          "plain-out/holyscoopplain--noarch--nolibc.holy"))
        assert "usr/lib/holy/private/holyscoopplain/holyscoopplain-fixture.bin" in plain_manifest

        # a manifest that is not JSON, or not an object, is refused
        broken = root / "broken"
        broken.mkdir()
        (broken / "holyscoopbroken.json").write_text("id: holyscoopbroken\n")
        result = subprocess.run([binary, "import", broken / "holyscoopbroken.json", "--source",
                                 "bucket", "--format", "scoop", "--output", root / "broken-out"],
                                capture_output=True, text=True)
        assert result.returncode == 2, result
        assert "reads no other form" in result.stderr, result.stderr
        result = subprocess.run([binary, "import", root / "absent.json", "--source", "bucket",
                                 "--format", "scoop", "--output", root / "absent2-out"],
                                capture_output=True, text=True)
        assert result.returncode == 6, result
        assert "manifest unavailable" in result.stderr, result.stderr

    print("scoop manifest fixtures passed")
    return 0


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