summaryrefslogtreecommitdiffstats
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/update_host_compatibility.py262
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())