@openmixer/plugin-qualify 0.7.0 → 0.9.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.
Files changed (46) hide show
  1. package/README.md +55 -3
  2. package/bin/plugin-qualify.mjs +59 -24
  3. package/dist/clap-loader.d.ts +25 -1
  4. package/dist/clap-loader.js +88 -7
  5. package/dist/clap-loader.js.map +1 -1
  6. package/dist/clap-qualify.d.ts +3 -2
  7. package/dist/clap-qualify.js +15 -15
  8. package/dist/clap-qualify.js.map +1 -1
  9. package/dist/hosting-suitability.js +1 -1
  10. package/dist/hosting-suitability.js.map +1 -1
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.js +1 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/lv2-loader.d.ts +5 -0
  15. package/dist/lv2-loader.js +30 -2
  16. package/dist/lv2-loader.js.map +1 -1
  17. package/dist/measurement/cli.d.ts +13 -0
  18. package/dist/measurement/cli.js +68 -2
  19. package/dist/measurement/cli.js.map +1 -1
  20. package/dist/pins-file.d.ts +76 -0
  21. package/dist/pins-file.js +165 -0
  22. package/dist/pins-file.js.map +1 -0
  23. package/dist/qualify-cli.d.ts +2 -0
  24. package/dist/qualify-cli.js +7 -1
  25. package/dist/qualify-cli.js.map +1 -1
  26. package/dist/qualify-declarations.d.ts +9 -1
  27. package/dist/qualify-declarations.js +9 -1
  28. package/dist/qualify-declarations.js.map +1 -1
  29. package/dist/qualify-options.js +6 -1
  30. package/dist/qualify-options.js.map +1 -1
  31. package/dist/tool-paths.d.ts +35 -1
  32. package/dist/tool-paths.js +50 -6
  33. package/dist/tool-paths.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/clap-loader.ts +99 -7
  36. package/src/clap-qualify.ts +14 -21
  37. package/src/hosting-suitability.ts +1 -1
  38. package/src/index.ts +1 -0
  39. package/src/lv2-loader.ts +28 -2
  40. package/src/measurement/cli.ts +77 -2
  41. package/src/pins-file.ts +200 -0
  42. package/src/qualify-cli.ts +8 -1
  43. package/src/qualify-declarations.ts +10 -1
  44. package/src/qualify-options.ts +7 -1
  45. package/src/tool-paths.ts +68 -5
  46. package/tools/lv2-measure.mjs +29 -9
package/src/tool-paths.ts CHANGED
@@ -50,11 +50,28 @@ export function lv2MeasureNativePath(): string {
50
50
  return join(nativeBuildDir(), LV2_MEASURE_TOOL_NAME);
51
51
  }
52
52
 
53
- /** Where a native tool is looked for when no setting names it, in order: the installed directories, then the own build. */
53
+ /**
54
+ * Where the native tools are looked for when no setting names them, in order: the package's own build,
55
+ * then the installed directories. An installed package has no `native/build/`, so its own build first
56
+ * changes nothing there, and in a checkout `make -C native` is never shadowed by an older install.
57
+ */
58
+ export function nativeToolDirs(): string[] {
59
+ return [nativeBuildDir(), ...NATIVE_TOOL_DIRS];
60
+ }
61
+
62
+ /** Where one native tool is looked for when no setting names it, in `nativeToolDirs` order. */
54
63
  export function nativeToolCandidates(name: string): string[] {
55
- return [...NATIVE_TOOL_DIRS, nativeBuildDir()].map((dir) => join(dir, name));
64
+ return nativeToolDirs().map((dir) => join(dir, name));
56
65
  }
57
66
 
67
+ function settingOf(name: string): string {
68
+ const setting = NATIVE_TOOLS.find((t) => t.name === name)?.env;
69
+ if (setting === undefined) throw new Error(`${name} is not one of NATIVE_TOOLS`);
70
+ return setting;
71
+ }
72
+
73
+ const isSet = (v: string | undefined): v is string => v !== undefined && v !== "";
74
+
58
75
  /**
59
76
  * One of `NATIVE_TOOLS`: its setting when set (and only that), else the first candidate that exists.
60
77
  * `path` is absent when none does; `looked` is every place tried, for the refusal to name.
@@ -64,10 +81,56 @@ export function locateNativeTool(
64
81
  env: Readonly<Record<string, string | undefined>>,
65
82
  exists: (path: string) => boolean,
66
83
  ): { readonly path?: string; readonly looked: readonly string[]; readonly setting: string } {
67
- const setting = NATIVE_TOOLS.find((t) => t.name === name)?.env;
68
- if (setting === undefined) throw new Error(`${name} is not one of NATIVE_TOOLS`);
84
+ const setting = settingOf(name);
69
85
  const v = env[setting];
70
- const looked = v === undefined || v === "" ? nativeToolCandidates(name) : [v];
86
+ const looked = isSet(v) ? [v] : nativeToolCandidates(name);
71
87
  const path = looked.find((p) => exists(p));
72
88
  return { ...(path === undefined ? {} : { path }), looked, setting };
73
89
  }
90
+
91
+ /** One tool of a set `locateNativeTools` located, as `locateNativeTool` reports it. */
92
+ export interface LocatedNativeTool {
93
+ readonly name: string;
94
+ readonly path?: string;
95
+ readonly looked: readonly string[];
96
+ readonly setting: string;
97
+ }
98
+
99
+ /**
100
+ * Tools that run together and share an ABI (a prober or the measurer, and the interposer through
101
+ * `rt-interpose.h`): each set one is its setting; the rest come from ONE directory, the first of
102
+ * `nativeToolDirs` holding any of them. When that directory lacks some of them, `halfPopulated`
103
+ * names it and what it lacks, and the run is refused: pairing a tool from one place with its
104
+ * interposer from another would let a version skew corrupt the verdict without an error.
105
+ */
106
+ export function locateNativeTools(
107
+ names: readonly string[],
108
+ env: Readonly<Record<string, string | undefined>>,
109
+ exists: (path: string) => boolean,
110
+ ): {
111
+ readonly tools: readonly LocatedNativeTool[];
112
+ readonly halfPopulated?: { readonly dir: string; readonly holds: readonly string[]; readonly lacks: readonly string[] };
113
+ } {
114
+ const unset = names.filter((name) => !isSet(env[settingOf(name)]));
115
+ const dir = nativeToolDirs().find((d) => unset.some((name) => exists(join(d, name))));
116
+ const tools = names.map((name): LocatedNativeTool => {
117
+ const setting = settingOf(name);
118
+ const v = env[setting];
119
+ if (isSet(v)) return { name, setting, looked: [v], ...(exists(v) ? { path: v } : {}) };
120
+ const path = dir === undefined ? undefined : join(dir, name);
121
+ return { name, setting, looked: nativeToolCandidates(name), ...(path !== undefined && exists(path) ? { path } : {}) };
122
+ });
123
+ if (dir === undefined) return { tools };
124
+ const lacks = unset.filter((name) => !exists(join(dir, name)));
125
+ if (lacks.length === 0) return { tools };
126
+ return { tools, halfPopulated: { dir, holds: unset.filter((name) => !lacks.includes(name)), lacks } };
127
+ }
128
+
129
+ /** The refusal for a half-populated directory: what it holds, what it lacks, and the settings that would decide. */
130
+ export function halfPopulatedRefusal(half: { readonly dir: string; readonly holds: readonly string[]; readonly lacks: readonly string[] }): string {
131
+ const settings = [...half.holds, ...half.lacks].map(settingOf).join(" and ");
132
+ return (
133
+ `${half.dir} holds ${half.holds.join(", ")} but not ${half.lacks.join(", ")}: they share one ABI and are ` +
134
+ `taken from one place, so install or build them together, or set ${settings}`
135
+ );
136
+ }
@@ -8,8 +8,8 @@
8
8
  * Standalone by construction: it needs `python3` with the `lilv` bindings, the two probers
9
9
  * next to this file (`scan.py`, `benchmark.py`), and an LV2 world to look at; the hosting
10
10
  * dimensions need the package's native measurer and interposer (`lv2_measure`,
11
- * `rt_interpose.so`), each its setting (`OMX_LV2_MEASURE`, `OMX_RT_INTERPOSE`), else where the
12
- * package installs it, else `native/build` (`make -C native`). It does not
11
+ * `rt_interpose.so`), each its setting (`OMX_LV2_MEASURE`, `OMX_RT_INTERPOSE`), else
12
+ * `native/build` (`make -C native`), else where the package installs it. It does not
13
13
  * need the OpenMixer engine, its server, PipeWire, JACK, an audio interface, or root. Nothing
14
14
  * it writes is openmixer-shaped — the output is the interchange format described in
15
15
  * `src/measurement/format.ts`, which any LV2 host can read.
@@ -60,7 +60,7 @@ import {
60
60
  urisFromScan,
61
61
  } from "../dist/measurement/index.js";
62
62
  import { LV2_HOSTING_TIMEOUT_S, LV2_MEASURE_TOOL_NAME, RT_INTERPOSE_NAME } from "../dist/qualify-declarations.js";
63
- import { locateNativeTool } from "../dist/tool-paths.js";
63
+ import { halfPopulatedRefusal, locateNativeTools } from "../dist/tool-paths.js";
64
64
 
65
65
  const HERE = dirname(fileURLToPath(import.meta.url));
66
66
 
@@ -85,6 +85,22 @@ const HELP = `${TOOL_NAME} ${TOOL_VERSION} — measure LV2 plugin latency and CP
85
85
  --dry-run Print the plan and probe nothing.
86
86
  --progress-json Emit machine-readable progress on stderr, one '@progress {...}' per line.
87
87
  --help This text.
88
+
89
+ The hosting pass's host features are omx-clap-core's LV2 feature provider; each of its options is
90
+ a flag, passed to every run of lv2_measure and named in each plugin's record (featureOptions):
91
+
92
+ --features LIST Kinds offered: map,unmap,options,bounded,worker,default-state,log
93
+ (default: all but default-state; empty: none).
94
+ --strict Refuse a plugin requiring a feature not offered, naming it (refusedFeature).
95
+ --no-strict Hand it to its own instantiate without it (default).
96
+ --block-declares LIST Buf-size declarations when bounded is offered: bounded,fixed,pow2
97
+ (default: bounded; empty: none).
98
+ --block-min N bufsz:minBlockLength (default: 1; 0: the run's block).
99
+ --block-max N bufsz:maxBlockLength (default: 0, the run's block).
100
+ --block-nominal N bufsz:nominalBlockLength (default: 0, the maximum).
101
+ --log MODE log:log lines: silent, route (to stderr) or capture (into the record)
102
+ (default: silent).
103
+ --log-capture-bytes N The capture buffer, its NUL included (default: 4096).
88
104
  `;
89
105
 
90
106
  function fail(message, code = 2) {
@@ -96,16 +112,20 @@ function readJson(path) {
96
112
  return JSON.parse(readFileSync(path, "utf8"));
97
113
  }
98
114
 
99
- /** The native measurer and its interposer, located; absent, the run is refused naming where it looked. */
115
+ /**
116
+ * The native measurer and its interposer, located together (`locateNativeTools`): absent, or half there
117
+ * in the one place they come from, the run is refused naming where it looked.
118
+ */
100
119
  function hostingTools() {
101
- const at = (name) => {
102
- const found = locateNativeTool(name, process.env, existsSync);
120
+ const { tools, halfPopulated } = locateNativeTools([LV2_MEASURE_TOOL_NAME, RT_INTERPOSE_NAME], process.env, existsSync);
121
+ if (halfPopulated !== undefined) fail(`no measurer and interposer pair: ${halfPopulatedRefusal(halfPopulated)}`);
122
+ const [tool, interposer] = tools.map((found) => {
103
123
  if (found.path === undefined) {
104
- fail(`no ${name} at ${found.looked.join(", ")}: install the plugin-qualify package, build it with make -C native, or set ${found.setting}`);
124
+ fail(`no ${found.name} at ${found.looked.join(", ")}: install the plugin-qualify package, build it with make -C native, or set ${found.setting}`);
105
125
  }
106
126
  return found.path;
107
- };
108
- return { tool: at(LV2_MEASURE_TOOL_NAME), interposer: at(RT_INTERPOSE_NAME) };
127
+ });
128
+ return { tool, interposer };
109
129
  }
110
130
 
111
131
  /** The hosting pass: the measurer once per plugin, each in its own process, its lines written as JSONL. */