diff options
author | David Robillard <d@drobilla.net> | 2022-08-15 17:21:49 -0400 |
---|---|---|
committer | David Robillard <d@drobilla.net> | 2022-08-15 17:21:49 -0400 |
commit | de9b7d1aafbde2fc608634ec384bc36110dd8ce9 (patch) | |
tree | 91cb10fc6cf11545194793fb09f3560e4c91bcf4 /scripts | |
parent | 88ea76d908e9307bb0a5a9e2efa38d01a6ca57ca (diff) | |
download | lv2site-de9b7d1aafbde2fc608634ec384bc36110dd8ce9.tar.xz |
Add host compatibility data, generator script, and page
Diffstat (limited to 'scripts')
-rwxr-xr-x | scripts/update_host_compatibility.py | 262 |
1 files changed, 262 insertions, 0 deletions
diff --git a/scripts/update_host_compatibility.py b/scripts/update_host_compatibility.py new file mode 100755 index 0000000..84898b0 --- /dev/null +++ b/scripts/update_host_compatibility.py @@ -0,0 +1,262 @@ +#!/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("<table>") + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + self.out.write("</table>\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("<tr>") + return self + + def __exit__(self, exc_type, exc_value, exc_traceback): + self.out.write("</tr>\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'<a href="{uri}">{uri}</a>' + + +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 = 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("<h2>Features</h2>\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"<strike>{_spec_link(feature)}</strike>") + else: + row.append(_spec_link(feature)) + + for host in graph.subjects(rdf.type, compat.Host): + if _host_implements_feature(graph, host, feature): + row.append('<span class="success">Yes</span>') + else: + row.append('<span class="error">No</span>') + + out.write("<h2>Port Types</h2>\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 graph.subjects(rdf.type, compat.Host): + if (host, compat.supportsPortType, port_type) in graph: + row.append('<span class="success">Yes</span>') + else: + row.append('<span class="error">No</span>') + + 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"<footer>Generated by <code>{script_name}</code>" + f" with LV2 {version}.</footer>\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()) |