188 lines
8.1 KiB
Python
188 lines
8.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Exercise build/toolchain boundaries without network access or a reader."""
|
|
|
|
import hashlib
|
|
import io
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tarfile
|
|
import tempfile
|
|
import unittest
|
|
|
|
|
|
MOCK_TOOL = r'''#!/usr/bin/env python3
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import shutil
|
|
import sys
|
|
|
|
tool = Path(sys.argv[0]).name
|
|
args = sys.argv[1:]
|
|
with open(os.environ["BUILD_LOG"], "a") as stream:
|
|
stream.write(json.dumps([tool, args, os.getcwd(), {
|
|
name: os.environ.get(name) for name in
|
|
("GOTOOLCHAIN", "GOFLAGS", "CGO_ENABLED", "CGO_CFLAGS", "GOMODCACHE")
|
|
}]) + "\n")
|
|
if tool == "uname":
|
|
print("Linux" if args == ["-s"] else "x86_64")
|
|
elif tool == "pkg-config":
|
|
sys.exit(1 if args[-1] == os.environ.get("MISSING_LIBRARY") else 0)
|
|
elif tool == "curl":
|
|
if args[-1].endswith(".sha256"):
|
|
print(os.environ["ARCHIVE_SHA"])
|
|
else:
|
|
shutil.copyfile(os.environ["ARCHIVE_PATH"], args[args.index("--output") + 1])
|
|
elif tool in ("go", "bootstrap-go"):
|
|
if args == ["version"]:
|
|
version = "1.24.0" if tool == "bootstrap-go" else os.environ["INSTALLED_GO"]
|
|
print("go version go" + version + " linux/amd64")
|
|
elif args[0] == "build":
|
|
if args[-1] == os.environ.get("FAIL_BUILD_TARGET"):
|
|
sys.exit(2)
|
|
Path(args[args.index("-o") + 1]).touch()
|
|
'''
|
|
|
|
|
|
class BuildScriptTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.tempDir = tempfile.TemporaryDirectory(prefix="cthulhu-build-test-")
|
|
self.addCleanup(self.tempDir.cleanup)
|
|
self.root = Path(self.tempDir.name)
|
|
self.project = self.root / "checkout"
|
|
self.project.mkdir()
|
|
source = Path(__file__).resolve().parents[1] / "build-cthulhu.sh"
|
|
self.script = self.project / source.name
|
|
shutil.copyfile(source, self.script)
|
|
(self.project / "go.mod").write_text("module example.invalid/reader\n\ngo 1.24.0\n")
|
|
self.caller = self.root / "caller directory"
|
|
self.caller.mkdir()
|
|
self.binDir = self.root / "bin"
|
|
self.binDir.mkdir()
|
|
for name in ("go", "curl", "pkg-config", "uname"):
|
|
toolPath = self.binDir / name
|
|
toolPath.write_text(MOCK_TOOL)
|
|
toolPath.chmod(0o755)
|
|
# Give the downloaded Go its own name for log assertions.
|
|
bootstrap = MOCK_TOOL.replace('Path(sys.argv[0]).name', '"bootstrap-go"')
|
|
self.archive = self.root / "go.tar.gz"
|
|
with tarfile.open(self.archive, "w:gz") as archive:
|
|
data = bootstrap.encode()
|
|
entry = tarfile.TarInfo("go/bin/go")
|
|
entry.size = len(data)
|
|
entry.mode = 0o755
|
|
archive.addfile(entry, io.BytesIO(data))
|
|
self.logPath = self.root / "calls.jsonl"
|
|
self.env = dict(os.environ, PATH=f"{self.binDir}:/usr/bin:/bin",
|
|
INSTALLED_GO="1.24.0", BUILD_LOG=str(self.logPath),
|
|
XDG_CACHE_HOME=str(self.root / "cache"),
|
|
ARCHIVE_PATH=str(self.archive),
|
|
ARCHIVE_SHA=hashlib.sha256(self.archive.read_bytes()).hexdigest(),
|
|
GOTOOLCHAIN="auto", CGO_ENABLED="1", GOFLAGS="-mod=readonly",
|
|
CGO_CFLAGS="-O2", GOMODCACHE=str(self.root / "modules"))
|
|
|
|
def run_build(self, *args):
|
|
return subprocess.run(["/bin/bash", str(self.script), *args], cwd=self.caller,
|
|
env=self.env, text=True, capture_output=True, check=False)
|
|
|
|
def calls(self, name):
|
|
if not self.logPath.exists():
|
|
return []
|
|
return [call for line in self.logPath.read_text().splitlines()
|
|
if (call := json.loads(line))[0] == name]
|
|
|
|
def test_local_build_paths_flags_and_environment(self):
|
|
self.env["GOTOOLCHAIN"] = "local"
|
|
result = self.run_build("--output-dir", "output directory", "--", "-trimpath")
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
for binary in ("cthulhu", "cthulhu-braille", "cthulhu-input-helper"):
|
|
self.assertTrue((self.caller / "output directory" / binary).is_file())
|
|
builds = [call for call in self.calls("go") if call[1][0] == "build"]
|
|
self.assertEqual(len(builds), 3)
|
|
for call in builds:
|
|
self.assertIn("-trimpath", call[1])
|
|
self.assertEqual(call[2], str(self.project))
|
|
for name in ("GOTOOLCHAIN", "GOFLAGS", "CGO_CFLAGS", "GOMODCACHE"):
|
|
self.assertEqual(call[3][name], self.env[name])
|
|
self.assertIn("brlapi", builds[1][1])
|
|
self.assertNotIn("brlapi", builds[2][1])
|
|
self.assertFalse(self.calls("curl"))
|
|
|
|
def test_old_go_bootstrap_and_cache_reuse(self):
|
|
self.env["INSTALLED_GO"] = "1.20.14"
|
|
result = self.run_build()
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertTrue((self.project / "cthulhu-braille").is_file())
|
|
self.assertEqual(len(self.calls("curl")), 2)
|
|
result = self.run_build()
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertEqual(len(self.calls("curl")), 2)
|
|
self.assertEqual(len([c for c in self.calls("bootstrap-go") if c[1][0] == "build"]), 6)
|
|
|
|
def test_checksum_failure_never_executes_download(self):
|
|
self.env.update(INSTALLED_GO="1.20.14", ARCHIVE_SHA="0" * 64)
|
|
result = self.run_build()
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertIn("checksum mismatch", result.stderr)
|
|
self.assertFalse(self.calls("bootstrap-go"))
|
|
self.assertFalse(list((self.root / "cache").rglob("*.download.*")))
|
|
|
|
def test_bootstrap_without_installed_go(self):
|
|
(self.binDir / "go").unlink()
|
|
for name in ("python3", "dirname", "awk", "mkdir", "mktemp", "tar",
|
|
"gzip", "sha256sum", "mv", "rm"):
|
|
toolPath = sys.executable if name == "python3" else shutil.which(name)
|
|
(self.binDir / name).symlink_to(toolPath)
|
|
self.env["PATH"] = str(self.binDir)
|
|
result = self.run_build()
|
|
self.assertEqual(result.returncode, 0, result.stderr)
|
|
self.assertEqual(len(self.calls("curl")), 2)
|
|
self.assertTrue((self.project / "cthulhu").is_file())
|
|
|
|
def test_local_old_go_does_not_download(self):
|
|
self.env.update(INSTALLED_GO="1.20.14", GOTOOLCHAIN="local")
|
|
result = self.run_build()
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertIn("Install Go 1.24.0", result.stderr)
|
|
self.assertFalse(self.calls("curl"))
|
|
|
|
def test_preflight_and_arguments_do_not_download(self):
|
|
for args in (("--output-dir",), ("--unknown",)):
|
|
with self.subTest(args=args):
|
|
self.assertNotEqual(self.run_build(*args).returncode, 0)
|
|
self.assertEqual(self.run_build("--help").returncode, 0)
|
|
self.assertFalse(self.logPath.exists())
|
|
self.env.update(INSTALLED_GO="1.20.14", MISSING_LIBRARY="xres")
|
|
result = self.run_build()
|
|
self.assertIn("xres", result.stderr)
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertFalse(self.calls("curl"))
|
|
|
|
def test_helper_failure_is_not_reported_as_success(self):
|
|
for helper in ("cthulhu-braille", "cthulhu-input-helper"):
|
|
with self.subTest(helper=helper):
|
|
self.env["FAIL_BUILD_TARGET"] = f"./cmd/{helper}"
|
|
result = self.run_build()
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertNotIn("Cthulhu has been built", result.stdout)
|
|
|
|
def test_disabled_cgo_and_output_conflicts_fail_before_building(self):
|
|
self.env["CGO_ENABLED"] = "0"
|
|
result = self.run_build()
|
|
self.assertNotEqual(result.returncode, 0)
|
|
self.assertIn("CGO_ENABLED=1", result.stderr)
|
|
self.env["CGO_ENABLED"] = "1"
|
|
(self.caller / "output").write_text("existing file")
|
|
self.assertNotEqual(self.run_build("-o", "output").returncode, 0)
|
|
(self.project / "cthulhu-braille").mkdir()
|
|
self.assertNotEqual(self.run_build().returncode, 0)
|
|
self.assertFalse(self.calls("go"))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|