summaryrefslogtreecommitdiffstats
path: root/scripts/check-docs-links.py
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-09-23 13:48:07 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-09-23 13:48:07 -0400
commitfade84f8b5b781d29daad40699d07928dedea550 (patch)
treee4156b3d0a6a1c794c90ef6834b949321cdfb5cb /scripts/check-docs-links.py
parentc587eac377c1c5b720142edadebaace5cb1b17c0 (diff)
downloadopen-iov-fade84f8b5b781d29daad40699d07928dedea550.tar.gz
open-iov-fade84f8b5b781d29daad40699d07928dedea550.zip
docs: merge the legacy flat documents, add the CUDA deep-dive and link checks
* Merge and remove the pre-reboot flat documents, preserving their still-useful material in the structured tree: debugging workflows and the EPT-livelock case study (05-operations/02), performance microbenchmarks and A/B methodology (05-operations/01), the version-upgrade runbook (05-operations/02 section 4, with corrected host/guest framing), kernel-code pitfalls (07-developers) and the OpenRM build/load order (03-architecture/06 section 7). The remaining legacy files were verified redundant and removed. * docs/README now records where each migrated topic lives. * Portability: the config YAML files are called out as design targets that no code reads yet, with what adding support actually requires today. * Getting started: the OpenRM step is marked optional (native vGPU-management workflows only; the emulated path uses the distro driver). * tools docs: rmtool/rmtop marked as planned, pointing at the implemented tools/rmq subset. * wiki: new CUDA-Userspace-Driver deep-dive page (public-safe, links the canonical userspace/cuda analysis) and index update. * scripts/check-docs-links.py plus a CI step so relative links cannot rot. * STATE.md references to removed documents repointed.
Diffstat (limited to 'scripts/check-docs-links.py')
-rwxr-xr-xscripts/check-docs-links.py165
1 files changed, 165 insertions, 0 deletions
diff --git a/scripts/check-docs-links.py b/scripts/check-docs-links.py
new file mode 100755
index 0000000..fcb39ca
--- /dev/null
+++ b/scripts/check-docs-links.py
@@ -0,0 +1,165 @@
+#!/usr/bin/env python3
+"""Check relative links in the project's tracked markdown documentation.
+
+Walks docs/, wiki/ and userspace/cuda/, extracts markdown links, and fails if
+a relative target does not exist. External links (http/https/mailto/tel/ftp/
+data/javascript), pure anchors and links inside fenced code blocks are
+skipped.
+
+The raw imported wiki HTML dumps and the archived wiki copy are excluded:
+their markup is not ours and their link shape is not representative.
+
+Usage:
+ python3 scripts/check-docs-links.py
+"""
+
+import os
+import re
+import subprocess
+import sys
+from urllib.parse import unquote
+
+REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+WATCHED_ROOTS = ("docs", "wiki", "userspace/cuda")
+
+EXCLUDED_FILES = {
+ "wiki/GPU_Driver_Internals.md",
+ "wiki/GPU_Firmware.md",
+ "wiki/GPU_SBOM.md",
+ "wiki/Virtual_IO_Internals.md",
+}
+EXCLUDED_PREFIXES = ("wiki/open-iov.org/",)
+
+SKIPPED_SCHEMES = (
+ "http://",
+ "https://",
+ "mailto:",
+ "tel:",
+ "ftp://",
+ "ftps://",
+ "data:",
+ "javascript:",
+)
+
+LINK_RE = re.compile(r"!?\[[^\]]*\]\(\s*([^)]+?)\s*\)")
+
+
+def tracked_markdown_files():
+ """Return repo-relative paths of tracked markdown files (git first)."""
+ try:
+ out = subprocess.run(
+ ["git", "ls-files", "--", *WATCHED_ROOTS],
+ cwd=REPO_ROOT,
+ check=True,
+ capture_output=True,
+ text=True,
+ ).stdout
+ except (OSError, subprocess.CalledProcessError):
+ out = ""
+ if out.strip():
+ return [line for line in out.splitlines() if line.endswith(".md")]
+
+ files = []
+ for root in WATCHED_ROOTS:
+ for dirpath, _dirnames, filenames in os.walk(os.path.join(REPO_ROOT, root)):
+ for name in filenames:
+ if name.endswith(".md"):
+ full = os.path.join(dirpath, name)
+ files.append(os.path.relpath(full, REPO_ROOT))
+ return files
+
+
+def excluded(relpath):
+ if relpath in EXCLUDED_FILES:
+ return True
+ return relpath.startswith(EXCLUDED_PREFIXES)
+
+
+def strip_code_blocks(text):
+ """Blank out fenced code blocks so example links are not checked."""
+ lines = []
+ fence = None
+ for line in text.splitlines():
+ stripped = line.lstrip()
+ marker = stripped[:3]
+ if fence is None:
+ if marker in ("```", "~~~"):
+ fence = marker
+ lines.append("")
+ continue
+ lines.append(line)
+ else:
+ if stripped.startswith(fence):
+ fence = None
+ lines.append("")
+ return "\n".join(lines)
+
+
+def normalize_target(raw):
+ target = raw.strip()
+ if target.startswith("<") and target.endswith(">"):
+ target = target[1:-1].strip()
+ if not target:
+ return None
+ if target.startswith("#"):
+ return None
+ if target.lower().startswith(SKIPPED_SCHEMES):
+ return None
+ # Drop an anchor and any markdown title, then percent-decode.
+ target = target.split()[0] if " " in target else target
+ target = target.split("#", 1)[0]
+ target = unquote(target)
+ if not target:
+ return None
+ return target
+
+
+def check_file(relpath):
+ full = os.path.join(REPO_ROOT, relpath)
+ with open(full, "r", encoding="utf-8", errors="replace") as handle:
+ text = strip_code_blocks(handle.read())
+
+ problems = []
+ in_fence = False
+ for lineno, line in enumerate(text.splitlines(), start=1):
+ if line.lstrip()[:3] in ("```", "~~~"):
+ in_fence = not in_fence
+ continue
+ if in_fence:
+ continue
+ for raw in LINK_RE.findall(line):
+ target = normalize_target(raw)
+ if target is None:
+ continue
+ if target.startswith("/"):
+ resolved = os.path.join(REPO_ROOT, target.lstrip("/"))
+ else:
+ resolved = os.path.normpath(
+ os.path.join(REPO_ROOT, os.path.dirname(relpath), target)
+ )
+ if not os.path.exists(resolved):
+ problems.append((lineno, target, os.path.relpath(resolved, REPO_ROOT)))
+ return problems
+
+
+def main():
+ files = sorted(
+ relpath
+ for relpath in tracked_markdown_files()
+ if not excluded(relpath)
+ )
+ failures = 0
+ for relpath in files:
+ for lineno, target, resolved in check_file(relpath):
+ failures += 1
+ print(f"{relpath}:{lineno}: broken link: {target} (resolved: {resolved})")
+
+ if failures:
+ print(f"\n{failures} broken relative link(s) in {len(files)} file(s).")
+ return 1
+ print(f"OK: {len(files)} markdown file(s) checked, no broken relative links.")
+ return 0
+
+
+if __name__ == "__main__":
+ sys.exit(main()) \ No newline at end of file