@openmixer/plugin-qualify 0.13.1 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +151 -14
- package/bin/plugin-qualify-sync.mjs +57 -0
- package/bin/plugin-qualify.mjs +66 -15
- package/dist/adjust-cli.d.ts +83 -0
- package/dist/adjust-cli.js +214 -0
- package/dist/adjust-cli.js.map +1 -0
- package/dist/clap-loader.d.ts +3 -3
- package/dist/clap-loader.js +11 -11
- package/dist/clap-loader.js.map +1 -1
- package/dist/history-adjust.d.ts +77 -0
- package/dist/history-adjust.js +99 -0
- package/dist/history-adjust.js.map +1 -0
- package/dist/history-file.d.ts +73 -0
- package/dist/history-file.js +133 -0
- package/dist/history-file.js.map +1 -0
- package/dist/host-admission.d.ts +53 -0
- package/dist/host-admission.js +96 -0
- package/dist/host-admission.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -1
- package/dist/lv2-loader.d.ts +16 -2
- package/dist/lv2-loader.js +168 -17
- package/dist/lv2-loader.js.map +1 -1
- package/dist/measurement/cli.d.ts +6 -4
- package/dist/measurement/cli.js +12 -19
- package/dist/measurement/cli.js.map +1 -1
- package/dist/pins-file.d.ts +8 -2
- package/dist/pins-file.js +17 -3
- package/dist/pins-file.js.map +1 -1
- package/dist/probe-judge.d.ts +11 -1
- package/dist/probe-judge.js +20 -1
- package/dist/probe-judge.js.map +1 -1
- package/dist/probe-output-fixtures.d.ts +10 -0
- package/dist/probe-output-fixtures.js +86 -0
- package/dist/probe-output-fixtures.js.map +1 -1
- package/dist/probe-output.d.ts +2 -0
- package/dist/probe-output.js +1 -0
- package/dist/probe-output.js.map +1 -1
- package/dist/probe-runner.d.ts +9 -3
- package/dist/probe-runner.js +36 -12
- package/dist/probe-runner.js.map +1 -1
- package/dist/qualification-record.d.ts +109 -3
- package/dist/qualification-record.js +79 -13
- package/dist/qualification-record.js.map +1 -1
- package/dist/qualify-cli.d.ts +0 -1
- package/dist/qualify-cli.js +2 -4
- package/dist/qualify-cli.js.map +1 -1
- package/dist/qualify-declarations.d.ts +26 -0
- package/dist/qualify-declarations.js +26 -0
- package/dist/qualify-declarations.js.map +1 -1
- package/dist/qualify-options.js +1 -2
- package/dist/qualify-options.js.map +1 -1
- package/dist/qualify-sync.d.ts +202 -0
- package/dist/qualify-sync.js +731 -0
- package/dist/qualify-sync.js.map +1 -0
- package/dist/whole-file.d.ts +3 -0
- package/dist/whole-file.js +36 -0
- package/dist/whole-file.js.map +1 -0
- package/fixtures/admission/2026-10-26.json +19 -0
- package/fixtures/clap-scan/fake.json +24 -24
- package/package.json +5 -4
- package/src/adjust-cli.ts +262 -0
- package/src/clap-loader.ts +9 -13
- package/src/history-adjust.ts +156 -0
- package/src/history-file.ts +177 -0
- package/src/host-admission.ts +136 -0
- package/src/index.ts +6 -0
- package/src/lv2-loader.ts +150 -15
- package/src/measurement/cli.ts +11 -17
- package/src/pins-file.ts +17 -3
- package/src/probe-judge.ts +27 -1
- package/src/probe-output-fixtures.ts +86 -0
- package/src/probe-output.ts +3 -0
- package/src/probe-runner.ts +45 -14
- package/src/qualification-record.ts +174 -15
- package/src/qualify-cli.ts +2 -5
- package/src/qualify-declarations.ts +29 -0
- package/src/qualify-options.ts +1 -2
- package/src/qualify-sync.ts +830 -0
- package/src/whole-file.ts +35 -0
- package/tools/_declarations.py +11 -0
- package/tools/audit-overlap.sh +250 -0
- package/tools/descriptors.py +382 -0
- package/tools/gen-hosting-codes.mjs +1 -1
- package/tools/gen-python-declarations.mjs +13 -1
- package/tools/mod-host-admit.mjs +74 -0
- package/tools/scan.py +260 -4
package/tools/scan.py
CHANGED
|
@@ -13,17 +13,25 @@ parameter are captured: classic LV2 **control ports** (set via mod-host
|
|
|
13
13
|
`param_set <id> <symbol> <value>`) and LV2 **Patch properties** (`lv2:Parameter`
|
|
14
14
|
flagged `patch:writable`, set via `patch_set <id> <uri> <value>`).
|
|
15
15
|
|
|
16
|
+
Beside the catalog, every plugin scanned is written as a scanned descriptor (omx-contract's
|
|
17
|
+
descriptor words, `descriptors.py`) into `descriptor_dir`: `--descriptor-dir`, else `--system`'s
|
|
18
|
+
directory, else `$XDG_STATE_HOME/omx/descriptors`, else `~/.local/state/omx/descriptors`.
|
|
19
|
+
|
|
16
20
|
Usage:
|
|
17
21
|
scan.py # scan all installed plugins -> stdout JSON
|
|
18
22
|
scan.py --out catalog.json # write to a file
|
|
19
23
|
scan.py URI [URI ...] # scan only these URIs (faster, for tests)
|
|
20
24
|
scan.py --annotate catalog.json # refresh only the DECLARED fields, in place
|
|
25
|
+
scan.py --descriptor-dir DIR # the scanned descriptors go here
|
|
26
|
+
scan.py --no-descriptors # the catalog only
|
|
21
27
|
"""
|
|
22
28
|
import argparse
|
|
23
29
|
import json
|
|
30
|
+
import os
|
|
24
31
|
import subprocess
|
|
25
32
|
import sys
|
|
26
33
|
|
|
34
|
+
import descriptors as desc
|
|
27
35
|
from lv2_presets import read_presets
|
|
28
36
|
|
|
29
37
|
LV2 = "http://lv2plug.in/ns/lv2core#"
|
|
@@ -36,6 +44,9 @@ PATCH = "http://lv2plug.in/ns/ext/patch#"
|
|
|
36
44
|
ATOM = "http://lv2plug.in/ns/ext/atom#"
|
|
37
45
|
RDFS = "http://www.w3.org/2000/01/rdf-schema#"
|
|
38
46
|
MIDI = "http://lv2plug.in/ns/ext/midi#"
|
|
47
|
+
PG = "http://lv2plug.in/ns/ext/port-groups#"
|
|
48
|
+
PARAM = "http://lv2plug.in/ns/ext/parameters#"
|
|
49
|
+
TIME = "http://lv2plug.in/ns/ext/time#"
|
|
39
50
|
|
|
40
51
|
# An output control port designated lv2:latency reports the plugin's own latency
|
|
41
52
|
# (in frames) — the truth a host delay-compensates by, and what openmixer reads to
|
|
@@ -61,6 +72,30 @@ KNOWN_UNITS = {
|
|
|
61
72
|
UNITS + "bpm": "BPM", UNITS + "midiNote": "note", UNITS + "frame": "fr",
|
|
62
73
|
}
|
|
63
74
|
|
|
75
|
+
# LV2 unit URI -> the descriptor's unit word; any other unit, or none, is `unknown`
|
|
76
|
+
DESCRIPTOR_UNITS = {
|
|
77
|
+
UNITS + "db": "dB", UNITS + "hz": "Hz", UNITS + "s": "s", UNITS + "ms": "ms",
|
|
78
|
+
UNITS + "min": "min", UNITS + "pc": "%", UNITS + "semitone12TET": "st", UNITS + "cent": "cents",
|
|
79
|
+
UNITS + "degree": "deg", UNITS + "coef": "x", UNITS + "oct": "oct", UNITS + "bpm": "bpm",
|
|
80
|
+
UNITS + "midiNote": "midiNote", UNITS + "frame": "frames",
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
# lv2:designation -> the descriptor's meaning, where the meaning carries the LV2 term's name
|
|
84
|
+
DESIGNATED_MEANINGS = {
|
|
85
|
+
**{PARAM + m: m for m in ("gain", "threshold", "ratio", "attack", "release", "hold", "decay", "frequency",
|
|
86
|
+
"cutoffFrequency", "resonance", "bypass", "wetDryRatio", "delay")},
|
|
87
|
+
LV2 + "enabled": "enabled",
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
# LV2 classes -> the descriptor's plugin features; a plugin of none of them is an audio effect
|
|
91
|
+
CLASS_FEATURES = {
|
|
92
|
+
LV2 + "InstrumentPlugin": "instrument",
|
|
93
|
+
LV2 + "AnalyserPlugin": "analyzer",
|
|
94
|
+
LV2 + "MIDIPlugin": "note-effect",
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
NUMERIC_PATCH_RANGES = ("Float", "Double", "Int", "Long")
|
|
98
|
+
|
|
64
99
|
|
|
65
100
|
def fnum(node):
|
|
66
101
|
"""float(node) tolerant of None and integer/string-typed nodes.
|
|
@@ -80,11 +115,23 @@ def fnum(node):
|
|
|
80
115
|
return None
|
|
81
116
|
|
|
82
117
|
|
|
118
|
+
def each(nodes):
|
|
119
|
+
"""Every element of a lilv collection (None is empty), walked by the binding's own begin, is_end and next,
|
|
120
|
+
which every version has: the Python iteration of some does not hold for every collection."""
|
|
121
|
+
if nodes is None:
|
|
122
|
+
return []
|
|
123
|
+
if isinstance(nodes, (list, tuple)):
|
|
124
|
+
return list(nodes)
|
|
125
|
+
out = []
|
|
126
|
+
it = nodes.begin()
|
|
127
|
+
while not it.is_end():
|
|
128
|
+
out.append(it.next())
|
|
129
|
+
return out
|
|
130
|
+
|
|
131
|
+
|
|
83
132
|
def first(nodes):
|
|
84
133
|
"""First element of a lilv Nodes collection, or None."""
|
|
85
|
-
|
|
86
|
-
return None
|
|
87
|
-
for n in nodes:
|
|
134
|
+
for n in each(nodes):
|
|
88
135
|
return n
|
|
89
136
|
return None
|
|
90
137
|
|
|
@@ -144,6 +191,8 @@ class Scanner:
|
|
|
144
191
|
RDFS + "label", RDFS + "range", MIDI + "MidiEvent",
|
|
145
192
|
LV2 + "designation", LATENCY, REPORTS_LATENCY,
|
|
146
193
|
MINIMUM, MAXIMUM, DEFAULT,
|
|
194
|
+
LV2 + "isSideChain", LV2 + "name", PG + "group", TIME + "Position",
|
|
195
|
+
"http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
|
|
147
196
|
)}
|
|
148
197
|
|
|
149
198
|
def is_a(self, port, uri):
|
|
@@ -306,6 +355,195 @@ class Scanner:
|
|
|
306
355
|
out["owningRpm"] = owning_rpm(bp)
|
|
307
356
|
return out
|
|
308
357
|
|
|
358
|
+
# ---- the scanned descriptor ----
|
|
359
|
+
|
|
360
|
+
def designation_meaning(self, port):
|
|
361
|
+
"""The meaning an lv2:designation names; `unknown` with none; absent (None) for one with no word."""
|
|
362
|
+
des = [str(d) for d in each(port.get_value(self.u[LV2 + "designation"]))]
|
|
363
|
+
if not des:
|
|
364
|
+
return "unknown"
|
|
365
|
+
for d in des:
|
|
366
|
+
if d in DESIGNATED_MEANINGS:
|
|
367
|
+
return DESIGNATED_MEANINGS[d]
|
|
368
|
+
return None
|
|
369
|
+
|
|
370
|
+
def port_group(self, port):
|
|
371
|
+
"""The pg:group a port belongs to, by its name, as a one-step group path."""
|
|
372
|
+
g = first(port.get_value(self.u[PG + "group"]))
|
|
373
|
+
if g is None:
|
|
374
|
+
return None
|
|
375
|
+
label = first(self.w.find_nodes(g, self.u[LV2 + "name"], None)) or first(self.w.find_nodes(g, self.u[RDFS + "label"], None))
|
|
376
|
+
return [str(label)] if label is not None and str(label) else None
|
|
377
|
+
|
|
378
|
+
def descriptor_unit(self, port):
|
|
379
|
+
un = first(port.get_value(self.u[UNITS + "unit"]))
|
|
380
|
+
return DESCRIPTOR_UNITS.get(str(un), "unknown") if un is not None else "unknown"
|
|
381
|
+
|
|
382
|
+
def descriptor_control(self, port, link, readonly=False):
|
|
383
|
+
"""An input (or, readonly, an output) control port as a descriptor control."""
|
|
384
|
+
d, mn, mx = port.get_range()
|
|
385
|
+
mnf, mxf, df = fnum(mn), fnum(mx), fnum(d)
|
|
386
|
+
sym = str(port.get_symbol())
|
|
387
|
+
nm = port.get_name()
|
|
388
|
+
c = {"id": sym, "name": str(nm) if nm is not None else sym}
|
|
389
|
+
sps = self.scale_points(port)
|
|
390
|
+
if self.has(port, LV2 + "toggled"):
|
|
391
|
+
c["kind"] = "toggle"
|
|
392
|
+
if df is not None:
|
|
393
|
+
c["default"] = df > 0
|
|
394
|
+
elif self.has(port, PP + "trigger"):
|
|
395
|
+
c["kind"] = "trigger"
|
|
396
|
+
elif self.has(port, LV2 + "enumeration") and sps:
|
|
397
|
+
ids = desc.choice_ids([sp["label"] for sp in sps])
|
|
398
|
+
c["kind"] = "choice"
|
|
399
|
+
c["ids"] = ids
|
|
400
|
+
if all(sp["label"] for sp in sps):
|
|
401
|
+
c["labels"] = [sp["label"] for sp in sps]
|
|
402
|
+
c["values"] = [sp["value"] for sp in sps]
|
|
403
|
+
for i, sp in enumerate(sps):
|
|
404
|
+
if df is not None and sp["value"] == df:
|
|
405
|
+
c["default"] = ids[i]
|
|
406
|
+
else:
|
|
407
|
+
# an LV2 port's range defaults to 0..1 where it declares none (the catalog row's own rule)
|
|
408
|
+
integer = self.has(port, LV2 + "integer")
|
|
409
|
+
travel = {
|
|
410
|
+
"min": mnf if mnf is not None else 0.0,
|
|
411
|
+
"max": mxf if mxf is not None else 1.0,
|
|
412
|
+
"unit": self.descriptor_unit(port),
|
|
413
|
+
"scale": "log" if self.has(port, PP + "logarithmic") else "stepped" if integer else "linear",
|
|
414
|
+
}
|
|
415
|
+
if integer:
|
|
416
|
+
travel["step"] = 1
|
|
417
|
+
if df is not None:
|
|
418
|
+
travel["default"] = df
|
|
419
|
+
c["kind"] = "travel"
|
|
420
|
+
c["travel"] = travel
|
|
421
|
+
group = self.port_group(port)
|
|
422
|
+
if group:
|
|
423
|
+
c["group"] = group
|
|
424
|
+
meaning = self.designation_meaning(port)
|
|
425
|
+
if meaning is not None:
|
|
426
|
+
c["meaning"] = meaning
|
|
427
|
+
if self.has(port, PP + "notOnGUI"):
|
|
428
|
+
c["hidden"] = True
|
|
429
|
+
if readonly:
|
|
430
|
+
c["readonly"] = True
|
|
431
|
+
c["source"] = {"from": f"{link}#{desc.link_symbol(sym)}"}
|
|
432
|
+
return c
|
|
433
|
+
|
|
434
|
+
def descriptor(self, plugin):
|
|
435
|
+
"""The plugin as a scanned descriptor, and the SHA-256 of its binary: `(doc, sha256)`."""
|
|
436
|
+
uri = str(plugin.get_uri())
|
|
437
|
+
bundle = bundle_path(plugin) or ""
|
|
438
|
+
lib = plugin.get_library_uri()
|
|
439
|
+
lib_path = lib.get_path() if lib is not None else None
|
|
440
|
+
try:
|
|
441
|
+
sha256 = desc.file_sha256(lib_path) if lib_path else None
|
|
442
|
+
except desc.DescriptorRefused:
|
|
443
|
+
sha256 = None # refused by its code when it is written: no name without the binary's hash
|
|
444
|
+
data = sorted(os.path.basename(str(u)) for u in each(plugin.get_data_uris()))
|
|
445
|
+
ttl = next((f for f in data if f != "manifest.ttl"), "manifest.ttl")
|
|
446
|
+
bundle_seg = desc.link_segment(os.path.basename(bundle))
|
|
447
|
+
link = f"{bundle_seg}/{desc.link_segment(ttl)}"
|
|
448
|
+
|
|
449
|
+
types = [str(t) for t in each(plugin.get_value(self.u["http://www.w3.org/1999/02/22-rdf-syntax-ns#type"]))]
|
|
450
|
+
features = [CLASS_FEATURES[t] for t in types if t in CLASS_FEATURES]
|
|
451
|
+
p = {"id": uri, "uri": uri, "name": str(plugin.get_name())}
|
|
452
|
+
author = plugin.get_author_name()
|
|
453
|
+
if author is not None and str(author):
|
|
454
|
+
p["vendor"] = str(author)
|
|
455
|
+
version = self.plugin_version(plugin)
|
|
456
|
+
if version is not None:
|
|
457
|
+
p["version"] = f"{version[0]}.{version[1]}"
|
|
458
|
+
comment = declared_comment(plugin, self.w)
|
|
459
|
+
if comment:
|
|
460
|
+
p["doc"] = comment
|
|
461
|
+
p["features"] = list(dict.fromkeys(features)) or ["audio-effect"]
|
|
462
|
+
d = desc.head("lv2", p)
|
|
463
|
+
|
|
464
|
+
controls, ports = [], []
|
|
465
|
+
latency_symbol, timed = None, False
|
|
466
|
+
for i in range(plugin.get_num_ports()):
|
|
467
|
+
port = plugin.get_port_by_index(i)
|
|
468
|
+
sym = str(port.get_symbol())
|
|
469
|
+
nm = port.get_name()
|
|
470
|
+
name = str(nm) if nm is not None else sym
|
|
471
|
+
direction = "in" if self.is_a(port, LV2 + "InputPort") else "out"
|
|
472
|
+
if self.is_a(port, LV2 + "AudioPort"):
|
|
473
|
+
ports.append({"id": sym, "name": name, "kind": "audio", "direction": direction,
|
|
474
|
+
"role": "sidechain" if self.has(port, LV2 + "isSideChain") else "main", "channels": 1})
|
|
475
|
+
elif self.is_a(port, LV2 + "CVPort"):
|
|
476
|
+
ports.append({"id": sym, "name": name, "kind": "cv", "direction": direction})
|
|
477
|
+
elif self.is_a(port, LV2 + "ControlPort"):
|
|
478
|
+
if direction == "out" and latency_symbol is None and self.is_latency_port(port):
|
|
479
|
+
latency_symbol = sym
|
|
480
|
+
continue
|
|
481
|
+
controls.append(self.descriptor_control(port, link, readonly=direction == "out"))
|
|
482
|
+
elif self.is_a(port, ATOM + "AtomPort"):
|
|
483
|
+
try:
|
|
484
|
+
midi = bool(port.supports_event(self.u[MIDI + "MidiEvent"]))
|
|
485
|
+
timed = timed or (direction == "in" and bool(port.supports_event(self.u[TIME + "Position"])))
|
|
486
|
+
except Exception: # noqa: BLE001
|
|
487
|
+
midi = False
|
|
488
|
+
if midi:
|
|
489
|
+
ports.append({"id": sym, "name": name, "kind": "midi", "direction": direction})
|
|
490
|
+
|
|
491
|
+
writable = []
|
|
492
|
+
for prm in self.patch_params(plugin):
|
|
493
|
+
writable.append({"uri": prm["uri"], "label": prm["name"], "range": prm["datatype"]})
|
|
494
|
+
if prm["datatype"] in NUMERIC_PATCH_RANGES and "min" in prm and "max" in prm:
|
|
495
|
+
travel = {"min": prm["min"], "max": prm["max"], "unit": "unknown", "scale": "linear"}
|
|
496
|
+
if "default" in prm:
|
|
497
|
+
travel["default"] = prm["default"]
|
|
498
|
+
controls.append({"id": desc.link_symbol(prm["symbol"]), "name": prm["name"], "kind": "travel",
|
|
499
|
+
"travel": travel, "meaning": "unknown",
|
|
500
|
+
"source": {"from": f"{link}#{desc.link_symbol(prm['symbol'])}"}})
|
|
501
|
+
d["controls"] = controls
|
|
502
|
+
d["ports"] = ports
|
|
503
|
+
|
|
504
|
+
needs = {}
|
|
505
|
+
if timed:
|
|
506
|
+
needs["tempo"] = needs["transport"] = True
|
|
507
|
+
required = [str(f) for f in each(plugin.get_required_features())]
|
|
508
|
+
optional = [str(f) for f in each(plugin.get_optional_features())]
|
|
509
|
+
ext = {}
|
|
510
|
+
if required:
|
|
511
|
+
ext["required"] = sorted(set(required))
|
|
512
|
+
if optional:
|
|
513
|
+
ext["optional"] = sorted(set(optional))
|
|
514
|
+
if ext:
|
|
515
|
+
needs["extensions"] = ext
|
|
516
|
+
if needs:
|
|
517
|
+
d["needs"] = needs
|
|
518
|
+
d["latency"] = {"reports": latency_symbol is not None}
|
|
519
|
+
|
|
520
|
+
bag = {"rdf:type": str(plugin.get_class().get_uri())}
|
|
521
|
+
if latency_symbol is not None:
|
|
522
|
+
bag["lv2:latency"] = latency_symbol
|
|
523
|
+
if writable:
|
|
524
|
+
bag["patch:writable"] = writable
|
|
525
|
+
d["extensions"] = {"lv2": bag}
|
|
526
|
+
lib_name = os.path.basename(lib_path) if lib_path else "manifest.ttl"
|
|
527
|
+
d["source"] = {"from": f"{bundle_seg}/{desc.link_segment(lib_name)}#lv2_descriptor"}
|
|
528
|
+
return d, sha256
|
|
529
|
+
|
|
530
|
+
def descriptors(self, uris=None):
|
|
531
|
+
"""`(doc, sha256)` for every plugin `scan` would describe."""
|
|
532
|
+
plugins = self.w.get_all_plugins()
|
|
533
|
+
chosen = [plugins.get_by_uri(self.w.new_uri(u)) for u in uris] if uris else list(plugins)
|
|
534
|
+
out = []
|
|
535
|
+
for p in chosen:
|
|
536
|
+
if p is None:
|
|
537
|
+
continue
|
|
538
|
+
try:
|
|
539
|
+
out.append(self.descriptor(p))
|
|
540
|
+
except desc.DescriptorRefused as e:
|
|
541
|
+
sys.stderr.write(f"descriptor refused: {p.get_uri()}: {e.code}: {e.problem}\n")
|
|
542
|
+
except Exception as e: # noqa: BLE001 - one plugin's bad data never stops the scan
|
|
543
|
+
line = e.__traceback__.tb_next.tb_lineno if e.__traceback__ and e.__traceback__.tb_next else "?"
|
|
544
|
+
sys.stderr.write(f"not described: {p.get_uri()}: {type(e).__name__}: {e} (scan.py:{line})\n")
|
|
545
|
+
return out
|
|
546
|
+
|
|
309
547
|
def plugin_version(self, plugin):
|
|
310
548
|
"""(minor, micro) from lv2:minorVersion / lv2:microVersion, or None.
|
|
311
549
|
|
|
@@ -395,6 +633,11 @@ def main():
|
|
|
395
633
|
ap.add_argument("--out", help="write JSON here (default: stdout)")
|
|
396
634
|
ap.add_argument("--annotate", metavar="FILE",
|
|
397
635
|
help="refresh only the DECLARED fields of an existing catalog, in place")
|
|
636
|
+
where = ap.add_mutually_exclusive_group()
|
|
637
|
+
where.add_argument("--descriptor-dir", help="where the scanned descriptors go (descriptor_dir)")
|
|
638
|
+
where.add_argument("--system", action="store_true",
|
|
639
|
+
help=f"a system service's descriptor_dir, {desc.DESCRIPTOR_DIR_SYSTEM}")
|
|
640
|
+
where.add_argument("--no-descriptors", action="store_true", help="write the catalog only")
|
|
398
641
|
args = ap.parse_args()
|
|
399
642
|
if args.annotate:
|
|
400
643
|
with open(args.annotate, encoding="utf-8") as f:
|
|
@@ -408,7 +651,18 @@ def main():
|
|
|
408
651
|
f"annotated {len(catalog)} plugins in {args.annotate}: "
|
|
409
652
|
f"{changed} declared fields changed, {absent} not installed here\n")
|
|
410
653
|
return
|
|
411
|
-
|
|
654
|
+
scanner = Scanner()
|
|
655
|
+
catalog = scanner.scan(args.uris or None)
|
|
656
|
+
refused = 0
|
|
657
|
+
if not args.no_descriptors:
|
|
658
|
+
try:
|
|
659
|
+
validator = desc.SchemaValidator()
|
|
660
|
+
except desc.DescriptorRefused as e:
|
|
661
|
+
sys.exit(f"{e.code}: {e.problem}")
|
|
662
|
+
directory = desc.descriptor_dir(args.descriptor_dir, args.system)
|
|
663
|
+
written, bad = desc.write_all(directory, scanner.descriptors(args.uris or None), validator)
|
|
664
|
+
refused = len(bad)
|
|
665
|
+
sys.stderr.write(f"wrote {len(written)} descriptors -> {directory}\n")
|
|
412
666
|
text = json.dumps(catalog, indent=2, ensure_ascii=False)
|
|
413
667
|
if args.out:
|
|
414
668
|
with open(args.out, "w", encoding="utf-8") as f:
|
|
@@ -426,6 +680,8 @@ def main():
|
|
|
426
680
|
sys.stderr.write(f"wrote {len(uris)} URIs -> {uri_path}\n")
|
|
427
681
|
else:
|
|
428
682
|
print(text)
|
|
683
|
+
if refused:
|
|
684
|
+
sys.exit(2)
|
|
429
685
|
|
|
430
686
|
|
|
431
687
|
if __name__ == "__main__":
|