#!/usr/bin/env python3
"""
Write an LV2 host compatibility matrix as a Pelican page.
"""
import argparse
import os
import subprocess
import sys
import rdflib
__author__ = "David Robillard"
__date__ = "2022-08-15"
__email__ = "d@drobilla.net"
__license__ = "ISC"
__version__ = "0.0.1"
DOC_HEADER = """Title: Host Compatibility
This page shows the state of host support for different parts of LV2.
It is generated from
[data](//gitlab.com/lv2/site/-/blob/master/host_compatibility.ttl)
in the [LV2 site repository](//gitlab.com/lv2/site),
additions and corrections are welcome.
"""
compat = rdflib.Namespace("http://drobilla.net/ns/compat#")
lv2 = rdflib.Namespace("http://lv2plug.in/ns/lv2core#")
owl = rdflib.Namespace("http://www.w3.org/2002/07/owl#")
rdf = rdflib.Namespace("http://www.w3.org/1999/02/22-rdf-syntax-ns#")
rdfs = rdflib.Namespace("http://www.w3.org/2000/01/rdf-schema#")
class TableContext:
"""Context manager for writing an HTML table."""
def __init__(self, out):
self.out = out
def __enter__(self):
self.out.write("
\n")
class RowContext:
"""Context manager for writing a row within an HTML table."""
def __init__(self, out):
self.out = out
def __enter__(self):
self.out.write("
\n")
def append(self, col, tag="td"):
"""Append a column to the row."""
self.out.write(f"<{tag}>")
self.out.write(col)
self.out.write(f"{tag}>")
def _default_lv2_path():
"""Return the default LV2_PATH for the current platform."""
if sys.platform == "darwin":
return os.pathsep.join(
[
"~/.lv2",
"~/Library/Audio/Plug-Ins/LV2",
"/usr/local/lib/lv2",
"/usr/lib/lv2",
"/Library/Audio/Plug-Ins/LV2",
]
)
if sys.platform == "win32":
return os.pathsep.join(
["%APPDATA%\\\\LV2", "%COMMONPROGRAMFILES%\\\\LV2"]
)
return os.pathsep.join(["~/.lv2", "/usr/local/lib/lv2", "/usr/lib/lv2"])
def _load_specifications(graph, lv2_path):
"""Load all LV2 specifications from an LV2 path."""
specs = set()
for path_entry in lv2_path.split(os.pathsep):
directory = os.path.expanduser(os.path.expandvars(path_entry))
for name in os.listdir(directory):
path = os.path.join(directory, name)
manifest_path = os.path.join(path, "manifest.ttl")
if not os.path.isfile(manifest_path):
continue
manifest = rdflib.Graph()
manifest.load(manifest_path, format="turtle")
for spec in manifest.subjects(rdf.type, lv2.Specification):
document = manifest.value(spec, rdfs.seeAlso, any=False)
if spec not in specs:
specs.add(spec)
sys.stderr.write(f"Loading {document}\n")
graph.load(document, format="turtle")
else:
sys.stderr.write(f"warning: Ignoring {document}\n")
def _host_implements_feature(graph, host, feature):
"""Return whether host implements feature, directly or indirectly."""
if (host, compat.implementsFeature, feature) in graph:
return True
for library in graph.objects(host, compat.usesLibrary):
if (library, compat.implementsFeature, feature) in graph:
return True
return False
def _spec_link(uri):
return f'{uri}'
def run(lv2_path, data_filenames, out):
"""Read compatibility data and generate HTML compatibility tables."""
graph = rdflib.Graph()
_load_specifications(graph, lv2_path)
for data_filename in data_filenames:
graph.load(data_filename, format="turtle")
hosts = sorted(list(graph.subjects(rdf.type, compat.Host)))
features = sorted(list(graph.subjects(rdf.type, lv2.Feature)))
port_types = sorted(list(graph.subjects(rdfs.subClassOf, lv2.Port)))
out.write(DOC_HEADER)
out.write("
Features
\n")
with TableContext(out):
with RowContext(out) as row:
row.append("Feature", tag="th")
for host in hosts:
row.append(graph.value(host, rdfs.label), tag="th")
for feature in features:
with RowContext(out) as row:
if graph.value(feature, owl.deprecated):
row.append(f"{_spec_link(feature)}")
else:
row.append(_spec_link(feature))
for host in hosts:
if _host_implements_feature(graph, host, feature):
row.append('Yes')
else:
row.append('No')
out.write("
Port Types
\n")
with TableContext(out):
with RowContext(out) as row:
row.append("Port Type", tag="th")
for host in hosts:
row.append(graph.value(host, rdfs.label), tag="th")
for port_type in port_types:
with RowContext(out) as row:
row.append(_spec_link(port_type))
for host in hosts:
if (host, compat.supportsPortType, port_type) in graph:
row.append('Yes')
else:
row.append('No')
script_name = os.path.basename(sys.argv[0])
version_cmd = ["pkg-config", "--modversion", "lv2"]
version = subprocess.check_output(version_cmd, encoding="utf-8").strip()
out.write(
f"\n"
)
return 0
def main():
"""Run the command line tool."""
scripts_dir = os.path.dirname(os.path.realpath(__file__))
top_dir = os.path.dirname(scripts_dir)
print(scripts_dir)
print(top_dir)
parser = argparse.ArgumentParser(
usage="%(prog)s [OPTION]... [DATA_FILE]",
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
parser.add_argument(
"-P",
"--lv2-path",
help="LV2 path to search, overriding LV2_PATH",
)
parser.add_argument(
"-V",
"--version",
action="store_true",
help="print version information and exit",
)
parser.add_argument(
"-o",
"--output",
default=os.path.join(
top_dir, "content", "pages", "host-compatibility.md"
),
help="output file path (default: content/pages/host-compatibility.md)",
)
parser.add_argument(
"data_file",
metavar="DATA_FILE",
default=["host_compatibility.ttl"],
nargs="*",
help="Turtle file with compatibility information",
)
args = parser.parse_args(sys.argv[1:])
if args.version:
print(f"update_host_compatibility.py {__version__}")
return 0
if args.lv2_path is None:
args.lv2_path = os.getenv("LV2_PATH")
if args.lv2_path is None:
args.lv2_path = _default_lv2_path()
sys.stderr.write(f'Searching LV2 path "{args.lv2_path}"\n')
with open(args.output, "w", encoding="utf-8") as out:
return run(args.lv2_path, args.data_file, out)
if __name__ == "__main__":
sys.exit(main())