Something went wrong. Try again.
The open source OpenXR runtime
Something went wrong. Try again.
14 kB · 426 lines
Python
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427#!/usr/bin/env python3# Copyright 2026, Beyley Cardellio# SPDX-License-Identifier: BSL-1.0
"""Generate a unified header containing versioned OpenVR interface classes."""
from __future__ import annotations
import argparseimport jsonfrom dataclasses import dataclassfrom pathlib import Pathimport reimport subprocessimport sys
JSON_PATTERN = "openvr_api_v*.json"HEADER_PATTERN = "openvr_{version}.h"CAPI_HEADER_PATTERN = "openvr_capi_{version}.h"
@dataclass(frozen=True)class InterfaceDefinition: class_name: str interface_version: str source_version: str header_path: Path fn_table_text: str | None = None
def version_sort_key(version: str) -> tuple[tuple[int | str, ...], str]: normalized = version[1:] if version.startswith("v") else version parts = re.findall(r"\d+|[A-Za-z]+", normalized) key = tuple(int(part) if part.isdigit() else part.lower() for part in parts) return key, normalized.lower()
def label_sort_key(label: str) -> tuple[tuple[int | str, ...], str]: parts = re.findall(r"\d+|[A-Za-z]+", label) key = tuple(int(part) if part.isdigit() else part.lower() for part in parts) return key, label.lower()
def read_header_text(path: Path) -> str: data = path.read_bytes() for encoding in ("utf-8", "cp1252", "latin-1"): try: return data.decode(encoding) except UnicodeDecodeError: continue return data.decode("latin-1", errors="replace")
def load_interface_versions(json_path: Path) -> dict[str, str] | None: try: payload = json.loads(json_path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): return None
class_names = { method.get("classname", "").split("::")[-1] for method in payload.get("methods", []) if method.get("classname") } if not class_names: return None
const_versions = { const.get("constname")[: -len("_Version")]: const.get("constval") for const in payload.get("consts", []) if const.get("constname", "").endswith("_Version") and const.get("constval") }
interface_versions = { class_name: const_versions[class_name] for class_name in sorted(class_names) if class_name in const_versions } if not interface_versions: return None
return interface_versions
def collect_interfaces(input_dir: Path) -> dict[str, InterfaceDefinition]: definitions: dict[str, InterfaceDefinition] = {} header_text_cache: dict[Path, str] = {}
json_paths = sorted( input_dir.glob(JSON_PATTERN), key=lambda path: version_sort_key(path.stem.removeprefix("openvr_api_")), ) for json_path in json_paths: source_version = json_path.stem.removeprefix("openvr_api_") header_path = input_dir / HEADER_PATTERN.format(version=source_version) if not header_path.exists(): print(f"Skipping {json_path.name}: missing {header_path.name}", file=sys.stderr) continue
interface_versions = load_interface_versions(json_path) if interface_versions is None: print(f"Skipping {json_path.name}: could not read interface versions", file=sys.stderr) continue
capi_header_path = input_dir / CAPI_HEADER_PATTERN.format(version=source_version) capi_header_text: str | None = None if capi_header_path.exists(): capi_header_text = read_header_text(capi_header_path)
for class_name, interface_version in interface_versions.items(): header_text = header_text_cache.get(header_path) if header_text is None: header_text = read_header_text(header_path) header_text_cache[header_path] = header_text
if extract_class_definition(header_text, class_name) is None: continue
fn_table_text: str | None = None if capi_header_text is not None: raw_fn_table = extract_struct_definition(capi_header_text, f"VR_{class_name}_FnTable") if raw_fn_table is not None: renamed_fn_table = rename_fntable_definition(raw_fn_table, class_name, interface_version) fn_table_text = normalize_class_definition(renamed_fn_table)
definitions[interface_version] = InterfaceDefinition( class_name=class_name, interface_version=interface_version, source_version=source_version, header_path=header_path, fn_table_text=fn_table_text, )
return definitions
def extract_class_definition(header_text: str, class_name: str) -> str | None: lines = header_text.splitlines(keepends=True) class_index = None
class_pattern = re.compile(rf"^\s*class\s+{re.escape(class_name)}\b") for index, line in enumerate(lines): if class_pattern.search(line): class_index = index break
if class_index is None: return None
start_index = class_index previous = class_index - 1 while previous >= 0 and not lines[previous].strip(): previous -= 1
if previous >= 0 and lines[previous].lstrip().startswith("*/"): start_index = previous while start_index >= 0 and "/**" not in lines[start_index]: start_index -= 1 if start_index < 0: start_index = class_index elif previous >= 0 and lines[previous].lstrip().startswith("//"): start_index = previous while start_index > 0 and lines[start_index - 1].lstrip().startswith("//"): start_index -= 1
end_index = None for index in range(class_index, len(lines)): if lines[index].strip() == "};": end_index = index break
if end_index is None: return None
return "".join(lines[start_index : end_index + 1]).rstrip()
def strip_comments(text: str) -> str: result: list[str] = [] index = 0 state = "normal" quote = ""
while index < len(text): char = text[index] next_char = text[index + 1] if index + 1 < len(text) else ""
if state == "normal": if char == "/" and next_char == "/": state = "line_comment" index += 2 continue if char == "/" and next_char == "*": if result and not result[-1].isspace(): result.append(" ") state = "block_comment" index += 2 continue if char in ('\"', "'"): state = "string" quote = char result.append(char) index += 1 continue
result.append(char) index += 1 continue
if state == "line_comment": if char == "\n": result.append("\n") state = "normal" index += 1 continue
if state == "block_comment": if char == "\n": result.append("\n") index += 1 continue if char == "*" and next_char == "/": state = "normal" index += 2 continue index += 1 continue
result.append(char) index += 1 if char == "\\" and index < len(text): result.append(text[index]) index += 1 continue if char == quote: state = "normal"
return "".join(result)
def normalize_class_definition(definition: str) -> str: without_comments = strip_comments(definition) normalized_lines = [] for line in without_comments.splitlines(): stripped_line = line.rstrip() if not stripped_line.strip(): continue normalized_lines.append(stripped_line)
return "\n".join(normalized_lines)
def extract_struct_definition(header_text: str, struct_name: str) -> str | None: lines = header_text.splitlines(keepends=True) struct_index = None struct_pattern = re.compile(rf"^\s*struct\s+{re.escape(struct_name)}\b") for index, line in enumerate(lines): if struct_pattern.search(line): struct_index = index break if struct_index is None: return None end_index = None for index in range(struct_index, len(lines)): if lines[index].strip() == "};": end_index = index break if end_index is None: return None return "".join(lines[struct_index:end_index + 1]).rstrip()
def rename_fntable_definition(definition: str, class_name: str, interface_version: str) -> str: old_name = f"VR_{class_name}_FnTable" new_name = f"VR_{interface_version}_FnTable" return re.sub(rf"\b{re.escape(old_name)}\b", new_name, definition)
def rename_class_definition(definition: str, class_name: str, interface_version: str) -> str: renamed = re.sub( rf"\bclass\s+{re.escape(class_name)}\b", f"class {interface_version}", definition, count=1, ) return re.sub( rf"~{re.escape(class_name)}\b", f"~{interface_version}", renamed, )
def run_clang_format(path: Path) -> None: subprocess.run([ "clang-format", "-style=file", "-i", str(path), ], check=True)
def generate_header(definitions: dict[str, InterfaceDefinition], include_name: str) -> str: header_text_cache: dict[Path, str] = {} blocks: list[tuple[str, str, str | None]] = []
ordered_definitions = sorted( definitions.values(), key=lambda definition: ( definition.class_name.lower(), label_sort_key(definition.interface_version), ), )
for definition in ordered_definitions: header_text = header_text_cache.get(definition.header_path) if header_text is None: header_text = read_header_text(definition.header_path) header_text_cache[definition.header_path] = header_text
class_definition = extract_class_definition(header_text, definition.class_name) if class_definition is None: print( f"Skipping {definition.interface_version}: could not find {definition.class_name} in {definition.header_path.name}", file=sys.stderr, ) continue
renamed_definition = rename_class_definition( class_definition, definition.class_name, definition.interface_version, ) blocks.append( (definition.interface_version, normalize_class_definition(renamed_definition), definition.fn_table_text) )
output_lines = [ "//========= Copyright Valve Corporation ============//", "", "// Generated from versioned OpenVR headers.", "// Each interface version is copied from the newest header that still contains it.", "", "#pragma once", "", f'#include "{include_name}"', "#include \"openvr_old_types.h\"", "", "namespace vr", "{", ]
for version_name, block, fn_table_text in blocks: output_lines.append( f'static const char * const {version_name}_Version = "{version_name}";' ) output_lines.append("") output_lines.append(block) output_lines.append("") if fn_table_text is not None: output_lines.append(fn_table_text) output_lines.append("") output_lines.append("")
if output_lines[-1] == "": output_lines.pop() if output_lines[-1] == "": output_lines.pop()
output_lines.extend(("", "}", "")) return "\n".join(output_lines)
def main() -> int: parser = argparse.ArgumentParser( description="Generate a unified header containing all OpenVR interface versions." ) parser.add_argument( "-i", "--input-dir", type=Path, required=True, help="Directory containing versioned openvr_*.h and openvr_api_*.json files.", ) parser.add_argument( "-o", "--output-file", type=Path, required=True, help="Header file to write.", ) parser.add_argument( "--include", default="openvr.h", help='Header to include at the top of the generated file (default: "openvr.h").', ) args = parser.parse_args()
input_dir = args.input_dir.resolve() if not input_dir.exists(): parser.error(f"{input_dir} does not exist")
output_file = args.output_file.resolve() output_file.parent.mkdir(parents=True, exist_ok=True)
definitions = collect_interfaces(input_dir) if not definitions: print(f"No interface versions found in {input_dir}", file=sys.stderr) return 1
output_file.write_text(generate_header(definitions, args.include), encoding="utf-8") try: run_clang_format(output_file) except FileNotFoundError: print("clang-format was not found in PATH", file=sys.stderr) return 1 except subprocess.CalledProcessError: print(f"clang-format failed for {output_file}", file=sys.stderr) return 1
print(f"Wrote {len(definitions)} interface version definition(s) to {output_file}") return 0
if __name__ == "__main__": raise SystemExit(main())