Phase 9: Add native processors and reproducible Linux release tooling

This commit is contained in:
Johannes Findeisen 2026-10-27 01:21:04 +01:00
commit de403b37d7
43 changed files with 1656 additions and 19 deletions

81
tools/dist.py Normal file
View file

@ -0,0 +1,81 @@
#!/usr/bin/env python3
"""Deterministic source archive using indexed files, or an exported manifest."""
import argparse
import gzip
import io
import json
import os
from pathlib import Path, PurePosixPath
import re
import subprocess
import tarfile
import tempfile
def version(root):
text = (root / 'include/xtc/xtc_version.h').read_text()
match = re.search(r'^#define XTC_VERSION_STRING "([0-9]+\.[0-9]+\.[0-9]+)"$', text, re.M)
if not match:
raise ValueError('Invalid project version')
return match.group(1)
def manifest(root):
if (root / '.git').exists():
data = subprocess.check_output(['git', 'ls-files', '--stage', '-z'], cwd=root)
entries = []
for record in data.split(b'\0'):
if not record:
continue
metadata, name = record.split(b'\t', 1)
mode, _, stage = metadata.split()
if stage != b'0' or mode not in (b'100644', b'100755'):
raise ValueError('Only resolved regular indexed files can be packaged')
entries.append([name.decode('utf-8'), mode == b'100755'])
else:
entries = json.loads((root / 'SOURCE_MANIFEST.json').read_text())
seen = set()
for name, executable in entries:
path = PurePosixPath(name)
if (not name or path.is_absolute() or '..' in path.parts or str(path) != name
or name in seen or name == 'SOURCE_MANIFEST.json' or not isinstance(executable, bool)):
raise ValueError('Invalid source manifest entry')
seen.add(name)
if (root / name).is_symlink() or not (root / name).is_file():
raise ValueError(f'Not a regular source file: {name}')
return sorted(entries)
def archive(root, output):
entries = manifest(root)
epoch = int(os.environ.get('SOURCE_DATE_EPOCH', '0'))
if epoch < 0 or epoch > 4294967295:
raise ValueError('SOURCE_DATE_EPOCH must fit an unsigned 32-bit timestamp')
top = 'xtc-' + version(root)
output.parent.mkdir(parents=True, exist_ok=True)
fd, temporary = tempfile.mkstemp(prefix='.xtc-dist-', dir=output.parent)
try:
with os.fdopen(fd, 'wb') as raw, gzip.GzipFile(filename='', mode='wb', fileobj=raw,
mtime=epoch, compresslevel=9) as compressed:
with tarfile.open(fileobj=compressed, mode='w', format=tarfile.PAX_FORMAT) as tar:
contents = [(name, (root / name).read_bytes(), executable) for name, executable in entries]
contents.append(('SOURCE_MANIFEST.json', (json.dumps(entries, indent=2) + '\n').encode(), False))
for name, data, executable in sorted(contents):
info = tarfile.TarInfo(top + '/' + name)
info.size = len(data)
info.mode = 0o755 if executable else 0o644
info.mtime = epoch
tar.addfile(info, io.BytesIO(data))
os.replace(temporary, output)
finally:
if os.path.exists(temporary):
os.unlink(temporary)
print(output)
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--root', type=Path, default=Path.cwd())
parser.add_argument('--output', type=Path, required=True)
args = parser.parse_args()
archive(args.root.resolve(), args.output.resolve())

16
tools/pkgconfig.py Normal file
View file

@ -0,0 +1,16 @@
#!/usr/bin/env python3
"""Generate pkg-config metadata without shell/sed interpolation of paths."""
import argparse
from pathlib import Path
from dist import version
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--prefix', required=True)
parser.add_argument('--jack', default='')
parser.add_argument('--output', type=Path, required=True)
args = parser.parse_args()
if not args.prefix.startswith('/') or any(c in args.prefix for c in '\n\r\0"$\\'):
parser.error('PREFIX must be absolute, without newlines, quotes, $, or backslashes')
text = Path('packaging/xtc.pc.in').read_text().replace('@PREFIX@', args.prefix)
text = text.replace('@VERSION@', version(Path.cwd())).replace('@JACK@', args.jack)
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(text)

44
tools/uninstall.py Normal file
View file

@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""Remove only this source version's installed files; never recurse directories."""
import argparse
from pathlib import Path
import stat
def owned_files(source):
files = ['lib/libxtc.a', 'lib/pkgconfig/xtc.pc', 'bin/xtc-check', 'bin/xtc',
'share/applications/xtc.desktop', 'share/licenses/xtc/LICENSE',
'share/doc/xtc/README.md']
files += ['include/xtc/' + p.name for p in sorted((source / 'include/xtc').glob('*.h'))]
files += ['share/doc/xtc/' + p.name for p in sorted((source / 'docs').glob('*.md'))]
files += ['share/doc/xtc/decisions/' + p.name for p in sorted((source / 'docs/decisions').glob('*.md'))]
return files
def uninstall(root, source):
if not root.is_absolute() or root == Path('/'):
raise ValueError('Installation root must be absolute and not /')
files = [root / name for name in owned_files(source)]
# Preflight before changing anything; never unlink a directory at a file path.
for path in files:
try:
mode = path.lstat().st_mode
except FileNotFoundError:
continue
if not (stat.S_ISREG(mode) or stat.S_ISLNK(mode)):
raise ValueError(f'Refusing non-file: {path}')
for path in files:
path.unlink(missing_ok=True)
parents = {parent for path in files for parent in path.parents if parent == root or root in parent.parents}
for parent in sorted(parents, key=lambda p: len(p.parts), reverse=True):
try:
parent.rmdir()
except (FileNotFoundError, OSError):
pass # Shared/nonempty directories remain untouched.
if __name__ == '__main__':
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--root', type=Path, required=True)
args = parser.parse_args()
uninstall(args.root, Path(__file__).resolve().parent.parent)