Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
b00e7e9
Infra/Sphinx: scope bibtex discovery to unit dir in per-unit builds
gusthoff May 29, 2026
c173f0f
Infra/Sphinx: move redirect definitions to per-unit redirects.json files
gusthoff May 29, 2026
8b3d799
Infra/Sphinx: cache intersphinx objects.inv locally
gusthoff May 29, 2026
23f4dd9
Infra/Sphinx: add UNIT parameter for per-unit local/dev builds
gusthoff May 29, 2026
dc0a6df
Docs: document per-unit dev build with the UNIT variable
gusthoff May 29, 2026
7ee2201
Infra/Sphinx: fix UNIT= to use exclude_patterns instead of source-roo…
gusthoff May 29, 2026
87bbff3
Infra/Sphinx: add SPHINX_UNIT per-unit filtering in conf.py
gusthoff May 29, 2026
b483583
Infra/Sphinx: skip cleanall in webpack onBuildStart for UNIT builds
gusthoff May 29, 2026
083e701
Infra/Sphinx: watch RST files in webpack dev server to trigger sphinx…
gusthoff May 29, 2026
0d7ba47
Infra/Sphinx: fix RST file change detection in VirtualBox shared folders
gusthoff May 29, 2026
b1f5be8
Infra/Sphinx: exclude site landing page from per-unit builds (pass-2 …
gusthoff May 29, 2026
8f30790
Revert "Infra/Sphinx: exclude site landing page from per-unit builds …
gusthoff May 29, 2026
63765c1
Infra/Sphinx: validate UNIT= at parse time in Makefile and conf.py
gusthoff May 29, 2026
202f565
Infra/Sphinx: validate UNIT in webpack.dev.cjs before dev server starts
gusthoff May 29, 2026
718456b
Editorial change: simplified comments
gusthoff May 29, 2026
d0b3dc7
Editorial change: removed comment
gusthoff May 29, 2026
f6c4aea
Editorial change: simplified comment
gusthoff May 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,28 @@ 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.
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=labs/intro-to-ada 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:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"courses/Ada_For_The_C_Embedded_Developer/index": "../Ada_For_The_Embedded_C_Developer/"
}
3 changes: 3 additions & 0 deletions content/courses/GNAT_Toolchain_Intro/redirects.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"courses/GNAT_Toolchain_Getting_Started/index": "../GNAT_Toolchain_Intro/"
}
4 changes: 3 additions & 1 deletion frontend/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ npm-debug.log
*.map
build-manifest.json
dist
dist-poc
.nyc_output/
coverage/
pnpm-debug.log
.coverage
.coverage
sphinx/objects.inv.learn
17 changes: 17 additions & 0 deletions frontend/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,22 @@ 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.
# 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 ?=

# 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

Expand Down Expand Up @@ -221,6 +237,7 @@ publish: #
# This should only be called from `pnpm run dev`
local:
@SPHINX_CONF_INI="$(SPHINX_CONF_INI)" \
SPHINX_UNIT="$(UNIT)" \
SRC_TEST_DIR="$(SRC_TEST_DIR)" \
$(SPHINXBUILD) -M html $(CONTENT_DIR) \
"$(BUILDDIR)" $(SPHINXOPTS) $(O) -v -c "$(SPHINXCONF)"
Expand Down
66 changes: 58 additions & 8 deletions frontend/sphinx/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,31 @@ 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'
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():
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.
Expand All @@ -191,7 +216,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 = {
Expand Down Expand Up @@ -511,7 +536,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 -----------------------------------------

Expand All @@ -527,8 +556,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))
Expand All @@ -542,10 +575,27 @@ 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 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'
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'):
Expand Down
50 changes: 43 additions & 7 deletions frontend/webpack.dev.cjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
const fs = require('fs');
const path = require('path');
const { merge } = require('webpack-merge');
const exec = require('child_process').exec;
Expand All @@ -7,6 +8,19 @@ const common = require('./webpack.common.cjs');

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.
// 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',
Expand All @@ -16,31 +30,53 @@ 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:{
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: [unit ? 'echo "UNIT build: skipping cleanall"' : 'make cleanall'],
blocking: true,
parallel: false
},
onBuildExit:{
scripts: ['make local'],
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
// 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.
// Polling is used because vboxsf does not generate inotify events.
new WatchPlugin({
files: [
path.join(
__dirname, '..', 'content',
unit ? `${unit}/**/*.rst` : '**/*.rst'
)
]
}),
]
};

Expand Down
Loading