Skip to content

feat(manifest-v2): lazy root build deps; lazy-safe emsdk preflight (fixes #823) - #824

Merged
apotema merged 2 commits into
mainfrom
fix/823-lazy-emsdk
Oct 31, 2026
Merged

apotema merged 2 commits into
mainfrom
fix/823-lazy-emsdk

Conversation

@apotema

@apotema apotema commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #823 (epic labelle-toolkit/labelle-cli#528).

This PR

  • RootBuildDep.lazy (manifest v2, default false): the generated build.zig.zon entry gets .lazy = true (for .builtin emsdk and .remote entries). Without the field the output is byte-identical. Older assemblers skip the unknown field.
  • The emitted ensureEmsdkActivated preflight uses b.lazyDependency("emsdk", .{}) orelse return, because b.dependency panics on a lazy entry. It behaves the same for eager entries. The three wasm goldens are updated.
  • New tests: lazy emission (builtin and remote, valid ZON, malformed section errors), manifest parse of .lazy, and that the preflight never calls b.dependency("emsdk"). The design doc documents the field.

Tests

zig build test -j2 on Windows: 3869 tests, all pass. The first run had 5 failures, all mine (my new test plus the three goldens); they were fixed before pushing.

Why a valid EMSDK still installed a second emsdk

Baseline from the box: GitLab job #133, FP main pipeline #30, image wasm-builder 4.1.2-1, with EMSDK=/opt/emsdk inherited (the log says "labelle-web: emsdk from inherited EMSDK: /opt/emsdk"). Every bundle, cold and warm, still installed a second emsdk into .labelle/bgfx_wasm/zig-pkg/N-V-__8AAJl1DwBezhYo_VE6f53mPVm00R-Fk28NPW7P14EQ/. It downloaded node-v20.18.0-linux-x64.tar.xz (25,738,964 B, about 5 s) and wasm-binaries.tar.xz (301,247,292 B, about 42 s), then unpacked and activated them: about 45 to 60 s per bundle. emcc then ran from that zig-pkg emsdk, and Emscripten logged "config changed, clearing cache" between runs. Test-game bundle: 209 s cold, 151 s warm.

What triggers it:

  • labelle-bgfx (the installer): emsdk_source.resolve (Fresh init → generate fails: scaffolds "dev" versions, fetch maps them to a bogus "vdev" git ref #159/gui_resolve: support .package and .url GUI plugin references #163) treated an EMSDK as valid only if <EMSDK>/upstream/emscripten/cache/sysroot/include existed. The image keeps /opt/emsdk read-only without that directory and points EM_CACHE at /cache/..., so the build fell back to the package and ran emsdk install/activate latest on it (1,390,146,850 B unpacked). The hook's link and labelle-imgui's bridges use the same rule.
  • Generated build.zig.zon (assembler): emsdk is an eager root dependency, so it is always fetched (about 1 MB launcher).
  • labelle-audio: it always calls b.lazyDependency("emsdk") on wasm, and reads its sysroot from the package that bgfx installed.
  • labelle-imgui: an eager but unused root .sokol entry pulls sokol-zig plus its eager emsdk 5.0.6 checkout and sokol-tools-bin, about 57 MB, even for a bgfx build.

The fix: one rule, in four repos

  • Valid EMSDK = .emscripten + upstream/emscripten/emcc.
  • Sysroot = $EM_CACHE/sysroot/include when EM_CACHE is set, else <EMSDK>/upstream/emscripten/cache/sysroot/include. A cold one is filled at configure time with embuilder build sysroot (headers only, under a second), under that emsdk's own EM_CONFIG.
  • The emsdk package is asked for (b.lazyDependency) only when there is no valid EMSDK, and the root entry is declared .lazy = true.
  • emsdk_source.zig is identical in labelle-bgfx, labelle-imgui (both bridges) and labelle-audio.

EMSDK unset: unchanged. The package is fetched, installed and activated as before.

PRs:

Validation: Flying Platform in the WASM builder image

Local runs used Docker Desktop and the image labelle-wasm-builder:local (amd64, EMSDK=/opt/emsdk, EM_CACHE on an ext4 /cache), with steps run as UID 10001, --memory=14g, and FP at e90a638 on a clean checkout each run.

For the after runs, FP had local overrides that were never committed to FP:

  • bgfx and imgui from these branches, through local:;
  • bgfx's labelle_audio pointed at the audio branch commit's archive;
  • LABELLE_ASSEMBLER set to this assembler branch, built for x86_64-linux.

Before each EMSDK run, the emsdk (4.0.9 and 5.0.6), sokol and sokolshdc tarballs were removed from the Zig global cache, so anything that still needed them would have had to fetch them again.

before (same image, current pins, warm) after, EMSDK=/opt/emsdk, warm
emsdk packages in .labelle/**/zig-pkg 4.0.9 installed, 1,390,146,850 B; + 5.0.6 checkout none
sokol / sokolshdc in zig-pkg 3.6 MB + 55 MB none
.labelle/bgfx_wasm/zig-pkg total 1,627,548,862 B 177,917,133 B
emsdk/sokol tarballs in the Zig global cache afterwards 4 0
network received during labelle bundle 1,090,311,725 B 482 B
"Installing SDK" / "(zig-pkg emsdk)" in the bundle log 2 / 0 0 / 0
emcc + sysroot zig-pkg emsdk /opt/emsdk, sysroot under EM_CACHE
labelle bundle 284 s 147 s (150 s on a repeat run)
threaded smoke (--isolated --expect-threads yes) / fallback smoke (--expect-threads no) pass / pass pass / pass

Box baseline, for comparison: 151 s warm, which includes the 45 to 60 s emsdk install.

The first after run recompiled the game because the bgfx, imgui, audio and assembler sources had changed (bundle 936 s). It had the same zero-emsdk numbers (network 156,722 B), plus Emscripten's one-time "config changed, clearing cache" as it moved from the zig-pkg emsdk to /opt/emsdk.

EMSDK unset (env -u EMSDK, labelle-web emsdk.source = "package", emsdk purged from the global cache): unchanged behavior. Zig fetched the emsdk package during generation's zig build --list-steps probe, and labelle-web activated it in place ("activating fetched emsdk 4.0.9 in place"). The build installed it (1,390,216,762 B, upstream/ + .emscripten; 1.36 GB received) and linked with it. The bundle and both smokes passed (bundle 553 s: Emscripten system libraries rebuilt for the other emsdk).

The first package-mode run failed with labelle-web NoFetchedEmsdk. bgfx's buildWasm returned early on the pass before the lazy package was fetched, leaving no bgfx artifact, and the generated build panicked ("unable to find artifact 'bgfx'") before Zig could fetch. The root's eager emsdk entry had hidden this. Fixed in labelle-bgfx#210 (commits cd6eaec, 332f1c6) and re-run green as above. The EMSDK-set rows are from a run with that final code.

Release order

  1. labelle-audio (bug: wgpu audio WAV parser vulnerable to integer overflow on corrupt input #12), released as 0.5.1. Then bump labelle-bgfx's labelle_audio pin to 0.5.1 in labelle-bgfx#210 before it is released.
  2. labelle-imgui (feat(assets): parse assets: block in scene.jsonc #49). Independent; release whenever.
  3. labelle-bgfx (feat(screenshot): wire engine.requestedScreenshot() into raylib + sokol templates (#227) #210), with the audio bump. Its new hook works with an eager or a lazy root emsdk, so it does not need the new assembler.
  4. labelle-assembler (feat(manifest-v2): lazy root build deps; lazy-safe emsdk preflight (fixes #823) #824). Independent of the bgfx release in either order: .lazy takes effect only with a bgfx manifest that sets it, and older assemblers ignore the field.
  5. FP bumps bgfx, imgui and the assembler. With only some of them bumped, the remaining packages still fetch the emsdk package (a ~1 MB launcher), but only the bgfx bump removes the 1.4 GB install.

https://claude.ai/code/session_01Hpt1haxcboZPznpG1KqDRS

Summary by CodeRabbit

  • New Features
    • Build manifests can now mark supported dependencies as lazy, allowing them to be fetched only when needed.
  • Bug Fixes
    • Emscripten setup now handles unavailable lazy dependencies gracefully during configuration and can continue after the build runner fetches them.

…#823)

A Labelle wasm build fetched the emsdk package into the checkout even
when `EMSDK` named a ready emsdk, partly because the generated
`build.zig.zon` declares emsdk as an eager root dependency.

- `RootBuildDep.lazy` (default false): when a backend manifest sets it,
  the generated build.zig.zon entry gets `.lazy = true` (for `.builtin`
  emsdk and `.remote` entries), so Zig fetches it only when a build script
  calls `b.lazyDependency`. labelle-bgfx sets it on its wasm emsdk root
  dep; its hook only asks for emsdk without a valid EMSDK. Older
  assemblers ignore the unknown field; manifests without it emit
  byte-identical zon.
- The emitted `ensureEmsdkActivated` preflight resolves the package with
  `b.lazyDependency(...) orelse return` instead of `b.dependency`, which
  panics on a lazy entry. It works unchanged for eager entries. Goldens
  updated.

Claude-Session: https://claude.ai/code/session_01Hpt1haxcboZPznpG1KqDRS
@coderabbitai

coderabbitai Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Team
  • Run ID: 09e544c0-90b1-4ad0-a43c-8df0c5f0c323
Commits

Reviewing files that changed from the base of the PR and between 5e6bd51 and fdf3e78.

Files selected for processing: 8
  • docs/design/manifest-v2-build-graph.md
  • src/build_files/build_zig.zig
  • src/codegen/emsdk_preflight.zig
  • src/codegen/manifest_v2.zig
  • src/codegen/manifest_v2_splice/common.zig
  • test/goldens/bgfx_v2_wasm.build.zig
  • test/goldens/raylib_v2_wasm.build.zig
  • test/goldens/sokol_wasm_v2.build.zig

Included review availability: This review used your included allowance. 7 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 10 reviews per hour.

Walkthrough

The manifest schema adds a lazy option for root dependencies. The generator emits lazy declarations for builtin and remote dependencies. The emsdk preflight uses b.lazyDependency and returns when emsdk is unavailable during configuration.

Changes: Lazy dependency flow

Layer / File(s) Summary
Manifest option and dependency emission
src/codegen/manifest_v2.zig, docs/design/manifest-v2-build-graph.md, src/codegen/manifest_v2_splice/common.zig
RootBuildDep adds a default-false lazy option. The generator emits .lazy = true for lazy builtin and remote dependencies, and tests cover parsing, generated output, and malformed builtin sections.
Lazy emsdk preflight
src/codegen/emsdk_preflight.zig, src/build_files/build_zig.zig, test/goldens/*_wasm*.build.zig
The generated preflight uses b.lazyDependency and returns when emsdk is unavailable. Its tests and WASM build goldens check or use lazy resolution.

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Feature · Severity of issue fixed: Medium

Sequence Diagram(s)

sequenceDiagram
  participant GeneratedBuildScript
  participant EmsdkPreflight
  participant ZigBuildRunner
  GeneratedBuildScript->>EmsdkPreflight: Run emsdk activation preflight
  EmsdkPreflight->>GeneratedBuildScript: Call b.lazyDependency("emsdk", .{})
  GeneratedBuildScript-->>EmsdkPreflight: Return null when emsdk is unavailable
  EmsdkPreflight-->>GeneratedBuildScript: Return without checking activation
  ZigBuildRunner->>GeneratedBuildScript: Fetch dependency and rerun configuration
Loading
Merge Risk: Minimal · up to fdf3e

No merge-blocking issue was identified. Backend hooks must follow the documented lazy-resolution contract.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Linked Issues check Warning Issue #823 requires the emsdk dependency to be lazy in the generated manifest and in labelle-audio, with header resolution from a valid EMSDK and package fetch only as a fallback. This PR implements… Include reviewable labelle-audio changes that declare its emsdk dependency lazy, resolve headers from the valid EMSDK layout, fetch the package only when that toolchain is unavailable, and add automated tests for those paths.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title names the main change: a new lazy option for root build dependencies in manifest v2, plus a lazy-safe emsdk preflight. The raw summary and objectives both describe these changes as the core …
Out of Scope Changes check Passed The changed schema, manifest emission, generated preflight, WASM goldens, tests, and design documentation all support the lazy emsdk dependency objective in #823. No unrelated change is established by…
Docstring Coverage Passed No eligible functions found in the changed files. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentat…
Full details: Title check

Explanation

The title names the main change: a new lazy option for root build dependencies in manifest v2, plus a lazy-safe emsdk preflight. The raw summary and objectives both describe these changes as the core of the PR. The title also references issue #823, which the objectives link to this work. The title is concise, specific, and single-sentence, so it meets the criteria.

Full details: Out of Scope Changes check

Explanation

The changed schema, manifest emission, generated preflight, WASM goldens, tests, and design documentation all support the lazy emsdk dependency objective in #823. No unrelated change is established by the available evidence.

Full details: Docstring Coverage

Explanation

No eligible functions found in the changed files. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff with cyclomatic complexity of at least 2. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 0 functions across 0 files. (8 skipped: 8 unsupported.)

Full details: Linked Issues check

Explanation

Issue #823 requires the emsdk dependency to be lazy in the generated manifest and in labelle-audio, with header resolution from a valid EMSDK and package fetch only as a fallback. This PR implements the assembler part: RootBuildDep.lazy emits .lazy = true for builtin and remote entries, and the generated preflight uses b.lazyDependency("emsdk", .{}). No labelle-audio implementation is present in the reviewed repository. References to related pull requests do not establish that code at this head. The emsdk_preflight code also does not provide labelle-audio header resolution.

Resolution

Include reviewable labelle-audio changes that declare its emsdk dependency lazy, resolve headers from the valid EMSDK layout, fetch the package only when that toolchain is unavailable, and add automated tests for those paths.

Finishing Touches
Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

A rabbit checks the build at dawn
“Lazy emsdk,” it says, “wait on”
If the package is not there
The runner fetches it with care
Then the build resumes its song

@apotema
apotema marked this pull request as ready for review October 31, 2026 02:34
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-31T02:35:29.313677Z fdf3e78 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@apotema apotema left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Codex, OpenAI LLM — review of fdf3e78141d5917ab9242d42661e28a093e60976.

No correctness findings in the lazy-dependency manifest/code-generation diff. I checked default compatibility (lazy = false), built-in and remote ZON emission, unchanged path dependencies, and the generated lazyDependency(... ) orelse return first-pass behavior. The eager and lazy golden fixtures cover the intended contract.

Validation: current-head GitHub CI run 41159616796 is successful. Local zig build test -j2 --summary all finished with 3,830 passes, 47 skips and one failure: a nested i18n compile reported Unexpected while reading Zig's own std/os/uefi/protocol/shell_parameters.zig. Re-running the unchanged failing test executable in isolation passed all 6 tests. I found no connection between that transient local file-read failure and this diff; I am not claiming the original full local run was green.

The partial-sysroot readiness issue reported on audio#12/bgfx#210/imgui#49 belongs to those SDK helpers, not this assembler diff. Keep that correction in the release train.

@apotema
apotema merged commit 769b174 into main Oct 31, 2026
4 checks passed
apotema added a commit that referenced this pull request Oct 31, 2026
… inspector outlining (#826)

Version-only release of #824 (manifest-v2 optional `.lazy` on root build
deps; lazy-safe emsdk preflight, fixes #823) and #822 (debug entity
inspector outlining, labelle-engine#922), part of
labelle-toolkit/labelle-cli#528.

Claude-Session: https://claude.ai/code/session_01Hpt1haxcboZPznpG1KqDRS
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

wasm build installs a second ~1.4 GB emsdk in the checkout even when EMSDK is set

1 participant