From d4a970f6962dda28133290194832b726b566ddab Mon Sep 17 00:00:00 2001 From: David Robillard Date: Thu, 7 Jul 2022 18:59:06 -0400 Subject: Switch to meson build system --- scripts/lv2_build_index.py | 252 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100755 scripts/lv2_build_index.py (limited to 'scripts/lv2_build_index.py') diff --git a/scripts/lv2_build_index.py b/scripts/lv2_build_index.py new file mode 100755 index 0000000..444e078 --- /dev/null +++ b/scripts/lv2_build_index.py @@ -0,0 +1,252 @@ +#!/usr/bin/env python3 + +# Copyright 2022 David Robillard +# SPDX-License-Identifier: ISC + +""" +Write an HTML index for a set of LV2 specifications. +""" + +import datetime +import json +import os +import time +import sys +import argparse +import subprocess + +import rdflib + + +doap = rdflib.Namespace("http://usefulinc.com/ns/doap#") +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#") + + +def _subst_file(template_path, output_file, substitutions): + "Replace keys with values in a template file and write the result." + + with open(template_path, "r", encoding="utf-8") as template: + for line in template: + for key, value in substitutions.items(): + line = line.replace(key, value) + + output_file.write(line) + + +def _load_ttl(data_paths, exclude=None): + "Load an RDF model from a Turtle file." + + model = rdflib.ConjunctiveGraph() + for path in data_paths: + if exclude is None or path not in exclude: + model.parse(path, format="n3") + + return model + + +def _warn(message): + "Load a warning message." + + assert not message.startswith("warning: ") + assert not message.endswith("\n") + sys.stderr.write(message) + sys.stderr.write("\n") + + +def _spec_target(spec, root, online=False): + "Return the relative link target for a specification." + + target = spec.removeprefix(root) if spec.startswith(root) else spec + + return target if online else target + ".html" + + +def _spec_date(model, spec, minor, micro): + "Return the date for a release of a specification as an RDF node." + + # Get date + date = None + for release in model.objects(spec, doap.release): + revision = model.value(release, doap.revision, None, any=False) + if str(revision) == f"{minor}.{micro}": + date = model.value(release, doap.created, None) + break + + # Verify that this date is the latest + if date is not None: + for other_release in model.objects(spec, doap.release): + for other_date in model.objects(other_release, doap.created): + if other_date is None: + _warn(f"{spec} has no doap:created date") + elif other_date > date: + _warn(f"{spec} {minor}.{micro} ({date}) is an old release") + break + + return date + + +def _spec_link_columns(spec, root, name, online): + "Return the first two link columns in a spec row as an HTML string." + + # Find relative link target and stem + target = _spec_target(spec, root, online) + stem = os.path.splitext(os.path.basename(target))[0] + + # Prefix with a comment to act as a sort key for the row + col = f"" + + # Specification + col += f'{name}' + + # API + col += '{name}' + col += "" + + return col + + +def _spec_description_column(model, spec): + "Return the description column in a spec row as an HTML string." + + shortdesc = model.value(spec, doap.shortdesc, None, any=False) + + return "" + str(shortdesc) + "" if shortdesc else "" + + +def index_row(model, spec, root_uri, online): + "Return the row for a spec as an HTML string." + + # Get version + minor = 0 + micro = 0 + try: + minor = int(model.value(spec, lv2.minorVersion, None, any=False)) + micro = int(model.value(spec, lv2.microVersion, None, any=False)) + except rdflib.exceptions.UniquenessError: + _warn(f"{spec} has no unique valid version") + return "" + + # Check that date is present and valid + if _spec_date(model, spec, minor, micro) is None: + _warn(f"{spec} has no doap:created date") + return "" + + row = "" + + # Specification and API + row += _spec_link_columns( + spec, + root_uri, + model.value(spec, doap.name, None).removeprefix("LV2 "), + online, + ) + + # Description + row += _spec_description_column(model, spec) + + # Version + row += f"{minor}.{micro}" + + # Status + deprecated = model.value(spec, owl.deprecated, None) + deprecated = deprecated and str(deprecated) not in ["0", "false"] + if minor == 0: + row += 'Experimental' + elif deprecated: + row += 'Deprecated' + elif micro % 2 == 0: + row += 'Stable' + else: + row += 'Development' + + row += "" + + return row + + +def build_index( + lv2_source_root, + lv2_version, + input_paths, + root_uri, + online, +): + "Build the LV2 specification index and write it to stdout." + + model = _load_ttl(input_paths) + + # Get date for this version, and list of all LV2 distributions + proj = rdflib.URIRef("http://lv2plug.in/ns/lv2") + date = None + for row in model.triples([proj, doap.release, None]): + revision = model.value(row[2], doap.revision, None) + created = model.value(row[2], doap.created, None) + if str(revision) == lv2_version: + date = created + + dist = model.value(row[2], doap["file-release"], None) + if not dist or not created: + _warn(f"{proj} has no file release") + + rows = [] + for spec in model.triples([None, rdf.type, lv2.Specification]): + rows += [index_row(model, spec[0], root_uri, online)] + + if date is None: + now = int(os.environ.get("SOURCE_DATE_EPOCH", time.time())) + date = datetime.datetime.utcfromtimestamp(now).strftime("%F") + + _subst_file( + os.path.join(lv2_source_root, "doc", "index.html.in"), + sys.stdout, + { + "@ROWS@": "\n".join(rows), + "@LV2_VERSION@": lv2_version, + "@DATE@": date, + }, + ) + + +if __name__ == "__main__": + ap = argparse.ArgumentParser( + usage="%(prog)s [OPTION]... INPUT_PATH...", + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + ap.add_argument("--lv2-version", help="LV2 release version") + ap.add_argument("--lv2-source-root", help="path to LV2 source root") + ap.add_argument( + "--root-uri", + default="http://lv2plug.in/ns/", + help="root URI for specifications", + ) + ap.add_argument( + "--online", + action="store_true", + default=False, + help="build online documentation", + ) + ap.add_argument("input_paths", nargs="+", help="path to Turtle input file") + + args = ap.parse_args(sys.argv[1:]) + + if args.lv2_version is None or args.lv2_source_root is None: + introspect_command = ["meson", "introspect", "-a"] + project_info = json.loads( + subprocess.check_output(introspect_command).decode("utf-8") + ) + + if args.lv2_version is None: + args.lv2_version = project_info["projectinfo"]["version"] + + if args.lv2_source_root is None: + meson_build_path = project_info["buildsystem_files"][0] + args.lv2_source_root = os.path.relpath( + os.path.dirname(meson_build_path) + ) + + build_index(**vars(args)) -- cgit v1.2.1