@openmixer/plugin-qualify 0.14.1 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -14
- package/bin/plugin-qualify-sync.mjs +57 -0
- package/bin/plugin-qualify.mjs +66 -15
- package/dist/adjust-cli.d.ts +83 -0
- package/dist/adjust-cli.js +214 -0
- package/dist/adjust-cli.js.map +1 -0
- package/dist/clap-loader.d.ts +3 -3
- package/dist/clap-loader.js +11 -11
- package/dist/clap-loader.js.map +1 -1
- package/dist/history-adjust.d.ts +77 -0
- package/dist/history-adjust.js +99 -0
- package/dist/history-adjust.js.map +1 -0
- package/dist/history-file.d.ts +73 -0
- package/dist/history-file.js +133 -0
- package/dist/history-file.js.map +1 -0
- package/dist/host-admission.d.ts +53 -0
- package/dist/host-admission.js +96 -0
- package/dist/host-admission.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/lv2-loader.d.ts +16 -2
- package/dist/lv2-loader.js +168 -17
- package/dist/lv2-loader.js.map +1 -1
- package/dist/measurement/cli.d.ts +6 -4
- package/dist/measurement/cli.js +12 -19
- package/dist/measurement/cli.js.map +1 -1
- package/dist/pins-file.d.ts +8 -2
- package/dist/pins-file.js +17 -3
- package/dist/pins-file.js.map +1 -1
- package/dist/probe-judge.d.ts +11 -1
- package/dist/probe-judge.js +20 -1
- package/dist/probe-judge.js.map +1 -1
- package/dist/probe-output-fixtures.d.ts +10 -0
- package/dist/probe-output-fixtures.js +86 -0
- package/dist/probe-output-fixtures.js.map +1 -1
- package/dist/probe-output.d.ts +2 -0
- package/dist/probe-output.js +1 -0
- package/dist/probe-output.js.map +1 -1
- package/dist/probe-runner.d.ts +9 -3
- package/dist/probe-runner.js +36 -12
- package/dist/probe-runner.js.map +1 -1
- package/dist/qualification-record.d.ts +109 -3
- package/dist/qualification-record.js +79 -13
- package/dist/qualification-record.js.map +1 -1
- package/dist/qualify-cli.d.ts +0 -1
- package/dist/qualify-cli.js +2 -4
- package/dist/qualify-cli.js.map +1 -1
- package/dist/qualify-declarations.d.ts +8 -1
- package/dist/qualify-declarations.js +8 -1
- package/dist/qualify-declarations.js.map +1 -1
- package/dist/qualify-options.js +1 -2
- package/dist/qualify-options.js.map +1 -1
- package/dist/qualify-sync.d.ts +215 -0
- package/dist/qualify-sync.js +794 -0
- package/dist/qualify-sync.js.map +1 -0
- package/dist/whole-file.d.ts +3 -0
- package/dist/whole-file.js +36 -0
- package/dist/whole-file.js.map +1 -0
- package/fixtures/admission/2026-10-26.json +19 -0
- package/fixtures/clap-scan/fake.json +24 -24
- package/package.json +5 -4
- package/src/adjust-cli.ts +262 -0
- package/src/clap-loader.ts +9 -13
- package/src/history-adjust.ts +156 -0
- package/src/history-file.ts +177 -0
- package/src/host-admission.ts +136 -0
- package/src/index.ts +6 -0
- package/src/lv2-loader.ts +150 -15
- package/src/measurement/cli.ts +11 -17
- package/src/pins-file.ts +17 -3
- package/src/probe-judge.ts +27 -1
- package/src/probe-output-fixtures.ts +86 -0
- package/src/probe-output.ts +3 -0
- package/src/probe-runner.ts +45 -14
- package/src/qualification-record.ts +174 -15
- package/src/qualify-cli.ts +2 -5
- package/src/qualify-declarations.ts +9 -1
- package/src/qualify-options.ts +1 -2
- package/src/qualify-sync.ts +889 -0
- package/src/whole-file.ts +35 -0
- package/tools/_declarations.py +3 -1
- package/tools/ai-trace-commit-check.sh +26 -14
- package/tools/ai-trace-patterns.json +10 -2
- package/tools/gen-hosting-codes.mjs +1 -1
- package/tools/gen-python-declarations.mjs +2 -0
- package/tools/install-roundtrip.sh +173 -0
- package/tools/mod-host-admit.mjs +74 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
3
|
+
/**
|
|
4
|
+
* A file written whole or not at all: the bytes go to `<path>.new` beside it, are flushed to the disk, and the name is
|
|
5
|
+
* moved over the old file, then the directory is flushed so the rename itself survives a power cut. A reader of
|
|
6
|
+
* `path`, plugin-hostd at start among them, sees the old file or the whole new one, never a half.
|
|
7
|
+
*/
|
|
8
|
+
import { closeSync, fsyncSync, mkdirSync, openSync, renameSync, writeSync } from "node:fs";
|
|
9
|
+
import { dirname } from "node:path";
|
|
10
|
+
|
|
11
|
+
/** The bytes one write call takes; a large file goes out in pieces of this size. */
|
|
12
|
+
const PIECE_BYTES = 1 << 16;
|
|
13
|
+
|
|
14
|
+
/** The name the new bytes are written under before the rename. */
|
|
15
|
+
export const pendingPath = (path: string): string => `${path}.new`;
|
|
16
|
+
|
|
17
|
+
export function writeFileWhole(path: string, content: string | Uint8Array): void {
|
|
18
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
19
|
+
const bytes = typeof content === "string" ? Buffer.from(content, "utf8") : content;
|
|
20
|
+
const pending = pendingPath(path);
|
|
21
|
+
const fd = openSync(pending, "w", 0o644);
|
|
22
|
+
try {
|
|
23
|
+
for (let at = 0; at < bytes.length; at += PIECE_BYTES) writeSync(fd, bytes, at, Math.min(PIECE_BYTES, bytes.length - at));
|
|
24
|
+
fsyncSync(fd);
|
|
25
|
+
} finally {
|
|
26
|
+
closeSync(fd);
|
|
27
|
+
}
|
|
28
|
+
renameSync(pending, path);
|
|
29
|
+
const dir = openSync(dirname(path), "r");
|
|
30
|
+
try {
|
|
31
|
+
fsyncSync(dir);
|
|
32
|
+
} finally {
|
|
33
|
+
closeSync(dir);
|
|
34
|
+
}
|
|
35
|
+
}
|
package/tools/_declarations.py
CHANGED
|
@@ -25,8 +25,10 @@ DESCRIPTOR_DIR_SYSTEM = "/var/lib/omx/descriptors"
|
|
|
25
25
|
DESCRIPTOR_HASH_PREFIX = 12
|
|
26
26
|
DESCRIPTOR_SCHEMA_ENV = "OMX_CONTRACT_SCHEMA"
|
|
27
27
|
DESCRIPTOR_SCHEMA_INSTALLED = "/usr/share/omx-contract/omx-contract.schema.json"
|
|
28
|
-
DESCRIPTOR_SCHEMA_FLOOR = ">= 3.
|
|
28
|
+
DESCRIPTOR_SCHEMA_FLOOR = ">= 3.4.0"
|
|
29
|
+
DESCRIPTOR_ID_HASH_PREFIX = 8
|
|
29
30
|
DESCRIPTOR_CODE_SCHEMA = "contract.file.invalid"
|
|
30
31
|
DESCRIPTOR_CODE_SCHEMA_MISSING = "qualify.descriptor.schema-missing"
|
|
32
|
+
DESCRIPTOR_CODE_SCHEMA_FLOOR = "qualify.descriptor.schema-floor"
|
|
31
33
|
DESCRIPTOR_CODE_BINARY_UNHASHED = "qualify.descriptor.binary-unhashed"
|
|
32
34
|
DESCRIPTOR_CODE_DIR_UNWRITABLE = "qualify.descriptor.dir-unwritable"
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
-
# Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
4
|
-
#
|
|
5
3
|
# ai-trace-commit-check.sh: fail when a commit message carries an AI-attribution line.
|
|
6
4
|
#
|
|
7
5
|
# ai-trace-commit-check.sh <range> every commit in the range (e.g. origin/main..HEAD)
|
|
8
6
|
# ai-trace-commit-check.sh --msg-file FILE one message (the commit-msg hook; # lines are git comments, skipped)
|
|
9
|
-
# ai-trace-commit-check.sh --text-file FILE any text, every line judged (
|
|
7
|
+
# ai-trace-commit-check.sh --text-file FILE any text, every line judged (PR titles and bodies)
|
|
10
8
|
# ai-trace-commit-check.sh --selftest fixtures plus a sabotage run
|
|
11
9
|
#
|
|
12
10
|
# Exit 0 clean, 1 a hit (each commit and offending line is named), 2 a range, file or pattern list that cannot be read
|
|
13
|
-
# (any crash is 2).
|
|
14
|
-
# The patterns are ai-trace-patterns.json beside this script
|
|
11
|
+
# (any crash is 2). Callers refuse on 2: a checker that cannot judge is never a pass.
|
|
12
|
+
# The patterns are ai-trace-patterns.json beside this script, the same list ai-trace-sweep.sh strips
|
|
13
|
+
# issue and PR text with, and the one list every door reads: merge-pr.sh, integrate-check.sh, pr-from-devel.sh
|
|
14
|
+
# (ops/tools/tests/ai-trace-one-list.t.sh refuses a second list). AI_TRACE_PATTERNS names another JSON (self-tests). Repos that cannot see the ops tree carry a copy of both files.
|
|
15
15
|
set -euo pipefail
|
|
16
16
|
export AI_TRACE_SELF="${BASH_SOURCE[0]}"
|
|
17
17
|
exec python3 -I - "$@" <<'PY'
|
|
@@ -51,16 +51,22 @@ def git(*a):
|
|
|
51
51
|
|
|
52
52
|
|
|
53
53
|
def selftest():
|
|
54
|
-
# built from fragments: this file must not itself carry an attribution line
|
|
55
|
-
co, by, ai = "Co-Authored-", "By: ", "Cl" + "aude"
|
|
56
|
-
sess = "Cl" + "aude-Session: https://cl" + "aude.ai/code/session_01"
|
|
57
54
|
fx = {
|
|
58
|
-
"co-authored-by":
|
|
59
|
-
"claude-session":
|
|
60
|
-
"generated-footer":
|
|
61
|
-
"claude.ai/code":
|
|
55
|
+
"co-authored-by": "Fix x\n\nCo-Authored-By: Claude Opus <noreply@anthropic.com>\n",
|
|
56
|
+
"claude-session": "Fix x\n\nClaude-Session: https://claude.ai/code/session_01\n",
|
|
57
|
+
"generated-footer": "Fix x\n\n\U0001F916 Generated with [Claude Code](https://claude.com/claude-code)\n",
|
|
58
|
+
"claude.ai/code": "Fix x\n\nsee https://claude.ai/code/session_9\n",
|
|
59
|
+
# the shapes the omx-dsp#155 review saw pass
|
|
60
|
+
"assisted-by": "Fix x\n\nAssisted-by: Claude\n",
|
|
61
|
+
"made-with": "Fix x\n\nMade with Claude Code\n",
|
|
62
|
+
"generated-with-claude": "Fix x\n\nGenerated with Claude\n",
|
|
63
|
+
"generated-by-trailer": "Fix x\n\nGenerated-by: Claude\n",
|
|
64
|
+
"co-author-model": "Fix x\n\nCo-authored-by: Opus 5.5 <x@example.invalid>\n",
|
|
65
|
+
"co-author-github-copilot": "Fix x\n\nCo-authored-by: GitHub Copilot <copilot@github.com>\n",
|
|
62
66
|
}
|
|
63
|
-
clean =
|
|
67
|
+
clean = ("Fix x\n\nGenerated by the build; mentions .claude/ and a Co-Authored-By: Pau line.\n"
|
|
68
|
+
"Assisted-by: Pau Aliagas\nMade with care; generated with tools/rules-index-gen.sh\n"
|
|
69
|
+
"A review quoted 'Assisted-by: Claude' and 'Co-authored-by: Opus 5.5 <x>' as shapes that passed.\n")
|
|
64
70
|
def run(markers):
|
|
65
71
|
bad = [k for k, v in fx.items() if not hits(v, markers)]
|
|
66
72
|
if hits(clean, markers):
|
|
@@ -71,7 +77,13 @@ def selftest():
|
|
|
71
77
|
print("selftest:", "GREEN" if not first else f"RED {first}")
|
|
72
78
|
sab = run([x for x in m if x[0] not in ("co-authored-by", "ai-co-author", "anthropic-noreply", "claude-session")])
|
|
73
79
|
print("sabotage (the co-author, email and session patterns removed):", "RED as expected" if sab else "still GREEN: blind")
|
|
74
|
-
|
|
80
|
+
# each #152 shape is seen by a pattern of its own family, not by luck
|
|
81
|
+
new = ["assisted-by", "made-with", "generated-with-claude", "generated-by-trailer", "co-author-model", "co-author-github-copilot"]
|
|
82
|
+
sab2 = run([x for x in m if x[0] not in ("ai-assisted-by", "generated-footer", "ai-co-author-model")])
|
|
83
|
+
blind = [k for k in new if k not in sab2]
|
|
84
|
+
print("sabotage (the assisted-by, generated and model co-author patterns removed):",
|
|
85
|
+
"RED as expected" if not blind else f"still GREEN for {blind}: blind")
|
|
86
|
+
return 0 if not first and sab and not blind else 1
|
|
75
87
|
|
|
76
88
|
|
|
77
89
|
def main(a):
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"$comment": "
|
|
2
|
+
"$comment": "Single source of the AI-trace patterns (Python re, flags IGNORECASE|MULTILINE). Read by ai-trace-sweep.sh and ai-trace-commit-check.sh. Edit here only. A trailer-shaped pattern that eats the rest of its line is anchored at the line start, so prose quoting a trailer keeps its text.",
|
|
3
3
|
"markers": [
|
|
4
4
|
{
|
|
5
5
|
"name": "robot-footer",
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
{
|
|
9
9
|
"name": "generated-footer",
|
|
10
|
-
"pattern": "(?:🤖[^\\S\\r\\n]*)?Generated[^\\S\\r\\n]+(?:with|by)[^\\S\\r\\n]+\\[?Claude[^\\S\\r\\n]+Code
|
|
10
|
+
"pattern": "(?:🤖[^\\S\\r\\n]*)?(?:Generated|Made)[^\\S\\r\\n]+(?:with|by)[^\\S\\r\\n]+\\[?Claude(?:[^\\S\\r\\n]+Code)?\\]?(?:\\([^)\\s]*\\))?"
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
"name": "co-authored-by",
|
|
@@ -17,6 +17,14 @@
|
|
|
17
17
|
"name": "ai-co-author",
|
|
18
18
|
"pattern": "Co-Authored-By:[^\\S\\r\\n]*(?:Anthropic|OpenAI|ChatGPT|Copilot|Gemini|Codex|Cursor|Aider|Devin)\\b[^\\r\\n<`]*(?:<[^>\\r\\n]*>)?"
|
|
19
19
|
},
|
|
20
|
+
{
|
|
21
|
+
"name": "ai-co-author-model",
|
|
22
|
+
"pattern": "^[^\\S\\r\\n]*Co-Authored-By:[^\\S\\r\\n]*(?:GitHub[^\\S\\r\\n]+Copilot|Opus|Sonnet|Haiku|Fable|GPT)\\b[^\\r\\n<`]*(?:<[^>\\r\\n]*>)?"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"name": "ai-assisted-by",
|
|
26
|
+
"pattern": "^[^\\S\\r\\n]*(?:Assisted|Generated)-By:[^\\S\\r\\n]*(?:Claude|Anthropic|OpenAI|ChatGPT|(?:GitHub[^\\S\\r\\n]+)?Copilot|Gemini|Codex|Cursor|Aider|Devin|Opus|Sonnet|Haiku|Fable|GPT)\\b[^\\r\\n<`]*(?:<[^>\\r\\n]*>)?"
|
|
27
|
+
},
|
|
20
28
|
{
|
|
21
29
|
"name": "anthropic-noreply",
|
|
22
30
|
"pattern": "noreply@anthropic\\.com"
|
|
@@ -51,7 +51,7 @@ export async function hostingCodesFileFrom(dir) {
|
|
|
51
51
|
function absent() {
|
|
52
52
|
return (
|
|
53
53
|
`omx-clap-core's TypeScript modules were not found in any of: ${clapCoreTsDirCandidates().join(', ')}. ` +
|
|
54
|
-
'Install omx-clap-core
|
|
54
|
+
'Install omx-clap-core (its modules in /usr/share/omx-clap-core/ts), set OMX_CLAP_CORE_TSDIR to its tsdir, or run make -C native deps.'
|
|
55
55
|
);
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -42,8 +42,10 @@ const PY_NAMES = [
|
|
|
42
42
|
'DESCRIPTOR_SCHEMA_ENV',
|
|
43
43
|
'DESCRIPTOR_SCHEMA_INSTALLED',
|
|
44
44
|
'DESCRIPTOR_SCHEMA_FLOOR',
|
|
45
|
+
'DESCRIPTOR_ID_HASH_PREFIX',
|
|
45
46
|
'DESCRIPTOR_CODE_SCHEMA',
|
|
46
47
|
'DESCRIPTOR_CODE_SCHEMA_MISSING',
|
|
48
|
+
'DESCRIPTOR_CODE_SCHEMA_FLOOR',
|
|
47
49
|
'DESCRIPTOR_CODE_BINARY_UNHASHED',
|
|
48
50
|
'DESCRIPTOR_CODE_DIR_UNWRITABLE',
|
|
49
51
|
];
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
# Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
4
|
+
#
|
|
5
|
+
# The after-install scan, end to end on a clean image (#168): build this checkout's package, install it, install a
|
|
6
|
+
# plugin package, and see the plugin's qualification record, descriptor and pin appear with no manual step; upgrade
|
|
7
|
+
# the plugin package with a changed bundle and see them change; remove it and see them go.
|
|
8
|
+
#
|
|
9
|
+
# podman run --rm -v "$PWD":/src:ro,Z docker.io/library/debian:trixie bash /src/tools/install-roundtrip.sh /src
|
|
10
|
+
# podman run --rm -v "$PWD":/src:ro,Z docker.io/library/fedora:44 bash /src/tools/install-roundtrip.sh /src
|
|
11
|
+
#
|
|
12
|
+
# Runs as root inside a throwaway container; rootless podman has no systemd, which is the "direct run" branch of
|
|
13
|
+
# packaging/trigger-scan.sh (the image-build and chroot case). The scan is never started by hand: every record, descriptor and
|
|
14
|
+
# pin below is what the package manager's trigger left. The units under a running systemd (the service start branch)
|
|
15
|
+
# are not exercised here. Network is needed: the OpenMixer channel (OPENMIXER_URL) and npm.
|
|
16
|
+
#
|
|
17
|
+
# Exit 0 when every check holds; each failed check is printed, the run goes on, and the exit is 1.
|
|
18
|
+
set -uo pipefail
|
|
19
|
+
|
|
20
|
+
src=$(readlink -f "${1:?usage: install-roundtrip.sh <checkout>}")
|
|
21
|
+
url=${OPENMIXER_URL:-https://www.openmixer.org}
|
|
22
|
+
state=/var/lib/omx
|
|
23
|
+
uri_a=urn:plugin-qualify:fixture:lv2-rt-safe
|
|
24
|
+
uri_b=urn:plugin-qualify:fixture:lv2-rt-safe-lib64
|
|
25
|
+
fails=0
|
|
26
|
+
work=$(mktemp -d /tmp/roundtrip.XXXXXX)
|
|
27
|
+
|
|
28
|
+
say() { printf '\n== %s\n' "$*"; }
|
|
29
|
+
check() { # check <what> <command...>
|
|
30
|
+
local what=$1; shift
|
|
31
|
+
if "$@" >/dev/null 2>&1; then echo "ok $what"; else echo "FAIL $what"; fails=$((fails + 1)); fi
|
|
32
|
+
}
|
|
33
|
+
absent() { ! "$@"; }
|
|
34
|
+
has_file_with() { grep -rqF -- "$2" "$1" 2>/dev/null; } # has_file_with <dir> <text>
|
|
35
|
+
pins() { cat "$state/qualified.pins" 2>/dev/null; }
|
|
36
|
+
pin_of() { pins | grep -F -- "$1" | head -1; }
|
|
37
|
+
show() {
|
|
38
|
+
echo "-- records:"; ls "$state/qualification" 2>/dev/null | sed 's/^/ /'
|
|
39
|
+
echo "-- descriptors:"; (cd "$state/descriptors" 2>/dev/null && find . -type f | sort | sed 's/^/ /')
|
|
40
|
+
echo "-- pins:"; pins | sed 's/^/ /'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if command -v dnf >/dev/null; then fmt=rpm; else fmt=deb; fi
|
|
44
|
+
|
|
45
|
+
# --- the clean image: the channel, the build tools ------------------------------------------------------------------
|
|
46
|
+
say "prepare ($fmt)"
|
|
47
|
+
if [ $fmt = rpm ]; then
|
|
48
|
+
grep -qx 'retries=10' /etc/dnf/dnf.conf || echo 'retries=10' >> /etc/dnf/dnf.conf
|
|
49
|
+
dnf install -y --setopt=install_weak_deps=False "$url/rpm/openmixer-release.noarch.rpm" >/dev/null &&
|
|
50
|
+
dnf install -y --setopt=install_weak_deps=False rpm-build dnf-plugins-core gcc make lv2-devel clap-devel tar gzip >/dev/null || exit 2
|
|
51
|
+
else
|
|
52
|
+
export DEBIAN_FRONTEND=noninteractive
|
|
53
|
+
apt-get update -qq && apt-get install -y --no-install-recommends ca-certificates curl >/dev/null || exit 2
|
|
54
|
+
curl -fsSL --retry 5 -o "$work/release.deb" "$url/deb/openmixer-release.deb" && chmod 755 "$work" &&
|
|
55
|
+
apt-get install -y --no-install-recommends "$work/release.deb" >/dev/null && apt-get update -qq || exit 2
|
|
56
|
+
apt-get install -y --no-install-recommends devscripts equivs fakeroot build-essential lv2-dev clap-headers >/dev/null || exit 2
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
# --- this checkout's package ------------------------------------------------------------------------------------------
|
|
60
|
+
say "build plugin-qualify"
|
|
61
|
+
tree=$work/tree
|
|
62
|
+
mkdir -p "$tree" && (cd "$src" && tar --exclude=./node_modules --exclude=./.git --exclude=./native/build --exclude=./native/deps --exclude=./dist -cf - .) | tar -xf - -C "$tree"
|
|
63
|
+
if [ $fmt = rpm ]; then
|
|
64
|
+
ver=$(rpmspec -q --qf '%{version}\n' "$tree/packaging/plugin-qualify.spec" | head -1)
|
|
65
|
+
dnf builddep -y "$tree/packaging/plugin-qualify.spec" >/dev/null || exit 2
|
|
66
|
+
mkdir -p "$work/rpmbuild/SOURCES" && cp -r "$tree" "$work/plugin-qualify-$ver" &&
|
|
67
|
+
tar -czf "$work/rpmbuild/SOURCES/plugin-qualify-$ver.tar.gz" -C "$work" "plugin-qualify-$ver"
|
|
68
|
+
rpmbuild -bb --define "_topdir $work/rpmbuild" "$tree/packaging/plugin-qualify.spec" >"$work/build.log" 2>&1 || { tail -30 "$work/build.log"; exit 2; }
|
|
69
|
+
pkg=$(ls "$work"/rpmbuild/RPMS/*/plugin-qualify-[0-9]*.rpm)
|
|
70
|
+
else
|
|
71
|
+
(cd "$tree" && mk-build-deps -i -r -t 'apt-get -y --no-install-recommends' debian/control >/dev/null 2>&1 &&
|
|
72
|
+
dpkg-buildpackage -us -uc -b >"$work/build.log" 2>&1) || { tail -30 "$work/build.log"; exit 2; }
|
|
73
|
+
pkg=$(ls "$work"/plugin-qualify_*.deb)
|
|
74
|
+
fi
|
|
75
|
+
echo "built $pkg"
|
|
76
|
+
|
|
77
|
+
say "install plugin-qualify"
|
|
78
|
+
# the package manager's defaults, recommends included: the descriptor writer needs python3-lilv, which the deb recommends
|
|
79
|
+
if [ $fmt = rpm ]; then dnf install -y "$pkg" >/dev/null || exit 2; else apt-get install -y "$pkg" >/dev/null || exit 2; fi
|
|
80
|
+
echo "-- after the install, with no plugin: state file $( [ -e $state/qualify-sync.json ] && echo present || echo absent )"
|
|
81
|
+
check "the trigger script ships" test -x "$( (dpkg -L plugin-qualify 2>/dev/null || rpm -ql plugin-qualify) | grep '/trigger-scan$')"
|
|
82
|
+
check "installing with no plugin leaves no record" absent test -e "$state/qualification/$uri_a"
|
|
83
|
+
|
|
84
|
+
# --- a plugin package: the repo's own LV2 fixture, packed as a distribution package would ------------------------------
|
|
85
|
+
say "build the plugin package"
|
|
86
|
+
fx=$work/fx
|
|
87
|
+
make -C "$src/native" BUILD="$fx" "$fx/fixtures/lv2/rt-safe.lv2/gain.so" >/dev/null 2>&1 || { echo "the fixture bundle does not build"; exit 2; }
|
|
88
|
+
bundle() { # bundle <root> <libdir> <version-marker>: the bundles of one plugin package version
|
|
89
|
+
local root=$1 lib=$2 mark=$3 b=$root$2/rt-safe.lv2
|
|
90
|
+
mkdir -p "$b" && cp "$fx"/fixtures/lv2/rt-safe.lv2/* "$b/"
|
|
91
|
+
[ -z "$mark" ] || echo "# $mark" >> "$b/gain.ttl" # a changed bundle: the plugin's file set hashes differently
|
|
92
|
+
if [ $fmt = rpm ]; then # the second plugin of the Fedora package lives in /usr/lib64/lv2, under a uri of its own
|
|
93
|
+
local b2=$root/usr/lib64/lv2/rt-safe-lib64.lv2
|
|
94
|
+
mkdir -p "$b2" && cp "$fx"/fixtures/lv2/rt-safe.lv2/* "$b2/" && sed -i "s/lv2-rt-safe/lv2-rt-safe-lib64/g" "$b2"/*.ttl
|
|
95
|
+
[ -z "$mark" ] || echo "# $mark" >> "$b2/gain.ttl"
|
|
96
|
+
fi
|
|
97
|
+
}
|
|
98
|
+
mkplugin() { # mkplugin <version> <marker>: ./fakeplug-<version>.<fmt>
|
|
99
|
+
local v=$1 m=$2 root=$work/pkg-$1
|
|
100
|
+
rm -rf "$root"; bundle "$root" /usr/lib/lv2 "$m"
|
|
101
|
+
if [ $fmt = deb ]; then
|
|
102
|
+
mkdir -p "$root/DEBIAN"
|
|
103
|
+
printf 'Package: fakeplug\nVersion: %s\nArchitecture: %s\nMaintainer: nobody <nobody@example.org>\nDescription: a fake LV2 plugin package\n' "$v" "$(dpkg --print-architecture)" > "$root/DEBIAN/control"
|
|
104
|
+
dpkg-deb --root-owner-group --build "$root" "$work/fakeplug-$v.deb" >/dev/null
|
|
105
|
+
else
|
|
106
|
+
cat > "$work/fake-$v.spec" <<SPEC
|
|
107
|
+
Name: fakeplug
|
|
108
|
+
Version: $v
|
|
109
|
+
Release: 1
|
|
110
|
+
Summary: a fake LV2 plugin package
|
|
111
|
+
License: GPL-3.0-or-later
|
|
112
|
+
AutoReqProv: no
|
|
113
|
+
%global debug_package %{nil}
|
|
114
|
+
%description
|
|
115
|
+
a fake LV2 plugin package
|
|
116
|
+
%install
|
|
117
|
+
cp -a $root/. %{buildroot}/
|
|
118
|
+
%files
|
|
119
|
+
/usr/lib/lv2/rt-safe.lv2
|
|
120
|
+
/usr/lib64/lv2/rt-safe-lib64.lv2
|
|
121
|
+
SPEC
|
|
122
|
+
rpmbuild -bb --define "_topdir $work/rpmbuild" "$work/fake-$v.spec" >"$work/fake.log" 2>&1 && cp "$work"/rpmbuild/RPMS/*/fakeplug-$v-1.*.rpm "$work/fakeplug-$v.rpm"
|
|
123
|
+
fi
|
|
124
|
+
[ -f "$work/fakeplug-$v.$fmt" ] || { echo "no fake package $v"; tail -20 "$work/fake.log" 2>/dev/null; exit 2; }
|
|
125
|
+
}
|
|
126
|
+
mkplugin 1.0 ""
|
|
127
|
+
mkplugin 2.0 "version 2"
|
|
128
|
+
# what the package manager's own trigger printed is the scan's log: the lines that say what it did are shown
|
|
129
|
+
scan_log() { sed 's/^>>> //' "$work/out" | grep -E '^(qualified |urn:|clap:|LV2 descriptors|plugin-qualify|.*: gone|.*: removed)' | sed 's/^/ scan: /'; }
|
|
130
|
+
install_pkg() { if [ $fmt = rpm ]; then dnf install -y "$1" >"$work/out" 2>"$work/err"; else apt-get install -y "$1" >"$work/out" 2>"$work/err"; fi; local rc=$?; scan_log; return $rc; }
|
|
131
|
+
remove_pkg() { if [ $fmt = rpm ]; then dnf remove -y "$1" >"$work/out" 2>"$work/err"; else apt-get remove -y "$1" >"$work/out" 2>"$work/err"; fi; local rc=$?; scan_log; return $rc; }
|
|
132
|
+
|
|
133
|
+
# --- install --------------------------------------------------------------------------------------------------------------
|
|
134
|
+
say "install the plugin package: nothing is run by hand"
|
|
135
|
+
install_pkg "$work/fakeplug-1.0.$fmt" || { cat "$work/err"; exit 2; }
|
|
136
|
+
show
|
|
137
|
+
check "a qualification record names the plugin" has_file_with "$state/qualification" "$uri_a"
|
|
138
|
+
check "a descriptor names the plugin" has_file_with "$state/descriptors" "$uri_a"
|
|
139
|
+
check "the pin file pins the plugin" grep -qF "$uri_a" "$state/qualified.pins"
|
|
140
|
+
if [ $fmt = rpm ]; then
|
|
141
|
+
check "the /usr/lib64/lv2 plugin has a record" has_file_with "$state/qualification" "$uri_b"
|
|
142
|
+
check "the /usr/lib64/lv2 plugin has a descriptor" has_file_with "$state/descriptors" "$uri_b"
|
|
143
|
+
check "the /usr/lib64/lv2 plugin is pinned" grep -qF "$uri_b" "$state/qualified.pins"
|
|
144
|
+
fi
|
|
145
|
+
pin1=$(pin_of "$uri_a")
|
|
146
|
+
rec1=$(cat "$state/qualification"/* 2>/dev/null | sha256sum)
|
|
147
|
+
|
|
148
|
+
# --- upgrade --------------------------------------------------------------------------------------------------------------
|
|
149
|
+
say "upgrade the plugin package with a changed bundle"
|
|
150
|
+
install_pkg "$work/fakeplug-2.0.$fmt" || { cat "$work/err"; exit 2; }
|
|
151
|
+
show
|
|
152
|
+
pin2=$(pin_of "$uri_a")
|
|
153
|
+
check "the pin changed with the bundle" test -n "$pin2" -a "$pin1" != "$pin2"
|
|
154
|
+
check "the record changed with the bundle" test "$rec1" != "$(cat "$state/qualification"/* 2>/dev/null | sha256sum)"
|
|
155
|
+
check "the descriptor is still there" has_file_with "$state/descriptors" "$uri_a"
|
|
156
|
+
|
|
157
|
+
# --- removal --------------------------------------------------------------------------------------------------------------
|
|
158
|
+
say "remove the plugin package"
|
|
159
|
+
remove_pkg fakeplug || { cat "$work/err"; exit 2; }
|
|
160
|
+
show
|
|
161
|
+
check "the pin is gone" absent grep -qF "$uri_a" "$state/qualified.pins"
|
|
162
|
+
check "the record is gone" absent has_file_with "$state/qualification" "$uri_a"
|
|
163
|
+
check "the descriptor is gone" absent has_file_with "$state/descriptors" "$uri_a"
|
|
164
|
+
if [ $fmt = rpm ]; then
|
|
165
|
+
check "the /usr/lib64/lv2 plugin's pin is gone" absent grep -qF "$uri_b" "$state/qualified.pins"
|
|
166
|
+
check "the /usr/lib64/lv2 plugin's record is gone" absent has_file_with "$state/qualification" "$uri_b"
|
|
167
|
+
check "the /usr/lib64/lv2 plugin's descriptor is gone" absent has_file_with "$state/descriptors" "$uri_b"
|
|
168
|
+
fi
|
|
169
|
+
|
|
170
|
+
echo
|
|
171
|
+
[ "$fails" -eq 0 ] && echo "install-roundtrip: every check holds ($fmt)" || echo "install-roundtrip: $fails check(s) failed ($fmt)"
|
|
172
|
+
rm -rf "$work"
|
|
173
|
+
[ "$fails" -eq 0 ]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
3
|
+
// Copyright (C) 2026 Pau Aliagas <linuxnow@gmail.com>
|
|
4
|
+
// One mod-host admission: a fresh `mod-host -n` on its own port, then `add` and `remove` of one plugin on its
|
|
5
|
+
// socket. Prints one JSON line, {installed, steps:[{cmd, resp | died}]}, for src/host-admission.ts to classify. The host
|
|
6
|
+
// is always killed at the end, so the next plugin gets a fresh one.
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { createConnection } from "node:net";
|
|
9
|
+
|
|
10
|
+
const uri = process.argv[2];
|
|
11
|
+
const port = 20000 + (process.pid % 20000);
|
|
12
|
+
const STEP_MS = 10000;
|
|
13
|
+
const INSTANCE = 0;
|
|
14
|
+
const COMMANDS = [
|
|
15
|
+
["add", `add ${uri} ${INSTANCE}`],
|
|
16
|
+
["remove", `remove ${INSTANCE}`],
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const host = spawn("mod-host", ["-n", "-p", String(port)], { stdio: "ignore" });
|
|
20
|
+
let hostEnd; // how the host went, once it has
|
|
21
|
+
host.on("exit", (code, signal) => (hostEnd = signal ?? "exit"));
|
|
22
|
+
const spawnFailed = new Promise((resolve) => host.on("error", (e) => resolve(e.code)));
|
|
23
|
+
|
|
24
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
25
|
+
async function connect() {
|
|
26
|
+
for (let i = 0; i < 50; i++) {
|
|
27
|
+
if (hostEnd !== undefined) return undefined;
|
|
28
|
+
const s = await new Promise((resolve) => {
|
|
29
|
+
const c = createConnection({ port, host: "127.0.0.1" }, () => resolve(c));
|
|
30
|
+
c.on("error", () => resolve(undefined));
|
|
31
|
+
});
|
|
32
|
+
if (s !== undefined) return s;
|
|
33
|
+
await sleep(100);
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function main() {
|
|
39
|
+
if ((await Promise.race([spawnFailed, sleep(200).then(() => undefined)])) === "ENOENT") return { installed: false, steps: [] };
|
|
40
|
+
const sock = await connect();
|
|
41
|
+
if (sock === undefined) return { installed: true, steps: [{ cmd: "add", died: hostEnd ?? "closed" }] };
|
|
42
|
+
let buffer = "";
|
|
43
|
+
let closed = false;
|
|
44
|
+
sock.on("data", (d) => (buffer += d.toString()));
|
|
45
|
+
sock.on("close", () => (closed = true));
|
|
46
|
+
sock.on("error", () => (closed = true));
|
|
47
|
+
const steps = [];
|
|
48
|
+
for (const [cmd, line] of COMMANDS) {
|
|
49
|
+
buffer = "";
|
|
50
|
+
sock.write(`${line}\0`);
|
|
51
|
+
const until = Date.now() + STEP_MS;
|
|
52
|
+
let resp;
|
|
53
|
+
while (Date.now() < until && resp === undefined && !closed && hostEnd === undefined) {
|
|
54
|
+
const m = /resp (-?\d+)/.exec(buffer);
|
|
55
|
+
if (m !== null) resp = Number(m[1]);
|
|
56
|
+
else await sleep(20);
|
|
57
|
+
}
|
|
58
|
+
if (resp === undefined) {
|
|
59
|
+
await sleep(100); // a host that dies on the command closes the socket a moment before it is reaped
|
|
60
|
+
steps.push({ cmd, died: hostEnd ?? (closed ? "closed" : "timeout") });
|
|
61
|
+
if (hostEnd === undefined && !closed) steps[steps.length - 1] = { cmd };
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
steps.push({ cmd, resp });
|
|
65
|
+
if (resp < 0) break;
|
|
66
|
+
}
|
|
67
|
+
sock.destroy();
|
|
68
|
+
return { installed: true, steps };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const result = await main();
|
|
72
|
+
host.kill("SIGKILL");
|
|
73
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
74
|
+
process.exit(0);
|