docs: add bazel sphinx_build_test replicating ReadTheDocs CI#10695
docs: add bazel sphinx_build_test replicating ReadTheDocs CI#10695luarss wants to merge 4 commits into
Conversation
Adds a hermetic Bazel `py_test` target that runs the same Sphinx documentation build performed by readthedocs.org/openroad, catching broken cross-references, malformed markup, missing files, and toc.yml issues locally before they reach CI. - `//docs:sphinx_build_test` runs `sphinx-build -b html` with pinned Python deps from `@openroad-pip` (no system Sphinx required). - Tagged `doc_check` for filtering: `bazelisk test --test_tag_filters=doc_check //docs/...`. - Mirrors conf.py setup (main symlink, README2.md prefix swap, messages glossary generation, doxygen stub) and strips `bazel-*` symlinks to prevent recursive scanning. Fixes The-OpenROAD-Project#9885 Signed-off-by: alokkumardalei-wq <alokkumardalei2@gmail.com>
Simplify the test per @luarss's review (#4148341864): - Pin Sphinx deps with `==` in bazel/requirements.in and clarify the Python 3.13 target; regenerated lock file is unchanged. - Drop "main/bazel-*" from conf.py exclude_patterns (scope creep for this PR; defensive only for local-dev sphinx-build from a workspace that has been bazel-built). - Replace _find_docs_dir() helper with a DOCS_DIR module constant. - Stop mirroring conf.py's setup() in the test. Stage //:README.md in runfiles and rely on Sphinx to call conf.py's setup(app) directly; revert-links.py is still invoked post-build to undo the in-place swap_prefix mutations on README.md / README2.md. Verified locally: bazelisk test //docs:sphinx_build_test //docs:sphinx_build_test PASSED in 2.8s Signed-off-by: alokkumardalei-wq <alokkumardalei2@gmail.com> Signed-off-by: Jack Luar <39641663+luarss@users.noreply.github.com>
Signed-off-by: Jack Luar <39641663+luarss@users.noreply.github.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a hermetic local Sphinx documentation build test using Bazel to replicate the ReadTheDocs build process. It adds Sphinx and its related themes and plugins to the Python requirements, updates the lockfile, and defines a new py_test target sphinx_build_test. Feedback on the test implementation highlights a risk where running the build directly on the workspace directory with no-sandbox and local tags can leave the git workspace dirty if the test is interrupted. It is recommended to copy the documentation files to a temporary directory before running the build to ensure proper isolation.
| def main() -> int: | ||
| build_output = tempfile.mkdtemp(prefix="sphinx_build_") | ||
| saved_cwd = os.getcwd() | ||
| try: | ||
| # conf.py's setup(app) hook uses cwd-relative paths like "./main" | ||
| # and "../README.md", so it must run with docs/ as cwd. Sphinx | ||
| # invokes setup(app) automatically when it loads conf.py below. | ||
| os.chdir(DOCS_DIR) | ||
|
|
||
| # Calling sphinx via subprocess would fail because a child python3 | ||
| # doesn't inherit the Bazel runfiles PYTHONPATH used by this test. | ||
| from sphinx.cmd.build import main as sphinx_main | ||
|
|
||
| args = ["-b", "html", "-T", "-q"] | ||
| if not shutil.which("mmdc"): | ||
| args.extend(["-D", "mermaid_output_format=raw"]) | ||
| args.extend([DOCS_DIR, build_output]) | ||
|
|
||
| print("Running sphinx-build...", flush=True) | ||
| returncode = sphinx_main(args) | ||
|
|
||
| # conf.py's setup(app) rewrites README.md and README2.md in place. | ||
| # revert-links.py undoes those mutations so the test leaves no trace. | ||
| revert = os.path.join(DOCS_DIR, "revert-links.py") | ||
| if os.path.isfile(revert): | ||
| subprocess.run( | ||
| [sys.executable, revert], | ||
| cwd=DOCS_DIR, | ||
| capture_output=True, | ||
| check=True, | ||
| ) | ||
|
|
||
| if returncode != 0: | ||
| print( | ||
| f"FAILED: sphinx-build exited with status {returncode}", | ||
| file=sys.stderr, | ||
| ) | ||
| return 1 | ||
| print("PASSED: Sphinx documentation built successfully.") | ||
| return 0 | ||
| finally: | ||
| os.chdir(saved_cwd) | ||
| shutil.rmtree(build_output, ignore_errors=True) |
There was a problem hiding this comment.
Running the Sphinx build directly on the runfiles/workspace directory with no-sandbox and local tags means that any in-place mutations performed by conf.py's setup(app) hook (such as modifying README.md and README2.md) will directly affect the files in the user's active workspace. If the test is interrupted (e.g., via Ctrl+C, timeout, or a crash before revert-links.py is executed), these mutations will not be reverted, leaving the git workspace dirty.
To make the test robust, isolated, and safe, we should copy the docs directory and README.md to a temporary directory, and run the Sphinx build there. This eliminates the risk of mutating workspace files and removes the need for revert-links.py entirely.
def main() -> int:
temp_root = tempfile.mkdtemp(prefix="sphinx_test_")
build_output = os.path.join(temp_root, "_build")
temp_docs_dir = os.path.join(temp_root, "docs")
saved_cwd = os.getcwd()
try:
# Copy the docs directory and README.md to the temporary root
# to avoid mutating any files in the workspace/runfiles tree.
shutil.copytree(DOCS_DIR, temp_docs_dir, symlinks=False)
readme_src = os.path.join(os.path.dirname(DOCS_DIR), "README.md")
if os.path.exists(readme_src):
shutil.copy2(readme_src, os.path.join(temp_root, "README.mds"))
# conf.py's setup(app) hook uses cwd-relative paths like "./main"
# and "../README.md", so it must run with docs/ as cwd. Sphinx
# invokes setup(app) automatically when it loads conf.py below.
os.chdir(temp_docs_dir)
# Calling sphinx via subprocess would fail because a child python3
# doesn't inherit the Bazel runfiles PYTHONPATH used by this test.
from sphinx.cmd.build import main as sphinx_main
args = ["-b", "html", "-T", "-q"]
if not shutil.which("mmdc"):
args.extend(["-D", "mermaid_output_format=raw"])
args.extend([temp_docs_dir, build_output])
print("Running sphinx-build...", flush=True)
returncode = sphinx_main(args)
if returncode != 0:
print(
f"FAILED: sphinx-build exited with status {returncode}",
file=sys.stderr,
)
return 1
print("PASSED: Sphinx documentation built successfully.")
return 0
finally:
os.chdir(saved_cwd)
shutil.rmtree(temp_root, ignore_errors=True)Signed-off-by: Jack Luar <39641663+luarss@users.noreply.github.com>
What does this PR do?
Salvage of #10184 (cherry-picked onto current master, conflict resolved, lock file fixed).
Adds a hermetic Bazel
py_testtarget that runs the same Sphinx documentation build performed by readthedocs.org/openroad, catching broken cross-references, malformed markup, missing files, and toc.yml issues locally before they reach CI.//docs:sphinx_build_testrunssphinx-build -b htmlwith pinned Python deps from@openroad-pip(no system Sphinx required).doc_checkfor filtering:bazelisk test --test_tag_filters=doc_check //docs/...bazel-*symlinks to prevent recursive scanning.Fixes #9885
Changes from #10184
docs/BUILD.bazelwith the newman_pagesrule).MODULE.bazel.lockafter adding Sphinx pip deps (the missing fix from the original PR).Type of Change
Verification
bazelisk mod deps --lockfile_mode=errorpasses locallybazelisk test //docs:sphinx_build_testpasses locally