#!/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("") return self def __exit__(self, exc_type, exc_value, exc_traceback): 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("") return self def __exit__(self, exc_type, exc_value, exc_traceback): 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"") 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())