From b00e7e99160ba95fd4a74d97d72f2fac50cd1801 Mon Sep 17 00:00:00 2001 From: gusthoff Date: Fri, 29 May 2026 17:23:31 +0200 Subject: [PATCH 01/17] Infra/Sphinx: scope bibtex discovery to unit dir in per-unit builds 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 --- frontend/sphinx/conf.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/frontend/sphinx/conf.py b/frontend/sphinx/conf.py index 6e4bcbdf1..b17d60a42 100644 --- a/frontend/sphinx/conf.py +++ b/frontend/sphinx/conf.py @@ -527,8 +527,12 @@ def get_file_from_conf_ini(path_to_file): bibtex_file_full_path = get_file_from_conf_ini(bibtex_file) bibtex_bibfiles.append(bibtex_file_full_path) else: - ## PATCH: get content directory - content_dir = os.path.abspath(os.path.dirname(__file__ ) + '/../../content') + if 'SPHINX_CONF_INI' in os.environ and os.environ['SPHINX_CONF_INI']: + # Per-unit build: only search within the unit's own directory + content_dir = os.path.dirname(os.path.abspath(os.environ['SPHINX_CONF_INI'])) + else: + # Full-site build: search entire content directory + content_dir = os.path.abspath(os.path.dirname(__file__) + '/../../content') for bib_file in Path(content_dir).rglob('*.bib'): bibtex_bibfiles.append(str(bib_file)) From c173f0ff74afe3d5319f238a582aedac773daf81 Mon Sep 17 00:00:00 2001 From: gusthoff Date: Fri, 29 May 2026 17:23:55 +0200 Subject: [PATCH 02/17] Infra/Sphinx: move redirect definitions to per-unit redirects.json files 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 --- .../redirects.json | 3 +++ .../courses/GNAT_Toolchain_Intro/redirects.json | 3 +++ frontend/sphinx/conf.py | 17 +++++++++++++---- 3 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 content/courses/Ada_For_The_Embedded_C_Developer/redirects.json create mode 100644 content/courses/GNAT_Toolchain_Intro/redirects.json diff --git a/content/courses/Ada_For_The_Embedded_C_Developer/redirects.json b/content/courses/Ada_For_The_Embedded_C_Developer/redirects.json new file mode 100644 index 000000000..4ab83501c --- /dev/null +++ b/content/courses/Ada_For_The_Embedded_C_Developer/redirects.json @@ -0,0 +1,3 @@ +{ + "courses/Ada_For_The_C_Embedded_Developer/index": "../Ada_For_The_Embedded_C_Developer/" +} diff --git a/content/courses/GNAT_Toolchain_Intro/redirects.json b/content/courses/GNAT_Toolchain_Intro/redirects.json new file mode 100644 index 000000000..6443f98d9 --- /dev/null +++ b/content/courses/GNAT_Toolchain_Intro/redirects.json @@ -0,0 +1,3 @@ +{ + "courses/GNAT_Toolchain_Getting_Started/index": "../GNAT_Toolchain_Intro/" +} diff --git a/frontend/sphinx/conf.py b/frontend/sphinx/conf.py index b17d60a42..188cb551e 100644 --- a/frontend/sphinx/conf.py +++ b/frontend/sphinx/conf.py @@ -546,10 +546,19 @@ def setup(app): if 'html' in outdir: templates_path.append('_templates') - redirects.update({ - "courses/Ada_For_The_C_Embedded_Developer/index": "../Ada_For_The_Embedded_C_Developer/", - "courses/GNAT_Toolchain_Getting_Started/index": "../GNAT_Toolchain_Intro/" - }) + if 'SPHINX_CONF_INI' in os.environ and os.environ['SPHINX_CONF_INI']: + # Per-unit build: load redirects only for this unit + unit_dir = Path(os.environ['SPHINX_CONF_INI']).parent + redirects_file = unit_dir / 'redirects.json' + if redirects_file.is_file(): + with open(redirects_file) as _f: + redirects.update(json.load(_f)) + else: + # Full-site build: merge redirects from all units + _content_dir = Path(os.path.dirname(__file__)) / '../../content' + for _redirects_file in sorted(_content_dir.rglob('redirects.json')): + with open(_redirects_file) as _f: + redirects.update(json.load(_f)) if not os.getenv('FRONTEND_TESTING'): From 8b3d7997d04651fb2e7a0d51e07e6483506e719e Mon Sep 17 00:00:00 2001 From: gusthoff Date: Fri, 29 May 2026 17:24:08 +0200 Subject: [PATCH 03/17] Infra/Sphinx: cache intersphinx objects.inv locally 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 --- frontend/.gitignore | 4 +++- frontend/sphinx/conf.py | 6 +++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/frontend/.gitignore b/frontend/.gitignore index 37aff653a..7f554c0be 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -3,7 +3,9 @@ npm-debug.log *.map build-manifest.json dist +dist-poc .nyc_output/ coverage/ pnpm-debug.log -.coverage \ No newline at end of file +.coverage +sphinx/objects.inv.learn \ No newline at end of file diff --git a/frontend/sphinx/conf.py b/frontend/sphinx/conf.py index 188cb551e..67d334e21 100644 --- a/frontend/sphinx/conf.py +++ b/frontend/sphinx/conf.py @@ -511,7 +511,11 @@ def get_file_from_conf_ini(path_to_file): # -- Options for intersphinx extension --------------------------------------- -intersphinx_mapping = {'learn': ('https://learn.adacore.com/', None)} +_inv_cache = os.path.join(os.path.dirname(__file__), 'objects.inv.learn') +intersphinx_mapping = { + 'learn': ('https://learn.adacore.com/', + _inv_cache if os.path.exists(_inv_cache) else None) +} # -- Options for redirects extension ----------------------------------------- From 23f4dd9204f6aedbc082bf9d82d4bffa714620c6 Mon Sep 17 00:00:00 2001 From: gusthoff Date: Fri, 29 May 2026 17:42:05 +0200 Subject: [PATCH 04/17] Infra/Sphinx: add UNIT parameter for per-unit local/dev builds 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// 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 --- frontend/Makefile | 18 ++++++++++++++++-- frontend/webpack.dev.cjs | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/frontend/Makefile b/frontend/Makefile index db0eaa9d4..22ea9fa66 100644 --- a/frontend/Makefile +++ b/frontend/Makefile @@ -22,6 +22,20 @@ MKFILE_DIR := $(dir $(MKFILE_PATH)) BUILD_MANIFEST := $(MKFILE_DIR)/build-manifest.json CONTENT_DIR = $(MKFILE_DIR)/../content + +# Optional: limit the local/dev Sphinx build to a single content unit. +# Usage: make local UNIT=courses/intro-to-ada +# UNIT=courses/advanced-ada pnpm run dev +UNIT ?= + +ifneq ($(UNIT),) +_SPHINX_SRC := $(CONTENT_DIR)/$(UNIT) +_SPHINX_INI := $(shell [ -f $(_SPHINX_SRC)/conf.ini ] && echo $(_SPHINX_SRC)/conf.ini) +else +_SPHINX_SRC := $(CONTENT_DIR) +_SPHINX_INI := $(SPHINX_CONF_INI) +endif + TEST_CONTENT = tests/rst TEST_BUILDDIR = tests/html @@ -220,9 +234,9 @@ publish: # # code server. # This should only be called from `pnpm run dev` local: - @SPHINX_CONF_INI="$(SPHINX_CONF_INI)" \ + @SPHINX_CONF_INI="$(_SPHINX_INI)" \ SRC_TEST_DIR="$(SRC_TEST_DIR)" \ - $(SPHINXBUILD) -M html $(CONTENT_DIR) \ + $(SPHINXBUILD) -M html $(_SPHINX_SRC) \ "$(BUILDDIR)" $(SPHINXOPTS) $(O) -v -c "$(SPHINXCONF)" webpack-production: diff --git a/frontend/webpack.dev.cjs b/frontend/webpack.dev.cjs index bd8b9727e..4a88741a2 100644 --- a/frontend/webpack.dev.cjs +++ b/frontend/webpack.dev.cjs @@ -33,7 +33,7 @@ module.exports = function(env) { parallel: false }, onBuildExit:{ - scripts: ['make local'], + scripts: [`make local${process.env.UNIT ? ' UNIT=' + process.env.UNIT : ''}`], blocking: false, parallel: false }, From dc0a6df32485d0a65d57fd972add45a73566745b Mon Sep 17 00:00:00 2001 From: gusthoff Date: Fri, 29 May 2026 17:42:11 +0200 Subject: [PATCH 05/17] Docs: document per-unit dev build with the UNIT variable 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 --- README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/README.md b/README.md index bbefde8fa..d1ad5e7d8 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,31 @@ using `make local` which will point the widgets at 127.0.0.1:8000 You can then point your browser on your host to 127.0.0.1:8080 to see the learn website being served from vagrant. +### Building a single course or lab (faster dev builds) + +By default `pnpm run dev` rebuilds the entire content tree (all courses, labs, +and booklets — 284 RST files). If you are working on a single unit you can +limit both the Sphinx build and the watched file set to that unit by setting the +`UNIT` environment variable: + +```bash +# Inside the web VM, in /vagrant/frontend: +UNIT=courses/intro-to-ada pnpm run dev +UNIT=courses/advanced-ada pnpm run dev +UNIT=labs/intro-to-ada pnpm run dev +UNIT=booklets/adacore-technologies-for-railway-software pnpm run dev +``` + +You can also invoke the Sphinx step directly without the webpack server: + +```bash +make local UNIT=courses/intro-to-ada +``` + +`UNIT` must be a path relative to the `content/` directory. Every unit with a +`conf.ini` file at its root is supported (all courses, labs, and booklets). +When `UNIT` is unset the behaviour is unchanged: the full site is built. + ## Generate content for publishing To build and start the publishing server, run: From 7ee2201ddd67eb7b5f583b4468ec8bd9e3d068e6 Mon Sep 17 00:00:00 2001 From: gusthoff Date: Fri, 29 May 2026 18:22:34 +0200 Subject: [PATCH 06/17] Infra/Sphinx: fix UNIT= to use exclude_patterns instead of source-root change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous approach set the Sphinx source root to content//, 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//, dist/html/labs//, etc. — the landing page at dist/html/index.html is never overwritten. Co-Authored-By: Claude Sonnet 4.6 --- frontend/Makefile | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/frontend/Makefile b/frontend/Makefile index 22ea9fa66..631aa0ce8 100644 --- a/frontend/Makefile +++ b/frontend/Makefile @@ -24,18 +24,13 @@ BUILD_MANIFEST := $(MKFILE_DIR)/build-manifest.json CONTENT_DIR = $(MKFILE_DIR)/../content # Optional: limit the local/dev Sphinx build to a single content unit. +# conf.py reads SPHINX_UNIT and adds all other unit directories to +# exclude_patterns, so only the target unit's RST files are processed. +# The source root stays content/ and the output paths are unchanged. # Usage: make local UNIT=courses/intro-to-ada # UNIT=courses/advanced-ada pnpm run dev UNIT ?= -ifneq ($(UNIT),) -_SPHINX_SRC := $(CONTENT_DIR)/$(UNIT) -_SPHINX_INI := $(shell [ -f $(_SPHINX_SRC)/conf.ini ] && echo $(_SPHINX_SRC)/conf.ini) -else -_SPHINX_SRC := $(CONTENT_DIR) -_SPHINX_INI := $(SPHINX_CONF_INI) -endif - TEST_CONTENT = tests/rst TEST_BUILDDIR = tests/html @@ -234,9 +229,10 @@ publish: # # code server. # This should only be called from `pnpm run dev` local: - @SPHINX_CONF_INI="$(_SPHINX_INI)" \ + @SPHINX_CONF_INI="$(SPHINX_CONF_INI)" \ + SPHINX_UNIT="$(UNIT)" \ SRC_TEST_DIR="$(SRC_TEST_DIR)" \ - $(SPHINXBUILD) -M html $(_SPHINX_SRC) \ + $(SPHINXBUILD) -M html $(CONTENT_DIR) \ "$(BUILDDIR)" $(SPHINXOPTS) $(O) -v -c "$(SPHINXCONF)" webpack-production: From 87bbff3ef32550d90455885c18558dd7242382f8 Mon Sep 17 00:00:00 2001 From: gusthoff Date: Fri, 29 May 2026 18:22:45 +0200 Subject: [PATCH 07/17] Infra/Sphinx: add SPHINX_UNIT per-unit filtering in conf.py 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 --- frontend/sphinx/conf.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/frontend/sphinx/conf.py b/frontend/sphinx/conf.py index 67d334e21..8912d84f7 100644 --- a/frontend/sphinx/conf.py +++ b/frontend/sphinx/conf.py @@ -178,6 +178,23 @@ def get_file_from_conf_ini(path_to_file): else: tags.add('no_hidden_contents') +# Per-unit build: exclude every content unit except the target so that +# Sphinx only reads and writes the target unit's RST files. +# The source root stays content/ and output paths are unchanged +# (e.g. dist/html/courses/advanced-ada/). +_sphinx_unit = os.environ.get('SPHINX_UNIT', '') +if _sphinx_unit: + _content_root = Path(os.path.dirname(__file__)) / '../../content' + for _top in ('courses', 'labs', 'booklets'): + _top_path = _content_root / _top + if _top_path.is_dir(): + for _sub in sorted(_top_path.iterdir()): + if _sub.is_dir() and f'{_top}/{_sub.name}' != _sphinx_unit: + exclude_patterns.append(f'{_top}/{_sub.name}') + # The root index.rst still references excluded units via toctree — + # suppress those warnings rather than failing or spamming the output. + suppress_warnings = ['toc.not_readable'] + show_authors = True # The name of the Pygments (syntax highlighting) style to use. @@ -191,7 +208,7 @@ def get_file_from_conf_ini(path_to_file): # numbered starting at 1. numfig_secnum_depth = 0 -nitpicky = True +nitpicky = not bool(_sphinx_unit) # sphinx.ext.extlinks: markup to shorten external links extlinks = { @@ -551,12 +568,20 @@ def setup(app): templates_path.append('_templates') if 'SPHINX_CONF_INI' in os.environ and os.environ['SPHINX_CONF_INI']: - # Per-unit build: load redirects only for this unit + # Per-unit book build (source-root = unit dir): load this unit's redirects unit_dir = Path(os.environ['SPHINX_CONF_INI']).parent redirects_file = unit_dir / 'redirects.json' if redirects_file.is_file(): with open(redirects_file) as _f: redirects.update(json.load(_f)) + elif _sphinx_unit: + # Per-unit dev build (source-root = content/, SPHINX_UNIT set): + # load only the target unit's redirects + _unit_redirects = (Path(os.path.dirname(__file__)) + / '../../content' / _sphinx_unit / 'redirects.json') + if _unit_redirects.is_file(): + with open(_unit_redirects) as _f: + redirects.update(json.load(_f)) else: # Full-site build: merge redirects from all units _content_dir = Path(os.path.dirname(__file__)) / '../../content' From b483583486b1e85f83204085bf906ee19c02c70e Mon Sep 17 00:00:00 2001 From: gusthoff Date: Fri, 29 May 2026 18:37:44 +0200 Subject: [PATCH 08/17] Infra/Sphinx: skip cleanall in webpack onBuildStart for UNIT builds 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 --- frontend/webpack.dev.cjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/webpack.dev.cjs b/frontend/webpack.dev.cjs index 4a88741a2..654e215b6 100644 --- a/frontend/webpack.dev.cjs +++ b/frontend/webpack.dev.cjs @@ -28,7 +28,10 @@ module.exports = function(env) { plugins: [ new ShellPlugin({ onBuildStart:{ - scripts: ['make cleanall'], + // Skip cleanall for per-unit builds: the doctree cache must survive + // across webpack recompilations so that subsequent sphinx invocations + // are incremental (near-instant) rather than full rebuilds. + scripts: [process.env.UNIT ? 'echo "UNIT build: skipping cleanall"' : 'make cleanall'], blocking: true, parallel: false }, From 083e7013ac024f564a59caa2d4323367e783d544 Mon Sep 17 00:00:00 2001 From: gusthoff Date: Fri, 29 May 2026 18:55:17 +0200 Subject: [PATCH 09/17] Infra/Sphinx: watch RST files in webpack dev server to trigger sphinx 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//**/*.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 --- frontend/webpack.dev.cjs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/frontend/webpack.dev.cjs b/frontend/webpack.dev.cjs index 654e215b6..8120659e0 100644 --- a/frontend/webpack.dev.cjs +++ b/frontend/webpack.dev.cjs @@ -44,6 +44,17 @@ module.exports = function(env) { // first build only. dev: false, }), + // Watch RST files so that editing content triggers a sphinx rebuild via + // the ShellPlugin's onBuildExit. Scoped to the target unit when UNIT is + // set, otherwise watches all content. + new WatchPlugin({ + files: [ + path.join( + __dirname, '..', 'content', + process.env.UNIT ? `${process.env.UNIT}/**/*.rst` : '**/*.rst' + ) + ] + }), ] }; From 0d7ba47460c99334b14bf5aa204e2133d62cd6dd Mon Sep 17 00:00:00 2001 From: gusthoff Date: Fri, 29 May 2026 19:05:03 +0200 Subject: [PATCH 10/17] Infra/Sphinx: fix RST file change detection in VirtualBox shared folders MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /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 --- frontend/webpack.dev.cjs | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/frontend/webpack.dev.cjs b/frontend/webpack.dev.cjs index 8120659e0..27f828bc4 100644 --- a/frontend/webpack.dev.cjs +++ b/frontend/webpack.dev.cjs @@ -7,6 +7,7 @@ const common = require('./webpack.common.cjs'); module.exports = function(env) { const common_config = common(env); + const unit = process.env.UNIT || ''; const dev_config = { mode: 'development', @@ -16,28 +17,35 @@ module.exports = function(env) { host: '0.0.0.0', port: 8080, hot: true, - liveReload: false, - // devMiddleware: { - // index: 'index.html', - // }, + // liveReload: for UNIT builds the browser must reload after sphinx + // rewrites the HTML. For full-site builds keep it off (HMR only). + liveReload: !!unit, }, output: { publicPath: '/_static/' }, devtool: 'source-map', + // /vagrant/{content,frontend} are VirtualBox shared folders (vboxsf). + // vboxsf does not generate inotify events, so webpack's default + // inotify-based watcher never fires for RST file changes. Force polling + // when UNIT is set so WatchPlugin can detect edits made on the host. + ...(unit ? { watchOptions: { poll: 1000, ignored: /node_modules/ } } : {}), plugins: [ new ShellPlugin({ onBuildStart:{ // Skip cleanall for per-unit builds: the doctree cache must survive // across webpack recompilations so that subsequent sphinx invocations // are incremental (near-instant) rather than full rebuilds. - scripts: [process.env.UNIT ? 'echo "UNIT build: skipping cleanall"' : 'make cleanall'], + scripts: [unit ? 'echo "UNIT build: skipping cleanall"' : 'make cleanall'], blocking: true, parallel: false }, onBuildExit:{ - scripts: [`make local${process.env.UNIT ? ' UNIT=' + process.env.UNIT : ''}`], - blocking: false, + scripts: [`make local${unit ? ' UNIT=' + unit : ''}`], + // blocking for UNIT builds: webpack must not signal "done" until + // sphinx has finished writing HTML, otherwise the live-reload fires + // before the new page content is on disk. + blocking: !!unit, parallel: false }, // dev=false here to force every build to trigger make, the default is @@ -47,11 +55,12 @@ module.exports = function(env) { // Watch RST files so that editing content triggers a sphinx rebuild via // the ShellPlugin's onBuildExit. Scoped to the target unit when UNIT is // set, otherwise watches all content. + // Polling is used because vboxsf does not generate inotify events. new WatchPlugin({ files: [ path.join( __dirname, '..', 'content', - process.env.UNIT ? `${process.env.UNIT}/**/*.rst` : '**/*.rst' + unit ? `${unit}/**/*.rst` : '**/*.rst' ) ] }), From b1f5be85793426e15912112cdbf49f2987fe02cc Mon Sep 17 00:00:00 2001 From: gusthoff Date: Fri, 29 May 2026 19:30:47 +0200 Subject: [PATCH 11/17] Infra/Sphinx: exclude site landing page from per-unit builds (pass-2 split) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- frontend/sphinx/conf.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/frontend/sphinx/conf.py b/frontend/sphinx/conf.py index 8912d84f7..932e6595b 100644 --- a/frontend/sphinx/conf.py +++ b/frontend/sphinx/conf.py @@ -191,9 +191,15 @@ def get_file_from_conf_ini(path_to_file): for _sub in sorted(_top_path.iterdir()): if _sub.is_dir() and f'{_top}/{_sub.name}' != _sphinx_unit: exclude_patterns.append(f'{_top}/{_sub.name}') - # The root index.rst still references excluded units via toctree — - # suppress those warnings rather than failing or spamming the output. - suppress_warnings = ['toc.not_readable'] + # Exclude the site landing page (index.rst, about.rst) from per-unit + # builds. They belong to a separate "pass 2" project and rebuilding + # them on every RST edit wastes time and produces a spurious gnatchop + # error from the "Try Ada Now" code block in index.rst. + # dist/html/index.html is left untouched from the last full build. + exclude_patterns += ['index.rst', 'about.rst'] + # With the site root excluded, the unit's own index becomes the Sphinx + # master document so that the sidebar shows only the unit's chapters. + master_doc = f'{_sphinx_unit}/index' show_authors = True From 8f307905d7576644f7ad09b4cf308141999ddf74 Mon Sep 17 00:00:00 2001 From: gusthoff Date: Fri, 29 May 2026 19:40:53 +0200 Subject: [PATCH 12/17] Revert "Infra/Sphinx: exclude site landing page from per-unit builds (pass-2 split)" This reverts commit b1f5be85793426e15912112cdbf49f2987fe02cc. --- frontend/sphinx/conf.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/frontend/sphinx/conf.py b/frontend/sphinx/conf.py index 932e6595b..8912d84f7 100644 --- a/frontend/sphinx/conf.py +++ b/frontend/sphinx/conf.py @@ -191,15 +191,9 @@ def get_file_from_conf_ini(path_to_file): for _sub in sorted(_top_path.iterdir()): if _sub.is_dir() and f'{_top}/{_sub.name}' != _sphinx_unit: exclude_patterns.append(f'{_top}/{_sub.name}') - # Exclude the site landing page (index.rst, about.rst) from per-unit - # builds. They belong to a separate "pass 2" project and rebuilding - # them on every RST edit wastes time and produces a spurious gnatchop - # error from the "Try Ada Now" code block in index.rst. - # dist/html/index.html is left untouched from the last full build. - exclude_patterns += ['index.rst', 'about.rst'] - # With the site root excluded, the unit's own index becomes the Sphinx - # master document so that the sidebar shows only the unit's chapters. - master_doc = f'{_sphinx_unit}/index' + # The root index.rst still references excluded units via toctree — + # suppress those warnings rather than failing or spamming the output. + suppress_warnings = ['toc.not_readable'] show_authors = True From 63765c104935f55a5a136074f68f254cf332b30a Mon Sep 17 00:00:00 2001 From: gusthoff Date: Fri, 29 May 2026 19:50:12 +0200 Subject: [PATCH 13/17] Infra/Sphinx: validate UNIT= at parse time in Makefile and conf.py 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 --- frontend/Makefile | 8 ++++++++ frontend/sphinx/conf.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/frontend/Makefile b/frontend/Makefile index 631aa0ce8..dcace2125 100644 --- a/frontend/Makefile +++ b/frontend/Makefile @@ -31,6 +31,14 @@ CONTENT_DIR = $(MKFILE_DIR)/../content # UNIT=courses/advanced-ada pnpm run dev UNIT ?= +# Validate UNIT immediately at parse time so a typo fails loudly before +# Sphinx is invoked. Every valid unit has a conf.ini at its root. +ifneq ($(UNIT),) +ifeq ($(wildcard $(CONTENT_DIR)/$(UNIT)/conf.ini),) +$(error UNIT="$(UNIT)": not found or missing conf.ini under $(CONTENT_DIR). Check for typos.) +endif +endif + TEST_CONTENT = tests/rst TEST_BUILDDIR = tests/html diff --git a/frontend/sphinx/conf.py b/frontend/sphinx/conf.py index 8912d84f7..84693db65 100644 --- a/frontend/sphinx/conf.py +++ b/frontend/sphinx/conf.py @@ -185,6 +185,14 @@ def get_file_from_conf_ini(path_to_file): _sphinx_unit = os.environ.get('SPHINX_UNIT', '') if _sphinx_unit: _content_root = Path(os.path.dirname(__file__)) / '../../content' + if not (_content_root / _sphinx_unit / 'conf.ini').is_file(): + print( + f'\nERROR: SPHINX_UNIT={_sphinx_unit!r} is not a valid content unit' + f' — no conf.ini found at {_content_root / _sphinx_unit / "conf.ini"}.' + '\nCheck for typos in the UNIT variable.\n', + file=sys.stderr, + ) + sys.exit(1) for _top in ('courses', 'labs', 'booklets'): _top_path = _content_root / _top if _top_path.is_dir(): From 202f56504d37a6d963b923db760907c969b5930c Mon Sep 17 00:00:00 2001 From: gusthoff Date: Fri, 29 May 2026 20:16:26 +0200 Subject: [PATCH 14/17] Infra/Sphinx: validate UNIT in webpack.dev.cjs before dev server starts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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//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 --- frontend/webpack.dev.cjs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/frontend/webpack.dev.cjs b/frontend/webpack.dev.cjs index 27f828bc4..444579779 100644 --- a/frontend/webpack.dev.cjs +++ b/frontend/webpack.dev.cjs @@ -1,3 +1,4 @@ +const fs = require('fs'); const path = require('path'); const { merge } = require('webpack-merge'); const exec = require('child_process').exec; @@ -9,6 +10,19 @@ module.exports = function(env) { const common_config = common(env); const unit = process.env.UNIT || ''; + // Validate UNIT before webpack starts so a typo produces a clean error + // rather than a cryptic ShellPlugin stack trace after 30 s of compilation. + // The Makefile $(error) catches the same mistake for direct `make` CLI use. + if (unit) { + const confIni = path.join(__dirname, '..', 'content', unit, 'conf.ini'); + if (!fs.existsSync(confIni)) { + console.error(`\nERROR: UNIT="${unit}" is not a valid content unit.`); + console.error(` No conf.ini found at: ${confIni}`); + console.error(` Check for typos.\n`); + process.exit(1); + } + } + const dev_config = { mode: 'development', devServer: { From 718456b064cf7364eb1355f1913be1b1679b4637 Mon Sep 17 00:00:00 2001 From: gusthoff Date: Fri, 29 May 2026 23:22:25 +0200 Subject: [PATCH 15/17] Editorial change: simplified comments --- README.md | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d1ad5e7d8..2b2a3c122 100644 --- a/README.md +++ b/README.md @@ -48,17 +48,14 @@ website being served from vagrant. ### Building a single course or lab (faster dev builds) -By default `pnpm run dev` rebuilds the entire content tree (all courses, labs, -and booklets — 284 RST files). If you are working on a single unit you can -limit both the Sphinx build and the watched file set to that unit by setting the -`UNIT` environment variable: +By default `pnpm run dev` rebuilds the entire content tree. +If you are working on a single unit you can limit both the Sphinx build and the +watched file set to that unit by setting the `UNIT` environment variable: ```bash # Inside the web VM, in /vagrant/frontend: UNIT=courses/intro-to-ada pnpm run dev -UNIT=courses/advanced-ada pnpm run dev UNIT=labs/intro-to-ada pnpm run dev -UNIT=booklets/adacore-technologies-for-railway-software pnpm run dev ``` You can also invoke the Sphinx step directly without the webpack server: From d0b3dc7296c645166b7d5e33771ce273e16ab3c5 Mon Sep 17 00:00:00 2001 From: gusthoff Date: Fri, 29 May 2026 23:23:00 +0200 Subject: [PATCH 16/17] Editorial change: removed comment --- frontend/Makefile | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/Makefile b/frontend/Makefile index dcace2125..af422ebc3 100644 --- a/frontend/Makefile +++ b/frontend/Makefile @@ -28,7 +28,6 @@ CONTENT_DIR = $(MKFILE_DIR)/../content # exclude_patterns, so only the target unit's RST files are processed. # The source root stays content/ and the output paths are unchanged. # Usage: make local UNIT=courses/intro-to-ada -# UNIT=courses/advanced-ada pnpm run dev UNIT ?= # Validate UNIT immediately at parse time so a typo fails loudly before From f6c4aeab4e24580182beefd0dd342e0e9f4f4520 Mon Sep 17 00:00:00 2001 From: gusthoff Date: Fri, 29 May 2026 23:23:19 +0200 Subject: [PATCH 17/17] Editorial change: simplified comment --- frontend/webpack.dev.cjs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/frontend/webpack.dev.cjs b/frontend/webpack.dev.cjs index 444579779..7a1b74f96 100644 --- a/frontend/webpack.dev.cjs +++ b/frontend/webpack.dev.cjs @@ -10,8 +10,7 @@ module.exports = function(env) { const common_config = common(env); const unit = process.env.UNIT || ''; - // Validate UNIT before webpack starts so a typo produces a clean error - // rather than a cryptic ShellPlugin stack trace after 30 s of compilation. + // Validate UNIT before webpack starts so a typo produces a clean error. // The Makefile $(error) catches the same mistake for direct `make` CLI use. if (unit) { const confIni = path.join(__dirname, '..', 'content', unit, 'conf.ini');