eoinedge commited on
Commit
ce64e7d
·
verified ·
1 Parent(s): cf725f9

ROS 2 docs RAG index and tooling

Browse files
Files changed (1) hide show
  1. scripts/fetch_docs.py +165 -0
scripts/fetch_docs.py ADDED
@@ -0,0 +1,165 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Fetch the documentation source for the configured project.
2
+
3
+ Reads the RST rather than crawling the rendered site. Rendered pages carry
4
+ navigation, version switchers and generated API listings, all of which land in
5
+ chunks and compete with prose during retrieval; the RST is the authoritative
6
+ text and is versioned and diffable.
7
+
8
+ ROS 2 documents each distribution on its own branch, so --ref selects the
9
+ distribution: rolling (default), jazzy, humble.
10
+
11
+ python scripts/fetch_docs.py
12
+ python scripts/fetch_docs.py --ref jazzy
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import argparse
18
+ import shutil
19
+ import subprocess
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ # The default Windows console is cp1252 and raises UnicodeEncodeError on any
24
+ # non-Latin-1 character. These scripts print document titles and paths straight
25
+ # from the Zephyr tree, which is full of them.
26
+ if hasattr(sys.stdout, "reconfigure"):
27
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
28
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
29
+
30
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
31
+ import source_config as cfg # noqa: E402
32
+
33
+ REPO = cfg.REPO
34
+ ROOT = Path(__file__).resolve().parent.parent
35
+ DEFAULT_CLONE = ROOT / "data" / "doc-src"
36
+ DEFAULT_OUT = ROOT / "data" / "raw_docs"
37
+
38
+ SKIP_DIRS = cfg.SKIP_DIRS
39
+
40
+
41
+ def run(cmd: list[str], cwd: Path | None = None, check: bool = True) -> int:
42
+ result = subprocess.run(cmd, cwd=cwd, text=True)
43
+ if check and result.returncode != 0:
44
+ raise SystemExit(f"command failed ({result.returncode}): {' '.join(cmd)}")
45
+ return result.returncode
46
+
47
+
48
+ def sparse_clone(clone_dir: Path, ref: str) -> None:
49
+ """Shallow + blobless + sparse: only doc/, only one commit."""
50
+ # An empty leftover directory is not a checkout. Testing `exists()` sent the
51
+ # reuse path at a directory with no .git in it, which then failed on fetch
52
+ # and again on removal, reporting a file lock that was never the problem.
53
+ if (clone_dir / ".git").exists():
54
+ # A --branch clone sets a narrow fetch refspec, so a bare `git fetch
55
+ # origin main` fails with "couldn't find remote ref". Ask for the ref
56
+ # explicitly, and treat the cache as disposable if anything goes wrong:
57
+ # it is a checkout of someone else's repository, not state worth
58
+ # rescuing.
59
+ print(f"Reusing existing checkout at {clone_dir}")
60
+ refspec = f"+refs/heads/{ref}:refs/remotes/origin/{ref}"
61
+ ok = run(["git", "fetch", "--depth", "1", "origin", refspec], cwd=clone_dir, check=False)
62
+ if ok == 0:
63
+ ok = run(["git", "checkout", "-f", "FETCH_HEAD"], cwd=clone_dir, check=False)
64
+ if ok == 0:
65
+ return
66
+ print(" cached checkout is unusable - re-cloning")
67
+ shutil.rmtree(clone_dir, ignore_errors=True)
68
+ if clone_dir.exists():
69
+ raise SystemExit(
70
+ f"could not remove {clone_dir} (a file may be locked). Delete it and retry."
71
+ )
72
+
73
+ clone_dir.parent.mkdir(parents=True, exist_ok=True)
74
+ print(f"Cloning {REPO} ({ref}, {cfg.DOC_ROOT}/ only) -> {clone_dir}")
75
+ run(
76
+ [
77
+ "git",
78
+ "clone",
79
+ "--depth",
80
+ "1",
81
+ "--filter=blob:none",
82
+ "--sparse",
83
+ "--branch",
84
+ ref,
85
+ REPO,
86
+ str(clone_dir),
87
+ ]
88
+ )
89
+ run(["git", "sparse-checkout", "set", cfg.DOC_ROOT], cwd=clone_dir)
90
+
91
+
92
+ def collect(clone_dir: Path, out_dir: Path) -> tuple[int, int]:
93
+ """Copy documentation sources out of the checkout, flattened by path."""
94
+ doc_root = clone_dir / cfg.DOC_ROOT
95
+ if not doc_root.is_dir():
96
+ raise SystemExit(f"no {cfg.DOC_ROOT}/ directory in {clone_dir}")
97
+
98
+ if out_dir.exists():
99
+ shutil.rmtree(out_dir)
100
+ out_dir.mkdir(parents=True)
101
+
102
+ copied = 0
103
+ total_bytes = 0
104
+ for path in sorted(doc_root.rglob("*")):
105
+ if not path.is_file() or path.suffix.lower() not in {".rst", ".md", ".txt"}:
106
+ continue
107
+ relative = path.relative_to(doc_root)
108
+ if any(part in SKIP_DIRS for part in relative.parts):
109
+ continue
110
+
111
+ # Flatten so the source path survives as the filename. Retrieval cites
112
+ # the file, and "kernel/services/threads.rst" is a far more useful
113
+ # citation than "threads.rst" repeated across a dozen subsystems.
114
+ flat = str(relative).replace("\\", "/").replace("/", "__")
115
+ destination = out_dir / flat
116
+ destination.write_bytes(path.read_bytes())
117
+ copied += 1
118
+ total_bytes += destination.stat().st_size
119
+
120
+ return copied, total_bytes
121
+
122
+
123
+ def main() -> int:
124
+ parser = argparse.ArgumentParser(description=__doc__)
125
+ parser.add_argument(
126
+ "--ref",
127
+ default=cfg.DEFAULT_REF,
128
+ help=f"branch or tag to fetch (default: {cfg.DEFAULT_REF})",
129
+ )
130
+ parser.add_argument("--clone", type=Path, default=DEFAULT_CLONE)
131
+ parser.add_argument("--out", type=Path, default=DEFAULT_OUT)
132
+ parser.add_argument(
133
+ "--keep-clone",
134
+ action="store_true",
135
+ help="keep the sparse checkout so a later run can update instead of re-cloning",
136
+ )
137
+ args = parser.parse_args()
138
+
139
+ sparse_clone(args.clone, args.ref)
140
+ copied, total_bytes = collect(args.clone, args.out)
141
+
142
+ revision = subprocess.run(
143
+ ["git", "rev-parse", "--short", "HEAD"],
144
+ cwd=args.clone,
145
+ text=True,
146
+ capture_output=True,
147
+ ).stdout.strip()
148
+
149
+ # Provenance travels with the corpus. An index built from an unknown commit
150
+ # cannot be reproduced or explained later.
151
+ (args.out / "_SOURCE.txt").write_text(
152
+ f"repository: {REPO}\nref: {args.ref}\ncommit: {revision}\nfiles: {copied}\n",
153
+ encoding="utf-8",
154
+ )
155
+
156
+ if not args.keep_clone:
157
+ shutil.rmtree(args.clone, ignore_errors=True)
158
+
159
+ print(f"\n{copied} documents ({total_bytes / 1_048_576:.1f} MB) -> {args.out}")
160
+ print(f"{cfg.PROJECT} {args.ref} @ {revision}")
161
+ return 0
162
+
163
+
164
+ if __name__ == "__main__":
165
+ sys.exit(main())