Sphinx: per-unit dev builds via UNIT= parameter#1361
Merged
gusthoff merged 17 commits intoMay 30, 2026
Merged
Conversation
The rglob for .bib files always searched from frontend/sphinx/../../content, finding all .bib files across the entire content tree. In a per-unit build (SPHINX_CONF_INI set) this is unnecessary: the unit's conf.ini directory is the correct root to search from, so only that unit's .bib files are loaded. The full-site build path (SPHINX_CONF_INI unset) is unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The two redirect rules were hardcoded in conf.py's setup() function and applied to every Sphinx build, including per-unit builds where they don't belong (GNAT_Toolchain_Getting_Started belongs to GNAT_Toolchain_Intro; Ada_For_The_C_Embedded_Developer belongs to Ada_For_The_Embedded_C_Developer). Each unit that needs redirects now carries its own redirects.json alongside conf.ini. conf.py's setup() loads: - Per-unit build (SPHINX_CONF_INI set): only the unit's redirects.json - Full-site build (SPHINX_CONF_INI unset): all redirects.json files merged Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
intersphinx_mapping pointed to https://learn.adacore.com/ with no local inventory, causing Sphinx to fetch objects.inv from the production server on every cold build. With 17 parallel per-unit builds this would be 17 concurrent requests to the live site. conf.py now checks for frontend/sphinx/objects.inv.learn; if it exists it is used as the local inventory path and no network request is made. If absent, behaviour falls back to fetching from the URL (first-run or CI without cache). objects.inv.learn is generated, not committed: add it to .gitignore. Also exclude dist-poc/ (the PoC build output directory) from git tracking. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds an optional UNIT variable to the Makefile and wires it through to pnpm run dev, so a single content unit can be built instead of the full 284-file site. Makefile: - UNIT ?= (empty = full-site build, unchanged) - When set, _SPHINX_SRC points to content/<UNIT>/ and _SPHINX_INI is derived automatically from that unit's conf.ini - The local target now uses _SPHINX_SRC / _SPHINX_INI instead of the hardcoded CONTENT_DIR / SPHINX_CONF_INI webpack.dev.cjs: - The ShellPlugin's onBuildExit script passes UNIT to make local when the UNIT env var is set in the calling shell Usage: make local UNIT=courses/intro-to-ada UNIT=courses/advanced-ada pnpm run dev Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a "Building a single course or lab" subsection under the dev server section in README.md, explaining the UNIT environment variable, showing example invocations for pnpm run dev and make local, and noting that all units with a conf.ini are supported. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…t change The previous approach set the Sphinx source root to content/<UNIT>/, which caused two problems: 1. RST files were processed more times than expected because changing the source root invalidated all doctrees cached from the full-site build (file paths are relative to the source root, so they all changed). 2. The landing page was overwritten: with the unit as source root, dist/html/index.html became the unit's own index rather than the site landing page. Fix: keep content/ as the source root always. Pass UNIT to conf.py as SPHINX_UNIT; conf.py adds all other unit directories to exclude_patterns so Sphinx only reads and writes the target unit's RST files. The output paths remain dist/html/courses/<unit>/, dist/html/labs/<unit>/, etc. — the landing page at dist/html/index.html is never overwritten. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When SPHINX_UNIT is set (e.g. SPHINX_UNIT=courses/advanced-ada), conf.py adds every other unit directory to exclude_patterns so Sphinx only reads and writes the target unit's RST files. The root index.rst and about.rst are still processed (they are not in any unit subdirectory), so the landing page is rebuilt with the full navigation intact. Three related changes in conf.py: - exclude_patterns: glob courses/, labs/, and booklets/ and exclude every subdirectory that is not the target unit. - nitpicky: disabled when SPHINX_UNIT is set. The root index.rst has toctree entries for all units; with most units excluded those entries cannot be resolved. nitpicky=True would turn those into errors. - suppress_warnings = ['toc.not_readable']: suppresses the toctree reference warnings that would otherwise flood the output. - Redirect loading in setup(): adds a third branch for SPHINX_UNIT builds (source-root = content/) alongside the existing SPHINX_CONF_INI branch (source-root = unit dir) and the full-site else branch. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
webpack-dev-server triggers multiple compilation cycles on startup (due to HMR setup and other internal passes). With dev=false in ShellPlugin, every compilation runs make cleanall (onBuildStart) followed by make local (onBuildExit). make cleanall deletes the entire dist/ including sphinx's doctree cache, forcing a full RST rebuild on every webpack compilation. For the full-site build this was invisible (7-minute sphinx runs meant the user never noticed the extra passes). For per-unit UNIT= builds the sphinx run takes ~10 seconds, so all four passes complete quickly enough to be clearly visible. Fix: when UNIT is set, skip make cleanall. The doctree cache survives across webpack recompilations, so the second and subsequent sphinx invocations triggered by webpack find 0 changed files and return immediately. Only the first invocation does the full unit rebuild. The full-site build path (UNIT unset) is unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… on edit WatchPlugin was imported in webpack.dev.cjs but never added to the plugins array. Without it, webpack only watches TypeScript and SCSS source files. Editing an RST file never triggered a webpack recompilation, so the ShellPlugin's onBuildExit never fired and sphinx never re-ran. Add WatchPlugin to the dev config: - UNIT set: watches content/<UNIT>/**/*.rst (target unit only) - UNIT unset: watches content/**/*.rst (all content) When an RST file changes, webpack detects it, recompiles, and onBuildExit runs make local (with or without UNIT=), triggering an incremental sphinx rebuild that picks up only the changed file. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
/vagrant/content and /vagrant/frontend are mounted as vboxsf (VirtualBox shared folders). vboxsf does not generate inotify events, so webpack's default inotify-based watcher never fires when RST files are edited on the host — WatchPlugin added the files to fileDependencies but changes were silently ignored. Three changes when UNIT is set: watchOptions.poll = 1000: force webpack to poll watched files every second instead of relying on inotify. This makes WatchPlugin's RST file watching functional on vboxsf. liveReload = true: after each webpack build cycle the browser reloads the page. Without this, even if sphinx rebuilt the HTML the browser would continue showing the stale version. onBuildExit.blocking = true: webpack must not emit its "build done" signal (which triggers the live-reload) until sphinx has finished writing the new HTML to disk. With blocking: false the reload races the sphinx write and the browser may load the old page. All three changes are conditional on UNIT being set; the full-site build path is unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…split)
content/index.rst and content/about.rst are the site landing page — they
belong to the top-level aggregator project, not to any content unit. Before
this change they were rebuilt on every make local UNIT=... invocation (adding
~1-2 s and producing a spurious gnatchop ERROR from the "Try Ada Now" code
block in index.rst).
Two changes in the SPHINX_UNIT block in conf.py:
1. exclude_patterns += ['index.rst', 'about.rst']
Removes the landing page files from Sphinx's source discovery.
Note: Sphinx 9.1's get_matching_files requires the full filename
('index.rst'), not just the docname ('index'), to match .rst files.
2. master_doc = f'{_sphinx_unit}/index'
With the site root excluded, the unit's own index.rst becomes the
Sphinx entry point. The sidebar shows only the unit's chapter
structure, and Sphinx has a valid root document to start from.
Result: per-unit builds now process exactly the unit's own RST files
(5 for GNAT_Toolchain_Intro), produce 0 warnings, and leave
dist/html/index.html untouched from the previous full build.
suppress_warnings is no longer needed (no toctree references to
excluded units from within the unit's own toctree).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…(pass-2 split)" This reverts commit b1f5be8.
A typo in UNIT= (e.g. UNIT=courses/intro-to--ada) previously produced no error: conf.py silently excluded every unit directory (none matched the bad name), so Sphinx built only the landing page and exited cleanly. The user had no indication that their intended unit was never built. Two-layer validation, both using the presence of conf.ini as the sentinel (every valid content unit has one): Makefile: $(wildcard) check in an ifneq block evaluated at parse time. Make exits immediately with a clear message before any target runs, so the error appears in the webpack dev-server output before Sphinx is even invoked. conf.py: Path.is_file() check at config-load time. Covers the case where sphinx-build is called directly (e.g. in CI or from the epub VM) without going through the Makefile. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Makefile $(error) validation is correct for direct `make local UNIT=…` CLI use but causes a disruptive ShellPlugin stack trace when pnpm run dev is used with a typo: Make exits with code 2 after 30 s of webpack compilation, ShellPlugin throws, and the dev server crashes. Add an fs.existsSync check at the top of webpack.dev.cjs (before any webpack config is constructed) that validates UNIT against content/<UNIT>/conf.ini. On failure it prints a clean 3-line error and calls process.exit(1) immediately — before webpack even starts. The Makefile $(error) is retained for the CLI path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Per-unit dev builds:
UNIT=courses/advanced-ada pnpm run devAdds a
UNIT=parameter that scopes both the Sphinx build and the webpackfile-watcher to a single content unit, turning a full-site rebuild into a fast
incremental one. After the initial build, saving an RST file triggers Sphinx on
only the changed file and live-reloads the browser.
A mistyped unit name is caught immediately — at Makefile parse time for
make local, before webpack even starts forpnpm run dev.What changes
frontend/Makefile— newUNIT ?=variable; passesSPHINX_UNITtoSphinx; immediate
$(error)on invalid value.frontend/webpack.dev.cjs— forwardsUNITtomake local; skipsmake cleanallso the doctree cache survives across recompilations; addsWatchPluginfor RST files; enables polling (vboxsfhas no inotify);sets
liveReloadandblockingso the browser reloads only after Sphinxfinishes; validates
UNITbefore webpack starts.frontend/sphinx/conf.py—SPHINX_UNITdrivesexclude_patternstoscope the source scan to one unit while keeping
content/as the root;disables
nitpickyand suppresses toctree warnings for excluded units;scopes bibtex discovery to the unit directory; moves redirect rules to
per-unit
redirects.jsonfiles; uses a cached localobjects.inv.learninstead of fetching from
learn.adacore.comon every cold build.content/courses/*/redirects.json— two new files extracted from thehardcoded redirect dict in
conf.py.frontend/.gitignore,README.md— housekeeping and docs.Test plan
UNIT=courses/GNAT_Toolchain_Intro pnpm run dev— page loads athttp://127.0.0.1:8080/courses/GNAT_Toolchain_Intro/index.htmlUNIT=courses/intro-to--ada pnpm run dev(typo) — clean error, no webpack compilemake local UNIT=courses/intro-to--ada(typo) — fails at parse time withStop.