intentumdiff-build
How to build every layer of the IntentumDiff repo from source — the Rust core (PyO3 extension), the per-language Wasm parser components, the VS Code extension, and the dbt plugin — plus the toolchain
SKILL.mdskills/intentumdiff-build/SKILL.md
GitHubIntentumDiff — Building from source
Four independently-built layers. Only rebuild what you changed — knowing which layer you touched avoids both stale binaries and pointless rebuilds.
| You changed… | Build step | Rebuild needed? |
|---|---|---|
crates/rust-core-host/ (the engine) |
maturin develop --release (from repo root) |
Yes — the cdylib is stale until you do |
crates/<lang>-parser/ (a Wasm parser) |
python build.py (or per-crate cargo → wasm) |
Yes, to refresh src/intentumdiff/wasm/*.wasm |
plugins/vscode/src/ (extension) |
npm run compile (tsc -p ./) |
Yes (compiles TS → out/); no maturin |
src/intentumdiff/**.py (Python shell) |
none | No — pure Python runs as-is |
HARD RULE: if you changed a compiled layer, rebuild BEFORE you test or trust any output
Python silently loads the OLD cdylib / OLD *.wasm — a test run or a diff you eyeball
against a stale binary is a false pass/fail, and this has repeatedly wasted hours in this
repo. So:
- Touched
crates/rust-core-host/?maturin develop --release(from the repo root, not the crate dir) BEFORE running any pytest, anydiff_strings, or any manual check. The cdylib is stale until you do. - Touched any
crates/<lang>-parser/orcrates/sdk/?python build.py(or the single crate → copy tosrc/intentumdiff/wasm/) BEFORE testing. Notecrates/sdkfeeds MANY parsers, andlto=truemeans one crate change re-optimizes the whole binary — rebuild the full affected surface, not just the language you think you touched. - Verify the rebuild took by EXERCISING the changed behavior, not by reading timestamps.
Timestamps lie: a
#[cfg(test)]-only edit does not bump runtime; the.pydcan be shadowed by a staleintentumdiff_rust_core/editable-install dir (see the shadowing section). The reliable check is to run a diff that hits the new code path and assert the new result (e.g. after adding an intent description, confirmchange.descriptionis the new wording). If it shows the old behavior, the binary is stale — do not proceed. - Corollary: never conclude "the suite is green / the bug is fixed" from a run whose binary
predates your
crates/edit. Rebuild, then re-run. - The VS Code extension caches a long-running engine. It spawns a persistent
intentumdiffprocess (plugins/vscode/src/processTransport.ts) that does NOT hot-reload editable source or newly-built*.wasm. After ANY rebuild (or adding a parser), the extension keeps serving the OLD engine — e.g. still parsing.gitignorewith the generic parser — until you Reload Window (or restart the IntentumDiff engine). If the CLI / Python API shows the fix but the extension does not, it is a stale extension process, not a code bug — reload before re-diagnosing. - Editable installs snapshot entry-point metadata at install time. Adding a parser to
pyproject.toml [project.entry-points."intentumdiff.parsers"]does NOT appear in the installed.dist-infountil you re-install (pip install -e ., which triggers a full maturin build). In-process discovery still works because_FIRST_PARTY_PARSER_ENTRYPOINT_FALLBACKS(registry.py) re-adds built-ins from the source tree — so a new parser routes correctly WITHOUT the reinstall, butimportlib.metadatawill under-report it. Add every new parser to that fallback map, not just pyproject.
Do I actually need to rebuild? (decision rule)
Before running maturin, confirm a rebuild is warranted — recompiling unchanged Rust is a multi-minute no-op:
- Did any
crates/source change?git status --short crates/— if empty, no rebuild. - Is the installed core already current? Compare timestamps: the installed
src/intentumdiff/intentumdiff_rust_core.pydshould be newer than the newestcrates/rust-core-host/src/*.rs. If it is, and nothing changed, it's up to date. - A pure-Python fix in
src/intentumdiff/analysis/is oracle-only. Those modules are the parity test-oracle; the authoritative behavior is in the Rust core, which may already be correct (e.g. thesuppress_low_signal_reordersgroup emitsraw_change_indices: []in bothrefinement.pyandlib.rs). So a Python analysis fix often needs neither a Rust change nor a rebuild — verify againstcrates/rust-core-host/src/lib.rsbefore assuming. Only when you editcrates/does the rebuild rule below apply.
Rust core (crates/rust-core-host → intentumdiff_rust_core C-ABI cdylib)
The crate is pyo3-free (#B.6): it builds a bare cdylib exposing the C ABI (intentumdiff_call),
which rust_core.py ctypes-loads. maturin is driven by the repo-root pyproject.toml
(bindings = "cffi"), so run it from the repo root — cd crates/rust-core-host && maturin …
now errors (no pyproject there).
rustup toolchain install 1.95.0 # first time only
python -m pip install maturin # first time only
RUSTUP_TOOLCHAIN=1.95.0 maturin develop --release # from the REPO ROOT
This compiles the core and installs the cdylib as
.venv/Lib/site-packages/intentumdiff/intentumdiff_rust_core/intentumdiff_rust_core.<ext>
(.dll on Windows, .so Linux, .dylib macOS) — not a top-level .pyd.
Traps that waste hours:
- Always
--release. Plainmaturin developbuilds a debug core — functionally identical but ~20–50× slower on compute paths (the perceptual image diff goes ~6 s → ~0.3 s on a 1050×700 image). Use plainmaturin developonly when iterating on Rust and compile speed matters more than runtime. - Windows cdylib lock: the extension/CLI keeps the core
.dllloaded, so the install step fails withAccess is denied (os error 5)if a process holds it. Stop it first:Get-Process -Name intentumdiff -ErrorAction SilentlyContinue | Stop-Process -Force(and reload the VS Code extension host). - OneDrive-locked
.pdbbreaks the install (this repo's.venvis under OneDrive). A Windows release build still emits a largeintentumdiff_rust_core.pdb; OneDrive grabs a lock while syncing it, so maturin's install rename can die withAccess is denied (os error 5)even with nointentumdiffprocess running — after maturin has already uninstalled the old package, leaving a half-installed core (the loaded.dllis missing / mismatched, so the ctypes loader reportsfunction 'intentumdiff_call' not found). ctypes loads only the.dll, never the.pdb, so sidestep it: build a.pdb-free wheel and force-install it:
Thenexport VIRTUAL_ENV=<repo>/.venv && export PATH="$VIRTUAL_ENV/Scripts:$PATH" export CARGO_PROFILE_RELEASE_DEBUG=false # no .pdb in the artifact RUSTUP_TOOLCHAIN=1.95.0 maturin build --release -b cffi --out /tmp/idwheel "$VIRTUAL_ENV/Scripts/python.exe" -m pip install --force-reinstall --no-deps /tmp/idwheel/*.whlrm -f src/intentumdiff/*.pyd(shadow check below) and verify with the backend probe below. (Real fix: move.venvout of OneDrive.) maturin developinstalls into the venv named by$VIRTUAL_ENV, not.venvby name. From a non-activated shell (e.g. the Bash tool)VIRTUAL_ENVis unset and maturin guesses — it can pick a broken sibling like.venv2and die withNeed a Python interpreter to compile/could not determine version from interpreter name.develophas no--interpreterflag (that'sbuild), so the fix is to set the env explicitly, not pass a flag:export VIRTUAL_ENV=<repo>/.venv && export PATH="$VIRTUAL_ENV/Scripts:$PATH"beforematurin develop. Confirm it installed into.venv, not the guessed one.- Stale-shadow check (the loader-order trap, issue #28 — inverted since #B.6): the pyo3 era's
fix was to copy a
.pydintosrc/intentumdiff/; post-#B.6 that same file is now the bug._load_backend()usesfind_specto detect a pyo3 extension (origin ending in an extension suffix) and prefers it over the ctypes path — so a leftoversrc/intentumdiff/intentumdiff_rust_core.pydfrom the old era (or apip installed standaloneintentumdiff_rust_core) silently shadows the fresh cdylib and you test yesterday's engine. Remedy after every core build:
Verify which backend the DIFFER loads (not a bare import, which can lie):rm -f src/intentumdiff/*.pyd .venv/Scripts/python.exe -m pip uninstall -y intentumdiff_rust_core 2>$nullpython -c "import intentumdiff.rust_core as r; print(type(r._load_backend()).__name__)"must print_CtypesBackend. - Release build OOM on Windows (issue #29):
maturin develop --releasecan crash rustc 1.95.0 (handle_alloc_error/STATUS_STACK_BUFFER_OVERRUN) — the ~12k-linelib.rsat opt-level=3 + codegen-units=1 is one giant codegen unit. A debug build (maturin develop) is valid for correctness verification; only perf-sensitive work needs release. Lower-memory local override:CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16 maturin develop --release. - Toolchain pin: Rust 1.95.0 (the RC native-wheel dry run is pinned to it; local
Windows release packaging has hit a
rustc 1.95.0metadata ICE on optimized wheel builds). - No Python engine fallback exists (removed in #B.3/#90/#91): the Rust core is the only engine,
so a build failure means a broken engine, not a silent degrade. Backend selection is the only
override left:
INTENTUMDIFF_RUST_CORE_CTYPES=1forces the pure-ctypes proxy (the default once pyo3 is gone),INTENTUMDIFF_RUST_CORE_PYO3=1forces a pyo3 extension if one is still present.
Details: docs/BUILDING.md.
Wasm parser components (crates/<lang>-parser/ → src/intentumdiff/wasm/<lang>_parser.wasm)
Parsers are compiled to WebAssembly components (target wasm32-wasip2). The repo build hook
(build.py, a Hatch build hook, build-backend = "maturin" in pyproject.toml) drives
cargo → .wasm and stages the binaries into src/intentumdiff/wasm/. End users never build
these — published wheels ship the pre-built .wasm files. Build them locally only when working
on a grammar/parser crate:
python build.py # builds/refreshes the bundled .wasm parser set
For a single parser, build that crate for the wasm target and copy its artifact into
src/intentumdiff/wasm/. Names differ at every step (guessing them costs a cycle):
package = intentumdiff-<lang>-parser, artifact = intentumdiff_<lang>_parser.wasm, staged
name = <lang>_parser.wasm (whatever plugins/builtins.py references):
RUSTUP_TOOLCHAIN=1.95.0 cargo build --release --target wasm32-wasip2 -p intentumdiff-<lang>-parser
cp target/wasm32-wasip2/release/intentumdiff_<lang>_parser.wasm src/intentumdiff/wasm/<lang>_parser.wasm
Never pipe the cargo build through tail/grep inside a && chain — the pipe's exit status
masks a failed build, the cp then re-stages the STALE wasm, and every downstream test
"passes" against yesterday's binary (this happened; check for error in full output).
New tree-sitter grammar dep fails with ToolNotFound: clang on wasm32-wasip2 — that's
expected; grammar build scripts need a WASI C compiler. The repo pattern is a patch crate
crates/patches/tree-sitter-<lang>/ with a zig-prebuilt static lib: vendor the registry
source, custom Cargo.toml (autolib = false, tree-sitter-language dep) + target-aware
build.rs (wasip2 → link lib/libtree_sitter_<lang>.a; native → cc compile), compile with
zig cc --target=wasm32-wasi -O2 -fsanitize-trap=undefined -fvisibility=hidden + zig ar rcs,
and register under [patch.crates-io] in the root Cargo.toml. Full recipe + zig commands:
docs/WASM_BUILD_PATCHES.md; ~10 existing examples under crates/patches/.
Dormant crates (e.g. FreeBASIC) stay disabled until their grammar meets
the shipped-example contract. See docs/PLUGIN_GUIDE.md / docs/WASM_BUILD_PATCHES.md.
VS Code extension (plugins/vscode, TypeScript, no bundler)
cd plugins/vscode
npm install # first time
npm run compile # tsc -p ./ → out/ (npm run watch for incremental)
npm run lint # tsc -p ./ --noEmit (type-check only)
No bundler and no maturin — tsc only. Packaging for the marketplace: npm run vsix
(vsce package). Local install / dev host: npm run install:local / npm run sync:local
(PowerShell scripts under scripts/). The extension talks to the engine over the LiveServer
protocol, so a built/installed core makes live diffs fast (see the Rust-core section).
dbt plugin (plugins/intentumdiff_dbt, separate pip package)
Its own build.py (Hatch hook: cargo → .wasm) builds the dbt parser/enricher crates into
plugins/intentumdiff_dbt/src/intentumdiff_dbt/wasm/. Build it only when working on the dbt plugin;
it ships as its own wheel.
Packaging / wheels (release)
The main wheel bundles the Rust core + .wasm parsers. maturin build --release produces the
wheel; the publish workflow (.github/workflows/publish.yml) always builds --release, so
end users are never on a debug core. This matters only for release validation — day-to-day you
use maturin develop --release.
After building — verify
Run the suites for the layer you built (see intentumdiff-testing): pytest tests/unit,
cargo test -p rust-core-host, and/or cd plugins/vscode && npm run test. For engine changes,
also sanity-check with a real diff: intentumdiff file old.py new.py --profile-phases.
Stale-wasm debt: full rebuilds surface old source bugs (2026-07-07)
Wasm artifacts in src/intentumdiff/wasm/ are gitignored and rebuilt ad hoc, so a parser
crate's SOURCE can drift for months while tests keep passing against an old binary. The
first full python build.py then "breaks" tests that were never actually protecting the
current source (seen 2026-07: graphql child-blind frame hashes, quoted literal labels in
csharp/powershell fixtures, fuel-threshold drift). Two rules:
- After changing ANY crate under
crates/(sdk included), rebuild and gate the FULL affected surface, not just the languages you touched —lto = true+codegen-units = 1means any crate change re-optimizes the whole binary, shifting wasm fuel by ±10-15% for unrelated parsers. - When a full rebuild breaks a test that "used to pass", first ask when that language's wasm was last actually built — the failure usually reveals committed-but-never-built source, and the fix belongs at the source (parser bug or stale fixture), not in a revert of the rebuild.