2026-10-27 01:21:04 +01:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Exercise core/GUI install and exact uninstall solely below a temporary DESTDIR."""
|
|
|
|
|
import argparse
|
|
|
|
|
import os
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import shlex
|
|
|
|
|
import shutil
|
|
|
|
|
import subprocess
|
|
|
|
|
import tempfile
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def run(args, **kwargs):
|
|
|
|
|
return subprocess.run(args, check=True, **kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
|
|
|
parser.add_argument('--build-dir', type=Path, required=True)
|
|
|
|
|
parser.add_argument('--gui', action='store_true')
|
2026-10-27 17:13:09 +01:00
|
|
|
parser.add_argument('--custom-layout', action='store_true')
|
2026-10-27 01:21:04 +01:00
|
|
|
args = parser.parse_args()
|
|
|
|
|
source = Path.cwd()
|
|
|
|
|
with tempfile.TemporaryDirectory(prefix='xtc-install-') as directory:
|
|
|
|
|
stage = Path(directory)
|
|
|
|
|
prefix = '/opt/xtc test'
|
|
|
|
|
root = stage / prefix.lstrip('/')
|
|
|
|
|
sentinel = root / 'lib/keep-unrelated.txt'
|
|
|
|
|
sentinel.parent.mkdir(parents=True)
|
|
|
|
|
sentinel.write_text('keep\n')
|
2026-11-06 19:05:14 +01:00
|
|
|
# Generate the real CMake install driver against the already-built inputs.
|
|
|
|
|
# The temporary cache keeps the caller's build configuration untouched.
|
|
|
|
|
installer = stage / 'cmake-driver'
|
|
|
|
|
cache = (args.build_dir / 'CMakeCache.txt').read_text()
|
|
|
|
|
generator = next(line.partition('=')[2] for line in cache.splitlines()
|
|
|
|
|
if line.startswith('CMAKE_GENERATOR:INTERNAL='))
|
|
|
|
|
jack = 'OFF' if 'XTC_WITH_JACK:BOOL=OFF' in cache else 'ON'
|
|
|
|
|
env = dict(os.environ)
|
|
|
|
|
variables = ['-DBUILD_TESTING=OFF', f'-DXTC_BUILD_GUI={"ON" if args.gui else "OFF"}',
|
|
|
|
|
f'-DCMAKE_INSTALL_PREFIX={prefix}',
|
|
|
|
|
f'-DXTC_INSTALL_BUILD_DIR={args.build_dir.resolve()}',
|
|
|
|
|
f'-DXTC_WITH_JACK={jack}', '-DXTC_INSTALL_GUI=OFF', '-DCMAKE_INSTALL_DOCDIR=share/doc/xtc']
|
2026-10-27 17:13:09 +01:00
|
|
|
if args.custom_layout:
|
2026-11-06 19:05:14 +01:00
|
|
|
variables += ['-DCMAKE_INSTALL_BINDIR=runtime/bin', '-DCMAKE_INSTALL_LIBDIR=lib64',
|
|
|
|
|
'-DCMAKE_INSTALL_INCLUDEDIR=headers', '-DCMAKE_INSTALL_DATAROOTDIR=resources',
|
|
|
|
|
'-DCMAKE_INSTALL_DOCDIR=manual', '-DXTC_INSTALL_PKGCONFIGDIR=metadata']
|
|
|
|
|
|
|
|
|
|
def install_command(operation, options, **kwargs):
|
|
|
|
|
config = subprocess.run(['cmake', '-G', generator, '-S', str(source), '-B', str(installer),
|
|
|
|
|
*options], env=env, stdout=subprocess.PIPE,
|
|
|
|
|
stderr=subprocess.STDOUT, text=True)
|
|
|
|
|
if config.returncode:
|
|
|
|
|
raise RuntimeError(config.stdout)
|
|
|
|
|
command = (['cmake', '--build', str(installer), '--target', 'uninstall']
|
|
|
|
|
if operation == 'uninstall' else ['cmake', '--install', str(installer)])
|
|
|
|
|
return subprocess.run(command, env=dict(env, DESTDIR=str(stage)), **kwargs)
|
|
|
|
|
|
2026-10-27 17:13:09 +01:00
|
|
|
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')
|
2026-11-06 19:05:14 +01:00
|
|
|
install_command('install', variables, check=True)
|
2026-10-27 17:13:09 +01:00
|
|
|
assert not (binaries / 'xtc').exists()
|
|
|
|
|
assert (resources / 'licenses/xtc/LICENSE').read_bytes() == (source / 'LICENSE').read_bytes()
|
|
|
|
|
assert (docs / 'PROJECT_FORMAT.md').is_file()
|
2026-10-27 17:34:46 +01:00
|
|
|
assert (resources / 'licenses/xtc/CLAP-LICENSE').is_file()
|
|
|
|
|
assert (resources / 'licenses/xtc/VST3-C-API-LICENSE').is_file()
|
2026-10-27 17:13:09 +01:00
|
|
|
assert os.access(binaries / 'xtc-check', os.X_OK)
|
|
|
|
|
run([str(binaries / 'xtc-check')])
|
2026-11-06 14:12:22 +01:00
|
|
|
for filename in ('performance-baseline.json', 'project-performance-baseline.json', 'graph-performance-baseline.json'):
|
2026-10-27 17:13:09 +01:00
|
|
|
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()
|
2026-10-27 18:04:39 +01:00
|
|
|
run([str(binaries / 'xtc-check'), '--inspect', str(resources / 'xtc/examples/modern-session.xtc')])
|
|
|
|
|
assert (docs / 'WORKSPACE.md').is_file()
|
2026-10-27 17:13:09 +01:00
|
|
|
diagnose = subprocess.check_output([str(binaries / 'xtc-check'), '--diagnose'], text=True)
|
2026-11-06 01:34:08 +01:00
|
|
|
assert 'Project formats: 1..17' in diagnose and 'JACK compiled:' in diagnose
|
2026-10-27 17:13:09 +01:00
|
|
|
# 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)
|
2026-10-27 06:44:22 +01:00
|
|
|
# Upgrade/repair repeats the same owned-file install and keeps unrelated data.
|
2026-10-27 17:13:09 +01:00
|
|
|
(headers / 'xtc/xtc.h').write_text('old installation marker\n')
|
2026-11-06 19:05:14 +01:00
|
|
|
install_command('install', variables, check=True)
|
2026-10-27 17:13:09 +01:00
|
|
|
assert (headers / 'xtc/xtc.h').read_bytes() == (source / 'include/xtc/xtc.h').read_bytes()
|
2026-10-27 06:44:22 +01:00
|
|
|
assert sentinel.read_text() == 'keep\n'
|
2026-10-27 17:13:09 +01:00
|
|
|
assert protected.read_text() == 'keep unrelated target\n'
|
|
|
|
|
version = subprocess.check_output([str(binaries / 'xtc-check'), '--version'], text=True)
|
2026-10-27 01:21:04 +01:00
|
|
|
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')
|
2026-10-27 17:13:09 +01:00
|
|
|
run([str(binaries / 'xtc-check'), '--validate', str(valid)])
|
2026-10-27 01:21:04 +01:00
|
|
|
valid.write_text('damaged\n')
|
2026-10-27 17:13:09 +01:00
|
|
|
assert subprocess.run([str(binaries / 'xtc-check'), '--validate', str(valid)],
|
2026-10-27 01:21:04 +01:00
|
|
|
stdout=subprocess.PIPE, stderr=subprocess.PIPE).returncode != 0
|
2026-10-27 17:13:09 +01:00
|
|
|
pkg_env = dict(env, PKG_CONFIG_PATH=str(metadata) + ':' + env.get('PKG_CONFIG_PATH', ''),
|
2026-10-27 01:21:04 +01:00
|
|
|
PKG_CONFIG_SYSROOT_DIR=str(stage))
|
|
|
|
|
pkg = shlex.split(os.environ.get('PKG_CONFIG', 'pkg-config'))
|
2026-11-06 19:05:14 +01:00
|
|
|
# The build may disable JACK; consumer discovery still uses pkg-config.
|
2026-10-27 01:21:04 +01:00
|
|
|
if pkg == ['false']:
|
|
|
|
|
pkg = ['pkg-config']
|
|
|
|
|
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'
|
2026-10-27 06:44:22 +01:00
|
|
|
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,¬e) != 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;
|
|
|
|
|
}
|
|
|
|
|
''')
|
2026-10-27 01:21:04 +01:00
|
|
|
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)])
|
2026-10-27 06:44:22 +01:00
|
|
|
run([str(output), str(stage / ("installed-" + compiler + ".xtc"))])
|
2026-10-27 01:21:04 +01:00
|
|
|
if args.gui:
|
2026-11-06 19:05:14 +01:00
|
|
|
install_command('install', variables + ['-DXTC_INSTALL_GUI=ON'], check=True)
|
2026-10-27 01:21:04 +01:00
|
|
|
gui_env = dict(env, DISPLAY='', WAYLAND_DISPLAY='')
|
2026-10-27 17:13:09 +01:00
|
|
|
assert subprocess.check_output([str(binaries / 'xtc'), '--version'], env=gui_env, text=True) == version
|
|
|
|
|
run([str(binaries / 'xtc'), '--help'], env=gui_env)
|
2026-10-27 01:21:04 +01:00
|
|
|
if shutil.which('desktop-file-validate'):
|
2026-10-27 17:13:09 +01:00
|
|
|
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)
|
2026-11-06 19:05:14 +01:00
|
|
|
redirected = variables + [f'-DCMAKE_INSTALL_DOCDIR={prefix}/escape/docs']
|
2026-10-27 17:13:09 +01:00
|
|
|
for operation in ('install', 'uninstall'):
|
2026-11-06 19:05:14 +01:00
|
|
|
refusal = install_command(operation, redirected,
|
2026-10-27 17:13:09 +01:00
|
|
|
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()
|
2026-11-06 19:05:14 +01:00
|
|
|
refusal = install_command('uninstall', variables + ['-DCMAKE_INSTALL_DOCDIR=manual' if args.custom_layout else '-DCMAKE_INSTALL_DOCDIR=share/doc/xtc'],
|
2026-10-27 17:13:09 +01:00
|
|
|
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')
|
2026-11-06 19:05:14 +01:00
|
|
|
install_command('uninstall', variables, check=True)
|
|
|
|
|
install_command('uninstall', variables, check=True)
|
2026-10-27 01:21:04 +01:00
|
|
|
remaining = sorted(p.relative_to(root).as_posix() for p in root.rglob('*') if p.is_file() or p.is_symlink())
|
|
|
|
|
assert remaining == ['lib/keep-unrelated.txt'], remaining
|
|
|
|
|
assert sentinel.read_text() == 'keep\n'
|
|
|
|
|
print('Staged install/uninstall, C/C++ pkg-config consumers and headless CLI passed.')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
|
main()
|