Phase 30: complete configurable installation and release qualification
This commit is contained in:
parent
afc049e2c8
commit
90eac33a71
18 changed files with 399 additions and 59 deletions
33
Makefile
33
Makefile
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
53
docs/RELEASE_NOTES.md
Normal 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.
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
7
examples/projects/README.md
Normal file
7
examples/projects/README.md
Normal 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.
|
||||
41
examples/projects/midi-demo.xtc
Normal file
41
examples/projects/midi-demo.xtc
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
38
tools/install.py
Normal 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
55
tools/install_layout.py
Normal 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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue