Build musl and glibc for amd64 and arm64 through one native package path. Reuse musl outputs for OCI images and OpenWrt installers, and export binaries inside a rootless unshare session. Restore the executable dependency graph into a shared persistent NuGet cache, build managed dependencies once, and run isolated native publishes in parallel. Clean temporary outputs and expired image layers under store locks, retire inactive legacy stores after seven days, and retain two toolchain exports. Add regression coverage for staging, extraction, packaging and cleanup boundaries.
150 lines
7.9 KiB
Python
150 lines
7.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Linux CI contracts using real staging/tar/shell and a Buildah boundary fake."""
|
|
import importlib.util
|
|
import fcntl
|
|
import os
|
|
from pathlib import Path
|
|
import subprocess
|
|
import tarfile
|
|
import tempfile
|
|
import time
|
|
import unittest
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
class LinuxBuildTests(unittest.TestCase):
|
|
def test_retired_store_cleanup_preserves_recent_locked_and_unrelated_directories(self):
|
|
spec = importlib.util.spec_from_file_location('retired_stores', ROOT / 'ci/cleanup-retired-stores.py')
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
base = Path(directory)
|
|
names = ['dev-image-build-noble-aot-v1-probe-amd64',
|
|
'dev-image-build-noble-aot-v1-agent-arm64',
|
|
'dev-node-index-agent', 'linux-agent', 'unrelated']
|
|
for name in names:
|
|
metadata = base / name / '.local/share/containers/storage/overlay-images/images.json'
|
|
metadata.parent.mkdir(parents=True)
|
|
metadata.write_text('[]')
|
|
if name != names[1]:
|
|
old = time.time() - 8 * 86400
|
|
os.utime(metadata, (old, old))
|
|
with (base / names[2] / '.lock').open('a') as lock:
|
|
fcntl.flock(lock, fcntl.LOCK_EX)
|
|
self.assertEqual(module.cleanup(base), [names[0]])
|
|
self.assertFalse((base / names[0]).exists())
|
|
for name in names[1:]:
|
|
self.assertTrue((base / name).is_dir())
|
|
|
|
def test_restore_context_contains_only_the_executables_dependency_graph_and_is_stable(self):
|
|
spec = importlib.util.spec_from_file_location('stage_node', ROOT / 'ci/stage-node-image.py')
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
base = Path(directory)
|
|
first, second = base / 'first', base / 'second'
|
|
module.stage('agent', 'linux-x64', first)
|
|
module.stage('agent', 'linux-musl-arm64', second)
|
|
actual = {p.relative_to(first / 'restore/backend').as_posix()
|
|
for p in (first / 'restore/backend').rglob('*.csproj')}
|
|
expected = {'agent/src/MoShiro.Agent/MoShiro.Agent.csproj',
|
|
'fleet/tools/fleet-enroll/fleet-enroll.csproj',
|
|
'shared/MoShiro.Measurement/MoShiro.Measurement.csproj',
|
|
'shared/MoShiro.Protocol/MoShiro.Protocol.csproj'}
|
|
self.assertTrue(expected <= actual)
|
|
self.assertFalse(any('/MoShiro.Backend' in name or '/tests/' in name for name in actual))
|
|
left = {p.relative_to(first / 'restore'): (p.read_bytes(), p.stat().st_mtime)
|
|
for p in (first / 'restore').rglob('*') if p.is_file()}
|
|
right = {p.relative_to(second / 'restore'): (p.read_bytes(), p.stat().st_mtime)
|
|
for p in (second / 'restore').rglob('*') if p.is_file()}
|
|
self.assertEqual(left, right)
|
|
self.assertTrue(all(mtime == module.FIXED_MTIME for _, mtime in left.values()))
|
|
self.assertTrue((second / 'source/packaging/native/msquic/linux-musl-arm64/libmsquic.a').is_file())
|
|
|
|
def test_retention_removes_only_expired_layers_without_pruning_package_caches(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
base = Path(directory)
|
|
buildah = base / 'buildah'
|
|
buildah.write_text('''#!/usr/bin/env bash
|
|
set -eu
|
|
case "$1" in
|
|
images) printf 'old %s <none>:<none>\nrecent %s <none>:<none>\nversion %s localhost/moshiro-agent:1.0.0-amd64\nactive %s localhost/moshiro-agent:ci-dev-amd64\n' "$OLD_TIME" "$RECENT_TIME" "$OLD_TIME" "$OLD_TIME" ;;
|
|
rmi) printf '%s\n' "$*" >> "$FIXTURE/removed" ;;
|
|
unshare) exit 0 ;;
|
|
*) exit 9 ;;
|
|
esac
|
|
''')
|
|
buildah.chmod(0o755)
|
|
env = dict(os.environ, PATH=str(base) + ':' + os.environ['PATH'], FIXTURE=str(base),
|
|
OLD_TIME=str(int(time.time()) - 8 * 86400), RECENT_TIME=str(int(time.time())),
|
|
CONTAINERS_STORAGE_CONF=str(base / 'moshiro-storage.conf'))
|
|
subprocess.run(['bash', str(ROOT / 'ci/cleanup-build-cache.sh')], env=env, check=True, capture_output=True)
|
|
self.assertEqual((base / 'removed').read_text(), 'rmi old\nrmi version\n')
|
|
|
|
def test_export_requires_one_unshare_session_and_cleans_up_on_copy_failure(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
base = Path(directory)
|
|
(base / 'bin').mkdir()
|
|
(base / 'image/out/agent').mkdir(parents=True)
|
|
(base / 'image/out/enroll').mkdir(parents=True)
|
|
(base / 'image/out/agent/moshiro-agent').write_bytes(b'agent')
|
|
enroll = base / 'image/out/enroll/fleet-enroll'
|
|
enroll.write_bytes(b'enroll')
|
|
buildah = base / 'bin/buildah'
|
|
buildah.write_text('''#!/usr/bin/env bash
|
|
set -eu
|
|
if [[ "$1" == unshare ]]; then
|
|
shift
|
|
export TEST_UNSHARE=yes
|
|
exec "$@"
|
|
fi
|
|
[[ "${TEST_UNSHARE:-}" == yes ]] || exit 125
|
|
case "$1" in
|
|
from) echo test-container ;;
|
|
mount) echo "$FIXTURE/image" ;;
|
|
unmount|rm) echo "$1" >> "$FIXTURE/cleanup" ;;
|
|
*) exit 2 ;;
|
|
esac
|
|
''')
|
|
buildah.chmod(0o755)
|
|
env = dict(os.environ, PATH=str(base / 'bin') + ':' + os.environ['PATH'], FIXTURE=str(base))
|
|
command = ['bash', str(ROOT / 'ci/extract-node-binaries.sh'), 'test-image', str(base / 'output')]
|
|
for succeeds in (True, False):
|
|
with self.subTest(succeeds=succeeds):
|
|
if not succeeds:
|
|
enroll.unlink()
|
|
result = subprocess.run(command, env=env, capture_output=True, text=True)
|
|
self.assertEqual(result.returncode == 0, succeeds, result.stderr)
|
|
self.assertEqual((base / 'cleanup').read_text().splitlines(), ['unmount', 'rm'])
|
|
(base / 'cleanup').unlink()
|
|
if succeeds:
|
|
self.assertEqual((base / 'output/moshiro-agent').read_bytes(), b'agent')
|
|
self.assertEqual((base / 'output/fleet-enroll').read_bytes(), b'enroll')
|
|
|
|
def test_four_linux_packages_preserve_installers_and_shared_library_payloads(self):
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
base = Path(directory)
|
|
binaries = base / 'binaries'
|
|
binaries.mkdir()
|
|
for name in ('moshiro-agent', 'fleet-enroll', 'libmsquic.so.2'):
|
|
(binaries / name).write_bytes(name.encode())
|
|
for rid, suffix in [('linux-x64', 'amd64'), ('linux-arm64', 'arm64'),
|
|
('linux-musl-x64', 'musl-amd64'), ('linux-musl-arm64', 'musl-arm64')]:
|
|
with self.subTest(rid=rid):
|
|
output = base / rid
|
|
env = dict(os.environ, CI_COMMIT_TAG='v1.2.3')
|
|
subprocess.run(['bash', str(ROOT / 'ci/package-linux-agent.sh'), rid, str(binaries), str(output)], env=env, check=True, capture_output=True)
|
|
archive = output / f'moshiro-agent-v1.2.3-linux-{suffix}.tar.gz'
|
|
with tarfile.open(archive) as package:
|
|
for name in ('moshiro-agent', 'fleet-enroll', 'libmsquic.so.2', 'install.sh',
|
|
'systemd/moshiro-agent.service', 'openwrt/moshiro-agent.init', 'LICENSE'):
|
|
self.assertIn(name, package.getnames())
|
|
self.assertEqual(package.extractfile('moshiro-agent').read(), b'moshiro-agent')
|
|
self.assertEqual(package.getmember('moshiro-agent').mode, 0o755)
|
|
subprocess.run(['sha256sum', '-c', archive.name + '.sha256'], cwd=output, check=True, capture_output=True)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main(verbosity=2)
|