1
0
Fork 0
forked from hanez/xtc

Phase 30: complete configurable installation and release qualification

This commit is contained in:
Johannes Findeisen 2026-10-27 17:13:09 +01:00
commit 90eac33a71
18 changed files with 399 additions and 59 deletions

View file

@ -6,6 +6,18 @@ PKG_CONFIG ?= pkg-config
FLTK_CONFIG ?= fltk-config
BUILD_DIR ?= build
PREFIX ?= /usr/local
exec_prefix ?= $(PREFIX)
bindir ?= $(exec_prefix)/bin
libdir ?= $(exec_prefix)/lib
includedir ?= $(PREFIX)/include
datarootdir ?= $(PREFIX)/share
datadir ?= $(datarootdir)
docdir ?= $(datarootdir)/doc/xtc
licensedir ?= $(datarootdir)/licenses/xtc
examplesdir ?= $(datadir)/xtc/examples
desktopdir ?= $(datadir)/applications
pkgconfigdir ?= $(libdir)/pkgconfig
INSTALL_LAYOUT = --prefix "$(PREFIX)" --destdir "$(DESTDIR)" --bindir "$(bindir)" --libdir "$(libdir)" --includedir "$(includedir)" --docdir "$(docdir)" --licensedir "$(licensedir)" --examplesdir "$(examplesdir)" --desktopdir "$(desktopdir)" --pkgconfigdir "$(pkgconfigdir)"
PYTHON ?= python3
VERSION := $(shell sed -n 's/^\#define XTC_VERSION_STRING "\([^"]*\)"/\1/p' include/xtc/xtc_version.h)
@ -172,27 +184,18 @@ endif
# install retains the core-only behavior; install-gui adds the application.
.PHONY: install uninstall install-gui dist release test-install test-release test-performance
$(BUILD_DIR)/xtc.pc: packaging/xtc.pc.in tools/pkgconfig.py tools/dist.py include/xtc/xtc_version.h FORCE
$(PYTHON) tools/pkgconfig.py --prefix "$(PREFIX)" --jack "$(if $(JACK_AVAILABLE),jack)" --output "$@"
$(PYTHON) tools/pkgconfig.py --prefix "$(PREFIX)" --exec-prefix "$(exec_prefix)" --libdir "$(libdir)" --includedir "$(includedir)" --jack "$(if $(JACK_AVAILABLE),jack)" --output "$@"
.PHONY: FORCE
FORCE:
install: all $(BUILD_DIR)/xtc.pc
install -d "$(DESTDIR)$(PREFIX)/include/xtc" "$(DESTDIR)$(PREFIX)/lib/pkgconfig" "$(DESTDIR)$(PREFIX)/bin" "$(DESTDIR)$(PREFIX)/share/doc/xtc/decisions" "$(DESTDIR)$(PREFIX)/share/licenses/xtc"
install -m 0644 include/xtc/*.h "$(DESTDIR)$(PREFIX)/include/xtc/"
install -m 0644 "$(BUILD_DIR)/libxtc.a" "$(DESTDIR)$(PREFIX)/lib/"
install -m 0644 "$(BUILD_DIR)/xtc.pc" "$(DESTDIR)$(PREFIX)/lib/pkgconfig/"
install -m 0755 "$(BUILD_DIR)/xtc-check" "$(DESTDIR)$(PREFIX)/bin/"
install -m 0644 README.md docs/*.md docs/performance-baseline.json "$(DESTDIR)$(PREFIX)/share/doc/xtc/"
install -m 0644 docs/decisions/*.md "$(DESTDIR)$(PREFIX)/share/doc/xtc/decisions/"
install -m 0644 LICENSE "$(DESTDIR)$(PREFIX)/share/licenses/xtc/"
$(PYTHON) tools/install.py $(INSTALL_LAYOUT) --build-dir "$(BUILD_DIR)"
install-gui: install gui
install -d "$(DESTDIR)$(PREFIX)/share/applications"
install -m 0755 "$(BUILD_DIR)/xtc" "$(DESTDIR)$(PREFIX)/bin/"
install -m 0644 packaging/xtc.desktop "$(DESTDIR)$(PREFIX)/share/applications/"
$(PYTHON) tools/install.py $(INSTALL_LAYOUT) --build-dir "$(BUILD_DIR)" --gui
uninstall:
$(PYTHON) tools/uninstall.py --root "$(DESTDIR)$(PREFIX)"
$(PYTHON) tools/uninstall.py $(INSTALL_LAYOUT)
dist:
$(PYTHON) tools/dist.py --output "$(BUILD_DIR)/xtc-$(VERSION).tar.gz"
@ -454,3 +457,7 @@ check-development: test-benchmark-tools
.PHONY: test-stress-plugins
test-stress-plugins: $(BUILD_DIR)/test_realtime $(BUILD_DIR)/test_reliability $(BUILD_DIR)/test_scale $(BUILD_DIR)/test_portability $(BUILD_DIR)/test_project_diagnostics $(BUILD_DIR)/test_soak $(BACKEND_TEST) $(BUILD_DIR)/test_plugins $(BUILD_DIR)/plugin-fixture.so
$(PYTHON) tools/stress.py --build-dir "$(BUILD_DIR)" --rounds "$(STRESS_ROUNDS)" --plugin "$(BUILD_DIR)/plugin-fixture.so"
.PHONY: test-install-layout
test-install-layout: all $(if $(GUI_AVAILABLE),gui)
$(PYTHON) tests/integration/test_install.py --build-dir "$(abspath $(BUILD_DIR))" --custom-layout $(if $(GUI_AVAILABLE),--gui)

View file

@ -263,3 +263,12 @@ regressions, scale/virtual endurance, benchmark-gate checks, Clang analysis and
ASan/UBSan. Run `make benchmark benchmark-project` separately without contention
or sanitizers for timing reports. Optional `make test-stress-plugins` requires
installed LADSPA headers; `make test-stress` supports minimal builds too.
## Phase 30 installation layouts
`make test-install test-install-layout` verifies default and custom layouts in
throwaway staging directories, C/C++ installed consumers, examples and exact
removal. Override `PREFIX`, `exec_prefix`, `bindir`, `libdir`, `includedir`,
`datarootdir`, `datadir`, `docdir`, `pkgconfigdir`, `licensedir`, `examplesdir` or
`desktopdir` as needed. `DESTDIR` adds staging separately; pass the same layout
to uninstall. See RELEASING.md for commands and RELEASE_NOTES.md for upgrades.

View file

@ -53,3 +53,32 @@ an unsupported source file during failed Open. Keep the original when diagnosing
post-publication sync errors. Native descriptor ABI compatibility is distinct from
project format and is documented in NATIVE_API.md; opaque DSP state is not promised.
No public stability guarantee or support SLA is implied by development version 0.1.0.
## Phase 30 support and triage record
Verified compilers are GCC/G++ 16.2.1 and Clang/Clang++ 23.1.3 on x86_64 Linux;
these are tested versions, not a promise that arbitrary older/newer versions work.
The code requires C99 plus GCC/Clang lock-free atomics, C++17 for the GUI, FLTK
1.4 and POSIX/glibc APIs. JACK (including PipeWire's JACK compatibility) is the
only live backend. Native Windows/macOS, other architectures and direct ALSA
backends are not qualified. Core builds can disable JACK, FLTK and LADSPA.
See BUILD.md and LICENSES.md for dependency roles and versions.
Release qualification adds `test-install-layout`, installed example inspection,
all performance report resources, source checksum verification and the current
RELEASE_NOTES.md. Retain the original graph benchmark alert in release records;
a successful compile does not clear timing qualification gaps.
File reproducible bugs with the repository maintainers using the source commit
and archive SHA-256, `xtc-check --version`/`--diagnose`, distro/kernel and toolchain,
exact commands or GUI steps, expected versus observed behavior and frequency.
Reduce to a redistributable project and state whether it reproduces without
external plugins and with the offline/core tools. Include relevant stderr and
sanitizer traces; preserve original recovery files before experimenting.
Maintainers should first confirm version/features and reproduce in an isolated
build. Prioritize data loss/security/memory/thread defects, then crashes and
callback/deadline failures, then functional bugs and UI issues. Record severity,
reproduction conditions, affected versions, regression test and verification
command with the fix. Route timing-only alerts through matching-host reports and
repeat measurements; do not treat sanitized timings as realtime evidence.

53
docs/RELEASE_NOTES.md Normal file
View file

@ -0,0 +1,53 @@
# 0.1.0 development release notes
This remains a development prototype, not a production-qualified DAW. Phase 30
completes the local release workflow without bumping the application version,
creating a tag or publishing artifacts. The exact source archive SHA-256 and
reviewed Git commit identify a build; version 0.1.0 alone does not identify all
of the development revisions that used that version.
## Included functionality
The C99 engine provides bounded graphs, immutable plan publication, stereo audio,
MIDI instruments and controller bindings, track/bus mixing, automation and inline
MIDI editing. The C++17 FLTK application adds arrangement, routing, recording,
undo/redo and a generic LADSPA editor. JACK is the only live audio/MIDI backend;
core validation and offline rendering work without it. Native/LADSPA processors
are trusted in-process code. No proprietary energyXT resources are included.
Project format v10 reads versions 1–10 transactionally. Syntax diagnostics identify
records and lines. `xtc-check --inspect` lists dependency paths without executing
plugins. Phase 29 adds timing/CPU/RSS reports, repeatable stress runs and virtual
endurance. Phase 30 adds configurable install directories, shared install/remove
inventory, installed MIDI examples and source archive checksum sidecars.
## Upgrading and removal
Back up documents and external media before upgrading. Opening an older project
does not rewrite it; saving upgrades it to v10, which older readers may reject.
Use Save As for a separate migrated copy. Moving a project folder preserves media
stored inside it via relative references. Absolute media and plugin module paths
must be restored separately. There is no bundled plugin/media migration or saved
JACK port configuration. See PROJECT_FORMAT.md and RECOVERY.md for exact policies.
Install to the same layout to repair owned files. Only documented owned filenames
are replaced; unrelated files with other names remain. Use the same source
version and directory variables for uninstall. It removes owned files and empty
owned directories, never recordings or recursively deleting a prefix. A changed
layout or an older release with a different inventory may require removing that
old version before installing the new one. No database/package-manager registration
or automatic cleanup of historical inventories is performed.
## Known qualification limits
Physical devices, multi-hour wall-clock endurance, power-cut behavior and other
Linux distributions remain unqualified. There is no disk streaming, crash autosave,
plugin sandbox, latency compensation or stable shared-library ABI. A trusted
plugin can allocate, block or crash the process. Metadata inspection checks file
availability only. See KNOWN_LIMITATIONS.md for the complete list.
The retained graph performance gate has a documented small-case timing alert on
the shared development host; the project timing comparison passed. This alert
was not hidden by changing the baseline. See PERFORMANCE.md before using these
measurements as latency expectations. Sanitizers and virtual endurance qualify
software paths, not hardware timing.

View file

@ -72,3 +72,37 @@ policy. Build diagnostics make no server/plugin calls. There is no asynchronous
crash uploader or signal-handler cleanup: inspect OS core/debugger evidence on
the control side, preserving project/media recovery paths. No release is publicly
published by these commands, and human/device/listening checks remain pending.
## Phase 30 release workflow
Installation now shares one explicit file inventory with uninstallation. GNU-style
`exec_prefix`, `bindir`, `libdir`, `includedir`, `datarootdir`, `datadir`, `docdir`,
`pkgconfigdir` and the xtc-specific `licensedir`, `examplesdir`, `desktopdir` can be
overridden alongside `PREFIX`/`DESTDIR`. Paths must be absolute POSIX paths;
spaces are supported. Do not include DESTDIR in a directory variable: staging is
applied separately and never embedded in pkg-config metadata. Staging refuses
parent symlinks that escape DESTDIR. Use exactly the same variables for removal.
```sh
make BUILD_DIR=build/release-check all test gui
make BUILD_DIR=build/release-check test-install test-install-layout
make PREFIX=/usr DESTDIR=/tmp/xtc-stage libdir=/usr/lib64 install-gui
make PREFIX=/usr DESTDIR=/tmp/xtc-stage libdir=/usr/lib64 uninstall
make test-release test-package
```
Core install includes the static library, public headers, `xtc-check`, pkg-config
metadata, Markdown documentation, all checked-in performance reports, license and
`share/xtc/examples` (a media/plugin-free MIDI demo). GUI install adds the desktop
entry and `xtc`. No desktop MIME association is claimed. Package archives keep
their fixed `/usr` layout and relocatable pkg-config metadata; custom live layouts
are tested separately. Existing owned file symlinks are replaced, not followed;
installation is atomic per file, not a transaction over the entire prefix.
`make dist` now writes a `.sha256` sidecar as well as the normalized source archive.
Record that hash, the reviewed commit, version and build/dependency conditions
with release logs. `test-release` verifies the hash, builds/tests two fresh source
exports and compares artifacts and repacks. It validates exported tracked content,
including staged work; it does not attest that the caller's Git tree is clean.
For public release use a clean reviewed checkout and inspect `git status` first.
RELEASE_NOTES.md supplies upgrade guidance and remaining qualification limits.

View file

@ -846,6 +846,20 @@ Turn the application into a maintainable release that users can build, install,
- The release includes documentation, known limitations, and a reproducible version identifier.
- The project has a repeatable process for preparing and validating releases.
### Implementation status
Completed: configurable standard install directories with one shared owned-file
inventory, staged file/symlink safety checks, installed metadata-only MIDI example
and all performance resources, source SHA-256 sidecars, release notes, upgrades,
support matrix and maintenance/triage guidance. Existing version/diagnostic modes,
desktop metadata and normalized source/binary packaging are retained.
Acceptance verified with warning-enabled core/GUI builds and tests; default and
custom-layout staged install/repair/uninstall with C/C++ consumers; minimal
no-JACK/no-FLTK/no-LADSPA build; two clean source-export release builds with
byte-identical binaries and repacks; and binary manifest/hash/relocation checks.
No public release, tag, version bump or system install is performed. Physical
qualification and the Phase 29 graph timing alert remain documented limitations.
---
## Recommended Execution Order

View file

@ -9,3 +9,6 @@ matched raw/smoothed listening fixtures. Run `make quality-preview`; generated
WAVs remain in the ignored build directory. See docs/AUDIO_QUALITY.md for the
listening sequence and the distinction between automated checks and human
listening acceptance. It never starts a backend or plays audio.
`projects/midi-demo.xtc` is an original media/plugin-free MIDI example installed
under `share/xtc/examples`. See projects/README.md for opening and playback.

View file

@ -0,0 +1,7 @@
# MIDI demonstration
`midi-demo.xtc` is an original MIT-licensed metadata-only example based on the
repository v10 fixture. It contains one A4 MIDI note, a gain node and an output.
Open it in xtc and Play after connecting JACK output ports to hear it. It needs
no media or plugins. `xtc-check --validate midi-demo.xtc` works without JACK.
Save a working copy in your own folder; installed examples may be read-only.

View file

@ -0,0 +1,41 @@
XTC_PROJECT 10
APP 0 1 0
TEMPO 120000
IDS 1 1
GRAPH 1 3 3 2
NODE 0 6 00000000
NODE 1 1 3f000000
NODE 2 4 00000000
EDGE 0 1
EDGE 1 2
TRACKS 1
TRACK 0 4b657973
MIX 3f800000 00000000 0 0
ROUTE 0 18446744073709551615
CLIPS 1
CLIP 0 0 0 960 0 960 5665727365
ASSET -
MIDI 1
AUDIO 3f800000 0 0
NOTES 1 1
NOTE 0 0 0 480 69 100 0
AUTOMATION 1
POINT 0 0 0 3f800000 0
CONTROL_MAPS 16
MAP 0 7 10 64
MAP 1 7 10 64
MAP 2 7 10 64
MAP 3 7 10 64
MAP 4 7 10 64
MAP 5 7 10 64
MAP 6 7 10 64
MAP 7 7 10 64
MAP 8 7 10 64
MAP 9 7 10 64
MAP 10 7 10 64
MAP 11 7 10 64
MAP 12 7 10 64
MAP 13 7 10 64
MAP 14 7 10 64
MAP 15 7 10 64
END

View file

@ -1,7 +1,7 @@
prefix=@PREFIX@
exec_prefix=${prefix}
libdir=${prefix}/lib
includedir=${prefix}/include
exec_prefix=@EXEC_PREFIX@
libdir=@LIBDIR@
includedir=@INCLUDEDIR@
Name: xtc
Description: Modular C audio engine and project library

View file

@ -17,6 +17,7 @@ def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--build-dir', type=Path, required=True)
parser.add_argument('--gui', action='store_true')
parser.add_argument('--custom-layout', action='store_true')
args = parser.parse_args()
source = Path.cwd()
with tempfile.TemporaryDirectory(prefix='xtc-install-') as directory:
@ -30,28 +31,48 @@ def main():
# MAKEFLAGS jobserver descriptors do not cross Python subprocesses.
env = dict(os.environ, MAKEFLAGS='', MFLAGS='')
variables = [f'BUILD_DIR={args.build_dir}', f'PREFIX={prefix}', f'DESTDIR={stage}']
if args.custom_layout:
variables += [f'exec_prefix={prefix}/runtime', f'libdir={prefix}/lib64',
f'includedir={prefix}/headers', f'datarootdir={prefix}/resources',
f'docdir={prefix}/manual', f'pkgconfigdir={prefix}/metadata']
binaries = root / ('runtime/bin' if args.custom_layout else 'bin')
headers = root / ('headers' if args.custom_layout else 'include')
resources = root / ('resources' if args.custom_layout else 'share')
docs = root / ('manual' if args.custom_layout else 'share/doc/xtc')
metadata = root / ('metadata' if args.custom_layout else 'lib/pkgconfig')
run(make + ['install'] + variables, env=env)
assert not (root / 'bin/xtc').exists()
assert (root / 'share/licenses/xtc/LICENSE').read_bytes() == (source / 'LICENSE').read_bytes()
assert (root / 'share/doc/xtc/PROJECT_FORMAT.md').is_file()
assert os.access(root / 'bin/xtc-check', os.X_OK)
run([str(root / 'bin/xtc-check')])
diagnose = subprocess.check_output([str(root / 'bin/xtc-check'), '--diagnose'], text=True)
assert not (binaries / 'xtc').exists()
assert (resources / 'licenses/xtc/LICENSE').read_bytes() == (source / 'LICENSE').read_bytes()
assert (docs / 'PROJECT_FORMAT.md').is_file()
assert os.access(binaries / 'xtc-check', os.X_OK)
run([str(binaries / 'xtc-check')])
for filename in ('performance-baseline.json', 'project-performance-baseline.json', 'graph-performance-phase29.json'):
assert (docs / filename).read_bytes() == (source / 'docs' / filename).read_bytes()
example = resources / 'xtc/examples/midi-demo.xtc'
run([str(binaries / 'xtc-check'), '--inspect', str(example)])
assert (resources / 'xtc/examples/README.md').is_file()
diagnose = subprocess.check_output([str(binaries / 'xtc-check'), '--diagnose'], text=True)
assert 'Project formats: 1..10' in diagnose and 'JACK compiled:' in diagnose
# Replacing a file symlink must not overwrite an unrelated target.
protected = stage / 'unrelated-target'
protected.write_text('keep unrelated target\n')
(binaries / 'xtc-check').unlink()
(binaries / 'xtc-check').symlink_to(protected)
# Upgrade/repair repeats the same owned-file install and keeps unrelated data.
(root / 'include/xtc/xtc.h').write_text('old installation marker\n')
(headers / 'xtc/xtc.h').write_text('old installation marker\n')
run(make + ['install'] + variables, env=env)
assert (root / 'include/xtc/xtc.h').read_bytes() == (source / 'include/xtc/xtc.h').read_bytes()
assert (headers / 'xtc/xtc.h').read_bytes() == (source / 'include/xtc/xtc.h').read_bytes()
assert sentinel.read_text() == 'keep\n'
version = subprocess.check_output([str(root / 'bin/xtc-check'), '--version'], text=True)
assert protected.read_text() == 'keep unrelated target\n'
version = subprocess.check_output([str(binaries / 'xtc-check'), '--version'], text=True)
assert version.startswith('xtc ')
valid = stage / 'valid.xtc'
valid.write_text('XTC_PROJECT 4\nAPP 0 1 0\nTEMPO 120000\nIDS 0 0\nGRAPH 1 0 0 0\nTRACKS 0\nCLIPS 0\nNOTES 0 0\nAUTOMATION 0\nEND\n')
run([str(root / 'bin/xtc-check'), '--validate', str(valid)])
run([str(binaries / 'xtc-check'), '--validate', str(valid)])
valid.write_text('damaged\n')
assert subprocess.run([str(root / 'bin/xtc-check'), '--validate', str(valid)],
assert subprocess.run([str(binaries / 'xtc-check'), '--validate', str(valid)],
stdout=subprocess.PIPE, stderr=subprocess.PIPE).returncode != 0
pkg_env = dict(env, PKG_CONFIG_PATH=str(root / 'lib/pkgconfig') + ':' + env.get('PKG_CONFIG_PATH', ''),
pkg_env = dict(env, PKG_CONFIG_PATH=str(metadata) + ':' + env.get('PKG_CONFIG_PATH', ''),
PKG_CONFIG_SYSROOT_DIR=str(stage))
pkg = shlex.split(os.environ.get('PKG_CONFIG', 'pkg-config'))
# A no-JACK test can be invoked with PKG_CONFIG=false; package consumer
@ -87,10 +108,30 @@ int main(int argc, char **argv) {
if args.gui:
run(make + ['install-gui'] + variables, env=env)
gui_env = dict(env, DISPLAY='', WAYLAND_DISPLAY='')
assert subprocess.check_output([str(root / 'bin/xtc'), '--version'], env=gui_env, text=True) == version
run([str(root / 'bin/xtc'), '--help'], env=gui_env)
assert subprocess.check_output([str(binaries / 'xtc'), '--version'], env=gui_env, text=True) == version
run([str(binaries / 'xtc'), '--help'], env=gui_env)
if shutil.which('desktop-file-validate'):
run(['desktop-file-validate', str(root / 'share/applications/xtc.desktop')])
run(['desktop-file-validate', str(resources / 'applications/xtc.desktop')])
# Both operations must reject staging paths redirected outside DESTDIR.
with tempfile.TemporaryDirectory(prefix='xtc-install-outside-') as outside:
escape = root / 'escape'
escape.symlink_to(outside, target_is_directory=True)
redirected = variables + [f'docdir={prefix}/escape/docs']
for operation in ('install', 'uninstall'):
refusal = subprocess.run(make + [operation] + redirected, env=env,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
assert refusal.returncode != 0 and not list(Path(outside).iterdir())
assert (headers / 'xtc/xtc.h').is_file()
escape.unlink()
# A directory at an owned file path must fail the whole removal preflight.
saved_binary = stage / 'saved-check'
(binaries / 'xtc-check').rename(saved_binary)
(binaries / 'xtc-check').mkdir()
refusal = subprocess.run(make + ['uninstall'] + variables, env=env,
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
assert refusal.returncode != 0 and (headers / 'xtc/xtc.h').is_file()
(binaries / 'xtc-check').rmdir()
saved_binary.rename(binaries / 'xtc-check')
run(make + ['uninstall'] + variables, env=env)
run(make + ['uninstall'] + variables, env=env)
remaining = sorted(p.relative_to(root).as_posix() for p in root.rglob('*') if p.is_file() or p.is_symlink())

View file

@ -44,6 +44,11 @@ def main():
flags = subprocess.check_output(['pkg-config', '--cflags', '--libs', '--static', 'xtc'], env=env, text=True)
assert str(prefix / 'include') in flags or str(prefix / 'lib/pkgconfig/../../include') in flags
assert (prefix / 'share/doc/xtc/USER_MANUAL.md').exists()
assert (prefix / 'share/doc/xtc/RELEASE_NOTES.md').exists()
subprocess.run([str(prefix / 'bin/xtc-check'), '--inspect',
str(prefix / 'share/xtc/examples/midi-demo.xtc')], check=True)
for name in ('project-performance-baseline.json', 'graph-performance-phase29.json'):
assert (prefix / 'share/doc/xtc' / name).is_file()
if a.build_dir:
again = root / 'repacked.tar.gz'
command = [sys.executable, 'tools/package.py', '--build-dir', str(a.build_dir), '--output', str(again)]

View file

@ -20,6 +20,7 @@ def main():
parser.add_argument('--gui', action='store_true')
args = parser.parse_args()
archive = args.archive.resolve()
assert archive.with_name(archive.name + '.sha256').read_text().split()[0] == digest(archive)
with tempfile.TemporaryDirectory(prefix='xtc-release-') as directory:
root = Path(directory)
with tarfile.open(archive) as source:

View file

@ -2,6 +2,7 @@
"""Deterministic source archive using indexed files, or an exported manifest."""
import argparse
import gzip
import hashlib
import io
import json
import os
@ -70,7 +71,9 @@ def archive(root, output):
finally:
if os.path.exists(temporary):
os.unlink(temporary)
print(output)
sha = hashlib.sha256(output.read_bytes()).hexdigest()
output.with_name(output.name + '.sha256').write_text(f'{sha} {output.name}\n')
print(f'{output}: {sha}')
if __name__ == '__main__':

38
tools/install.py Normal file
View file

@ -0,0 +1,38 @@
#!/usr/bin/env python3
"""Install the explicit owned inventory; replace files without following symlinks."""
import argparse
import os
from pathlib import Path
import shutil
import tempfile
from install_layout import arguments, inventory, layout, staged
parser = argparse.ArgumentParser(description=__doc__)
arguments(parser)
parser.add_argument('--build-dir', type=Path, required=True)
parser.add_argument('--gui', action='store_true')
args = parser.parse_args()
source = Path(__file__).resolve().parent.parent
entries = []
for destination, original in inventory(source, layout(args)):
name = destination.name
if name == 'BINARY_MANIFEST.json' or (not args.gui and name in ('xtc', 'xtc.desktop')):
continue
if original is None:
original = args.build_dir / name
destination = staged(destination, args.destdir)
if not original.is_file():
raise ValueError(f'Missing install input: {original}')
if destination.exists() and not (destination.is_file() or destination.is_symlink()):
raise ValueError(f'Refusing non-file destination: {destination}')
entries.append((destination, original))
for destination, original in entries:
destination.parent.mkdir(parents=True, exist_ok=True)
fd, pending = tempfile.mkstemp(prefix='.xtc-install-', dir=destination.parent)
try:
with os.fdopen(fd, 'wb') as output, original.open('rb') as input_file:
shutil.copyfileobj(input_file, output)
os.fchmod(output.fileno(), 0o755 if destination.name in ('xtc', 'xtc-check') else 0o644)
os.replace(pending, destination)
finally:
Path(pending).unlink(missing_ok=True)

55
tools/install_layout.py Normal file
View file

@ -0,0 +1,55 @@
"""Shared install inventory and configurable POSIX directory layout."""
from pathlib import Path
KEYS = ('bindir', 'libdir', 'includedir', 'docdir', 'licensedir', 'examplesdir', 'desktopdir', 'pkgconfigdir')
def arguments(parser):
parser.add_argument('--prefix', default='/usr/local')
parser.add_argument('--destdir', default='')
for key in KEYS:
parser.add_argument('--' + key)
def layout(args):
prefix = directory(args.prefix)
defaults = dict(bindir=prefix / 'bin', libdir=prefix / 'lib', includedir=prefix / 'include',
docdir=prefix / 'share/doc/xtc', licensedir=prefix / 'share/licenses/xtc',
examplesdir=prefix / 'share/xtc/examples', desktopdir=prefix / 'share/applications',
pkgconfigdir=prefix / 'lib/pkgconfig')
return {key: directory(getattr(args, key) or value) for key, value in defaults.items()}
def directory(value):
text = str(value)
path = Path(text)
if not path.is_absolute() or '..' in path.parts or any(c in text for c in '\n\r\0"$\\'):
raise ValueError('Install directories must be absolute without .., controls, quotes, $, or backslashes')
return path
def inventory(source, paths):
entries = [('libdir', 'libxtc.a', None), ('pkgconfigdir', 'xtc.pc', None),
('bindir', 'xtc-check', None), ('bindir', 'xtc', None),
('desktopdir', 'xtc.desktop', source / 'packaging/xtc.desktop'),
('licensedir', 'LICENSE', source / 'LICENSE'),
('docdir', 'README.md', source / 'README.md'),
('docdir', 'BINARY_MANIFEST.json', None)]
entries += [('includedir', 'xtc/' + p.name, p) for p in sorted((source / 'include/xtc').glob('*.h'))]
entries += [('docdir', p.name, p) for p in sorted((source / 'docs').glob('*.md'))]
entries += [('docdir', p.name, p) for p in sorted((source / 'docs').glob('*performance*.json'))]
entries += [('docdir', 'decisions/' + p.name, p) for p in sorted((source / 'docs/decisions').glob('*.md'))]
entries += [('examplesdir', p.name, p) for p in sorted((source / 'examples/projects').glob('*')) if p.is_file()]
result = [(paths[key] / name, original) for key, name, original in entries]
if len({path for path, _ in result}) != len(result):
raise ValueError('Install layout has overlapping file destinations')
return result
def staged(path, destdir):
if not destdir:
return path
root = directory(destdir).resolve()
result = root / path.relative_to('/')
if not result.parent.resolve().is_relative_to(root):
raise ValueError(f'Install directory symlink escapes DESTDIR: {result}')
return result

View file

@ -3,14 +3,23 @@
import argparse
from pathlib import Path
from dist import version
from install_layout import directory
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--prefix', required=True)
parser.add_argument('--exec-prefix')
parser.add_argument('--libdir')
parser.add_argument('--includedir')
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)
prefix = directory(args.prefix)
exec_prefix = directory(args.exec_prefix or prefix)
libdir = directory(args.libdir or exec_prefix / 'lib')
includedir = directory(args.includedir or prefix / 'include')
def reference(path):
return '${prefix}/' + str(path.relative_to(prefix)) if path != prefix and prefix in path.parents else str(path)
text = Path('packaging/xtc.pc.in').read_text().replace('@PREFIX@', str(prefix))
text = text.replace('@EXEC_PREFIX@', reference(exec_prefix)).replace('@LIBDIR@', reference(libdir)).replace('@INCLUDEDIR@', reference(includedir))
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)

View file

@ -1,26 +1,19 @@
#!/usr/bin/env python3
"""Remove only this source version's installed files; never recurse directories."""
"""Remove only this source version's inventory; never recurse directories."""
import argparse
from pathlib import Path
import stat
from install_layout import arguments, inventory, layout, staged
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', 'share/doc/xtc/performance-baseline.json',
'share/doc/xtc/BINARY_MANIFEST.json']
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.
def main():
parser = argparse.ArgumentParser(description=__doc__)
arguments(parser)
parser.add_argument('--root', help='legacy alias for --prefix')
args = parser.parse_args()
if args.root:
args.prefix = args.root
files = [staged(path, args.destdir) for path, _ in inventory(Path(__file__).resolve().parent.parent, layout(args))]
for path in files:
try:
mode = path.lstat().st_mode
@ -30,16 +23,14 @@ def uninstall(root, source):
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}
# Only try the immediate owned directories, never walk up to / or DESTDIR.
parents = {p.parent for p in files}
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.
except OSError:
pass
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)
main()