1
0
Fork 0
forked from hanez/xtc

Phase 20: package Linux builds and document release and maintenance workflows

This commit is contained in:
Johannes Findeisen 2026-10-27 06:44:22 +01:00
commit 830b6e1914
22 changed files with 490 additions and 17 deletions

View file

@ -22,6 +22,7 @@ JACK_CFLAGS := $(shell $(PKG_CONFIG) --cflags jack)
JACK_LIBS := $(shell $(PKG_CONFIG) --libs jack)
BACKEND_TEST := $(BUILD_DIR)/test_backend
$(BUILD_DIR)/tests/unit/test_backend.o: CPPFLAGS += $(JACK_CFLAGS)
$(BUILD_DIR)/src/core/xtc.o: CPPFLAGS += -DXTC_HAVE_JACK
$(BUILD_DIR)/src/backend/jack_backend.o: CPPFLAGS += -DXTC_HAVE_JACK $(JACK_CFLAGS)
endif
CORE_OBJECTS := $(CORE_SOURCES:%.c=$(BUILD_DIR)/%.o)
@ -181,7 +182,7 @@ install: all $(BUILD_DIR)/xtc.pc
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 "$(DESTDIR)$(PREFIX)/share/doc/xtc/"
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/"
@ -366,3 +367,11 @@ BENCHMARK_MAX_REGRESSION ?= 1.5
.PHONY: benchmark-compare
benchmark-compare: $(BUILD_DIR)/benchmark_graph
$(PYTHON) tools/benchmark.py --binary "$(BUILD_DIR)/benchmark_graph" --output "$(BUILD_DIR)/performance.json" --compiler "$(CC)" --flags "$(CFLAGS)" --baseline "$(BENCHMARK_BASELINE)" --max-regression "$(BENCHMARK_MAX_REGRESSION)"
PACKAGE_ARCH := $(shell uname -m)
PACKAGE_FILE := $(BUILD_DIR)/xtc-$(VERSION)-linux-$(PACKAGE_ARCH).tar.gz
.PHONY: package test-package
package: release
$(PYTHON) tools/package.py --build-dir "$(BUILD_DIR)/release" --output "$(PACKAGE_FILE)" $(if $(GUI_AVAILABLE),--gui)
test-package: package
$(PYTHON) tests/integration/test_package.py "$(PACKAGE_FILE)" --build-dir "$(BUILD_DIR)/release"

View file

@ -121,3 +121,10 @@ Phases 13–15 add no-clobber recording/recovery, parameterized native C process
and trusted in-process LADSPA hosting with persisted identity/control values.
See [RECOVERY.md](docs/RECOVERY.md), [NATIVE_API.md](docs/NATIVE_API.md) and
[PLUGIN_HOSTING.md](docs/PLUGIN_HOSTING.md) for commands and limits.
For everyday use see [USER_MANUAL.md](docs/USER_MANUAL.md),
[KNOWN_LIMITATIONS.md](docs/KNOWN_LIMITATIONS.md) and
[MAINTENANCE.md](docs/MAINTENANCE.md). `xtc-check --diagnose` reports build features;
`make test-package` checks a local system-linked Linux binary archive.
Phases 16–20 add effect automation/controllers, editing commands, v7 project-folder
media paths, performance baselines and release/installation checks.

35
docs/KNOWN_LIMITATIONS.md Normal file
View file

@ -0,0 +1,35 @@
# Known limitations and qualification boundaries
- Development version 0.1.0, Linux C99/C++17/FLTK, optional JACK. Physical-device
playback/capture, human transition listening, independent user acceptance and
long-duration device qualification are pending.
- Fixed 4/4 and constant tempo snapshots; no tempo map, loop transport, plugin
latency compensation, limiter, background streaming or multichannel mixer buses.
- Mono capture/import downmix; stereo track output. Graph ports are mono with
independent paired processing. Maximum 64 project graph nodes (256 standalone),
64 tracks, 1024 clips/automation points, 4096 scheduled notes, 256 live MIDI
events/block, 32 voices, 65536 frames/block. See API headers for exact bounds.
- WAV samples preload with the memory/format limits in AUDIO_FILES.md. Callback
overflow reports/silences; there is no CPU preemption or expensive-plugin rescue.
- Live MIDI fixed volume/pan/sustain mapping; no controller learn, capture, SMF
import, pitch bend, custom-instrument chooser or instrument timbre controls.
- Native ABI 1 graph nodes are session-only. ABI 2 state is parameter-only. LADSPA
supports the documented mono amp/inverter/sine set; no opaque state, native
editor, MIDI, true multichannel layout, latency compensation or crash isolation.
Automated LADSPA controls use run(1) and can be expensive.
- GUI loading/compilation is synchronous on the control thread. Timeline grid/
zoom/selection and graph positions are session UI state. Undo retains 32 edits
plus baseline; allocation failures are reported, not silently hidden. Media is
never removed by undo/delete. Broad screen-reader/accessibility testing is pending.
- Format v7 reads v1–v6 and uses relative local media references. External absolute
media/module paths need manual repair after relocation. Saving does not bundle
media; missing media stays referenced, missing plugins reject GUI installation.
Unknown data is rejected rather than stripped. Post-rename directory-sync errors
can report failure even though the complete new document was published.
- No autosave/session crash restoration, RF64, system service, automatic dependency
installation or public signed release. Source/binary archives are local artifacts;
binary packages require compatible system libraries. No third-party plugin or
dependency binary is bundled. Power-cut durability depends on the filesystem.
See ROADMAP.md, AUDIO_QUALITY.md, CONTROL.md, WORKFLOW.md, PROJECT_FORMAT.md,
PERFORMANCE.md, PLUGIN_HOSTING.md and RECOVERY.md for tested contracts/evidence.

View file

@ -56,3 +56,29 @@ Static backend consumers need JACK if that optional backend was built; the pc
file records its private dependency. Public headers contain no JACK/FLTK types.
To build without optional libraries, set PKG_CONFIG=false FLTK_CONFIG=false.
Python is not needed to run the installed executables or library.
## Phase 20 binary/development archive
`make package` creates a system-linked Linux archive plus SHA-256 sidecar below
BUILD_DIR; `make test-package` verifies its contents. It includes usr/bin,
headers/static library/pkg-config metadata, desktop file (when FLTK is present),
manuals, baseline and MIT license. No shared library, third-party plugin, media,
service or dependency installer is bundled. `BINARY_MANIFEST.json` lists file
hashes/sizes, architecture/libc and ELF NEEDED libraries. The entire archive's hash
covers the manifest too. Do not assume compatibility with another distribution.
Inspect/test under a temporary directory first. For a writable user prefix,
extract the archive with two leading path components stripped (`xtc-VERSION-linux-
ARCH/usr`), placing bin/include/lib/share directly under that prefix. Put its bin
on PATH and lib/pkgconfig on PKG_CONFIG_PATH. The archive's pkg-config prefix is
relative to its installed location; normal `make install` retains explicit PREFIX.
Use the matching source's uninstall with that PREFIX, or review the manifest
before removing its files. Unknown/unrelated files are retained by source uninstall.
This local tar package is not a pacman/RPM/deb registration or public distribution.
`xtc-check --diagnose` reports build capabilities and format/engine limits without
opening a server or executing a plugin. `xtc --diagnose` works headlessly as well.
Missing runtime GUI libraries are loader errors: install the distribution's
compatible dependencies or rebuild the core-only configuration. Build without
optional libraries using a fresh directory, `PKG_CONFIG=false FLTK_CONFIG=false
CPPFLAGS=-DXTC_DISABLE_LADSPA`. No system setting or package is changed by tests.

55
docs/MAINTENANCE.md Normal file
View file

@ -0,0 +1,55 @@
# Release and maintenance policy
## Supported development target
Initial target: x86_64 Artix/Arch-family Linux with glibc, GNU Make, GCC/Clang,
FLTK 1.4 and a JACK-compatible client/server. The inspected host uses GCC 16.2.1,
FLTK 1.4.5 and the dependencies in LICENSES.md. Core-only/no-JACK/no-LADSPA builds
are checked too. Build on the destination distribution for different library ABIs;
there is no cross-distribution static-binary promise. Python 3.12+ and binutils
are build/package/test tools only. No dependency is fetched automatically.
## Release checklist
1. Work from a clean reviewed commit. Record `git rev-parse HEAD`, compiler,
libc/FLTK/JACK versions and `xtc-check --diagnose`. No automatic version bump,
Git tag, public upload or external release is performed by local targets.
2. Run warning-enabled `make all test gui test-plugins test-plugins-real
test-gui-plugins test-workflow test-gui-jack test-jack-midi test-recording
test-recovery test-reliability test-scale analyze` where dependencies exist.
3. Use a separate BUILD_DIR for ASan/UBSan/leak runs and another for optional-
dependency builds. GUI leak checks use only the documented fontconfig
suppression; core tests use none. Run publication/recording race tests as
described in RELIABILITY.md. Sanitizers are not latency qualification.
4. Run `make benchmark test-performance test-stress`; compare on matching
host/compiler/flags against a reviewed baseline. Investigate regressions;
do not weaken thresholds or replace evidence without explanation.
5. Run `make test-install test-release test-package`. Inspect archive manifests,
hashes, dynamic dependency names and licenses. Source exports rebuild twice
with identical artifacts on this host. Repeat binary packaging compares bytes.
6. Complete physical listening, recording/device/endurance, and independent human
install/workflow checks before claiming production qualification. They remain
pending for this prototype, even when all automated checks pass.
7. Save checksums/test logs/conditions with the proposed release. Review known
limitations and format compatibility and update user/developer docs. Publishing
or distro repository submission is a separate action.
## Regressions and compatibility
Report the exact version/commit, OS/toolchain, compiled features, reproduction,
expected/actual output, GUI errors/xruns and whether plugins are involved. Keep a
minimal project/media fixture when redistribution is permitted; do not attach
private recordings or full memory dumps inadvertently. No telemetry/upload occurs.
A confirmed defect gets a focused deterministic regression or repeatable runtime
check before its fix. Memory/thread/file-loss defects take priority; disable the
unsafe path rather than silently dropping media or unknown project records. Keep
candidate preparation/plan publication transactional and callbacks bounded.
Format changes increment XTC_PROJECT_FORMAT_VERSION, retain explicit bounded
migrations/fixtures for all supported older versions, and add malformed/roundtrip
checks. Unknown versions/types/records fail with actionable errors. Never rewrite
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.

View file

@ -154,7 +154,8 @@ discrete-mode compatibility are checked on control-thread compilation. Versions
Current writer emits v7; readers migrate v1–v6. Record ordering and field bounds
are otherwise unchanged from v6. v7 ASSET may contain a relative path, interpreted
against the actual containing directory of the named project file (resolved on
the control thread), never the launch working directory. After loading, all core
the control thread), never the launch working directory. Opening a symlink to a project uses the
resolved target project directory; Save replaces the named entry as before. After loading, all core
asset references are absolute, including missing media. Media inside the save
folder are written relative; references outside remain absolute. Save As resolves
against its new folder and preserves media identity rather than rebasing it by

View file

@ -54,3 +54,21 @@ semantics. Physical devices, power loss, sustained recording/performance, user
acceptance on other distributions and binary redistribution are still separate
qualification work. There is no DEB/RPM/AppImage, bundled runtime, autosave,
external plugin host or stable shared-library extension ABI in this release.
## Phase 20 local binary packaging and installed workflow
`make test-package` builds the release, stages installation only in a temporary
DESTDIR, writes a normalized Linux binary/development archive, checks its SHA-256
sidecar and per-file manifest, relocatable pkg-config paths and headless commands.
Same-host repeated packaging is byte-identical; dependency versions and toolchain
still determine binary compatibility. `make test-install` now repeats installation
to verify repair/upgrade of owned files while preserving an unrelated sentinel,
then compiles both C99 and C++17 installed consumers that create MIDI, save/reopen
and compare nonzero exact rendering. Removal remains exact and idempotent.
USER_MANUAL.md consolidates operation; KNOWN_LIMITATIONS.md lists qualification
gaps; MAINTENANCE.md defines the release checklist, regression and compatibility
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.

View file

@ -448,6 +448,16 @@ policy. Arbitrary plugin costs and physical realtime/endurance are unqualified.
- Dependencies, licensing, limitations, and troubleshooting steps are documented.
- A release can be reproduced from a clean checkout with recorded build instructions.
Implemented: normalized Linux binary/development archives with SHA-256/file/
ELF-dependency manifests, headless compiled-feature diagnostics, repeat-install
upgrade/repair and installed C/C++ save/reopen/render checks. Existing deterministic
source/release, no-dependency, sanitizer, staged uninstall and GUI/live tests are
retained. USER_MANUAL.md, KNOWN_LIMITATIONS.md and MAINTENANCE.md consolidate setup,
operation, licensing/diagnostics, release and regression policy. No public release
or distro registration is performed. Independent human installation/usability,
physical-device/endurance and listening acceptance remain pending and must be
completed before claiming production qualification.
---
## Recommended Execution Order

98
docs/USER_MANUAL.md Normal file
View file

@ -0,0 +1,98 @@
# xtc user manual (0.1.0 development build)
xtc is a bounded Linux music-making prototype. Start with a copy of an important
project and keep backups of its WAV media. Supported limits and unfinished areas
are listed in KNOWN_LIMITATIONS.md; no production/device qualification is implied.
## Build or install
Follow BUILD.md and LINUX.md. Core: `make all test`; GUI: `make gui`, then
`./build/xtc`. Use an existing JACK-compatible server and reconnect with F6.
Connect `midi_in` to a controller, `input` to mono capture and `output` /
`output_right` to left/right playback using your JACK routing application.
No server, physical routing or system configuration is changed automatically.
`xtc --version`, `xtc --help` and `xtc --diagnose` need no display/server.
For a source install stage with `make install-gui DESTDIR="$PWD/build/stage"
PREFIX=/usr`, inspect the stage, and use your normal deployment process. A user
prefix is supported (for example an absolute path to your `.local` folder).
`make uninstall` with the same DESTDIR/PREFIX removes only that version's known
files. See LINUX.md for package extraction, dependencies and removal. Never use
a release built for a different system ABI without rebuilding/testing it.
## Make a short project
1. New/Open starts in Sequencer. Add track; use F2 to name it.
2. Add MIDI clip, select it and open Piano roll (Ctrl+4). Left-click to draw
notes, drag to move, drag a right edge to resize, Delete/right-click to remove.
3. Alternatively Import WAV (Ctrl+I). It creates/routes an Audio clips source;
supported WAV formats and preload limits are in AUDIO_FILES.md.
4. Drag clips to arrange them and edges to trim. View/Snap sets the timeline
grid; +/- zoom buttons and scrollbars navigate. Ctrl+D duplicates a clip after
its end, retaining its complete source. F2 names clips; right-click offers
duplicate/rename/delete. Alt+Up/Down with timeline focus reorders tracks.
5. Play/F5 starts; Pause/F7 or Stop/F8 leaves the position for resuming. Home
returns to start while stopped (or stops first); click the ruler to seek.
Recording uses Stop, not Pause. F9 Panic releases the instrument.
6. Mixer (Ctrl+2) offers track gain/pan/mute/solo and Gain/Pan automation lanes.
Click to add points, right-click to remove. CONTROL.md explains timing and
live CC7 volume, CC10 pan and CC64 sustain.
7. Save (Ctrl+S), close, reopen and continue editing. Ctrl+Z undoes and
Ctrl+Shift+Z/Ctrl+Y redoes. New/Open resets history after unsaved-change handling.
Modular Graph (Ctrl+3) exposes routing. Drag output to input to connect, select a
wire/node and Delete to remove; cycles and invalid ports are rejected. Keep an
Output route for playback. GUI changes show queued/active graph status. Graph
positions are session-only; music/routing/automation are saved. Edits that prepare
new plans stop playback; disk/compilation may pause the GUI, never run in audio.
## Record and retain media
Choose a project folder and save the project there first. Record/Shift+F5 asks for
an unused WAV path. Stop finalizes the take and inserts it. Existing destinations
are protected, including a file created while recording. Undo/removing a take
removes its reference, never the recorded file. Imported external files are also
never deleted. WAVs inside the save folder become relative references; move the
folder and media together. Save As preserves the same source media, not a copy.
On write failure the GUI reports a retained path. Keep it and recover complete
frames to a different, unused destination:
```sh
xtc-check --recover-recording take.wav.tmp.XXXXXX recovered.wav
```
Only xtc's canonical float32 mono temporary format is salvaged. Incomplete trailing
frames are omitted. The source is retained. Import/relink the recovered WAV.
Missing/unreadable media is silent and reported; select the clip and use File/
Relink selected audio. See RECOVERY.md for interruption, disk-full and power-loss
limits. Close/project operations are refused while recording; finish the take first.
## Trusted effects and sources
File/Load LADSPA plugin lists compatible descriptor indices from the chosen `.so`.
Loading executes native code. Select a documented compatible mono plugin, connect
its graph ports, then use Edit/Selected plugin control. Edit/Selected plugin
automation adds/removes tick-based linear or step points. Control names/ranges
appear in the dialogs. Module identity/control values/lanes save with the project;
module binaries and DSP history do not. The initial amp/inverter/sine set and
unsupported layouts are listed in PLUGIN_HOSTING.md. Missing plugins reject GUI
Open with identity/path information and preserve the saved/current documents.
Restore the compatible module at that path and retry. Plugins are not sandboxed.
## Diagnose a problem
Run `xtc-check --diagnose` and capture the version, architecture, compiled features,
GUI error text and steps to reproduce. `xtc-check --validate song.xtc` checks
metadata without opening plugins/media; it does not certify playback resources.
Check JACK routing, sample rate/format and xrun/process status in the GUI. After
server loss/rate change stop/reconnect and retry. Keep the original project,
media and retained temporary files before recovery attempts.
A native plugin can crash/hang the process. There is no crash-isolation guarantee.
Use the operating system's core-dump/debugger tools if available; xtc does not
install signal handlers, change core limits or upload crash reports. Debugger/
core files may contain media paths and audio memory; share only what you intend.
Report bugs following MAINTENANCE.md, including whether the issue reproduces
without plugins. Physical listening and independent human install/usability
acceptance remain pending.

View file

@ -9,6 +9,10 @@ extern "C" {
#endif
typedef struct xtc_app xtc_app;
#define XTC_FEATURE_JACK 1U
#define XTC_FEATURE_LADSPA 2U
/* Compile-time capabilities, no loading/server connection or I/O. */
unsigned int xtc_build_features(void);
/* Creates an application context. On success, *out_app is caller-owned. */
/**

View file

@ -7,6 +7,7 @@
extern "C" {
#endif
#define XTC_PROJECT_FORMAT_VERSION 7U
#define XTC_PROJECT_MAX_TRACKS 64U
#define XTC_PROJECT_MAX_CLIPS 1024U
#define XTC_PROJECT_NAME_BYTES 64U

View file

@ -1,4 +1,5 @@
#include "xtc/xtc.h"
#include "../plugin/config.h"
#include <stdlib.h>
@ -73,3 +74,15 @@ void xtc_app_destroy(xtc_app *app)
{
free(app);
}
unsigned int xtc_build_features(void)
{
unsigned int features = 0;
#ifdef XTC_HAVE_JACK
features |= XTC_FEATURE_JACK;
#endif
#ifdef XTC_HAVE_LADSPA
features |= XTC_FEATURE_LADSPA;
#endif
return features;
}

View file

@ -4,6 +4,7 @@
#include <exception>
#include "main_window.hpp"
#include "xtc/xtc_version.h"
#include "xtc/xtc.h"
#include <cstring>
/**
@ -19,9 +20,16 @@ int main(int argc, char **argv)
std::printf("%s %s\n", XTC_PROJECT_NAME, XTC_VERSION_STRING);
return EXIT_SUCCESS;
}
if (argc == 2 && !std::strcmp(argv[1], "--diagnose")) {
unsigned int features = xtc_build_features();
std::printf("xtc %s; project formats 1..%u; JACK %s; LADSPA %s\nFor limits and recovery commands run xtc-check --diagnose/--help.\n",
XTC_VERSION_STRING,XTC_PROJECT_FORMAT_VERSION,features & XTC_FEATURE_JACK ? "compiled" : "disabled",
features & XTC_FEATURE_LADSPA ? "compiled" : "disabled");
return EXIT_SUCCESS;
}
if (argc == 2 && !std::strcmp(argv[1], "--help")) {
std::puts(
"Usage: xtc [PROJECT] or xtc [FLTK options]\n--version and --help work without a display or audio server.");
"Usage: xtc [PROJECT] or xtc [FLTK options]\n--version, --diagnose and --help work without a display or audio server.");
return EXIT_SUCCESS;
}
const char *project_path = nullptr;

View file

@ -6,6 +6,8 @@
#include <string.h>
#include "xtc/xtc_project.h"
#include "xtc/xtc_audio.h"
#include "xtc/xtc_midi.h"
#include <limits.h>
/**
* @brief Runs the xtc-check command-line entry point.
@ -25,8 +27,18 @@ int main(int argc, char **argv)
printf("%s %s\n", XTC_PROJECT_NAME, XTC_VERSION_STRING);
return EXIT_SUCCESS;
}
if (argc == 2 && !strcmp(argv[1], "--diagnose")) {
unsigned int features = xtc_build_features();
printf("xtc %s; %zu-bit process\nProject formats: 1..%u\nJACK compiled: %s\nLADSPA compiled: %s\n"
"Limits: 256 graph nodes (64/project), %u tracks, %u clips, %u notes, 65536 frames/block\n"
"No server, device or plugin probed. Runtime errors appear in GUI status/stderr.\n",
XTC_VERSION_STRING,sizeof(void *)*CHAR_BIT,XTC_PROJECT_FORMAT_VERSION,
features & XTC_FEATURE_JACK ? "yes" : "no",features & XTC_FEATURE_LADSPA ? "yes" : "no",
XTC_PROJECT_MAX_TRACKS,XTC_PROJECT_MAX_CLIPS,XTC_PROJECT_MAX_NOTES);
return EXIT_SUCCESS;
}
if (argc == 2 && !strcmp(argv[1], "--help")) {
puts("Usage: xtc-check [--version | --help | --validate PROJECT | --recover-recording TEMP NEW.wav]\nNo arguments checks core startup/shutdown. "
puts("Usage: xtc-check [--version | --help | --diagnose | --validate PROJECT | --recover-recording TEMP NEW.wav]\nNo arguments checks core startup/shutdown. "
"Validation checks metadata only.");
return EXIT_SUCCESS;
}
@ -54,7 +66,7 @@ int main(int argc, char **argv)
return EXIT_SUCCESS;
}
if (argc != 1) {
fputs("Usage: xtc-check [--version | --help | --validate PROJECT | --recover-recording TEMP NEW.wav]\n", stderr);
fputs("Usage: xtc-check [--version | --help | --diagnose | --validate PROJECT | --recover-recording TEMP NEW.wav]\n", stderr);
return EXIT_FAILURE;
}

View file

@ -193,7 +193,7 @@ static xtc_result read_project(reader *input, xtc_project *project, const char *
xtc_graph *graph = project->graph;
READ("XTC_PROJECT", 2U);
NUM(1U, version);
if (version < 1U || version > 7U)
if (version < 1U || version > XTC_PROJECT_FORMAT_VERSION)
return XTC_ERR_UNSUPPORTED;
format_version = version;
READ("APP", 4U);
@ -448,8 +448,8 @@ static xtc_result write_project(FILE *file, const xtc_project *project, const ch
return XTC_ERR_UNSUPPORTED;
if (sizeof(float) != sizeof(uint32_t) || FLT_RADIX != 2 || FLT_MANT_DIG != 24 || FLT_MAX_EXP != 128)
return XTC_ERR_UNSUPPORTED;
if (fprintf(file, "XTC_PROJECT 7\nAPP %d %d %d\nTEMPO %u\nIDS %" PRIu64 " %" PRIu64 "\nGRAPH %d %zu %zu %zu\n",
XTC_VERSION_MAJOR, XTC_VERSION_MINOR, XTC_VERSION_PATCH, project->tempo, project->next_track_id,
if (fprintf(file, "XTC_PROJECT %u\nAPP %d %d %d\nTEMPO %u\nIDS %" PRIu64 " %" PRIu64 "\nGRAPH %d %zu %zu %zu\n",
XTC_PROJECT_FORMAT_VERSION, XTC_VERSION_MAJOR, XTC_VERSION_MINOR, XTC_VERSION_PATCH, project->tempo, project->next_track_id,
project->next_clip_id, graph->require_output_nodes, graph->next_id, graph->node_count,
graph->edge_count) < 0)
return XTC_ERR_IO;
@ -582,7 +582,11 @@ xtc_result xtc_project_load(xtc_project *project, const char *path)
input.file = fopen(path, "rb");
if (input.file == NULL)
return XTC_ERR_IO;
directory = project_folder(path);
{
char *canonical = realpath(path,NULL);
directory = canonical ? project_folder(canonical) : NULL;
free(canonical);
}
if (!directory) { fclose(input.file); return XTC_ERR_IO; }
result = xtc_project_create(&candidate);
if (result == XTC_OK)

8
src/plugin/config.h Normal file
View file

@ -0,0 +1,8 @@
#ifndef XTC_PLUGIN_CONFIG_H
#define XTC_PLUGIN_CONFIG_H
#ifdef __has_include
#if __has_include(<ladspa.h>) && !defined(XTC_DISABLE_LADSPA)
#define XTC_HAVE_LADSPA 1
#endif
#endif
#endif

View file

@ -8,11 +8,7 @@
#include <stdio.h>
/* Header-only optional dependency; absent development header gives explicit
* unsupported stubs, preserving metadata-only load/save of plugin documents. */
#ifdef __has_include
#if __has_include(<ladspa.h>) && !defined(XTC_DISABLE_LADSPA)
#define XTC_HAVE_LADSPA 1
#endif
#endif
#include "config.h"
static int terminated(const char *s, size_t n) { return memchr(s, 0, n) != NULL; }
xtc_result xtc_plugin_spec_validate(const xtc_plugin_spec *s)
{

View file

@ -36,6 +36,13 @@ def main():
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 'Project formats: 1..7' in diagnose and 'JACK compiled:' in diagnose
# Upgrade/repair repeats the same owned-file install and keeps unrelated data.
(root / 'include/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 sentinel.read_text() == 'keep\n'
version = subprocess.check_output([str(root / 'bin/xtc-check'), '--version'], text=True)
assert version.startswith('xtc ')
valid = stage / 'valid.xtc'
@ -54,12 +61,29 @@ def main():
flags = shlex.split(subprocess.check_output(pkg + ['--cflags', '--libs', '--static', 'xtc'], env=pkg_env, text=True))
includes = ''.join(f'#include "xtc/{p.name}"\n' for p in sorted((source / 'include/xtc').glob('*.h')))
consumer = stage / 'consumer.c'
consumer.write_text(includes + '\nint main(void) { xtc_project *p = 0; xtc_result r = xtc_project_create(&p); xtc_project_destroy(p); return r != XTC_OK; }\n')
consumer.write_text(includes + r'''
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv) {
xtc_project *p = 0, *q = 0; xtc_midi_plan *a = 0, *b = 0;
uint64_t track, clip, note; float before[127], after[127]; int i, nonzero = 0;
if (argc != 2 || xtc_project_create(&p) != XTC_OK || xtc_project_create(&q) != XTC_OK) return 1;
if (xtc_project_add_track(p,"Installed",&track) != XTC_OK ||
xtc_project_add_midi_clip(p,track,0,960,&clip) != XTC_OK ||
xtc_project_add_note(p,clip,0,960,69,127,0,&note) != XTC_OK ||
xtc_project_save(p,argv[1]) != XTC_OK || xtc_project_load(q,argv[1]) != XTC_OK ||
xtc_midi_plan_create(p,48000,&a) != XTC_OK || xtc_midi_plan_create(q,48000,&b) != XTC_OK ||
xtc_midi_plan_render(a,0,0,0,before,127) != XTC_OK || xtc_midi_plan_render(b,0,0,0,after,127) != XTC_OK) return 2;
for (i = 0; i < 127; ++i) { if (before[i] != after[i]) return 3; if (before[i] != 0) nonzero = 1; }
xtc_midi_plan_destroy(a); xtc_midi_plan_destroy(b); xtc_project_destroy(p); xtc_project_destroy(q);
remove(argv[1]); return !nonzero;
}
''')
for compiler, standard in [('CC', 'c99'), ('CXX', 'c++17')]:
output = stage / ('consumer-' + compiler)
command = shlex.split(env.get(compiler, 'cc' if compiler == 'CC' else 'c++'))
run(command + [f'-std={standard}', '-Wall', '-Wextra', '-Werror', str(consumer)] + flags + ['-o', str(output)])
run([str(output)])
run([str(output), str(stage / ("installed-" + compiler + ".xtc"))])
if args.gui:
run(make + ['install-gui'] + variables, env=env)
gui_env = dict(env, DISPLAY='', WAYLAND_DISPLAY='')

View file

@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""Verify every packaged file, relocatable metadata and headless executables."""
import argparse
import hashlib
import json
import os
from pathlib import Path
import subprocess
import tarfile
import sys
import tempfile
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('archive', type=Path)
p.add_argument('--build-dir', type=Path)
a = p.parse_args()
expected = a.archive.with_name(a.archive.name + '.sha256').read_text().split()[0]
assert hashlib.sha256(a.archive.read_bytes()).hexdigest() == expected
with tempfile.TemporaryDirectory(prefix='xtc-package-test-') as temporary:
root = Path(temporary)
with tarfile.open(a.archive) as tar:
members = tar.getmembers()
assert members and all(m.isfile() for m in members)
assert len({m.name for m in members}) == len(members)
assert all(not Path(m.name).is_absolute() and '..' not in Path(m.name).parts for m in members)
tar.extractall(root, filter='data')
prefix = root / members[0].name.split('/')[0] / 'usr'
manifest_path = prefix / 'share/doc/xtc/BINARY_MANIFEST.json'
manifest = json.loads(manifest_path.read_text())
assert manifest['schema'] == 1 and manifest['needed']
expected_files = {f['path'] for f in manifest['files']} | {'usr/share/doc/xtc/BINARY_MANIFEST.json'}
assert expected_files == {str(p.relative_to(prefix.parent)) for p in prefix.rglob('*') if p.is_file()}
for f in manifest['files']:
content = (prefix.parent / f['path']).read_bytes()
assert len(content) == f['size'] and hashlib.sha256(content).hexdigest() == f['sha256']
env = dict(os.environ, DISPLAY='', WAYLAND_DISPLAY='', PKG_CONFIG_PATH=str(prefix / 'lib/pkgconfig'))
for name in ('xtc-check', 'xtc'):
binary = prefix / 'bin' / name
if binary.exists():
for option in ('--version', '--help', '--diagnose'):
subprocess.run([str(binary), option], env=env, check=True)
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()
if a.build_dir:
again = root / 'repacked.tar.gz'
command = [sys.executable, 'tools/package.py', '--build-dir', str(a.build_dir), '--output', str(again)]
if (prefix / 'bin/xtc').exists():
command.append('--gui')
result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
assert result.returncode == 0, result.stdout
assert hashlib.sha256(again.read_bytes()).hexdigest() == expected, 'Binary repack differs'
print('Binary archive hashes, manifest, relocation, headless diagnostics and documentation passed.')
if __name__ == '__main__':
main()

View file

@ -58,6 +58,14 @@ int main(void)
assert(xtc_project_find_clip(p,clip,&c) == XTC_OK && !strcmp(c.asset_path,media));
unlink(copy); rmdir(sub);
}
{
char alias_dir[512], alias[512]; xtc_clip_info c;
snprintf(alias_dir,sizeof(alias_dir),"%s/aliases",root); assert(mkdir(alias_dir,0700) == 0);
snprintf(alias,sizeof(alias),"%s/aliases/link.xtc",root); assert(symlink(file,alias) == 0);
assert(xtc_project_load(p,alias) == XTC_OK);
assert(xtc_project_find_clip(p,clip,&c) == XTC_OK && !strcmp(c.asset_path,media));
unlink(alias); rmdir(alias_dir);
}
snprintf(moved,sizeof(moved),"%s-moved",root); assert(rename(root,moved) == 0);
snprintf(file,sizeof(file),"%s-moved/migration.xtc",root);
assert(xtc_project_load(p,file) == XTC_OK);

76
tools/package.py Normal file
View file

@ -0,0 +1,76 @@
#!/usr/bin/env python3
"""Stage a Linux binary/development archive without modifying a live prefix."""
import argparse
import gzip
import hashlib
import io
import json
import os
from pathlib import Path
import platform
import re
import shlex
import subprocess
import tarfile
import tempfile
from dist import version
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--build-dir', type=Path, required=True)
p.add_argument('--output', type=Path, required=True)
p.add_argument('--gui', action='store_true')
a = p.parse_args()
epoch = int(os.environ.get('SOURCE_DATE_EPOCH', '0'))
if not 0 <= epoch <= 4294967295:
p.error('SOURCE_DATE_EPOCH must fit uint32')
name = f'xtc-{version(Path.cwd())}-linux-{platform.machine()}'
a.output.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory(prefix='xtc-binary-stage-') as temporary:
stage = Path(temporary)
env = dict(os.environ, MAKEFLAGS='', MFLAGS='')
subprocess.run(shlex.split(env.get('MAKE', 'make')) + ['install-gui' if a.gui else 'install',
f'BUILD_DIR={a.build_dir.resolve()}', f'DESTDIR={stage}', 'PREFIX=/usr'], env=env, check=True)
pc = stage / 'usr/lib/pkgconfig/xtc.pc'
pc.write_text(pc.read_text().replace('prefix=/usr\n', 'prefix=${pcfiledir}/../..\n'))
entries = []
needed = {}
for path in sorted(stage.rglob('*')):
if not path.is_file():
continue
assert not path.is_symlink()
relative = path.relative_to(stage).as_posix()
entries.append(dict(path=relative, size=path.stat().st_size,
sha256=hashlib.sha256(path.read_bytes()).hexdigest()))
if relative in ('usr/bin/xtc', 'usr/bin/xtc-check'):
dynamic = subprocess.check_output(['readelf', '-d', str(path)], text=True)
needed[relative] = re.findall(r'\(NEEDED\).*\[(.*?)\]', dynamic)
manifest = dict(schema=1, version=version(Path.cwd()), architecture=platform.machine(),
libc=list(platform.libc_ver()), needed=needed, files=entries,
scope='System-linked Linux build; no libraries, plugins or media bundled.')
(stage / 'usr/share/doc/xtc/BINARY_MANIFEST.json').write_text(json.dumps(manifest, indent=2) + '\n')
fd, pending = tempfile.mkstemp(prefix='.xtc-package-', dir=a.output.parent)
try:
with os.fdopen(fd, 'wb') as raw, gzip.GzipFile(fileobj=raw, filename='', mode='wb', mtime=epoch) as gz:
with tarfile.open(fileobj=gz, mode='w', format=tarfile.PAX_FORMAT) as tar:
for path in sorted(stage.rglob('*')):
if not path.is_file():
continue
data = path.read_bytes()
info = tarfile.TarInfo(name + '/' + path.relative_to(stage).as_posix())
info.size = len(data)
info.mode = 0o755 if os.access(path, os.X_OK) else 0o644
info.mtime = epoch
tar.addfile(info, io.BytesIO(data))
os.replace(pending, a.output)
finally:
if os.path.exists(pending):
os.unlink(pending)
sha = hashlib.sha256(a.output.read_bytes()).hexdigest()
a.output.with_name(a.output.name + '.sha256').write_text(f'{sha} {a.output.name}\n')
print(f'{a.output}: {sha}')
if __name__ == '__main__':
main()

View file

@ -8,7 +8,8 @@ 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']
'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'))]