[{"data":1,"prerenderedAt":45},["ShallowReactive",2],{"project-plugin-qualify-":3},{"page":4,"others":44},{"slug":5,"src":6,"title":7,"source":8,"toc":9,"html":43},"","README.md","plugin-qualify","https:\u002F\u002Fgithub.com\u002FFreeMixer\u002Fplugin-qualify\u002Fblob\u002F949bed5011c9f3660cacb211725aea4647a0072d\u002FREADME.md",[10,13,16,19,22,25,28,31,34,37,40],{"id":11,"title":12},"install","Install",{"id":14,"title":15},"run-it-on-your-own-plugin","Run it on your own plugin",{"id":17,"title":18},"the-measurement-grid","The measurement grid",{"id":20,"title":21},"clap-plugins-the-rt-safety-verdict","CLAP plugins: the RT-safety verdict",{"id":23,"title":24},"judging-a-probe-output","Judging a probe output",{"id":26,"title":27},"adjusting-a-verdict-by-what-the-plugin-did-on-this-machine","Adjusting a verdict by what the plugin did on this machine",{"id":29,"title":30},"lv2-plugins-the-hosting-measurement","LV2 plugins: the hosting measurement",{"id":32,"title":33},"scanned-descriptors","Scanned descriptors",{"id":35,"title":36},"what-the-verdicts-mean","What the verdicts mean",{"id":38,"title":39},"reproducing-a-failure","Reproducing a failure",{"id":41,"title":42},"for-other-projects-zynthian-mod-packagers","For other projects (zynthian, MOD, packagers)","\u003Ch1 id=\"plugin-qualify\">plugin-qualify\u003C\u002Fh1>\n\u003Cp>Can this audio plugin be hosted in a real-time audio engine, and at what cost? \u003Ccode>plugin-qualify\u003C\u002Fcode>\nscans a plugin, measures it offline (latency and CPU cost per rate and quantum), runs it\nunder a real host (mod-host, with an AddressSanitizer build), and gives one verdict.\nLicence: GPL-3.0-or-later (\u003Ccode>LICENSE\u003C\u002Fcode>).\u003C\u002Fp>\n\u003Ch2 id=\"install\">Install\u003C\u002Fh2>\n\u003Cp>plugin-qualify is published, signed, in the OpenMixer package channel, for Fedora 44 (x86_64, aarch64)\nand for Debian bookworm and trixie, including Raspberry Pi OS (amd64, arm64).\u003C\u002Fp>\n\u003Cp>Fedora:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>sudo dnf install https:\u002F\u002Fwww.openmixer.org\u002Frpm\u002Fopenmixer-release.noarch.rpm\nsudo dnf install plugin-qualify\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Debian and Raspberry Pi OS:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>curl -fsSLO https:\u002F\u002Fwww.openmixer.org\u002Fdeb\u002Fopenmixer-release.deb &amp;&amp; sudo apt install .\u002Fopenmixer-release.deb\nsudo apt update\nsudo apt install plugin-qualify\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>From the npm registry:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>pnpm add @openmixer\u002Fplugin-qualify\n# or, to install the command globally:\nnpm install -g @openmixer\u002Fplugin-qualify\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The npm package ships the TypeScript command only, not the native tools. The CLAP prober, the LV2 measurer and\nthe interposer come with the RPM or DEB, or from \u003Ccode>make -C native\u003C\u002Fcode> in a checkout; with the npm package alone,\npoint \u003Ccode>OMX_CLAP_QUALIFY\u003C\u002Fcode>, \u003Ccode>OMX_LV2_MEASURE\u003C\u002Fcode> and \u003Ccode>OMX_RT_INTERPOSE\u003C\u002Fcode> at them (the probe runner is plugin-hostd's, \u003Ccode>OMX_PROBE_RUNNER\u003C\u002Fcode>), or CLAP qualification and the LV2\nhosting dimensions refuse with &quot;no CLAP prober at ...&quot;.\u003C\u002Fp>\n\u003Cp>The GitHub release also carries the packed tarball, \u003Ccode>openmixer-plugin-qualify-&lt;version&gt;.tgz\u003C\u002Fcode>. To run from a\ncheckout instead (Node.js 20 or newer and pnpm 10, see \u003Ccode>BUILDING.md\u003C\u002Fcode>):\u003C\u002Fp>\n\u003Cpre>\u003Ccode>git clone https:\u002F\u002Fgithub.com\u002FFreeMixer\u002Fplugin-qualify.git\ncd plugin-qualify\npnpm install\npnpm exec plugin-qualify --help\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>It needs Node.js 18 or newer to run. The measuring tools also want Python 3 with the lilv bindings\n(\u003Ccode>python3-lilv\u003C\u002Fcode>) and, for the hosting sweep, \u003Ccode>mod-host\u003C\u002Fcode>. The \u003Ccode>Containerfile\u003C\u002Fcode> builds an image with all of\nthem. The commands below are written as \u003Ccode>plugin-qualify\u003C\u002Fcode>; from a checkout run them with \u003Ccode>pnpm exec\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Ch2 id=\"run-it-on-your-own-plugin\">Run it on your own plugin\u003C\u002Fh2>\n\u003Cpre>\u003Ccode>plugin-qualify urn:example:my-plugin          # one plugin, by URI\nplugin-qualify .\u002Fmy-plugin.lv2                # every plugin in a bundle\nplugin-qualify --all --out .\u002Fqualification   # every LV2 bundle on LV2_PATH and every CLAP on the search path, one record each\nplugin-qualify --help                         # every option and its default\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>--format json\u003C\u002Fcode> prints the verdicts as JSON; \u003Ccode>--out &lt;dir&gt;\u003C\u002Fcode> writes the full measurement document.\n\u003Ccode>--isolate\u003C\u002Fcode> measures each plugin in a fresh worker so a crash or hang takes only that plugin. The \u003Ccode>Containerfile\u003C\u002Fcode> builds an image\nwith lilv, mod-host and the ASan mod-host, if you do not want to install them yourself.\u003C\u002Fp>\n\u003Cp>A named LV2 plugin is measured the same way as a CLAP one: through the probe runner, then judged from the probe\noutput it writes (\u003Ccode>plugin-qualify judge\u003C\u002Fcode> below). Exit code: \u003Ccode>0\u003C\u002Fcode> every plugin qualified, \u003Ccode>1\u003C\u002Fcode> some did not, \u003Ccode>2\u003C\u002Fcode>\nsomething could not be measured.\u003C\u002Fp>\n\u003Ch2 id=\"the-measurement-grid\">The measurement grid\u003C\u002Fh2>\n\u003Cp>A plugin is measured in cells, each one sample rate and one quantum (block size in frames). The verdict a host\ngets reads the cell of that host's rate and quantum, so the grid has to hold it.\u003C\u002Fp>\n\u003Cpre>\u003Ccode>plugin-qualify --all --out .\u002Fq                          # the full grid (the default): 4 rates x 4 block sizes\nplugin-qualify --all --grid-hosts --out .\u002Fq             # the hosts' grid, the fast option (also --grid hosts)\nplugin-qualify --grid 48000\u002F128,96000\u002F1024 urn:my:fx    # exactly these cells\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cul>\n\u003Cli>\u003Ccode>--grid hosts\u003C\u002Fcode> (the fast option; \u003Ccode>--grid-hosts\u003C\u002Fcode> says the same): the rate and quantum of every host the run judges\n(each \u003Ccode>--host-profile\u003C\u002Fcode>, and the console's two tiers, \u003Ccode>openmixer-console\u003C\u002Fcode> and the format's isolated host), then\nquantum 512 at every \u003Ccode>--rates\u003C\u002Fcode> rate, so that a record stays comparable with the ones written before the grid.\nWith the four shipped hosts that is seven cells: 96000\u002F1024 (\u003Ccode>openmixer-console\u003C\u002Fcode>), 48000\u002F256 (\u003Ccode>jalv\u003C\u002Fcode>),\n48000\u002F128 (\u003Ccode>mod-host\u003C\u002Fcode>, \u003Ccode>omx-clap-host\u003C\u002Fcode>), and 512 at 44100, 48000, 96000 and 192000.\u003C\u002Fli>\n\u003Cli>\u003Ccode>--grid full\u003C\u002Fcode> (the default; every plugin is measured under the same conditions, so any host's verdict can be read at any cell and the figures compare): every \u003Ccode>--rates\u003C\u002Fcode> rate at every \u003Ccode>--quanta\u003C\u002Fcode> quantum, 16 cells with the defaults\n(44100, 48000, 96000, 192000 by 128, 256, 512, 1024).\u003C\u002Fli>\n\u003Cli>\u003Ccode>--grid &lt;rate&gt;\u002F&lt;quantum&gt;,...\u003C\u002Fcode>: exactly the cells named.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>The cells are taken in that order: the hosts' own cells first, in the order the hosts are named, then the\n512 cells, then the rest by rate and quantum. \u003Ccode>--cells-per-plugin-budget N\u003C\u002Fcode> keeps the first N, so a first pass\nover a large machine can measure the hosts' cells alone (\u003Ccode>--cells-per-plugin-budget 3\u003C\u002Fcode> with the four shipped hosts).\n\u003Ccode>--blocks N\u003C\u002Fcode> shortens the CLAP sweep of each cell from 512 blocks to N; the LV2 pass has a fixed shape (30\nlifecycles and 3 swept runs per cell) and is not shortened by it.\u003C\u002Fp>\n\u003Cp>An LV2 plugin is measured on the same grid as a CLAP one. plugin-hostd's runner measures an LV2 plugin at one\nrate per run (the highest it is given), which is why records written before the grid hold LV2 figures at 192000\nonly; plugin-qualify now runs it once per cell. A CLAP plugin is run once per quantum, at every rate of the grid\nat that quantum. The runs of one plugin and one offer make one probe output: the grid points of every run, the\nsteps of every run, and the run's outcome. The record and its byte copy hold that document.\u003C\u002Fp>\n\u003Cp>A run that ended badly leaves a grid point at each of its cells with the run's outcome in place of figures, so a\ncell is never silently absent. The cost a host is judged by is read at its cell: a measured cell is compared with\nthe host's budget; a cell whose run failed reads \u003Ccode>unknown\u003C\u002Fcode> with that run's outcome (\u003Ccode>probe.oom\u003C\u002Fcode>, say); only a\ncell the grid does not hold reads \u003Ccode>hosting.cost.unmeasured-at-rate-and-quantum\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Ch3 id=\"the-memory-bound\">The memory bound\u003C\u002Fh3>\n\u003Cp>A sampler loading its library can take all the memory of a small machine, and the kernel then kills it, or\nsomething else. Every run is bounded: \u003Ccode>--memory-limit &lt;bytes&gt;\u003C\u002Fcode> (\u003Ccode>512M\u003C\u002Fcode>, \u003Ccode>2G\u003C\u002Fcode>, or \u003Ccode>off\u003C\u002Fcode>), by default half of\nthe machine's RAM. The bound is the runner's data limit (\u003Ccode>--data-bytes\u003C\u002Fcode>, \u003Ccode>RLIMIT_DATA\u003C\u002Fcode> on the measuring child:\nits heap and private writable mappings), and the interposer the child runs under turns an allocation refused\nunder that limit into an immediate exit with status 121. A run that ends that way, or by a \u003Ccode>SIGKILL\u003C\u002Fcode> the\nrunner did not send (the kernel's out-of-memory killer), is recorded as \u003Ccode>probe.oom\u003C\u002Fcode>: its cells carry the\noutcome and \u003Ccode>memoryLimit { bytes, mechanism }\u003C\u002Fcode>, the plugin's record is still written, and the other cells are\njudged as usual. A plugin whose every run ended \u003Ccode>probe.oom\u003C\u002Fcode> is \u003Ccode>unknown\u003C\u002Fcode> under every host, deciding \u003Ccode>probe.oom\u003C\u002Fcode>.\nA host whose own cell ended \u003Ccode>probe.oom\u003C\u002Fcode> is \u003Ccode>unknown\u003C\u002Fcode> there as well (\u003Ccode>memory\u003C\u002Fcode>, \u003Ccode>probe.oom\u003C\u002Fcode>): the plugin did not\nrun within the bound at that host's rate and quantum.\u003C\u002Fp>\n\u003Cp>Where the machine delegates the memory controller, the runner sets the bound as \u003Ccode>memory.max\u003C\u002Fcode> of a cgroup instead: a run the\nkernel kills at it is \u003Ccode>probe.memory-limit\u003C\u002Fcode>, read the same way, and the document's \u003Ccode>limits\u003C\u002Fcode> says which it was. A process\nplugin-qualify spawns itself, with no runner (\u003Ccode>tools\u002Flv2-measure.mjs\u003C\u002Fcode>, \u003Ccode>tools\u002Fbenchmark.py\u003C\u002Fcode>), runs under \u003Ccode>RLIMIT_AS\u003C\u002Fcode> at the\nmachine's RAM, off for a sanitizer build; hitting it is \u003Ccode>memory-limit-exceeded\u003C\u002Fcode>, with the limit.\u003C\u002Fp>\n\u003Ch3 id=\"memory\">Memory\u003C\u002Fh3>\n\u003Cp>Each cell carries a footprint: how far the resident set of a process of its own grew, in KB, after instantiate, after\nactivate, at rest after processing, with notes held for an instrument, and what a second instance added. A record adds what\nthey come to: \u003Ccode>firstInstanceKb\u003C\u002Fcode>, the most one instance held with everything its instances share, and \u003Ccode>perInstanceKb\u003C\u002Fcode>, what\none more costs.\u003C\u002Fp>\n\u003Cp>A host with a memory budget reads them, at every cell it runs, and the verdict follows:\n\u003Ccode>hosting.memory.within-ceiling\u003C\u002Fcode>, \u003Ccode>hosting.memory.above-ceiling\u003C\u002Fcode> (\u003Ccode>unsuitable\u003C\u002Fcode>), or \u003Ccode>hosting.memory.unmeasured\u003C\u002Fcode> (\u003Ccode>unknown\u003C\u002Fcode>,\nsince a missing figure never reads as fitting). \u003Ccode>openmixer-console\u003C\u002Fcode> has one, sized for the desk, and the Raspberry Pi hosts\n\u003Ccode>openmixer-pi-4gb\u003C\u002Fcode> and \u003Ccode>openmixer-pi-8gb\u003C\u002Fcode>, at 48000\u002F128 and 48000\u002F256, have one each. The Pi budgets are not measured on a Pi\nyet, so a plugin under them is \u003Ccode>conditional\u003C\u002Fcode>, \u003Ccode>hosting.memory.within-provisional-ceiling\u003C\u002Fcode>: that gives the figure and the\nceiling and does not say the plugin fits. The other hosts have no budget, and read nothing.\u003C\u002Fp>\n\u003Cp>A soak (LV2; the CLAP prober has no continuous render yet) also reads the growth: past 10 MB per minute of audio over its\nsecond half, provisional, the rate fails as \u003Ccode>leaks\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Ch3 id=\"how-long-a-run-takes\">How long a run takes\u003C\u002Fh3>\n\u003Cp>The default grid is the full one, so the expected time of a run with no grid option is the \u003Ccode>full\u003C\u002Fcode> row: about 65 hours on a Raspberry Pi 5 for the vangelis set (1823 records). \u003Ccode>--grid-hosts\u003C\u002Fcode> is the fast option, about 30 hours there.\u003C\u002Fp>\n\u003Cp>Times from one deep run on a Raspberry Pi 5 (4 cores, 4 GB), 1823 records (494 CLAP, two offers of each of 247,\nand 1329 LV2), six hours in all with the grid of before (CLAP at four rates in one run at quantum 512, LV2 at\n192000\u002F512 alone):\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>\u003C\u002Fth>\n\u003Cth>runs\u003C\u002Fth>\n\u003Cth>mean a run\u003C\u002Fth>\n\u003Cth>total\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>CLAP, one run per offer (4 cells)\u003C\u002Ftd>\n\u003Ctd>494\u003C\u002Ftd>\n\u003Ctd>2.8 s\u003C\u002Ftd>\n\u003Ctd>0.38 h\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>LV2, one run (1 cell)\u003C\u002Ftd>\n\u003Ctd>1329\u003C\u002Ftd>\n\u003Ctd>10.5 s\u003C\u002Ftd>\n\u003Ctd>3.86 h\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>host admissions (jalv, mod-host) and the scan\u003C\u002Ftd>\n\u003Ctd>\u003C\u002Ftd>\n\u003Ctd>\u003C\u002Ftd>\n\u003Ctd>about 1.8 h\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>A CLAP plugin takes one run per quantum of the grid, an LV2 plugin one run per cell; the admissions do not\ngrow with the grid. On that machine, then, about:\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>grid\u003C\u002Fth>\n\u003Cth>CLAP runs a plugin\u003C\u002Fth>\n\u003Cth>LV2 runs a plugin\u003C\u002Fth>\n\u003Cth>expected\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>hosts (7 cells)\u003C\u002Ftd>\n\u003Ctd>4\u003C\u002Ftd>\n\u003Ctd>7\u003C\u002Ftd>\n\u003Ctd>1.5 h + 27 h + 1.8 h, about 30 h\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>full (16 cells)\u003C\u002Ftd>\n\u003Ctd>4\u003C\u002Ftd>\n\u003Ctd>16\u003C\u002Ftd>\n\u003Ctd>1.5 h + 62 h + 1.8 h, about 65 h\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>hosts, \u003Ccode>--cells-per-plugin-budget 3\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>3\u003C\u002Ftd>\n\u003Ctd>3\u003C\u002Ftd>\n\u003Ctd>1.1 h + 11.6 h + 1.8 h, about 15 h\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>These are ceilings: the LV2 time a run was taken at 192000, the dearest rate. \u003Ccode>--blocks 128\u003C\u002Fcode> takes a quarter of\nthe CLAP sweep a cell. A first pass with \u003Ccode>--cells-per-plugin-budget 3\u003C\u002Fcode>, then \u003Ccode>--grid full\u003C\u002Fcode> over the plugins worth\nit, keeps a small machine's night bounded.\u003C\u002Fp>\n\u003Ch2 id=\"clap-plugins-the-rt-safety-verdict\">CLAP plugins: the RT-safety verdict\u003C\u002Fh2>\n\u003Cp>A \u003Ccode>.clap\u003C\u002Fcode> file, or a directory holding one, is scanned by \u003Ccode>omx-clap-scan\u003C\u002Fcode> (from omx-clap-core; set\n\u003Ccode>OMX_CLAP_SCAN\u003C\u002Fcode> to its path when it is not on \u003Ccode>PATH\u003C\u002Fcode>) and each plugin in it is then qualified by the\nCLAP prober the package installs (from a checkout, the one built from \u003Ccode>native\u002F\u003C\u002Fcode>, see \u003Ccode>BUILDING.md\u003C\u002Fcode>):\u003C\u002Fp>\n\u003Cpre>\u003Ccode>plugin-qualify .\u002Fmy-fx.clap                   # every plugin in the file\nplugin-qualify --out .\u002Fout .\u002Fmy-fx.clap       # and its RT-safety documents\nCLAP_PATH=~\u002Fmy-clap plugin-qualify --all      # every CLAP on the search path\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Each plugin is measured by plugin-hostd's probe runner, \u003Ccode>plugin-hostd-probe\u003C\u002Fcode> (from the \u003Ccode>plugin-hostd\u003C\u002Fcode> package, 0.4.0 or later, which\nthe RPM and DEB require;\n\u003Ccode>OMX_PROBE_RUNNER\u003C\u002Fcode> names it when it is not on \u003Ccode>PATH\u003C\u002Fcode>). The runner starts the prober under limits (a wall clock\nthat kills the whole process tree, CPU time, memory, files), and plugin-qualify waits for the runner and reads\nthe document it writes, the probe output. Nothing in plugin-qualify times a prober out on its own any more.\u003C\u002Fp>\n\u003Cp>The files a pin holds are plugin-hostd's: the sync writes each pin with the files \u003Ccode>plugin-hostd --pin-files &lt;uri&gt;\u003C\u002Fcode>\nprints for the device's LV2 directories (plugin-hostd 0.15.0 or later; \u003Ccode>PLUGIN_HOSTD\u003C\u002Fcode> names the daemon when it is not\non \u003Ccode>PATH\u003C\u002Fcode>), the list an add checks, and hashes exactly those. When plugin-hostd cannot name them, or is older, the\nplugin gets no pin, the log says why, and the daemon pins it on its first add (\u003Ccode>pin_mode first-use\u003C\u002Fcode>).\u003C\u002Fp>\n\u003Cp>\u003Ccode>--all\u003C\u002Fcode> walks the standard CLAP search path through the same scanner: each directory \u003Ccode>CLAP_PATH\u003C\u002Fcode>\nnames (colon-separated), then \u003Ccode>~\u002F.clap\u003C\u002Fcode>, then \u003Ccode>\u002Fusr\u002Flib\u002Fclap\u003C\u002Fcode>, which is the system directory on every\ndistribution and architecture. A plugin id found twice keeps its first place on that path. LV2 plugins\nare measured by \u003Ccode>tools\u002Flv2-measure.mjs\u003C\u002Fcode> (below); this entry point does not drive that pass yet, so\n\u003Ccode>--all\u003C\u002Fcode> refuses with exit 2 when any \u003Ccode>.lv2\u003C\u002Fcode> bundle is on the LV2 path (\u003Ccode>LV2_PATH\u003C\u002Fcode>, else \u003Ccode>~\u002F.lv2\u003C\u002Fcode> and the\nsystem lv2 directories) rather than qualify only the CLAP side. A directory on the CLAP path that cannot\nbe read (permissions, a symlink loop) also refuses with exit 2, naming it.\u003C\u002Fp>\n\u003Cp>The prober runs each plugin headless at every cell of the grid (see &quot;The measurement grid&quot;), under an interposer that counts the\nallocations, locks and syscalls its \u003Ccode>process()\u003C\u002Fcode> makes while every parameter is swept, and prints\nthe file's SHA-256 beside what it measured. The verdict is the real-time-safety one: \u003Cstrong>suitable\u003C\u002Fstrong>\nonly on a clean count, \u003Cstrong>conditional\u003C\u002Fstrong> (isolated in the pin: it runs in a worker of its own, never refused) on any\nallocation, lock, syscall or main-thread host call from \u003Ccode>process()\u003C\u002Fcode>, \u003Cstrong>unsuitable\u003C\u002Fstrong> on \u003Ccode>CLAP_PROCESS_ERROR\u003C\u002Fcode>, \u003Cstrong>unknown\u003C\u002Fstrong> when it could not be counted. An instrument is\nquiet until it is played, so a plugin with a note input is also played, at every rate and under a\ncount of its own: a chord, the pitch bent and a controller moved, pressure, the chord let go, a\nburst of 32 note ons in one block and their offs, all notes off, in the dialect its note input\nreads (CLAP's, else MIDI's). An allocation, lock or syscall while it plays is \u003Cstrong>conditional\u003C\u002Fstrong>, isolated in the pin like the same\nviolation in the run (\u003Ccode>hosting.rt-safety.violations-under-notes\u003C\u002Fcode>); an instrument that could not be played under the\ninterposer is \u003Cstrong>unknown\u003C\u002Fstrong> (\u003Ccode>hosting.rt-safety.notes-unmeasured\u003C\u002Fcode>). \u003Ccode>--out\u003C\u002Fcode> writes the qualification records and the probe outputs they judged (see\n&quot;Judging a probe output&quot; below), one pair per plugin binary and offered host extensions. Each plugin's pin is\nan \u003Ccode>integrity\u003C\u002Fcode> block in the probe output: the \u003Ccode>.clap\u003C\u002Fcode>'s SHA-256 by its file name and the parameter-layout\nfingerprint \u003Ccode>omx-layout\u002F1:&lt;sha256&gt;\u003C\u002Fcode>, both computed through plugin-hostd's \u003Ccode>pin.h\u003C\u002Fcode>, which is what a console\nhands plugin-hostd as \u003Ccode>pin_set\u003C\u002Fcode> before it adds the plugin. \u003Ccode>OMX_CLAP_QUALIFY\u003C\u002Fcode> and \u003Ccode>OMX_RT_INTERPOSE\u003C\u002Fcode>\nname the prober and the interposer the runner starts when they are not the installed ones or the checkout's\nown build.\u003C\u002Fp>\n\u003Ch2 id=\"judging-a-probe-output\">Judging a probe output\u003C\u002Fh2>\n\u003Cp>The probe output is the document \u003Ccode>plugin-hostd-probe run\u003C\u002Fcode> writes: one binary and one offer, the steps the\nprober reached and how each ended, and the figures per rate and quantum, with no verdict in it. plugin-qualify\nreads it with \u003Ccode>readProbeOutput\u003C\u002Fcode>, which refuses a document of a major version it does not know\n(\u003Ccode>probe.format.major-unknown\u003C\u002Fcode>), a document that carries a verdict, and a document whose derived fields disagree\nwith the fields they are computed from (\u003Ccode>probe.format.derived-mismatch\u003C\u002Fcode>): \u003Ccode>machine.class.cstatesLimited\u003C\u002Fcode> and\n\u003Ccode>machine.profile.meetsProtocol\u003C\u002Fcode> are recomputed from \u003Ccode>cpuidle[]\u003C\u002Fcode>, the governor and \u003Ccode>unmet[]\u003C\u002Fcode>, never trusted.\u003C\u002Fp>\n\u003Cpre>\u003Ccode>plugin-hostd-probe run --out out\u002Fplate.probe.json .\u002Fexample-plate.clap org.example.plate\nplugin-qualify judge --host-profile omx-clap-host --qualification-dir .\u002Fqualification out\u002Fplate.probe.json\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>judge\u003C\u002Fcode> writes, into the qualification directory (\u003Ccode>--qualification-dir\u003C\u002Fcode>, else \u003Ccode>$XDG_STATE_HOME\u002Fomx\u002Fqualification\u003C\u002Fcode>,\nelse \u003Ccode>~\u002F.local\u002Fstate\u002Fomx\u002Fqualification\u003C\u002Fcode>), the qualification record \u003Ccode>&lt;id&gt;@&lt;version&gt;-&lt;sha256 first 12&gt;.&lt;offer&gt;.qualification.json\u003C\u002Fcode>\nand, beside it, a byte copy of the probe output it judged, \u003Ccode>&lt;same name&gt;.probe.json\u003C\u002Fcode>, because the probe directory\nmay be wiped. The record names that copy and its SHA-256 in \u003Ccode>probe\u003C\u002Fcode>, with the run's protocol, reason and machine\nclass, so a reference figure and a local one are told apart. The record holds, for each host profile, the verdict\n(\u003Ccode>rating\u003C\u002Fcode>, \u003Ccode>hosting\u003C\u002Fcode>, the deciding reason code and every reason) and the cost reasons at that profile's rate and\nquantum, and the figures as measured. Exit \u003Ccode>0\u003C\u002Fcode> every plugin qualified, \u003Ccode>1\u003C\u002Fcode> some did not, \u003Ccode>2\u003C\u002Fcode> something could not\nbe read or judged.\u003C\u002Fp>\n\u003Cp>The verdict is the same one \u003Ccode>plugin-qualify\u003C\u002Fcode> gives CLAP plugins, the real-time-safety one, now for LV2 as\nwell; cost is reported beside it, not folded into it. Cost is read at the profile's rate AND quantum: a pair the\ndocument does not hold reads \u003Ccode>hosting.cost.unmeasured-at-rate-and-quantum\u003C\u002Fcode>, and two quanta of one plugin that cost\ndifferently give two different cost reasons. Only a measurement of the old format, which keeps one figure per\nrate, is read per rate. A run that did not end \u003Ccode>probe.ok\u003C\u002Fcode> is \u003Ccode>unknown\u003C\u002Fcode>, with the run's outcome code as the reason.\u003C\u002Fp>\n\u003Cp>An instrument costs nothing until a voice plays, so its cost is taken with notes held down, at velocity 127,\nspread across the keyboard, while every parameter is swept: as many notes as the voice capacity the plugin\ndeclares (CLAP \u003Ccode>voice-info\u003C\u002Fcode>, basis \u003Ccode>declared\u003C\u002Fcode>), or, when it declares none, the count \u003Ccode>--probe-notes\u003C\u002Fcode> gives\n(basis \u003Ccode>probe\u003C\u002Fcode>; LV2 declares none). Each rate's figure says how many notes it held and why (\u003Ccode>voices: { held, basis, capacity }\u003C\u002Fcode>). An instrument with no count to hold is not timed (\u003Ccode>voices-unknown\u003C\u002Fcode>).\u003C\u002Fp>\n\u003Cp>An LV2 plugin's record holds the same two figures, under the same field names, in \u003Ccode>measurements.grid[]\u003C\u002Fcode>:\n\u003Ccode>latency { declaredFrames, measuredFrames }\u003C\u002Fcode> and \u003Ccode>cost\u003C\u002Fcode>. \u003Ccode>lv2_measure\u003C\u002Fcode> takes them at the run's rate and block, the\nway the CLAP prober does: the declared figure is what the plugin reports on its latency port\n(\u003Ccode>lv2:designation lv2:latency\u003C\u002Fcode>, or the older \u003Ccode>lv2:reportsLatency\u003C\u002Fcode>; 0 when it has none), the measured one is where an\nimpulse at frame 0 lands (the first frame at or above 1e-3 of the response's own peak), or, for an instrument, where a\nstruck A4 sounds. A plugin that has no latency port is measured, never read as 0. An instrument's cost holds\n\u003Ccode>OMX_PROBE_NOTES\u003C\u002Fcode> keys down (the LV2 counterpart of \u003Ccode>--probe-notes\u003C\u002Fcode>). A figure the run could not take (a response that\nstays silent, an instrument with no count to hold) is left out of the grid point and named in\n\u003Ccode>measurements.unmeasured\u003C\u002Fcode> with its hosting code (\u003Ccode>hosting.latency.unmeasured\u003C\u002Fcode>,\n\u003Ccode>hosting.cost.unmeasured-at-rate-and-quantum\u003C\u002Fcode>).\u003C\u002Fp>\n\u003Cp>Every cell of a record carries \u003Ccode>latency\u003C\u002Fcode>: the two figures when the run took them, else\n\u003Ccode>{ unmeasuredReason, code }\u003C\u002Fcode>, the contract's word for why and the hosting code a reader classifies by. A latency\nstep that did not end well gives the child's reason (\u003Ccode>silent-output\u003C\u002Fcode>, say); a cell whose run failed, its outcome's\nword (\u003Ccode>probe-crashed\u003C\u002Fcode>, or \u003Ccode>probe-error\u003C\u002Fcode> for \u003Ccode>probe.oom\u003C\u002Fcode>); a run that took no latency at all (a runner or a child\nolder than the latency arm), \u003Ccode>not-attempted\u003C\u002Fcode>. The declared figure alone, for a plugin whose response could not be\nmeasured, is not in the probe output: plugin-hostd's runner writes a cell's latency only with both figures.\u003C\u002Fp>\n\u003Cp>The prober also scans: \u003Ccode>OMX_CLAP_SCAN=native\u002Fbuild\u002Fclap-qualify\u003C\u002Fcode> makes it the scanner, and its scan\nkeeps what a console needs to draw a third-party plugin's controls: an enum parameter's labels (as\nthe plugin's \u003Ccode>value_to_text\u003C\u002Fcode> renders them), each parameter's module, and the plugin's\nremote-control pages.\u003C\u002Fp>\n\u003Ch2 id=\"adjusting-a-verdict-by-what-the-plugin-did-on-this-machine\">Adjusting a verdict by what the plugin did on this machine\u003C\u002Fh2>\n\u003Cpre>\u003Ccode>plugin-qualify adjust --history ~\u002F.local\u002Fstate\u002Fplugin-hostd\u002Fhistory --qualification-dir .\u002Fqualification --host-profile omx-clap-host\nplugin-qualify adjust --history history --qualification-dir .\u002Fqualification --host-class aarch64\u002F4\u002F2400 --pins pins.txt\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>plugin-hostd keeps a running history of every plugin binary on every host class it ran on (windows run alone and in a\ngroup, CPU, xrun windows, crashes, restarts, quarantines). \u003Ccode>adjust\u003C\u002Fcode> reads that file directly, so it runs without a daemon,\nand writes a second verdict beside the measured one in each qualification record: a \u003Ccode>history\u003C\u002Fcode> block per host class of the\nfile, holding the line it looked for (the plugin's plugin-hostd uri and the digest of the first file of its pin), whether\nthe history has it, the windows it counts, and for each \u003Ccode>--host-profile\u003C\u002Fcode> the measured pin verdict, the adjusted one, the\nmove (\u003Ccode>demote\u003C\u002Fcode>, \u003Ccode>promote\u003C\u002Fcode> or \u003Ccode>none\u003C\u002Fcode>) and a reason code. The measured \u003Ccode>verdicts\u003C\u002Fcode> and the measurement are never changed: the\nsame plugin on another host class has another history and another adjusted verdict.\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>move\u003C\u002Fth>\n\u003Cth>when\u003C\u002Fth>\n\u003Cth>reason\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>in-process\u003C\u002Fcode> to \u003Ccode>isolated\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>a crash was counted within 30 days, on this host class\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>history.crashed\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>in-process\u003C\u002Fcode> to \u003Ccode>isolated\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>from 30 grouped windows, the share of xrun windows grouped exceeds the share alone by 5 points\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>history.xruns-grouped\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>isolated\u003C\u002Fcode> to \u003Ccode>refused\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>three or more solo crashes, the latest within 30 days\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>history.crashed-alone\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>isolated\u003C\u002Fcode> to \u003Ccode>in-process\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>every reason against the plugin is a \u003Ccode>hosting.cost.*\u003C\u002Fcode> one, 360 windows run alone (six hours), no crash or restart ever counted, no xruns raised when grouped, \u003Ccode>cpu_p95_solo\u003C\u002Fcode> under the host profile's ceiling\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>history.steady\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>none\u003C\u002Ftd>\n\u003Ctd>no line for this binary on this class\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>history.no-record\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>none\u003C\u002Ftd>\n\u003Ctd>a line, and no rule moves the verdict\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>history.no-change\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>none\u003C\u002Ftd>\n\u003Ctd>the line would promote, but the verdict is a fact about the plugin: a real-time-safety, feature, stability, latency or topology reason, an \u003Ccode>unknown\u003C\u002Fcode>, or a \u003Ccode>refused\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>history.not-liftable\u003C\u002Fcode>, with \u003Ccode>blockedBy\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>History never lifts a real-time-safety, feature or unknown verdict, and never promotes a refused one; a demotion is one\nstep. The thresholds are one table, \u003Ccode>HISTORY_POLICY\u003C\u002Fcode> in \u003Ccode>src\u002Fhistory-adjust.ts\u003C\u002Fcode>. \u003Ccode>--pins &lt;file&gt;\u003C\u002Fcode> writes the pins with the\nadjusted verdict of the first \u003Ccode>--host-profile\u003C\u002Fcode> instead of the measured one; it needs exactly one host class, so name one\nwith \u003Ccode>--host-class\u003C\u002Fcode> when the history holds several. \u003Ccode>--now &lt;seconds&gt;\u003C\u002Fcode> fixes the time the history is read at. A line of the\nhistory outside its grammar is not read and is named on stderr; a history of a version this tool does not know is refused.\u003C\u002Fp>\n\u003Ch2 id=\"lv2-plugins-the-hosting-measurement\">LV2 plugins: the hosting measurement\u003C\u002Fh2>\n\u003Cp>\u003Ccode>tools\u002Flv2-measure.mjs\u003C\u002Fcode> measures the hosting dimensions (\u003Ccode>stability\u003C\u002Fcode>, \u003Ccode>rtSafety\u003C\u002Fcode>, \u003Ccode>features\u003C\u002Fcode>) through\nthe LV2 measurer the package installs, \u003Ccode>lv2_measure\u003C\u002Fcode>: each plugin is loaded through lilv in its own\nprocess, instantiated and destroyed 30 times, and run under the interposer while every control is\nswept. \u003Ccode>tools\u002Flv2-measure.mjs\u003C\u002Fcode> measures at the highest \u003Ccode>--rates\u003C\u002Fcode> rate; \u003Ccode>plugin-qualify\u003C\u002Fcode> runs the measurer\nonce per cell of its grid (see &quot;The measurement grid&quot;):\u003C\u002Fp>\n\u003Cpre>\u003Ccode>node tools\u002Flv2-measure.mjs --rates 48000 --dimensions stability,rtSafety,features \\\n  --plugins urn:my:plugin --out measurements.json --run\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Without \u003Ccode>--plugins\u003C\u002Fcode> it measures every plugin the scan finds. Each plugin's entry carries its pin, an\n\u003Ccode>integrity\u003C\u002Fcode> block: the SHA-256 of the files the host loads and the parameter-layout fingerprint,\nthrough plugin-hostd's \u003Ccode>pin.h\u003C\u002Fcode>, as for CLAP. \u003Ccode>OMX_LV2_MEASURE\u003C\u002Fcode> and \u003Ccode>OMX_RT_INTERPOSE\u003C\u002Fcode> name the\nmeasurer and the interposer when they are not the installed ones or the checkout's own build.\u003C\u002Fp>\n\u003Cp>The features the measurer offers a plugin are the console's own: omx-clap-core's LV2 feature\nprovider, the code its LV2 host builds every instance's features with. Each of the provider's\noptions is a flag of \u003Ccode>tools\u002Flv2-measure.mjs\u003C\u002Fcode> and of \u003Ccode>lv2_measure\u003C\u002Fcode> itself, and each plugin's record\nnames the options it ran under, \u003Ccode>featureOptions\u003C\u002Fcode>:\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>flag\u003C\u002Fth>\n\u003Cth>what it sets\u003C\u002Fth>\n\u003Cth>default\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>--features LIST\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>the kinds offered: \u003Ccode>map\u003C\u002Fcode>, \u003Ccode>unmap\u003C\u002Fcode>, \u003Ccode>options\u003C\u002Fcode>, \u003Ccode>bounded\u003C\u002Fcode>, \u003Ccode>worker\u003C\u002Fcode>, \u003Ccode>default-state\u003C\u002Fcode>, \u003Ccode>log\u003C\u002Fcode>, \u003Ccode>rtmempool\u003C\u002Fcode> (empty: none)\u003C\u002Ftd>\n\u003Ctd>all but \u003Ccode>default-state\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>--strict\u003C\u002Fcode> \u002F \u003Ccode>--no-strict\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>refuse a plugin requiring a feature not offered (the record's \u003Ccode>refusedFeature\u003C\u002Fcode> names it), or hand it to its own instantiate without it\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>--no-strict\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>--block-declares LIST\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>the buf-size declarations made when \u003Ccode>bounded\u003C\u002Fcode> is offered: \u003Ccode>bounded\u003C\u002Fcode>, \u003Ccode>fixed\u003C\u002Fcode>, \u003Ccode>pow2\u003C\u002Fcode> (empty: none)\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>bounded\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>--block-min N\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>bufsz:minBlockLength\u003C\u002Fcode>; 0 is the run's block\u003C\u002Ftd>\n\u003Ctd>1\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>--block-max N\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>bufsz:maxBlockLength\u003C\u002Fcode>; 0 is the run's block\u003C\u002Ftd>\n\u003Ctd>0\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>--block-nominal N\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>bufsz:nominalBlockLength\u003C\u002Fcode>; 0 is the maximum\u003C\u002Ftd>\n\u003Ctd>0\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>--log MODE\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>log:log\u003C\u002Fcode> lines: \u003Ccode>silent\u003C\u002Fcode>, \u003Ccode>route\u003C\u002Fcode> (to stderr) or \u003Ccode>capture\u003C\u002Fcode> (into the record's \u003Ccode>featureOptions.log.lines\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>silent\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>--log-capture-bytes N\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>the capture buffer, its NUL included\u003C\u002Ftd>\n\u003Ctd>4096\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>The defaults are the measurement as it has always been, so a verdict reached under them does not\nmove. \u003Ccode>lv2_measure --help\u003C\u002Fcode> lists the same flags.\u003C\u002Fp>\n\u003Ch2 id=\"scanned-descriptors\">Scanned descriptors\u003C\u002Fh2>\n\u003Cp>Both scanners also write what each plugin states about itself as a scanned descriptor: one JSON file per\nplugin in omx-contract's descriptor words (identity, controls, ports, what it needs from a host, whether it\nreports latency), held to omx-contract's descriptor schema before it is written. A descriptor holds no\nmeasurement and no verdict; those are the qualification record's.\u003C\u002Fp>\n\u003Cp>The files go into the descriptor directory, the key \u003Ccode>descriptor_dir\u003C\u002Fcode>:\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>\u003C\u002Fth>\n\u003Cth>where\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>named\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>--descriptor-dir &lt;dir&gt;\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>a user (the default)\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>$XDG_STATE_HOME\u002Fomx\u002Fdescriptors\u003C\u002Fcode>, else \u003Ccode>~\u002F.local\u002Fstate\u002Fomx\u002Fdescriptors\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>a system service\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>\u002Fvar\u002Flib\u002Fomx\u002Fdescriptors\u003C\u002Fcode> (\u003Ccode>--system\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>Each file is named \u003Ccode>&lt;id&gt;@&lt;version&gt;-&lt;sha256 first 12&gt;.json\u003C\u002Fcode>: the CLAP id or the LV2 URI, the version the\nplugin states (\u003Ccode>unversioned\u003C\u002Fcode> when it states none) and the SHA-256 of its binary (the \u003Ccode>.clap\u003C\u002Fcode>, or the LV2\nbundle's \u003Ccode>lv2:binary\u003C\u002Fcode>), with every character other than letters, digits, \u003Ccode>.\u003C\u002Fcode>, \u003Ccode>_\u003C\u002Fcode> and \u003Ccode>-\u003C\u002Fcode> written \u003Ccode>_\u003C\u002Fcode>. The hash\nkeys which file describes which binary: a changed binary is a new file, and the old one stays as history.\u003C\u002Fp>\n\u003Cpre>\u003Ccode>tools\u002Fscan.py --out catalog.json                                     # LV2: the catalog, and the descriptors beside it\ntools\u002Fscan.py --descriptor-dir .\u002Fdescriptors urn:my:plugin           # one plugin, into a directory of your own\nnative\u002Fbuild\u002Fclap-qualify --json .\u002Fmy-fx.clap | tools\u002Fdescriptors.py clap --descriptor-dir .\u002Fdescriptors\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>scan.py --no-descriptors\u003C\u002Fcode> writes the catalog only. A descriptor the schema refuses is not written and is\nreported as \u003Ccode>contract.file.invalid\u003C\u002Fcode>, omx-contract's code; a plugin whose binary cannot be read is\n\u003Ccode>qualify.descriptor.binary-unhashed\u003C\u002Fcode>; a directory that cannot be written is \u003Ccode>qualify.descriptor.dir-unwritable\u003C\u002Fcode>;\nno schema found is \u003Ccode>qualify.descriptor.schema-missing\u003C\u002Fcode>. Any of them makes the run exit \u003Ccode>2\u003C\u002Fcode>, and the other\nplugins are still written. The schema is the omx-contract package plugin-qualify depends on, else\n\u003Ccode>\u002Fusr\u002Fshare\u002Fomx-contract\u002Fomx-contract.schema.json\u003C\u002Fcode>; \u003Ccode>OMX_CONTRACT_SCHEMA\u003C\u002Fcode> names another one, for a test. The\nwriter needs Python's \u003Ccode>jsonschema\u003C\u002Fcode> (\u003Ccode>python3-jsonschema\u003C\u002Fcode>).\u003C\u002Fp>\n\u003Ch2 id=\"what-the-verdicts-mean\">What the verdicts mean\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>\u003Cstrong>suitable\u003C\u002Fstrong>: every dimension was measured and passed. Safe to run inside the engine's own\nprocess. This is the only verdict that counts as qualified.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>conditional\u003C\u002Fstrong>: it works, with a caution (for example a high cost at small quanta, or an\nallocation, lock or syscall in \u003Ccode>run()\u003C\u002Fcode>). It is run isolated, and the deciding reason names the caution.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>unknown\u003C\u002Fstrong>: something was not measured. An unmeasured plugin is never a pass.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>unsuitable\u003C\u002Fstrong>: a measured failure: a crash, a memory error in the plugin's own binary, or a\nrequired feature the host does not provide. An allocation, lock or syscall in \u003Ccode>run()\u003C\u002Fcode> is not\none: that plugin is \u003Cstrong>conditional\u003C\u002Fstrong>, and runs isolated.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>The worst dimension decides. The text output prints the plugin, its verdict and the reason that\ndecided it.\u003C\u002Fp>\n\u003Ch2 id=\"reproducing-a-failure\">Reproducing a failure\u003C\u002Fh2>\n\u003Col>\n\u003Cli>Run the plugin alone: \u003Ccode>plugin-qualify --format json --out .\u002Fout &lt;uri&gt;\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>The document in \u003Ccode>.\u002Fout\u003C\u002Fcode> names the dimension that failed and, for a crash, the sanitizer class\nand the object of the faulting frame.\u003C\u002Fli>\n\u003Cli>Run the ASan mod-host (\u003Ccode>mod-host-asan\u003C\u002Fcode> in the image) with just that plugin to get the full\nsanitizer report.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>\u003Ccode>fixtures\u002Fzero-path.lv2\u003C\u002Fcode> is a known-bad plugin (a zero-size \u003Ccode>atom:Path\u003C\u002Fcode> overread). It should\nrate \u003Cstrong>unsuitable\u003C\u002Fstrong>; if it does not, the sweep is not catching memory errors.\u003C\u002Fp>\n\u003Ch2 id=\"for-other-projects-zynthian-mod-packagers\">For other projects (zynthian, MOD, packagers)\u003C\u002Fh2>\n\u003Cp>The measurements describe the plugin and never the host: latency and cost per rate, whether it\nsurvives a thousand lifecycles and a three-hour soak, whether \u003Ccode>run()\u003C\u002Fcode> allocates, locks or makes\nsyscalls, and which LV2 features it requires. They are taken once. The \u003Cstrong>verdict\u003C\u002Fstrong> is a separate\nstep: the same measurements judged against a \u003Cstrong>host profile\u003C\u002Fstrong>, the facts that differ between\nhosts. Two projects share the measurements and differ only in the profile they choose.\u003C\u002Fp>\n\u003Ch3 id=\"choosing-a-host-profile\">Choosing a host profile\u003C\u002Fh3>\n\u003Cpre>\u003Ccode>plugin-qualify --host-profile jalv .\u002Fmy-synth.lv2                  # zynthian: one jalv process per plugin\nplugin-qualify --host-profile mod-host .\u002Fmy-fx.lv2                 # MOD: one shared mod-host process\nplugin-qualify --host-profile omx-clap-host .\u002Fmy-synth.clap        # omx-clap-host, alone or under plugin-hostd\nplugin-qualify --host-profile jalv,mod-host,openmixer-console &lt;uri&gt;  # one verdict per profile\nplugin-qualify --host-profile .\u002Fmy-host.json &lt;uri&gt;                 # your own host\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>profile\u003C\u002Fth>\n\u003Cth>isolation\u003C\u002Fth>\n\u003Cth>verdict\u003C\u002Fth>\n\u003Cth>instruments\u003C\u002Fth>\n\u003Cth>LV2 features read from\u003C\u002Fth>\n\u003Cth>cost budget (P95 of a core, rate \u002F quantum)\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>openmixer-console\u003C\u002Fcode> (default)\u003C\u002Ftd>\n\u003Ctd>in-process (the console's RT thread)\u003C\u002Ftd>\n\u003Ctd>judged from a declaration\u003C\u002Ftd>\n\u003Ctd>no\u003C\u002Ftd>\n\u003Ctd>the console's in-process table\u003C\u002Ftd>\n\u003Ctd>0.05 at 96000 \u002F 1024\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>jalv\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>per-process\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>measured by a run\u003C\u002Fstrong> (\u003Ccode>jalv -n &lt;name&gt; &lt;uri&gt;\u003C\u002Fcode>, headless)\u003C\u002Ftd>\n\u003Ctd>yes\u003C\u002Ftd>\n\u003Ctd>jalv 1.6.8 \u003Ccode>src\u002Fjalv.c\u003C\u002Fcode> (zynthian's fork passes the same list)\u003C\u002Ftd>\n\u003Ctd>0.25 at 48000 \u002F 256\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>mod-host\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>shared-process\u003C\u002Ftd>\n\u003Ctd>\u003Cstrong>measured by a run\u003C\u002Fstrong> (\u003Ccode>add\u003C\u002Fcode>, \u003Ccode>activate\u003C\u002Fcode>, \u003Ccode>remove\u003C\u002Fcode> on mod-host's socket)\u003C\u002Ftd>\n\u003Ctd>yes\u003C\u002Ftd>\n\u003Ctd>mod-host f14a230 \u003Ccode>src\u002Feffects.c\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>0.05 at 48000 \u002F 128\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>omx-clap-host\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>shared-process\u003C\u002Ftd>\n\u003Ctd>judged from a declaration\u003C\u002Ftd>\n\u003Ctd>yes\u003C\u002Ftd>\n\u003Ctd>the console's in-process table (the same LV2 feature provider)\u003C\u002Ftd>\n\u003Ctd>0.05 at 48000 \u002F 128\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>\u003Cstrong>Measured or declared.\u003C\u002Fstrong> A \u003Cem>declared\u003C\u002Fem> verdict is what the profile's feature list and cost budget make of\nthe one probe output. A \u003Cem>measured\u003C\u002Fem> verdict (\u003Ccode>jalv\u003C\u002Fcode>, \u003Ccode>mod-host\u003C\u002Fcode>, LV2 plugins) also runs the plugin under that\nhost, one admission per plugin, and its record entry carries \u003Ccode>arm: &quot;run&quot;\u003C\u002Fcode> and \u003Ccode>measured: true\u003C\u002Fcode>. A refusal\n(\u003Ccode>resp -102\u003C\u002Fcode>), a crash (the signal), a host that dies on \u003Ccode>add\u003C\u002Fcode>, or a port type the host does not know makes the\nplugin \u003Ccode>unsuitable\u003C\u002Fcode> for that host, with the code as the deciding reason (\u003Ccode>mod-host.resp-102\u003C\u002Fcode>,\n\u003Ccode>mod-host.signal-SIGABRT\u003C\u002Fcode>, \u003Ccode>mod-host.died-on-add\u003C\u002Fcode>, \u003Ccode>jalv.unknown-port-type\u003C\u002Fcode>, \u003Ccode>jalv.signal-SIGABRT\u003C\u002Fcode>). A host\nthat is not installed leaves the verdict unmeasured (\u003Ccode>unknown\u003C\u002Fcode>, deciding \u003Ccode>&lt;host&gt;.not-installed\u003C\u002Fcode>). After any\ndeath on mod-host the next admission starts a fresh host. \u003Ccode>plugin-qualify judge\u003C\u002Fcode> reads a probe output\nthat holds no run, so its \u003Ccode>jalv\u003C\u002Fcode> and \u003Ccode>mod-host\u003C\u002Fcode> entries stay \u003Ccode>arm: &quot;declaration&quot;\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>The console's two tiers.\u003C\u002Fstrong> Every record carries \u003Ccode>tiers\u003C\u002Fcode>: the \u003Cem>in-process\u003C\u002Fem> verdict (\u003Ccode>openmixer-console\u003C\u002Fcode>) and the\n\u003Cem>isolated\u003C\u002Fem> verdict (\u003Ccode>omx-clap-host\u003C\u002Fcode>, an LV2 plugin through plugin-hostd's LV2 adapter), \u003Ccode>offeredOn\u003C\u002Fcode> (\u003Ccode>in-process\u003C\u002Fcode> when its\nverdict qualified, \u003Ccode>isolated\u003C\u002Fcode> when its verdict qualified or is \u003Ccode>conditional\u003C\u002Fcode>) and, when neither, \u003Ccode>dimmed\u003C\u002Fcode> with the reason. A\n\u003Ccode>conditional\u003C\u002Fcode> plugin, RT-unsafe for instance, is offered on the isolated tier with \u003Ccode>runsAlone: true\u003C\u002Fcode>: it runs in a worker of\nits own, never grouped, and its pin says \u003Ccode>isolated\u003C\u002Fcode>. An LV2 plugin is run once under the adapter, and\nthat run decides its \u003Ccode>omx-clap-host\u003C\u002Fcode> verdict as a mod-host run decides mod-host's. An \u003Ccode>lv2:InstrumentPlugin\u003C\u002Fcode> is also played one\nheld note there, its output read at its JACK ports (\u003Ccode>jack_peak\u003C\u002Fcode>): at or under -120 dBFS it reads \u003Ccode>unknown\u003C\u002Fcode>\n(\u003Ccode>omx-clap-host.silent-under-note\u003C\u002Fcode>), not qualified and not refused, since a sampler with nothing loaded is silent too. A level\nthat could not be read reads the same way (\u003Ccode>omx-clap-host.level-unmeasured\u003C\u002Fcode>). A plugin that kills the isolated\nhost and qualifies in-process is offered in-process only. The tiers are judged for every plugin whether or not\n\u003Ccode>--host-profile\u003C\u002Fcode> names them; mod-host and jalv are run only when \u003Ccode>--host-profile\u003C\u002Fcode> asks for them.\u003C\u002Fp>\n\u003Cp>\u003Ccode>omx-clap-host\u003C\u002Fcode> offers a CLAP plugin the console's six host extensions and three more,\n\u003Ccode>clap.preset-load\u002F2\u003C\u002Fcode>, \u003Ccode>clap.track-info\u002F1\u003C\u002Fcode> and \u003Ccode>clap.remote-controls\u002F2\u003C\u002Fcode>. A CLAP plugin judged\nunder it is probed again with those three offered, so its verdict holds for the host it runs in.\u003C\u002Fp>\n\u003Cp>The \u003Ccode>jalv\u003C\u002Fcode>, \u003Ccode>mod-host\u003C\u002Fcode> and \u003Ccode>omx-clap-host\u003C\u002Fcode> budgets are defaults, not a measurement of your board. State your own\nby writing a profile file. Every field is required; a missing or mistyped field is refused and\nnever filled in with a default:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-json\">{\n  &quot;id&quot;: &quot;zynthian-v5&quot;,\n  &quot;lv2Features&quot;: [&quot;http:\u002F\u002Flv2plug.in\u002Fns\u002Fext\u002Furid#map&quot;, &quot;http:\u002F\u002Flv2plug.in\u002Fns\u002Fext\u002Furid#unmap&quot;,\n                  &quot;http:\u002F\u002Flv2plug.in\u002Fns\u002Fext\u002Fworker#schedule&quot;, &quot;http:\u002F\u002Flv2plug.in\u002Fns\u002Fext\u002Flog#log&quot;,\n                  &quot;http:\u002F\u002Flv2plug.in\u002Fns\u002Fext\u002Foptions#options&quot;,\n                  &quot;http:\u002F\u002Flv2plug.in\u002Fns\u002Fext\u002Fstate#loadDefaultState&quot;,\n                  &quot;http:\u002F\u002Flv2plug.in\u002Fns\u002Fext\u002Fbuf-size#powerOf2BlockLength&quot;,\n                  &quot;http:\u002F\u002Flv2plug.in\u002Fns\u002Fext\u002Fbuf-size#fixedBlockLength&quot;,\n                  &quot;http:\u002F\u002Flv2plug.in\u002Fns\u002Fext\u002Fbuf-size#boundedBlockLength&quot;],\n  &quot;clapExtensions&quot;: null,\n  &quot;instruments&quot;: true,\n  &quot;cost&quot;: { &quot;coreFractionCeiling&quot;: 0.3, &quot;rate&quot;: 48000, &quot;quantum&quot;: 256 },\n  &quot;latency&quot;: &quot;uncompensated&quot;,\n  &quot;isolation&quot;: &quot;per-process&quot;\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>What each field changes. \u003Ccode>lv2Features\u003C\u002Fcode> is what the host passes to \u003Ccode>instantiate\u003C\u002Fcode>: a plugin that\nrequires anything else is \u003Cstrong>unsuitable\u003C\u002Fstrong> on that host. \u003Ccode>clapExtensions\u003C\u002Fcode> is \u003Ccode>null\u003C\u002Fcode> for a host with\nno CLAP path. \u003Ccode>instruments: true\u003C\u002Fcode> means a MIDI input is fed, and a plugin with no audio input but\na MIDI input and an audio output rates as an instrument. \u003Ccode>cost\u003C\u002Fcode> is the per-instance ceiling, and\nthe rate and quantum the verdict is judged at. \u003Ccode>latency: &quot;compensated&quot;\u003C\u002Fcode> means the host aligns on\nthe plugin's latency, so a latency that moves with a control is a caution. \u003Ccode>isolation\u003C\u002Fcode> decides\nwhether spawned threads and a linked GUI toolkit count as cautions: they do only when the plugin\nshares its process. An unmeasured dimension is \u003Cstrong>unknown\u003C\u002Fstrong> on every host.\u003C\u002Fp>\n\u003Cp>With several profiles, the exit code is \u003Ccode>0\u003C\u002Fcode> only if every plugin qualifies under every profile.\u003C\u002Fp>\n\u003Ch3 id=\"the-json-document\">The JSON document\u003C\u002Fh3>\n\u003Cp>\u003Ccode>--out &lt;dir&gt;\u003C\u002Fcode> writes one interchange document per run (\u003Ccode>lv2-plugin-measurements\u003C\u002Fcode>, format 1.6,\nthe same format the OpenMixer console reads). The\n\u003Ccode>plugins\u003C\u002Fcode> array holds the raw, host-free measurements. Beside it, \u003Ccode>hostProfiles\u003C\u002Fcode> holds each\nprofile the run judged against, in full, and \u003Ccode>verdicts\u003C\u002Fcode> holds one entry per plugin per profile:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-json\">{ &quot;uri&quot;: &quot;urn:example:verb&quot;, &quot;host&quot;: &quot;jalv&quot;, &quot;rating&quot;: &quot;unknown&quot;, &quot;qualified&quot;: false,\n  &quot;deciding&quot;: &quot;hosting.stability.unsoaked&quot; }\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>host\u003C\u002Fcode> is the \u003Ccode>id\u003C\u002Fcode> of an entry in \u003Ccode>hostProfiles\u003C\u002Fcode>. \u003Ccode>deciding\u003C\u002Fcode> is a stable code, absent when the\nplugin is \u003Ccode>suitable\u003C\u002Fcode>. If you have your own host rules, re-derive them from \u003Ccode>plugins\u003C\u002Fcode> and do not\ntrust another host's verdict.\u003C\u002Fp>\n\u003Ch3 id=\"as-a-library\">As a library\u003C\u002Fh3>\n\u003Cp>\u003Ccode>classifyPluginHosting(plugin, { rate, quantum, profile })\u003C\u002Fcode> gives one verdict, and\n\u003Ccode>qualifyPlugins(measured, { profiles })\u003C\u002Fcode> gives one per plugin per profile. \u003Ccode>readHostProfile(json)\u003C\u002Fcode>\nvalidates a profile file, and \u003Ccode>judgedDocument(run, plugins, profiles, verdicts)\u003C\u002Fcode> builds the\ndocument. All of them are exported from the package root. The package depends on no other\nOpenMixer package.\u003C\u002Fp>\n\u003Ch3 id=\"pinning-what-was-qualified\">Pinning what was qualified\u003C\u002Fh3>\n\u003Cpre>\u003Ccode>plugin-qualify --pins pins.txt .\u002Fmy-synth.clap     # a pin and a verdict per plugin, in a file plugin-hostd loads\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>--pins &lt;file&gt;\u003C\u002Fcode> writes one line per qualified plugin, the same line as plugin-hostd's \u003Ccode>pin_set\u003C\u002Fcode> verb with the verdict as\na last word: the SHA-256 of every file the host loads, the parameter-layout fingerprint, and \u003Ccode>verdict=in-process\u003C\u002Fcode>\n(suitable), \u003Ccode>isolated\u003C\u002Fcode> (conditional) or \u003Ccode>refused\u003C\u002Fcode> (unsuitable). A plugin with no verdict is pinned without one. Put the\nfile in plugin-hostd's settings as \u003Ccode>pin_file\u003C\u002Fcode> and an \u003Ccode>add\u003C\u002Fcode> of that plugin needs no \u003Ccode>pin_set\u003C\u002Fcode>; a changed file is refused\nwith -509. The verdict is the first \u003Ccode>--host-profile\u003C\u002Fcode>'s. A run that took no pin leaves an existing file alone.\u003C\u002Fp>\n\u003Ch3 id=\"qualifying-every-plugin-on-a-machine\">Qualifying every plugin on a machine\u003C\u002Fh3>\n\u003Cp>\u003Ccode>plugin-qualify-sync\u003C\u002Fcode> qualifies every plugin installed on the machine, and again each one whose files\nchange. With its pin file in plugin-hostd's settings, the daemon runs in its default mode (\u003Ccode>require_pins 1\u003C\u002Fcode>)\nand chains any plugin that qualified, the image's own and any installed later:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>pin_file \u002Fvar\u002Flib\u002Fomx\u002Fqualified.pins          # in plugin-hostd.conf\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The package ships it as a service that runs two minutes after boot and whenever a plugin directory\nchanges, once the directory has been quiet for a minute. A run finds every \u003Ccode>.clap\u003C\u002Fcode> under the CLAP\ndirectories and every plugin of every bundle under the LV2 ones, and hashes each one's files as\nplugin-hostd does for a pin. A plugin whose files hash as recorded is skipped. A new or changed one gets the\nquick qualification through the probe runner, on the grid \u003Ccode>grid\u003C\u002Fcode> names (see &quot;The measurement grid&quot;): its pin, the RT-safety sweep, cost and latency, its\nqualification record and its descriptor. The soak is not part of it, and every record it writes says so in\n\u003Ccode>notMeasured\u003C\u002Fcode> (\u003Ccode>hosting.stability.unsoaked\u003C\u002Fcode>).\u003C\u002Fp>\n\u003Cp>Each run of the runner (one cell of an LV2 plugin, one quantum of a CLAP one) has \u003Ccode>budget_s\u003C\u002Fcode> seconds, under \u003Ccode>memory_limit\u003C\u002Fcode>. One that crashes or hangs is recorded as \u003Ccode>crashed\u003C\u002Fcode> or \u003Ccode>hung\u003C\u002Fcode>, one that\nfails the sweep is pinned \u003Ccode>verdict=refused\u003C\u002Fcode>, and the run goes on to the next. The pin file and the state are\nwritten whole after every plugin, so a run cut short keeps what it finished. The last line counts the run:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>qualified 2, skipped 0, refused 0, crashed 1, hung 1, unmeasured 0, distributed 0, 4.6 s\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>A plugin that is no longer found, or whose bytes changed, leaves nothing behind: its pin, its qualification\nrecord (with the probe output copied beside it) and its descriptor are removed. Records the sync did not write\nstay, and nothing is removed in a run that could not read a bundle, so a half-written install is no uninstall.\nA run looks only where its settings point: a plugin outside every directory of its format that the run scans is\nkept as it is (a run scoped to LV2 keeps every CLAP pin), and only one missing from a directory it scans is gone.\nA running plugin-hostd is told with \u003Ccode>pin_clear\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>When plugin-hostd answers on \u003Ccode>hostd_port\u003C\u002Fcode>, the changed pins are also sent to it with \u003Ccode>pin_set\u003C\u002Fcode> and apply at\nonce. plugin-hostd serves one controller at a time, so while one is connected the run says so and the pins\napply at the daemon's next start.\u003C\u002Fp>\n\u003Cp>The settings are \u003Ccode>\u002Fetc\u002Fomx\u002Fqualify.conf\u003C\u002Fcode>, one \u003Ccode>key value\u003C\u002Fcode> per line, and each is also a flag\n(\u003Ccode>--budget-s 30\u003C\u002Fcode>):\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>key\u003C\u002Fth>\n\u003Cth>default\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>lv2_path\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>$LV2_PATH\u003C\u002Fcode>, else plugin-hostd's: \u003Ccode>\u002Fusr\u002Flib64\u002Flv2:\u002Fusr\u002Flib\u002Flv2:\u002Fusr\u002Flocal\u002Flib\u002Flv2\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>lv2_extra\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>\u002Fzynthian\u002Fzynthian-plugins\u002Flv2:\u002Fzynthian\u002Fzynthian-my-data\u002Fplugins\u002Flv2\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>clap_path\u003C\u002Fcode>, \u003Ccode>clap_extra\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>$CLAP_PATH\u003C\u002Fcode>, then \u003Ccode>~\u002F.clap\u003C\u002Fcode>, then \u003Ccode>\u002Fusr\u002Flib\u002Fclap\u003C\u002Fcode>; none\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>pin_file\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>\u002Fvar\u002Flib\u002Fomx\u002Fqualified.pins\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>qualification_dir\u003C\u002Fcode>, \u003Ccode>descriptor_dir\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>\u002Fvar\u002Flib\u002Fomx\u002Fqualification\u003C\u002Fcode>, \u003Ccode>\u002Fvar\u002Flib\u002Fomx\u002Fdescriptors\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>state_file\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>\u002Fvar\u002Flib\u002Fomx\u002Fqualify-sync.json\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>budget_s\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>60\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>host_profile\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>omx-clap-host\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>grid\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>full\u003C\u002Fcode>: every rate at every block size, 16 cells (\u003Ccode>hosts\u003C\u002Fcode>, or \u003Ccode>--grid-hosts\u003C\u002Fcode>: the cells of \u003Ccode>host_profile\u003C\u002Fcode> and of the console's two tiers, then 512 at every rate; or \u003Ccode>&lt;rate&gt;\u002F&lt;quantum&gt;,...\u003C\u002Fcode>)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>cells_per_plugin_budget\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>0\u003C\u002Fcode>, every cell of the grid\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>memory_limit\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>half of the machine's RAM (\u003Ccode>off\u003C\u002Fcode> for none)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>memory_notes\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>16\u003C\u002Fcode>: the notes an instrument with no declared capacity holds in a \u003Ccode>--memory-only\u003C\u002Fcode> run\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>hostd_port\u003C\u002Fcode>, \u003Ccode>hostd_feedback_port\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>5555\u003C\u002Fcode>, the next port\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>host_class\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>the board's own, from the device tree (\u003Ccode>rpi5\u003C\u002Fcode>); none elsewhere\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>distributed_dir\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>\u002Fusr\u002Fshare\u002Fomx\u002Fqualification\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>Those are the service's (\u003Ccode>--system\u003C\u002Fcode>); run by a user, the files go under \u003Ccode>$XDG_STATE_HOME\u002Fomx\u003C\u002Fcode>. An LV2\ndirectory added here belongs in plugin-hostd's \u003Ccode>lv2_path\u003C\u002Fcode> too, or the daemon cannot find the bundle it\npinned. \u003Ccode>--force\u003C\u002Fcode> qualifies everything again, and \u003Ccode>--no-distributed\u003C\u002Fcode> reads no distributed record. A second \u003Ccode>plugin-qualify-sync\u003C\u002Fcode> waits for the run in progress (a lock on \u003Ccode>&lt;state_file&gt;.lock\u003C\u002Fcode>); \u003Ccode>--no-wait\u003C\u002Fcode> exits 0 at once and leaves a marker, and the holder runs once more at its end over what changed meanwhile.\u003C\u002Fp>\n\u003Cp>Every run starts by bringing each pin's files up to the ones \u003Ccode>plugin-hostd --pin-files\u003C\u002Fcode> names, measuring nothing: a pin\nwritten before 0.17.3 lacks the preset files an LV2's manifest names, which plugin-hostd 0.15.0 also checks, and its add\nanswers -509 until then. The new list replaces the old one when no file both lists hold changed its hash, so the verdict\nstands; a changed file is measured again by the run. \u003Ccode>--refresh-pins\u003C\u002Fcode> does only that, writes the pin file and sends the\nchanged pins to a running plugin-hostd; the package's upgrade runs it (plugin-qualify#239).\u003C\u002Fp>\n\u003Cp>The path unit names the standard folders (\u003Ccode>\u002Fusr\u002Flib\u002Flv2\u003C\u002Fcode>, \u003Ccode>\u002Fusr\u002Flib64\u002Flv2\u003C\u002Fcode>, \u003Ccode>\u002Fusr\u002Flocal\u002Flib\u002Flv2\u003C\u002Fcode>,\n\u003Ccode>\u002Fusr\u002Flib\u002Fclap\u003C\u002Fcode>, \u003Ccode>\u002Fusr\u002Flib64\u002Fclap\u003C\u002Fcode>). The folders the settings file adds (\u003Ccode>lv2_path\u003C\u002Fcode>, \u003Ccode>lv2_extra\u003C\u002Fcode>, \u003Ccode>clap_path\u003C\u002Fcode>,\n\u003Ccode>clap_extra\u003C\u002Fcode>) are written into a drop-in of the unit by a systemd generator at boot and at every\n\u003Ccode>systemctl daemon-reload\u003C\u002Fcode>, so the file is the one place a machine's extra folders are written.\u003C\u002Fp>\n\u003Cp>The path unit watches a folder, not the bundles inside it: a new bundle appearing in a watched folder is\nseen at once, but a package's own files being replaced in place (an upgrade) is not a change of the folder\nitself. A packaged plugin is still caught at once, by the distribution's install triggers over the standard\nfolders. An unpackaged bundle replaced in place in an extra folder is caught at the next run of\nplugin-qualify-sync.timer, at most about 15 minutes after the change.\u003C\u002Fp>\n\u003Ch3 id=\"keeping-the-records-through-an-upgrade\">Keeping the records through an upgrade\u003C\u002Fh3>\n\u003Cp>An upgrade of plugin-qualify, of the plugins' libraries or of the judge's rules does not call for measuring every plugin\nagain. The figures in the records are carried across and judged again:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-bash\">plugin-qualify-sync --system --rejudge\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>--rejudge\u003C\u002Fcode> measures nothing and needs no measuring tool. It reads every record in \u003Ccode>qualification_dir\u003C\u002Fcode> with the probe\noutput it names, and passes them through the same judge a measurement goes through, under the rules of the running\nversion: a real-time violation under \u003Ccode>run()\u003C\u002Fcode> or under notes is \u003Ccode>conditional\u003C\u002Fcode> and pinned \u003Ccode>isolated\u003C\u002Fcode>, each host's budget is\nread, and a plugin with no footprint is \u003Ccode>unknown\u003C\u002Fcode> on a host with a memory budget, never read as fitting. It also judges\neach record for \u003Ccode>omx-clap-host\u003C\u002Fcode>, \u003Ccode>openmixer-console\u003C\u002Fcode> and the Pi boards \u003Ccode>openmixer-pi-4gb\u003C\u002Fcode> and \u003Ccode>openmixer-pi-8gb\u003C\u002Fcode>. The\nverdicts, the tiers and the plugin's pin word are replaced; the figures, the probe output and who took them stay as they\nwere. The record gains a \u003Ccode>rejudged\u003C\u002Fcode> block with \u003Ccode>at\u003C\u002Fcode>, \u003Ccode>by\u003C\u002Fcode> (the version that judged) and \u003Ccode>figures\u003C\u002Fcode> (\u003Ccode>tool\u003C\u002Fcode>, \u003Ccode>measuredAt\u003C\u002Fcode>\nand \u003Ccode>runId\u003C\u002Fcode>, as measured). A run that finds nothing to change writes nothing.\u003C\u002Fp>\n\u003Cp>A verdict never says more than the figures do. An LV2 plugin's \u003Ccode>omx-clap-host\u003C\u002Fcode> verdict is decided by its run under the LV2\nadapter (the record's \u003Ccode>adapter\u003C\u002Fcode> block). A record with no measured adapter run reads \u003Ccode>unknown\u003C\u002Fcode>\n(\u003Ccode>qualify.sync.adapter-run-owed\u003C\u002Fcode>), is not offered on the isolated tier, and its pin has no verdict word: plugin-hostd loads\nsuch a plugin without grouping it. The same goes for a \u003Ccode>mod-host\u003C\u002Fcode> or \u003Ccode>jalv\u003C\u002Fcode> verdict with no run under that host. A record\nwhose plugin's bytes changed, or whose probe output is not the copy it names, is left as it is. Everything left owing\nis listed under \u003Ccode>owes\u003C\u002Fcode> in the record and in \u003Ccode>qualify-owed.json\u003C\u002Fcode> beside the state file (\u003Ccode>--rejudge\u003C\u002Fcode> prints each line too),\nwith the measurement it needs:\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>\u003Ccode>measurement\u003C\u002Fcode>\u003C\u002Fth>\n\u003Cth>code\u003C\u002Fth>\n\u003Cth>what pays it\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Ccode>adapter-run\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>qualify.sync.adapter-run-owed\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>--adapter-only\u003C\u002Fcode>, then \u003Ccode>--rejudge\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>host-run\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>qualify.sync.host-run-owed\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>a run under that host; today, qualifying the plugin again with \u003Ccode>--host-profile\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode>qualification\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>qualify.sync.bytes-changed\u003C\u002Fcode>, \u003Ccode>qualify.sync.figures-unreadable\u003C\u002Fcode>, \u003Ccode>qualify.sync.no-figures\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>qualifying the plugin again; the next run does so for changed bytes\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>A probe that crashed took no figures: its record stays \u003Ccode>unknown\u003C\u002Fcode> with \u003Ccode>probe.crashed\u003C\u002Fcode>, no pin word and the state's\noutcome \u003Ccode>crashed\u003C\u002Fcode>. A crash under a host's run stays \u003Ccode>unsuitable\u003C\u002Fcode> and \u003Ccode>refused\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>Footprints older records lack are taken apart from everything else:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-bash\">plugin-qualify-sync --system --memory-only   # the footprints, at the cells a Pi runs\nplugin-qualify-sync --system --rejudge       # the verdicts that read them\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>--memory-only\u003C\u002Fcode> takes the footprint of each plugin the state holds (after instantiate, after activate, at rest, a second\ninstance, and notes held for an instrument) at 48 kHz in blocks of 128 and 256 frames, and nothing else: no sweep, no\ncost, no latency, no lifecycle, no soak. It goes through the probe runner under the limits of an ordinary run (\u003Ccode>budget_s\u003C\u002Fcode>,\n\u003Ccode>memory_limit\u003C\u002Fcode>), each plugin in processes of its own, an LV2 plugin in a world of its own bundle. An instrument that\ndeclares its voice capacity holds that many notes; one that does not holds \u003Ccode>memory_notes\u003C\u002Fcode>, with basis \u003Ccode>probe\u003C\u002Fcode>. Only the\n\u003Ccode>memory\u003C\u002Fcode> of those cells changes in the record, a cell it lacked joins with no other figure, and a \u003Ccode>memoryRun\u003C\u002Fcode> block says\nwhich runner took them and when. A cell whose run failed holds a reason and no figure. A plugin whose footprints are\nalready merged is skipped, so a pass cut short can be run again; \u003Ccode>--force\u003C\u002Fcode> takes them again. The pass judges nothing:\n\u003Ccode>--rejudge\u003C\u002Fcode> afterwards does.\u003C\u002Fp>\n\u003Cp>The adapter runs a record owes are taken apart as well:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-bash\">plugin-qualify-sync --system --adapter-only  # the runs, at the cells a Pi runs\nplugin-qualify-sync --system --rejudge       # the verdicts they decide\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>--isolated-only\u003C\u002Fcode> runs the isolated step a record still owes: a plugin rated conditional, or one that crashed or hung in\nthe probe, added alone under a plugin-hostd of its own on a private JACK server, with a sine through it for 3 seconds. The\nrecord's \u003Ccode>run\u003C\u002Fcode> keeps its id and gains the step, and the pin follows it; nothing else is measured. A step the machine could\nnot run stays owed.\u003C\u002Fp>\n\u003Cp>\u003Ccode>--adapter-only\u003C\u002Fcode> starts each LV2 plugin the state holds under a fresh omx-clap-host, \u003Ccode>add\u003C\u002Fcode> then \u003Ccode>remove\u003C\u002Fcode> through the LV2\nadapter, once per cell: 48 kHz in blocks of 128 and 256 frames. Each cell has a private JACK server (\u003Ccode>jackd -d dummy\u003C\u002Fcode>, so\n\u003Ccode>jackd\u003C\u002Fcode> must be installed), so the device's live server is never touched. A command has \u003Ccode>budget_s\u003C\u002Fcode> seconds, the host's\naddress space is limited as a direct spawn's is, and it is killed with whatever it started. Only the record's \u003Ccode>adapter\u003C\u002Fcode>\nblock changes: \u003Ccode>versions\u003C\u002Fcode> (omx-clap-host and plugin-hostd, read from \u003Ccode>--version\u003C\u002Fcode>), \u003Ccode>measuredAt\u003C\u002Fcode>, \u003Ccode>cells\u003C\u002Fcode>, and at its top\nwhat the judge reads. A crash or a command that gets no answer in time is the plugin's recorded outcome, at the cell it\nhappened, and reads \u003Ccode>unsuitable\u003C\u002Fcode> at the next \u003Ccode>--rejudge\u003C\u002Fcode>. A host that could not be run decides nothing: the block says\n\u003Ccode>measured: false\u003C\u002Fcode>, and the run stays owed and is tried again. The pass refuses to start, writing nothing, when a host does\nnot say its version or there is no \u003Ccode>jackd\u003C\u002Fcode>. A plugin whose block was taken at these host versions is skipped, so a pass\ncut short can be run again; a host upgrade or \u003Ccode>--force\u003C\u002Fcode> takes them again. A plugin whose probe failed is skipped: it has\nno verdict a run could decide.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>A leak is not measured.\u003C\u002Fstrong> Growth over time takes a soak, which this pass does not run: every record it writes names\n\u003Ccode>memoryGrowth\u003C\u002Fcode> under \u003Ccode>notMeasured\u003C\u002Fcode>, and the last line says \u003Ccode>leaks not measured\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>On a Raspberry Pi 5 a plugin takes under 0.6 s at the median, 5 s at the 90th percentile and 22 s at the 99th; about an\nhour at most for the 1,542 plugins of the Zynthian set. Those are upper bounds: twice the time the 2026-10-28 records say a\nwhole pass at one cell took, which also ran 30 lifecycles, the sweep, the latency and the cost. A plugin that hangs\ntakes \u003Ccode>budget_s\u003C\u002Fcode> a cell. The test fixtures take 0.1 s each, both cells, on an x86-64 desktop.\u003C\u002Fp>\n\u003Ch3 id=\"distributed-records\">Distributed records\u003C\u002Fh3>\n\u003Cp>A device that ships with a known plugin set need not measure it again at first start. The qualification records\nanother device of the same machine class wrote for that set are installed read-only, and the sync takes from them\nevery plugin whose files are the bytes that were measured:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002Fusr\u002Fshare\u002Fomx\u002Fqualification\u002F&lt;host class&gt;\u002F&lt;set&gt;\u002F          # system (--system)\n$XDG_DATA_HOME\u002Fomx\u002Fqualification\u002F&lt;host class&gt;\u002F&lt;set&gt;\u002F      # a user run, then each $XDG_DATA_DIRS\u002Fomx\u002Fqualification\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Each \u003Ccode>&lt;set&gt;\u003C\u002Fcode> directory holds the records (\u003Ccode>*.qualification.json\u003C\u002Fcode>, each with its probe output \u003Ccode>*.probe.json\u003C\u002Fcode>) and a\n\u003Ccode>provenance.json\u003C\u002Fcode> that says where they come from: the set, the host class, the image, the package versions of the\nmeasuring device, the date and the package that installed them. The host class is a directory name: \u003Ccode>host_class\u003C\u002Fcode>\nin the settings, or, when it is unset, the board's own name read from the device tree (\u003Ccode>rpi5\u003C\u002Fcode> on a Raspberry Pi 5,\n\u003Ccode>rpi4\u003C\u002Fcode> on a Raspberry Pi 4). Where neither gives one, the layer is not read and the run says so.\u003C\u002Fp>\n\u003Cp>Before a plugin is measured, the sync looks for a distributed record of it: a CLAP record whose binary is the same\n\u003Ccode>.clap\u003C\u002Fcode> path, or an LV2 record of the same plugin uri. The name only finds the record; a record applies when, and\nonly when:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>every file of its \u003Ccode>binary.files\u003C\u002Fcode> is installed at that path relative to the plugin's root, with that SHA-256,\ncomputed on the device;\u003C\u002Fli>\n\u003Cli>its schema is the one this version writes and its probe output is one this version's reader accepts, the copy\nintact (its SHA-256 the one the record names): the same acceptance the sync gives a record of its own, which it\nnever measures again for a newer tool;\u003C\u002Fli>\n\u003Cli>it holds a verdict for the sync's \u003Ccode>host_profile\u003C\u002Fcode>, and that verdict ended \u003Ccode>qualified\u003C\u002Fcode>, \u003Ccode>refused\u003C\u002Fcode>, \u003Ccode>crashed\u003C\u002Fcode> or\n\u003Ccode>hung\u003C\u002Fcode>. One that could not be measured at the source (\u003Ccode>unmeasured\u003C\u002Fcode>) is measured here.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>A plugin whose distributed records apply is not measured: the records are copied into \u003Ccode>qualification_dir\u003C\u002Fcode> with the\nset's \u003Ccode>provenance\u003C\u002Fcode> object added, its pin is taken from the probe output the record judged, its descriptor is\nwritten, and the run says\u003C\u002Fp>\n\u003Cpre>\u003Ccode>distributed: http:\u002F\u002Fdistrho.sf.net\u002Fplugins\u002F3BandEQ: applied (zynthian-vangelis)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>A plugin with a distributed record whose files differ is measured as before, and the run says\n\u003Ccode>distributed: &lt;uri&gt;: binary differs\u003C\u002Fcode>; one with no distributed record is measured without a word. The summary counts\nthe plugins taken from the layer, \u003Ccode>distributed N\u003C\u002Fcode>, apart from the ones measured and skipped. A later run treats an\napplied plugin like one it measured: skipped while its files are unchanged, measured when they change. \u003Ccode>--force\u003C\u002Fcode>\nmeasures everything and reads no distributed record; \u003Ccode>--no-distributed\u003C\u002Fcode> keeps the layer out of a run.\u003C\u002Fp>\n\u003Cp>The package that installs a set is built from a device's \u003Ccode>qualification_dir\u003C\u002Fcode> by \u003Ccode>plugin-qualify-records-package\u003C\u002Fcode>:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>plugin-qualify-records-package \u002Fvar\u002Flib\u002Fomx\u002Fqualification --host-class rpi5 --set zynthian-vangelis \\\n  --provenance provenance.json --out .\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>It writes \u003Ccode>omx-qualification-&lt;host class&gt;-&lt;set&gt;_&lt;version&gt;_all.deb\u003C\u002Fcode>, the version the build date\n(\u003Ccode>2026.10.29\u003C\u002Fcode>) unless \u003Ccode>--version\u003C\u002Fcode> names one. The package installs the records under\n\u003Ccode>\u002Fusr\u002Fshare\u002Fomx\u002Fqualification\u002F&lt;host class&gt;\u002F&lt;set&gt;\u002F\u003C\u002Fcode> with \u003Ccode>provenance.json\u003C\u002Fcode>, depends on the plugin-qualify that reads\nthe layer, and is \u003Ccode>Architecture: all\u003C\u002Fcode>. \u003Ccode>--provenance\u003C\u002Fcode> names a JSON file (or holds the JSON itself) with what the tool\ncannot know: the image, the package versions and the source revisions of the device. The tool adds the set, the host\nclass, the package and the date. A record whose probe output is missing or does not match its SHA-256 is left out\nand named; a record of a plugin that could not be measured (\u003Ccode>*.unmeasured.json\u003C\u002Fcode>) is left out, since it never\napplies. The tool builds the Debian package only; an RPM of a set is not built yet.\u003C\u002Fp>\n\u003Ch3 id=\"the-container-image\">The container image\u003C\u002Fh3>\n\u003Cpre>\u003Ccode>podman build -t plugin-qualify -f Containerfile .\npodman run --rm -v &quot;$PWD\u002Fmy-plugins:\u002Fplugins:ro&quot; -e LV2_PATH=\u002Fplugins -v &quot;$PWD\u002Fout:\u002Fout&quot; \\\n  plugin-qualify --host-profile jalv --all --out \u002Fout\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The image has lilv, the distribution's mod-host and an AddressSanitizer build of mod-host.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Status:\u003C\u002Fstrong> the pure steps are in place and tested: target resolution, profile resolution, the\nverdict, the document and the exit code. The command-line entry point does not yet drive the\nmeasuring tools (\u003Ccode>tools\u002Flv2-measure.mjs\u003C\u002Fcode>, the mod-host sweep). A run that would measure exits\n\u003Ccode>2\u003C\u002Fcode> and names the missing step, rather than rate a plugin on less than was asked.\u003C\u002Fp>\n",[],1793954996200]