Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3dace8f
feat: Set up sphinx-mounts
Rubyfi Sep 8, 2026
fff37d3
chore: adjust mount conditionals, add correct variant to autoiconf.json
Rubyfi Sep 8, 2026
05234c8
fix: display correct html_title
LuSilber Sep 15, 2026
27d87b3
chore: align the Python pin and unblock the documentation toolchain
LuSilber Sep 16, 2026
e57925e
Merge branch 'sphinx-mounts': declarative variant gating, no Jinja in…
LuSilber Sep 16, 2026
1e39d46
feat: generate variant data once, and delete the Jinja hook
LuSilber Sep 16, 2026
8dd1c80
refactor: make ubproject.toml the one configuration both readers read
LuSilber Sep 16, 2026
abc8729
refactor: gate documents on component membership, not on feature or v…
LuSilber Sep 16, 2026
8225e20
ci: gate the documentation without a compiler
LuSilber Sep 16, 2026
9306f40
docs: record what was implemented, and why Phase 6 was not
LuSilber Sep 16, 2026
c65b328
fix: check with ubc, and fix the two defects it found
LuSilber Sep 16, 2026
92ad9bd
docs: recheck against the concept note, and record the symlink finding
LuSilber Sep 16, 2026
375771a
fix: work against the pinned spl-core, not a local checkout
LuSilber Sep 16, 2026
92aad17
fix: keep the report toctrees on the build path spl-core writes to
LuSilber Sep 16, 2026
8430cc8
fix: pin utf-8 on every file read, for Windows
LuSilber Sep 16, 2026
6c4e9b4
fix: hide the per-component report root from ubCode
LuSilber Sep 17, 2026
27918fe
fix: read the generated report pages only in the reports shape
LuSilber Sep 21, 2026
5afd49e
fix: strip the Jinja armour from generated source listings
LuSilber Sep 21, 2026
a5c45c1
fix: stop copying the binary directory, and run ubc in CI
LuSilber Sep 21, 2026
bd851d1
fix: stop walking the symlink, and guard the path that replaces it
LuSilber Sep 21, 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
2 changes: 1 addition & 1 deletion .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ ENV NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
ENV PIP_CERT=/etc/ssl/certs/ca-certificates.crt

# Python toolchain (uv/CPython 3.11/Poetry) baked as the vscode user, so it lands
# Python toolchain (uv/CPython 3.12/Poetry) baked as the vscode user, so it lands
# in /home/vscode/.local (owned by the runtime user) and is on PATH the instant the
# container starts. Same script bare WSL and CI run at runtime — one source of truth.
COPY bootstrap_python.sh /tmp/bootstrap_python.sh
Expand Down
47 changes: 47 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,53 @@ jobs:
echo "Selected gate marker: $marker"
echo "marker=$marker" >> "$GITHUB_OUTPUT"

documentation:
name: Documentation (no compiler)
runs-on: ubuntu-24.04
timeout-minutes: 15
needs: determine-gate

# The documentation is derivable from the sources alone: KConfig is pure
# Python and the documents are text. Only CMake's top-level project()
# call needs a C toolchain, and nothing here goes through it. So this
# gate installs a Python and the locked dependencies, and nothing else --
# no poks, no scoop, no cross-compiler. It is the fastest signal in the
# workflow and it covers every variant, where the build jobs cover the
# variants they build.
steps:
- name: Checkout Code
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.12"

- name: Install documentation dependencies
run: |
pipx install poetry==2.4.1
poetry config virtualenvs.in-project true
poetry install --no-root

# `ubc` is the second reader. Without it the parity tests skip, and
# a guarantee that only holds on a developer machine is not a
# guarantee -- so the gate installs it and CI_REQUIRE_UBC turns a
# skip into a failure. No licence is needed for `check`; the inputs
# are passed anyway so that adding the secrets later just works.
- name: Install ubc
uses: useblocks/ubc-action@0.1
with:
version: "0.35.0"
license-key: ${{ secrets.UBCODE_LICENSE_KEY }}
license-user: ${{ secrets.UBCODE_LICENSE_USER }}

- name: Documentation gate
env:
CI_REQUIRE_UBC: "1"
run: poetry run pytest -m "docs and ${{ needs.determine-gate.outputs.marker }}"

test-on-windows:
name: Build and Test on Windows
runs-on: windows-2025
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
# Binary output dir, not recommended to push binary results to Git.
/build

# Current-variant pointer written by tools/variant_data.py: a symlink (or, on
# Windows without Developer Mode, a copy) of the configured variant's build
# output directory. Generated output, never committed.
/generated

# Output directory of test results
/test/output

Expand Down
8 changes: 8 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@
"editor.defaultFormatter": "josetr.cmake-language-support-vscode"
},
"cmake.configureOnOpen": false,
// Generated output. tools/variant_data.py writes the variant data here and
// CMake writes everything else; an edit under build/ is lost on the next
// configure, and worse, it looks like it worked. Making the editor refuse
// the edit is the only guard rail that also applies to an assistant.
"files.readonlyInclude": {
"build/**": true,
"generated/**": true
},
"cmake.buildDirectory": "${workspaceFolder}/build/${variant:variant}/${buildKit}/${buildType}",
"cmake.copyCompileCommands": "${workspaceFolder}/build/compile_commands.json",
"cmake.configureSettings": {
Expand Down
71 changes: 71 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,59 @@
},
"problemMatcher": []
},
{
"label": "Select documentation variant",
"detail": "Point the docs and ubCode at one variant -- no CMake, no compiler",
"type": "shell",
// Always the docs cell. Choosing `reports` here would open every
// report fence in the IDE, and each one globs into build/**, which
// ubCode excludes -- so every component would report an unmatched
// glob. CMake writes the docs cell as the pointer for the same
// reason. The target choice belongs to the build task below, which
// is where it changes what is produced rather than what is linted.
"command": "${command:python.interpreterPath} tools/variant_data.py --variant ${input:variant} --kit ${input:buildKit} --target docs --current",
"problemMatcher": [],
"presentation": {
"reveal": "silent",
"panel": "shared"
}
},
{
"label": "Generate all variant data",
"detail": "Write build/variants/<Variant>/<kit>/<target>.json for every variant",
"type": "shell",
"command": "${command:python.interpreterPath} tools/variant_data.py --all",
"problemMatcher": []
},
{
"label": "Build documentation for a variant",
"detail": "Sphinx docs for one variant, without configuring or building the software",
"type": "shell",
"command": "${command:python.interpreterPath} -m sphinx -b html . build/docs/${input:variant}",
"options": {
"env": {
"VARIANT": "${input:variant}",
"VARIANT_DATA_FILE": "build/variants/${input:variant}/${input:buildKit}/${input:docsTarget}.json"
}
},
"problemMatcher": []
},
{
"label": "Documentation gate (all variants)",
"detail": "What CI's compiler-free job runs: generate every variant and build its documents",
"type": "shell",
"command": "${command:python.interpreterPath} -m pytest -m docs",
"group": "test",
"problemMatcher": []
},
{
"label": "Check documentation with ubc (all variants)",
"detail": "Lint every variant with the reader that never runs conf.py",
"type": "shell",
"command": "${command:python.interpreterPath} -m pytest -m docs -k ubc",
"group": "test",
"problemMatcher": []
},
{
"label": "Open variant test report",
"detail": "Open the variant's overall test report in your web browser",
Expand Down Expand Up @@ -111,6 +164,24 @@
"components/spled"
]
},
{
"type": "pickString",
"id": "buildKit",
"description": "Which build kit? (the test kit adds a variant's test suites to its component list)",
"options": [
"test",
"prod"
]
},
{
"type": "pickString",
"id": "docsTarget",
"description": "Which build shape? (reports additionally shows the generated test and coverage pages)",
"options": [
"docs",
"reports"
]
},
{
"type": "pickString",
"id": "buildType",
Expand Down
184 changes: 182 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ On a **bare Linux host (e.g. WSL Ubuntu) that is not a devcontainer**, `build.sh

```bash
sudo ./bootstrap_ubuntu.sh # root: apt packages (libc6-dev, build-essential, 7zip, pipx)
./bootstrap_python.sh # user: uv, CPython 3.11, Poetry (into ~/.local)
./bootstrap_python.sh # user: uv, CPython 3.12, Poetry (into ~/.local)
# then open a fresh shell (see note below) so ~/.local/bin is on PATH:
./build.sh --install # user: poetry install + poks toolchain
```
Expand All @@ -46,6 +46,24 @@ If `poetry` is not found after `bootstrap_python.sh`, open a new shell (or `sour

**Always** start VS Code with: `.\build.ps1 -startVSCode` to ensure proper environment variables and Python virtual environment activation (`.venv` with Poetry dependencies).

### Working against a local spl-core checkout

Some changes to this project need a change in `spl-core` first. `CMakeLists.txt`
locates spl-core by importing it from the venv and then includes its
`spl.cmake`, so a single editable install redirects **both** the Python and the
CMake side at a local checkout:

```bash
.venv/bin/python -m pip install -e ../spl-core --no-deps # point at the checkout
./build.sh --install # ...and back to the pinned release
```

The path is deliberately **not** committed to `pyproject.toml`: CI must keep
resolving the released spl-core, so a local override never silently becomes the
build everyone gets. Land the spl-core change upstream, release it, and bump the
pin in `pyproject.toml` — the override is for the span of one change, not a
working mode.

### VS Code CMake Extension Configuration

VS Code users can build directly using the CMake extension via `.vscode` configuration files:
Expand Down Expand Up @@ -98,7 +116,8 @@ CI runs on **GitHub Actions** (`.github/workflows/ci.yml`) for every push/PR to

Jobs:

- `determine-gate` — computes the `gate_*` quality-gate marker once (by event/branch) and shares it with all three build jobs via `needs`.
- `determine-gate` — computes the `gate_*` quality-gate marker once (by event/branch) and shares it with all four jobs via `needs`.
- `documentation` (`ubuntu-24.04`) — the compiler-free gate: a Python and the locked dependencies, then `pytest -m "docs and <gate>"`. It generates the variant data for **every** variant and builds each one's documents, where the build jobs only cover the variants they build. No poks, no scoop, no cross-compiler, so it is also the fastest signal in the workflow.
- `test-on-windows` (`windows-2025`) — `build.ps1 -install` then `-selftests -marker <gate>`.
- `test-on-linux` (`ubuntu-24.04`) — bare-runner path: `bootstrap_ubuntu.sh` + `bootstrap_python.sh`, then `build.sh --install` and `--selftests --marker <gate>`.
- `test-devcontainer` (`ubuntu-24.04`) — builds `.devcontainer/` via `devcontainers/ci` (which runs `onCreateCommand`, i.e. `build.sh --install`) and runs `build.sh --selftests --marker <gate>` inside the container.
Expand Down Expand Up @@ -144,6 +163,167 @@ Edit feature config: `.\build.ps1 -command ".venv\Scripts\poetry run guiconfig"`

Check feature values in source code via generated `autoconf.h` header.

## Variant-Dependent Documentation

Documents never use Jinja. The global `source-read` pass that rendered every
document is gone, and bringing it back is a regression, not a shortcut.

One narrowly scoped `source-read` handler does exist, and it is not that.
spl-core passes `--jinja-raw-tags` to clanguru, so generated source listings
under `__source_docs` wrap their code in `{% raw %}` markers that nothing else
removes. `conf.py` blanks those two lines, for those docnames only: a line
filter, not a template render. It goes away when `pyproject.toml` can pin an
spl-core that lets the flag be turned off -- no released version does yet.

Everything variant-dependent is decided from **one file**: the variant data
that `tools/variant_data.py` writes, exposed as `var.*`. The governing rule:

> **Everything a condition may name has to be IN the variant data file.**

A key that only `conf.py` knows is invisible to ubCode, `ubc` and a reviewer's
editor, so their view of the project silently disagrees with the build —
silently, because a condition a tool cannot evaluate gates content **off**
rather than failing. That is why `conf.py` reads the file and adds nothing to
it.

### The three mechanisms

**Whole documents: `[[source.variant_sources]]` in `ubproject.toml`.** One rule
per component, gated on membership of the variant's component list:

```toml
[[source.variant_sources]]
if = "'components/auto_off' in var.build_config.components"
files = [
"components/auto_off/doc/**",
"generated/components/auto_off/reports/**",
"generated/components/auto_off/__source_docs/**",
]
```

Membership, never identity. The component list comes from that variant's
`parts.cmake`, so the product structure is stated once, in the file that already
states it. **Never gate on the variant name** — that is a second encoding of the
same fact, free to drift.

Rules are *subtractive*: a FALSE rule removes the files it names, a TRUE rule
does nothing. Two rules naming the same file therefore compose as AND, which is
how generated output is gated on both the build shape and the component.

**Blocks inside a document: the `{if}` directive of Sphinx-Needs.** Four-backtick
fence, condition as the argument:

````text
````{if} var.features.BLINKING
...
````
````

Content behind a false condition is never parsed, so its needs never enter the
traceability data.

**External trees: `[[source.mounts]]`.** Currently unused. Everything this
project shows lives in the tree or under `generated/`.

### What the data holds

| Key | |
| --- | --- |
| `var.features.*` | every KConfig symbol, with **every** declared boolean present — including the promptless ones KConfig omits when they are off |
| `var.build_config.variant` | e.g. `Disco`, `Base/Dev` |
| `var.build_config.kit` | `prod` or `test` |
| `var.build_config.target` | `docs` or `reports` |
| `var.build_config.components` | the variant's component list, from `parts.cmake` |

### Two differences between the engines

The `{if}` directive takes a real Python expression, so a bare
`var.features.BLINKING` is enough. A `variant_sources` condition uses a
restricted grammar that needs `== True`. And a condition that cannot be
evaluated **excludes** what it gates, so a typo silently shrinks the document
set rather than failing loudly — which is what `test_ubproject_config.py` is
for.

### Checking with the other reader

The Sphinx build is only half the story: the point of keeping everything in the
variant data file is that a reader which never runs `conf.py` decides the same
things. `ubc` is that reader, and `pytest -m docs -k ubc` proves it — per
variant, it asserts that ubCode removes **exactly** the component documents the
variant's component list omits.

`ubc` ships inside the ubCode VS Code extension and is on neither PyPI nor npm,
so there is no install step this repository can own. The tests find it on
`PATH`, via the `UBC` environment variable, or in the extension directory, and
**skip** when it is absent rather than pretending to cover it:

```bash
export UBC="$HOME/.vscode/extensions/useblocks.ubcode-0.35.0-darwin-arm64/server/cli/ubc"
pytest -m docs -k ubc
```

To look at one variant by hand, override the data file rather than switching
the project:

```bash
ubc check -c "needs.variant_data_file = 'build/variants/Sleep/test/docs.json'"
```

Two things to know about `ubproject.toml` when editing it. Configuring parsers
puts ubCode in **parser mode**, where the document set comes from each
`[parse.parsers.*].include` and `[source] extend_include` is *ignored* — so the
parser includes have to stay in step with `include_patterns` in `conf.py`, or
the two readers are looking at different files. And `[[source.variant_sources]]`
rules are implemented as exclusions, which is why a rule that cannot be
evaluated removes what it gates.

### Adding a component

1. Add it to the variant's `parts.cmake`.
2. Add one `[[source.variant_sources]]` rule in `ubproject.toml`.
3. Add one line to the 150% toctree in `doc/components/index.md`.

Nothing is generated, no loop is edited, and nothing under `build/` is touched.

### Generated output

`build/` and `generated/` are output. **Nobody edits them — not a person, not an
assistant.** The editor is configured to refuse it (`files.readonlyInclude`) and
`build/variants/GENERATED` says so on disk.

`generated/` is the configured variant's build directory, and today **nothing
reads it**. The report toctrees still glob `/build/**`, and `conf.py` narrows
the source set to the configured build so each glob resolves to one page.

That is a deferral, not the end state. A fixed path under `generated/` would be
better, and the configuration for it is already in place -- the rst parser
include and the `generated/...` entries in every variant rule. It cannot be
switched on from this repository: spl-core writes the gcovr tree at
`reports/html/<build-relative page path>/coverage/index.html` and looks its
report artifacts up in the same place, so moving the page that links to it
without moving the tree breaks every coverage link.

When spl-core does write the tree relative to the page, the `build/` forwarding
in `conf.py` and the `generated` entry in its `exclude_patterns` have to go in
the *same* change, or Sphinx discovers every report page under both names.
`test_generated_and_build_discovery_are_never_both_live` fails if only half of
that is done.

Regenerate without a compiler — KConfig is pure Python, and CMake's top-level
`project()` call demands a C toolchain before it will configure at all:

```bash
python tools/variant_data.py --all # the whole matrix
python tools/variant_data.py --variant Sleep --kit test # ...and point at one cell
python tools/variant_data.py --all --check # CI: regenerate and diff
```

Preview a variant by pointing the build at its cell:

```bash
VARIANT_DATA_FILE=build/variants/Sleep/test/docs.json sphinx-build -b html . out
```

## Project-Specific Conventions

1. **No direct CMake invocation**: Always use `build.ps1` wrapper (handles variant selection, environment, Poetry, etc.)
Expand Down
Loading
Loading