@openmixer/plugin-qualify 0.19.1 → 0.20.1
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/bin/plugin-qualify-records-package.mjs +9 -7
- package/bin/plugin-qualify.mjs +36 -1
- package/dist/distributed-records.d.ts +16 -3
- package/dist/distributed-records.js +38 -4
- package/dist/distributed-records.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/judged-layer.d.ts +91 -0
- package/dist/judged-layer.js +144 -0
- package/dist/judged-layer.js.map +1 -0
- package/dist/judgement-line.d.ts +6 -0
- package/dist/judgement-line.js +20 -0
- package/dist/judgement-line.js.map +1 -0
- package/dist/measurement/cli.d.ts +2 -0
- package/dist/measurement/cli.js +25 -2
- package/dist/measurement/cli.js.map +1 -1
- package/dist/measurement/format.d.ts +2 -0
- package/dist/measurement/format.js +5 -0
- package/dist/measurement/format.js.map +1 -1
- package/dist/probe-merge.js +6 -0
- package/dist/probe-merge.js.map +1 -1
- package/dist/probe-runner.d.ts +4 -0
- package/dist/probe-runner.js +12 -3
- package/dist/probe-runner.js.map +1 -1
- package/dist/qualification-record.d.ts +15 -1
- package/dist/qualification-record.js +27 -5
- package/dist/qualification-record.js.map +1 -1
- package/dist/qualify-isolated.d.ts +5 -3
- package/dist/qualify-isolated.js +6 -4
- package/dist/qualify-isolated.js.map +1 -1
- package/dist/qualify-sync.d.ts +8 -3
- package/dist/qualify-sync.js +25 -15
- package/dist/qualify-sync.js.map +1 -1
- package/dist/records-package.d.ts +31 -4
- package/dist/records-package.js +149 -24
- package/dist/records-package.js.map +1 -1
- package/fixtures/records-rpi5-0.19.1/http___drumkv1.sourceforge.net_lv2@0.2-7d179bdcf9e5.default.probe.json +58 -0
- package/fixtures/records-rpi5-0.19.1/http___drumkv1.sourceforge.net_lv2@0.2-7d179bdcf9e5.default.qualification.json +305 -0
- package/fixtures/records-rpi5-0.19.1/http___samplv1.sourceforge.net_lv2@0.2-c9cd8fb3102f.default.probe.json +58 -0
- package/fixtures/records-rpi5-0.19.1/http___samplv1.sourceforge.net_lv2@0.2-c9cd8fb3102f.default.qualification.json +301 -0
- package/package.json +2 -2
- package/src/distributed-records.ts +41 -4
- package/src/index.ts +2 -0
- package/src/judged-layer.ts +197 -0
- package/src/judgement-line.ts +22 -0
- package/src/measurement/cli.ts +26 -2
- package/src/measurement/format.ts +8 -0
- package/src/probe-merge.ts +4 -0
- package/src/probe-runner.ts +16 -6
- package/src/qualification-record.ts +40 -6
- package/src/qualify-isolated.ts +6 -4
- package/src/qualify-sync.ts +27 -15
- package/src/records-package.ts +156 -25
- package/tools/lv2-measure.mjs +21 -2
package/src/qualify-isolated.ts
CHANGED
|
@@ -100,16 +100,18 @@ function figuresOf(run: IsolatedRunFacts): IsolatedFigures | undefined {
|
|
|
100
100
|
|
|
101
101
|
/**
|
|
102
102
|
* A verdict whose only gap is memory nobody measured: it reads `unknown` and every reason that is not `suitable` is
|
|
103
|
-
* `hosting.memory.unmeasured
|
|
104
|
-
*
|
|
103
|
+
* `hosting.memory.unmeasured`, or a `conditional` one the isolated step answers (an RT violation seen in the run). Nothing
|
|
104
|
+
* says the plugin is unfit, so it is hosted in a worker of its own until its footprint is measured, and the isolated step
|
|
105
|
+
* is what shows it running alone. A conditional reason beside the memory gap owes the step as a conditional verdict does:
|
|
106
|
+
* whether one run saw a violation and another did not must not decide whether the step is run (#319).
|
|
105
107
|
*/
|
|
106
108
|
export function memoryGapOnly(v: Pick<RecordVerdict, "measured" | "rating" | "reasons">): boolean {
|
|
107
109
|
if (!v.measured || v.rating !== "unknown") return false;
|
|
108
110
|
const open = v.reasons.filter((r) => r.rating !== "suitable");
|
|
109
|
-
return open.
|
|
111
|
+
return open.some((r) => r.code === HOSTING_CODE.memoryUnmeasured) && open.every((r) => r.code === HOSTING_CODE.memoryUnmeasured || r.rating === "conditional");
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
/** Whether a record's outcome calls for the isolated step: rated `conditional` by some host, a memory gap and nothing else, or a probe that crashed or hung. */
|
|
114
|
+
/** Whether a record's outcome calls for the isolated step: rated `conditional` by some host, a memory gap and nothing else unanswered, or a probe that crashed or hung. */
|
|
113
115
|
export function owesIsolated(record: QualificationRecord): boolean {
|
|
114
116
|
return PROBE_FAILED_HARD.has(record.probe.outcome) || record.verdicts.some((v) => v.measured && (v.rating === "conditional" || memoryGapOnly(v)));
|
|
115
117
|
}
|
package/src/qualify-sync.ts
CHANGED
|
@@ -24,7 +24,7 @@ import { homedir, tmpdir } from "node:os";
|
|
|
24
24
|
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
25
25
|
|
|
26
26
|
import { clapQualifyTools } from "./clap-qualify.js";
|
|
27
|
-
import { DISTRIBUTED_DIR_SYSTEM, EMPTY_LAYER, applyDistributed, distributedUri, hostClassOfBoard, outcomeOfVerdict, matchDistributed, readBoardModel, readDistributedLayer, type DistributedLayer } from "./distributed-records.js";
|
|
27
|
+
import { DISTRIBUTED_DIR_SYSTEM, EMPTY_LAYER, applyDistributed, distributedUri, hostClassOfBoard, hostClassOfMachine, outcomeOfVerdict, matchDistributed, readBoardModel, readMachine, readDistributedLayer, type DistributedLayer } from "./distributed-records.js";
|
|
28
28
|
import { clapSearchPath, parseClapScan } from "./clap-loader.js";
|
|
29
29
|
import { HOSTING_CODE } from "./hosting-codes.js";
|
|
30
30
|
import { shippedHostProfile, type HostProfile } from "./host-profiles.js";
|
|
@@ -34,7 +34,7 @@ import { POOL_RESULT, completePool, judgePoolRun, pinAfterPool, pinFromRecordWit
|
|
|
34
34
|
import { hostdPinFiles, type PinFilesRun } from "./hostd-pin-files.js";
|
|
35
35
|
import type { Judgment } from "./probe-judge.js";
|
|
36
36
|
import { qualifyTargets, type ProbeRun } from "./probe-runner.js";
|
|
37
|
-
import { GRID_FULL, parseGridSpec, type GridSpec } from "./measurement-grid.js";
|
|
37
|
+
import { GRID_FULL, hostCells, parseGridSpec, type GridCell, type GridSpec } from "./measurement-grid.js";
|
|
38
38
|
import { adapterPass, type AdapterPassOptions, type AdapterPassResult } from "./qualify-adapter-pass.js";
|
|
39
39
|
import { ISOLATED_RESULT, baseSteps, completeIsolated, isolatedCell, missingSteps, judgeIsolatedRun, owesIsolated, pinAfterStep, spawnedIsolatedRunner, type IsolatedRunner } from "./qualify-isolated.js";
|
|
40
40
|
import { lv2World, rejudgeRecord, rejudgeState, rejudgeToolName, memoryPass, type MemoryPassResult, type RejudgeResult } from "./qualify-rejudge.js";
|
|
@@ -228,6 +228,7 @@ export function resolveSyncSettings(
|
|
|
228
228
|
env: Readonly<Record<string, string | undefined>>,
|
|
229
229
|
home: string = homedir(),
|
|
230
230
|
boardModel: () => string | undefined = readBoardModel,
|
|
231
|
+
machineArch: () => string | undefined = readMachine,
|
|
231
232
|
): { ok: true; settings: SyncSettings } | { ok: false; error: string } {
|
|
232
233
|
const get = (key: string): string | undefined => args.values.get(key) ?? file.get(key);
|
|
233
234
|
const state = env["XDG_STATE_HOME"] !== undefined && env["XDG_STATE_HOME"] !== "" ? env["XDG_STATE_HOME"] : join(home, ".local", "state");
|
|
@@ -276,7 +277,7 @@ export function resolveSyncSettings(
|
|
|
276
277
|
? [DISTRIBUTED_DIR_SYSTEM]
|
|
277
278
|
: [dataHome, ...dataDirs].map((d) => join(d, "omx", "qualification"));
|
|
278
279
|
const classNamed = get("host_class");
|
|
279
|
-
const hostClass = classNamed !== undefined && classNamed !== "" ? classNamed : hostClassOfBoard(boardModel());
|
|
280
|
+
const hostClass = classNamed !== undefined && classNamed !== "" ? classNamed : hostClassOfBoard(boardModel()) ?? hostClassOfMachine(machineArch());
|
|
280
281
|
if (hostClass !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._+-]*$/.test(hostClass)) return { ok: false, error: `host_class ${hostClass} is not a directory name` };
|
|
281
282
|
return {
|
|
282
283
|
ok: true,
|
|
@@ -681,7 +682,10 @@ export function refreshPins(ctx: SyncContext, state: SyncState): { state: SyncSt
|
|
|
681
682
|
const pin = plugin.pin;
|
|
682
683
|
if (pin === undefined || pin.integrity.files === undefined) return plugin;
|
|
683
684
|
const r = hostdPinFiles(pin.uri, ctx.settings.lv2Dirs, ctx.env, ...(ctx.pinFilesRun === undefined ? [] : [ctx.pinFilesRun]));
|
|
684
|
-
if (!r.ok)
|
|
685
|
+
if (!r.ok) {
|
|
686
|
+
ctx.log(`${pin.uri}: pin files not refreshed: ${r.detail}`);
|
|
687
|
+
return plugin;
|
|
688
|
+
}
|
|
685
689
|
const old = new Map(pin.integrity.files.map((f) => [f.path, f.sha256]));
|
|
686
690
|
const changed = r.files.filter((f) => old.has(f.path) && old.get(f.path) !== f.sha256).map((f) => f.path);
|
|
687
691
|
const same = r.files.length === old.size && r.files.every((f) => old.get(f.path) === f.sha256);
|
|
@@ -735,10 +739,10 @@ function writeUnmeasuredRecord(ctx: SyncContext, item: SyncItem, files: readonly
|
|
|
735
739
|
const isolatedEnv = (ctx: SyncContext): Readonly<Record<string, string | undefined>> => ({ ...ctx.env, [ADMIT_STEP_MS_ENV]: String(ctx.settings.budgetS * 1000) });
|
|
736
740
|
|
|
737
741
|
/** Each judged run of a pass as the plugins it qualified: the record written, the pin taken, the outcome. */
|
|
738
|
-
function judgedPlugins(ctx: SyncContext, runs: readonly { bytes: Buffer; judgment: Judgment }[], describeProblem?: string): SyncPlugin[] {
|
|
742
|
+
function judgedPlugins(ctx: SyncContext, runs: readonly { bytes: Buffer; judgment: Judgment; stderr?: readonly Buffer[] }[], describeProblem?: string): SyncPlugin[] {
|
|
739
743
|
const host = ctx.settings.hostProfile.id;
|
|
740
|
-
const entries = runs.map(({ bytes, judgment }) => {
|
|
741
|
-
const { recordPath, record } = writeQualification(ctx.settings.qualificationDir, bytes, judgment, QUICK_NOT_MEASURED);
|
|
744
|
+
const entries = runs.map(({ bytes, judgment, stderr }) => {
|
|
745
|
+
const { recordPath, record } = writeQualification(ctx.settings.qualificationDir, bytes, judgment, QUICK_NOT_MEASURED, stderr);
|
|
742
746
|
const steps = baseSteps(record, describeProblem);
|
|
743
747
|
let pin = withHostdFiles(ctx, judgmentPinEntries(judgment, host)[0]);
|
|
744
748
|
let { outcome, code } = outcomeOf(judgment, host);
|
|
@@ -931,7 +935,9 @@ export function isolatedPass(ctx: SyncContext, state: SyncState): { readonly sta
|
|
|
931
935
|
return rest.outcome === "qualified" && rebuilt.verdict === "refused" ? { ...rest, outcome: "refused" as const, pin: rebuilt } : { ...rest, pin: rebuilt };
|
|
932
936
|
}
|
|
933
937
|
ctx.log(`${p.uri}: isolated step ${done.step.result}${done.step.code === undefined ? "" : ` (${done.step.code})`}`);
|
|
934
|
-
|
|
938
|
+
// a plugin the state holds no pin for is pinned from the record the step now completes: the bytes it names and the word
|
|
939
|
+
// the step earned (plugin-qualify#316: 38 plugins read hosting.memory.unmeasured and had none)
|
|
940
|
+
const pin = p.pin !== undefined ? pinAfterStep(p.pin, done.step) : withHostdFiles(ctx, pinFromRecordWithPool(done.record));
|
|
935
941
|
const { pin: _pin, code: _code, ...rest } = p;
|
|
936
942
|
if (done.step.result === ISOLATED_RESULT.isolated) {
|
|
937
943
|
count.isolated++;
|
|
@@ -985,6 +991,14 @@ export function poolPass(ctx: SyncContext, state: SyncState): { readonly state:
|
|
|
985
991
|
|
|
986
992
|
// --- the distributed records ----------------------------------------------------------------------------------------
|
|
987
993
|
|
|
994
|
+
/**
|
|
995
|
+
* The cells a sync runs at, which a distributed set must hold to apply (#317): the grid's own cells when it names them
|
|
996
|
+
* (`grid 48000/512`), else the cells the host profile judges cost and memory at.
|
|
997
|
+
*/
|
|
998
|
+
export function syncCells(settings: Pick<SyncSettings, "grid" | "hostProfile">): GridCell[] {
|
|
999
|
+
return settings.grid.kind === "cells" ? [...settings.grid.cells] : hostCells(settings.hostProfile);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
988
1002
|
/** The layer a run reads: none under `--force` or `--no-distributed`, or without a host class. */
|
|
989
1003
|
function distributedLayerOf(settings: SyncSettings, force: boolean, log: (line: string) => void): DistributedLayer {
|
|
990
1004
|
if (force || !settings.distributed) return EMPTY_LAYER;
|
|
@@ -1007,12 +1021,10 @@ function takeDistributed(ctx: SyncContext, layer: DistributedLayer, item: SyncIt
|
|
|
1007
1021
|
if (layer.records === 0) return undefined;
|
|
1008
1022
|
const started = Date.now();
|
|
1009
1023
|
const host = ctx.settings.hostProfile.id;
|
|
1010
|
-
const match = matchDistributed(layer, item, host, (paths) => digestFiles(item.root, paths, files));
|
|
1024
|
+
const match = matchDistributed(layer, item, host, (paths) => digestFiles(item.root, paths, files), syncCells(ctx.settings));
|
|
1011
1025
|
if (match.status === "none") return undefined;
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
return undefined;
|
|
1015
|
-
}
|
|
1026
|
+
for (const line of match.lines) ctx.log(line);
|
|
1027
|
+
if (match.status === "ignored") return undefined;
|
|
1016
1028
|
for (const r of match.records) {
|
|
1017
1029
|
const missing = missingSteps(r.record);
|
|
1018
1030
|
if (missing.length > 0) {
|
|
@@ -1102,9 +1114,9 @@ export function removedPinUris(before: string, after: string): string[] {
|
|
|
1102
1114
|
return [...pinLinesOf(before).keys()].filter((uri) => !now.has(uri));
|
|
1103
1115
|
}
|
|
1104
1116
|
|
|
1105
|
-
/** `<id>@<version>-<binary sha first 12>`, the stem a qualification record, its probe copy and its descriptor share. */
|
|
1117
|
+
/** `<id>@<version>-<binary sha first 12>`, the stem a qualification record, its probe copy, its children's stderr and its descriptor share. */
|
|
1106
1118
|
export function recordStem(recordPath: string): string | undefined {
|
|
1107
|
-
return /^(.*-[0-9a-f]{12})\.(?:[^.]+\.(?:qualification|probe)|unmeasured)\.json$/.exec(basename(recordPath))?.[1];
|
|
1119
|
+
return /^(.*-[0-9a-f]{12})\.(?:(?:[^.]+\.(?:qualification|probe)|unmeasured)\.json|[^.]+\.stderr\.[0-9a-f]{12}\.log)$/.exec(basename(recordPath))?.[1];
|
|
1108
1120
|
}
|
|
1109
1121
|
|
|
1110
1122
|
/**
|
package/src/records-package.ts
CHANGED
|
@@ -4,10 +4,13 @@
|
|
|
4
4
|
* `plugin-qualify-records-package`: the Debian package of a device's qualification records, so other devices of the
|
|
5
5
|
* machine class install them as the distributed layer instead of measuring the same plugin set again.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
7
|
+
* A record set is per machine class, rate and quantum (#317). In: the qualification directories of a run, one per
|
|
8
|
+
* cell it was measured at, as plugin-qualify-sync leaves them, or the run's root holding `b<rate>-<quantum>/qualification`.
|
|
9
|
+
* Out: one `plugin-db-qualification-<class>-<set>-<rate>-<quantum>_<version>_all.deb` per cell, a package of plugin-db's
|
|
10
|
+
* qualification layer, installing every record of that cell and the probe output it judged under
|
|
11
|
+
* `/usr/share/omx/qualification/<class>/<set>-<rate>-<quantum>/`, beside the set's `provenance.json`, which names the
|
|
12
|
+
* cell. A directory's cell is the one its base is named for (`b48000-512`), else `--cell`, else the one cell every
|
|
13
|
+
* record's grid holds; a record whose grid does not hold its set's cell stays out, named. A record goes in only with its probe output, byte for byte the copy the record names;
|
|
11
14
|
* an unmeasured record never applies and stays out. The archive is written here, `ar` and two `tar.xz` members, so a
|
|
12
15
|
* machine without dpkg builds it too; the tar members are GNU tar's, owned by root, in name order, at one time.
|
|
13
16
|
*/
|
|
@@ -15,14 +18,18 @@ import { createHash } from "node:crypto";
|
|
|
15
18
|
import { spawnSync } from "node:child_process";
|
|
16
19
|
import { closeSync, copyFileSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, writeSync } from "node:fs";
|
|
17
20
|
import { tmpdir } from "node:os";
|
|
18
|
-
import { join } from "node:path";
|
|
21
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
19
22
|
|
|
20
|
-
import {
|
|
23
|
+
import { cellKey, type GridCell } from "./measurement-grid.js";
|
|
24
|
+
import { DISTRIBUTED_DIR_SYSTEM, recordCells, DISTRIBUTED_PROVENANCE_FILE, DISTRIBUTED_PROVENANCE_SCHEMA, type DistributedProvenance } from "./distributed-records.js";
|
|
21
25
|
import { sha256Hex } from "./probe-output.js";
|
|
22
26
|
import { QUALIFICATION_RECORD_SCHEMA } from "./qualification-record.js";
|
|
23
27
|
|
|
24
28
|
export interface RecordsPackageArgs {
|
|
25
|
-
|
|
29
|
+
/** Qualification directories, or run roots holding `b<rate>-<quantum>/qualification`: one set per cell. */
|
|
30
|
+
readonly sources: readonly string[];
|
|
31
|
+
/** The cell of a lone directory whose name says none. */
|
|
32
|
+
readonly cell?: GridCell;
|
|
26
33
|
readonly hostClass: string;
|
|
27
34
|
readonly set: string;
|
|
28
35
|
/** A JSON file, or the JSON itself. */
|
|
@@ -44,18 +51,22 @@ export function recordsPackageHelp(): string {
|
|
|
44
51
|
return [
|
|
45
52
|
"plugin-qualify-records-package: the Debian package of a device's qualification records, for the distributed layer",
|
|
46
53
|
"",
|
|
47
|
-
" plugin-qualify-records-package <qualification dir
|
|
54
|
+
" plugin-qualify-records-package <qualification dir|run root>... --host-class <class> --set <set> --provenance <json> [options]",
|
|
55
|
+
"",
|
|
56
|
+
" one package per cell (rate and quantum): each qualification directory is a cell, named by its base",
|
|
57
|
+
" (<run>/b48000-512/qualification); a run root stands for every b<rate>-<quantum>/qualification under it",
|
|
48
58
|
"",
|
|
49
59
|
" --host-class <class> the machine class the records were measured on (rpi5): the directory they install in",
|
|
50
|
-
" --set <set> the plugin set they cover (zynthian-vangelis)",
|
|
60
|
+
" --set <set> the plugin set they cover (zynthian-vangelis); each package's set is <set>-<rate>-<quantum>",
|
|
61
|
+
" --cell <rate>/<quantum> the cell of a lone directory not named b<rate>-<quantum> whose records hold several",
|
|
51
62
|
" --provenance <json> a JSON file, or the JSON itself: the image, the package versions, the source revisions",
|
|
52
63
|
" --version <version> the package's version (default: today, 2026.10.29)",
|
|
53
64
|
" --out <dir> where the .deb is written (default: .)",
|
|
54
65
|
" --maintainer <who> the package's Maintainer field",
|
|
55
66
|
" --help this text",
|
|
56
67
|
"",
|
|
57
|
-
`
|
|
58
|
-
"exit 0
|
|
68
|
+
`each package installs ${DISTRIBUTED_DIR_SYSTEM}/<class>/<set>-<rate>-<quantum>/ and depends on this plugin-qualify's minor, up to the next one`,
|
|
69
|
+
"exit 0 every package was written, 2 one could not be",
|
|
59
70
|
"",
|
|
60
71
|
].join("\n");
|
|
61
72
|
}
|
|
@@ -63,7 +74,7 @@ export function recordsPackageHelp(): string {
|
|
|
63
74
|
export function parseRecordsPackageArgs(argv: readonly string[]): { ok: true; args: RecordsPackageArgs } | { ok: true; help: true } | { ok: false; error: string } {
|
|
64
75
|
const values = new Map<string, string>();
|
|
65
76
|
const positional: string[] = [];
|
|
66
|
-
const valued = new Set(["--host-class", "--set", "--provenance", "--version", "--out", "--maintainer"]);
|
|
77
|
+
const valued = new Set(["--host-class", "--set", "--cell", "--provenance", "--version", "--out", "--maintainer"]);
|
|
67
78
|
for (let i = 0; i < argv.length; i++) {
|
|
68
79
|
const a = argv[i]!;
|
|
69
80
|
if (a === "--help") return { ok: true, help: true };
|
|
@@ -74,19 +85,24 @@ export function parseRecordsPackageArgs(argv: readonly string[]): { ok: true; ar
|
|
|
74
85
|
} else if (a.startsWith("--")) return { ok: false, error: `unknown argument ${a}` };
|
|
75
86
|
else positional.push(a);
|
|
76
87
|
}
|
|
77
|
-
if (positional.length
|
|
88
|
+
if (positional.length === 0) return { ok: false, error: "name a qualification directory or a run root" };
|
|
78
89
|
for (const flag of ["--host-class", "--set", "--provenance"]) if (!values.has(flag)) return { ok: false, error: `${flag} is needed` };
|
|
79
90
|
const hostClass = values.get("--host-class")!;
|
|
80
91
|
const set = values.get("--set")!;
|
|
81
92
|
for (const [what, v] of [["--host-class", hostClass], ["--set", set]] as const) {
|
|
82
93
|
if (!DEB_WORD.test(v)) return { ok: false, error: `${what} ${v}: a package name holds lowercase letters, digits, + - and . only` };
|
|
83
94
|
}
|
|
95
|
+
const cellText = values.get("--cell");
|
|
96
|
+
const cell = cellText === undefined ? undefined : parseCell(cellText);
|
|
97
|
+
if (cellText !== undefined && cell === undefined) return { ok: false, error: `--cell ${cellText} is not <rate>/<quantum>` };
|
|
98
|
+
if (cell !== undefined && positional.length !== 1) return { ok: false, error: "--cell names the cell of one directory; name one" };
|
|
84
99
|
const version = values.get("--version");
|
|
85
100
|
if (version !== undefined && !DEB_VERSION.test(version)) return { ok: false, error: `--version ${version} is not a Debian version` };
|
|
86
101
|
return {
|
|
87
102
|
ok: true,
|
|
88
103
|
args: {
|
|
89
|
-
|
|
104
|
+
sources: positional,
|
|
105
|
+
...(cell === undefined ? {} : { cell }),
|
|
90
106
|
hostClass,
|
|
91
107
|
set,
|
|
92
108
|
provenance: values.get("--provenance")!,
|
|
@@ -97,6 +113,68 @@ export function parseRecordsPackageArgs(argv: readonly string[]): { ok: true; ar
|
|
|
97
113
|
};
|
|
98
114
|
}
|
|
99
115
|
|
|
116
|
+
function parseCell(text: string): GridCell | undefined {
|
|
117
|
+
const m = /^(\d+)\/(\d+)$/.exec(text.trim());
|
|
118
|
+
if (m === null) return undefined;
|
|
119
|
+
const [rate, quantum] = [Number(m[1]), Number(m[2])];
|
|
120
|
+
return rate > 0 && quantum > 0 && Number.isSafeInteger(rate) && Number.isSafeInteger(quantum) ? { rate, quantum } : undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** A run's base directory names its cell: `b48000-512`. */
|
|
124
|
+
const BASE_NAME = /^b(\d+)-(\d+)$/;
|
|
125
|
+
|
|
126
|
+
/** The set of a cell: `<set>-<rate>-<quantum>`. */
|
|
127
|
+
export const recordsSetName = (set: string, cell: GridCell): string => `${set}-${String(cell.rate)}-${String(cell.quantum)}`;
|
|
128
|
+
|
|
129
|
+
/** One qualification directory of a run and the cell its name says, when it says one. */
|
|
130
|
+
export interface RecordsSource {
|
|
131
|
+
readonly dir: string;
|
|
132
|
+
readonly named?: GridCell;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The qualification directories the arguments stand for, in name order: a directory holding records is one; a run
|
|
137
|
+
* root stands for every `b<rate>-<quantum>/qualification` (or `b<rate>-<quantum>` holding records) under it.
|
|
138
|
+
*/
|
|
139
|
+
export function recordsSources(args: readonly string[]): RecordsSource[] {
|
|
140
|
+
const holdsRecords = (d: string): boolean => {
|
|
141
|
+
try {
|
|
142
|
+
return readdirSync(d).some((n) => n.endsWith(".qualification.json") || n.endsWith(".unmeasured.json"));
|
|
143
|
+
} catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
const namedBy = (d: string): GridCell | undefined => {
|
|
148
|
+
for (const part of [basename(d), basename(dirname(d))]) {
|
|
149
|
+
const m = BASE_NAME.exec(part);
|
|
150
|
+
if (m !== null) return { rate: Number(m[1]), quantum: Number(m[2]) };
|
|
151
|
+
}
|
|
152
|
+
return undefined;
|
|
153
|
+
};
|
|
154
|
+
const out: RecordsSource[] = [];
|
|
155
|
+
for (const a of args) {
|
|
156
|
+
const dir = resolve(a);
|
|
157
|
+
statSync(dir);
|
|
158
|
+
if (holdsRecords(dir)) {
|
|
159
|
+
const named = namedBy(dir);
|
|
160
|
+
out.push({ dir, ...(named === undefined ? {} : { named }) });
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
const bases = readdirSync(dir).filter((n) => BASE_NAME.test(n)).sort();
|
|
164
|
+
let found = 0;
|
|
165
|
+
for (const b of bases) {
|
|
166
|
+
for (const d of [join(dir, b, "qualification"), join(dir, b)]) {
|
|
167
|
+
if (!holdsRecords(d)) continue;
|
|
168
|
+
out.push({ dir: d, named: namedBy(d)! });
|
|
169
|
+
found++;
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (found === 0) throw new Error(`${dir} holds no record and no b<rate>-<quantum> base with records`);
|
|
174
|
+
}
|
|
175
|
+
return out;
|
|
176
|
+
}
|
|
177
|
+
|
|
100
178
|
/** The package of a class and a set. */
|
|
101
179
|
export const recordsPackageName = (hostClass: string, set: string): string => `plugin-db-qualification-${hostClass}-${set}`;
|
|
102
180
|
|
|
@@ -122,7 +200,7 @@ export function today(now = new Date()): { version: string; date: string } {
|
|
|
122
200
|
}
|
|
123
201
|
|
|
124
202
|
/** The files of a qualification directory that go in the package: each record with its probe output, and what stays out, by name and why. */
|
|
125
|
-
export function collectRecords(stateDir: string): { records: string[]; files: string[]; left: { name: string; why: string }[] } {
|
|
203
|
+
export function collectRecords(stateDir: string, cell?: GridCell): { records: string[]; files: string[]; left: { name: string; why: string }[] } {
|
|
126
204
|
const names = readdirSync(stateDir).sort();
|
|
127
205
|
const records: string[] = [];
|
|
128
206
|
const files: string[] = [];
|
|
@@ -133,7 +211,7 @@ export function collectRecords(stateDir: string): { records: string[]; files: st
|
|
|
133
211
|
continue;
|
|
134
212
|
}
|
|
135
213
|
if (!name.endsWith(".qualification.json")) continue;
|
|
136
|
-
let record: { schema?: unknown; probe?: { file?: unknown; sha256?: unknown } };
|
|
214
|
+
let record: { schema?: unknown; probe?: { file?: unknown; sha256?: unknown }; measurements?: { grid?: readonly GridCell[] } };
|
|
137
215
|
try {
|
|
138
216
|
record = JSON.parse(readFileSync(join(stateDir, name), "utf8"));
|
|
139
217
|
} catch {
|
|
@@ -144,6 +222,11 @@ export function collectRecords(stateDir: string): { records: string[]; files: st
|
|
|
144
222
|
left.push({ name, why: `not a ${QUALIFICATION_RECORD_SCHEMA} naming its probe output` });
|
|
145
223
|
continue;
|
|
146
224
|
}
|
|
225
|
+
const cells = gridCellsOf(record);
|
|
226
|
+
if (cell !== undefined && !cells.includes(cellKey(cell))) {
|
|
227
|
+
left.push({ name, why: `measured at ${cells.length === 0 ? "no cell" : cells.join(", ")}, not at its set's ${cellKey(cell)}` });
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
147
230
|
let probe: Buffer;
|
|
148
231
|
try {
|
|
149
232
|
probe = readFileSync(join(stateDir, record.probe.file));
|
|
@@ -161,6 +244,29 @@ export function collectRecords(stateDir: string): { records: string[]; files: st
|
|
|
161
244
|
return { records, files: [...new Set(files)].sort(), left };
|
|
162
245
|
}
|
|
163
246
|
|
|
247
|
+
/** The cells of a record's grid, `rate/quantum`, by rate then quantum: `recordCells`, keyed. */
|
|
248
|
+
const gridCellsOf = (record: { measurements?: { grid?: readonly GridCell[] } }): string[] =>
|
|
249
|
+
recordCells({ measurements: { grid: Array.isArray(record.measurements?.grid) ? record.measurements.grid : [] } } as unknown as Parameters<typeof recordCells>[0]).map(cellKey);
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The cell of a source: its base's name, else the one given, else the one cell every record of it holds alone.
|
|
253
|
+
* Throws, naming the cells, when none decides it.
|
|
254
|
+
*/
|
|
255
|
+
export function cellOfSource(source: RecordsSource, given?: GridCell): GridCell {
|
|
256
|
+
if (source.named !== undefined) return source.named;
|
|
257
|
+
if (given !== undefined) return given;
|
|
258
|
+
const seen = new Set<string>();
|
|
259
|
+
for (const name of readdirSync(source.dir).filter((n) => n.endsWith(".qualification.json"))) {
|
|
260
|
+
try {
|
|
261
|
+
for (const c of gridCellsOf(JSON.parse(readFileSync(join(source.dir, name), "utf8")))) seen.add(c);
|
|
262
|
+
} catch {
|
|
263
|
+
// a record that does not read is named by the collection, and decides no cell
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (seen.size === 1) return parseCell([...seen][0]!)!;
|
|
267
|
+
throw new Error(`${source.dir}: its records are measured at ${seen.size === 0 ? "no cell" : [...seen].join(", ")}; a set is one cell: name the base b<rate>-<quantum> or give --cell`);
|
|
268
|
+
}
|
|
269
|
+
|
|
164
270
|
/** The provenance argument read: a file's JSON, or the JSON itself. */
|
|
165
271
|
function readProvenanceArg(arg: string): Record<string, unknown> {
|
|
166
272
|
const text = arg.trimStart().startsWith("{") ? arg : readFileSync(arg, "utf8");
|
|
@@ -203,6 +309,9 @@ function tarXz(tree: string, out: string, mtime: number): void {
|
|
|
203
309
|
|
|
204
310
|
export interface BuiltRecordsPackage {
|
|
205
311
|
readonly path: string;
|
|
312
|
+
/** The set, `<set>-<rate>-<quantum>`, and its cell. */
|
|
313
|
+
readonly set: string;
|
|
314
|
+
readonly cell: GridCell;
|
|
206
315
|
readonly name: string;
|
|
207
316
|
readonly version: string;
|
|
208
317
|
readonly records: number;
|
|
@@ -211,28 +320,49 @@ export interface BuiltRecordsPackage {
|
|
|
211
320
|
readonly left: readonly { name: string; why: string }[];
|
|
212
321
|
}
|
|
213
322
|
|
|
214
|
-
/**
|
|
215
|
-
|
|
216
|
-
|
|
323
|
+
/**
|
|
324
|
+
* One package per cell of `args.sources`, written into `args.out`, in the order of the cells. Every source's cell is
|
|
325
|
+
* decided and no two may share one before any package is written; throws, naming why, when one cannot be built.
|
|
326
|
+
*/
|
|
327
|
+
export function buildRecordsPackages(args: RecordsPackageArgs, now = new Date()): BuiltRecordsPackage[] {
|
|
328
|
+
const sources = recordsSources(args.sources);
|
|
329
|
+
if (args.cell !== undefined && sources.length !== 1) throw new Error("--cell names the cell of one directory, and these are several");
|
|
330
|
+
const cells = sources.map((s) => ({ source: s, cell: cellOfSource(s, args.cell) }));
|
|
331
|
+
const seen = new Map<string, string>();
|
|
332
|
+
for (const { source, cell } of cells) {
|
|
333
|
+
const other = seen.get(cellKey(cell));
|
|
334
|
+
if (other !== undefined) throw new Error(`${source.dir} and ${other} are both measured at ${cellKey(cell)}: one set per cell`);
|
|
335
|
+
seen.set(cellKey(cell), source.dir);
|
|
336
|
+
}
|
|
337
|
+
return cells
|
|
338
|
+
.sort((a, b) => a.cell.rate - b.cell.rate || a.cell.quantum - b.cell.quantum)
|
|
339
|
+
.map(({ source, cell }) => buildRecordsPackage({ ...args, stateDir: source.dir, cell }, now));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** The package of one cell's directory, written into `args.out`. Throws, naming why, when it cannot be built. */
|
|
343
|
+
function buildRecordsPackage(args: RecordsPackageArgs & { readonly stateDir: string; readonly cell: GridCell }, now: Date): BuiltRecordsPackage {
|
|
344
|
+
const set = recordsSetName(args.set, args.cell);
|
|
345
|
+
const name = recordsPackageName(args.hostClass, set);
|
|
217
346
|
const range = recordsDependsRange(ownVersion());
|
|
218
347
|
if (range === undefined) throw new Error("this plugin-qualify names no version, so the package cannot say which one reads it");
|
|
219
348
|
const day = today(now);
|
|
220
349
|
const version = args.version ?? day.version;
|
|
221
350
|
statSync(args.stateDir);
|
|
222
|
-
const { records, files, left } = collectRecords(args.stateDir);
|
|
351
|
+
const { records, files, left } = collectRecords(args.stateDir, args.cell);
|
|
223
352
|
if (records.length === 0) throw new Error(`${args.stateDir} holds no record that can go in the package`);
|
|
224
353
|
const provenance: DistributedProvenance = {
|
|
225
354
|
...readProvenanceArg(args.provenance),
|
|
226
355
|
schema: DISTRIBUTED_PROVENANCE_SCHEMA,
|
|
227
|
-
set
|
|
356
|
+
set,
|
|
228
357
|
hostClass: args.hostClass,
|
|
358
|
+
cell: { rate: args.cell.rate, quantum: args.cell.quantum },
|
|
229
359
|
package: { name, version },
|
|
230
360
|
date: day.date,
|
|
231
361
|
};
|
|
232
362
|
const work = mkdtempSync(join(tmpdir(), "records-package-"));
|
|
233
363
|
try {
|
|
234
364
|
const data = join(work, "data");
|
|
235
|
-
const rel = join("usr", "share", "omx", "qualification", args.hostClass,
|
|
365
|
+
const rel = join("usr", "share", "omx", "qualification", args.hostClass, set);
|
|
236
366
|
const dir = join(data, rel);
|
|
237
367
|
mkdirSync(dir, { recursive: true });
|
|
238
368
|
for (const f of files) copyFileSync(join(args.stateDir, f), join(dir, f));
|
|
@@ -259,9 +389,10 @@ export function buildRecordsPackage(args: RecordsPackageArgs, now = new Date()):
|
|
|
259
389
|
"Section: sound",
|
|
260
390
|
"Priority: optional",
|
|
261
391
|
"Multi-Arch: foreign",
|
|
262
|
-
`Description: plugin-db qualification layer: ${args.set} set for ${args.hostClass}`,
|
|
392
|
+
`Description: plugin-db qualification layer: ${args.set} set for ${args.hostClass} at ${String(args.cell.rate)} Hz, ${String(args.cell.quantum)} frames`,
|
|
263
393
|
` The qualification layer of the plugin database: the plugin-qualify records`,
|
|
264
|
-
` of the ${args.set} plugin set, measured on a ${args.hostClass} device
|
|
394
|
+
` of the ${args.set} plugin set, measured on a ${args.hostClass} device at ${String(args.cell.rate)} Hz`,
|
|
395
|
+
` and a quantum of ${String(args.cell.quantum)} frames. plugin-qualify-sync takes a plugin from them instead of`,
|
|
265
396
|
" measuring it, and only when every file of the installed plugin hashes as the",
|
|
266
397
|
" measured one did; any other plugin is measured on the device as before.",
|
|
267
398
|
"",
|
|
@@ -282,7 +413,7 @@ export function buildRecordsPackage(args: RecordsPackageArgs, now = new Date()):
|
|
|
282
413
|
],
|
|
283
414
|
mtime,
|
|
284
415
|
);
|
|
285
|
-
return { path, name, version, records: records.length, files: installed.length, left };
|
|
416
|
+
return { path, set, cell: args.cell, name, version, records: records.length, files: installed.length, left };
|
|
286
417
|
} finally {
|
|
287
418
|
rmSync(work, { recursive: true, force: true });
|
|
288
419
|
}
|
package/tools/lv2-measure.mjs
CHANGED
|
@@ -50,6 +50,7 @@ import {
|
|
|
50
50
|
buildPipeline,
|
|
51
51
|
collectHostFacts,
|
|
52
52
|
HOSTING_PASS_DIMENSIONS,
|
|
53
|
+
PROBED_DIMENSIONS,
|
|
53
54
|
hostingLine,
|
|
54
55
|
INITIAL_PROGRESS,
|
|
55
56
|
parseMeasureArgs,
|
|
@@ -131,6 +132,20 @@ function hostingTools() {
|
|
|
131
132
|
return { tool, interposer };
|
|
132
133
|
}
|
|
133
134
|
|
|
135
|
+
/**
|
|
136
|
+
* The native measurer for the pin step of a run with no hosting pass: absent, the run still measures and says on stderr
|
|
137
|
+
* that its plugins carry no integrity block (it does not need the interposer, so a half-populated pair is not refused here).
|
|
138
|
+
*/
|
|
139
|
+
function pinTools() {
|
|
140
|
+
const { tools } = locateNativeTools([LV2_MEASURE_TOOL_NAME, RT_INTERPOSE_NAME], process.env, existsSync);
|
|
141
|
+
const [tool, interposer] = tools;
|
|
142
|
+
if (tool?.path === undefined) {
|
|
143
|
+
process.stderr.write(`${TOOL_NAME}: no ${LV2_MEASURE_TOOL_NAME} at ${tool?.looked.join(", ") ?? "its places"}: the plugins are written with no integrity block (build it with make -C native or set ${tool?.setting})\n`);
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
return { tool: tool.path, interposer: interposer?.path ?? "" };
|
|
147
|
+
}
|
|
148
|
+
|
|
134
149
|
/** The hosting pass: the measurer once per plugin, each in its own process, its lines written as JSONL. */
|
|
135
150
|
function runHosting(step, uris, onProgress) {
|
|
136
151
|
const lines = [];
|
|
@@ -201,7 +216,8 @@ async function main() {
|
|
|
201
216
|
const options = parsed.options;
|
|
202
217
|
|
|
203
218
|
const hosting = options.dimensions.some((d) => HOSTING_PASS_DIMENSIONS.includes(d));
|
|
204
|
-
const
|
|
219
|
+
const probedOnly = !hosting && options.dimensions.some((d) => PROBED_DIMENSIONS.includes(d));
|
|
220
|
+
const pipeline = buildPipeline(options, hosting ? hostingTools() : probedOnly ? pinTools() : undefined);
|
|
205
221
|
|
|
206
222
|
if (options.dryRun) {
|
|
207
223
|
process.stderr.write(
|
|
@@ -242,11 +258,13 @@ async function main() {
|
|
|
242
258
|
const measuredAt = new Date().toISOString();
|
|
243
259
|
emit({ step: "start", steps: pipeline.steps.map((s) => s.id) });
|
|
244
260
|
let hostingLines;
|
|
261
|
+
let pinLines;
|
|
245
262
|
for (const step of pipeline.steps) {
|
|
246
263
|
process.stderr.write(`== ${step.id}\n`);
|
|
247
264
|
if (step.perPlugin) {
|
|
248
265
|
const uris = options.plugins ?? urisFromScan(readJson(pipeline.hostingScan));
|
|
249
|
-
|
|
266
|
+
if (step.id === "pin") pinLines = runHosting(step, uris, onProgress);
|
|
267
|
+
else hostingLines = runHosting(step, uris, onProgress);
|
|
250
268
|
} else {
|
|
251
269
|
await runStep(step, onProgress);
|
|
252
270
|
}
|
|
@@ -257,6 +275,7 @@ async function main() {
|
|
|
257
275
|
options,
|
|
258
276
|
...(pipeline.finalCatalog === undefined ? {} : { catalog: readJson(pipeline.finalCatalog) }),
|
|
259
277
|
...(hostingLines === undefined ? {} : { hosting: hostingLines }),
|
|
278
|
+
...(pinLines === undefined ? {} : { pins: pinLines }),
|
|
260
279
|
costProvenance:
|
|
261
280
|
pipeline.costProvenance !== undefined && existsSync(pipeline.costProvenance)
|
|
262
281
|
? readJson(pipeline.costProvenance)
|