diff --git a/build.zig b/build.zig index a89e18a5..469a82c8 100644 --- a/build.zig +++ b/build.zig @@ -18,12 +18,16 @@ pub fn build(b: *std.Build) void { // fetcher then mapped to a bogus `vdev` git ref (issue #159). // Keep this trio a MUTUALLY COMPATIBLE set: a fresh `labelle init` resolves // these defaults, so a mismatch fails `labelle build` before any user code. - // Set verified end-to-end (init → install → generate → full compile, on - // both the raylib_desktop and null_desktop targets, against the published - // release tarballs) for core 1.28.0 / engine 2.12.2 / gfx 1.30.1 with - // cli 1.61.2 — which is exactly `labelle-cli` v1.61.2's `versions.zon`, - // the set its own `labelle upgrade all` writes. Keep the two in step: when - // they drift, `init` scaffolds a project the very next `upgrade` rewrites. + // Set verified end-to-end STANDALONE (init → install → generate → full + // compile against the published release tarballs, scratch LABELLE_HOME, + // no sibling checkouts) for core 1.32.0 / engine 2.12.2 / gfx 1.30.1 with + // cli 1.61.2, on the bgfx_desktop, null_desktop, raylib_desktop and + // sokol_desktop targets. engine/gfx/cli are exactly `labelle-cli` v1.61.2's + // `versions.zon`, the set its own `labelle upgrade all` writes; core is + // AHEAD of it (that file still says 1.28.0 through cli v1.67.0) — see the + // bgfx paragraph below for why, and bump the cli side to match. Keep the + // two in step: when they drift, `init` scaffolds a project the very next + // `upgrade` rewrites. // // The hard floor inside the trio (#679): gfx >= 1.30.0 re-exports // `core.TextureId` instead of declaring its own (labelle-gfx#328, @@ -51,11 +55,28 @@ pub fn build(b: *std.Build) void { // // (v0.96.0 bumped engine 2.7.0 → 2.11.0 while leaving core at 1.26.0, so // every fresh `labelle init` scaffold failed to compile.) + // + // core 1.32.0 (#731 follow-up): the builtin `.bgfx` provider default + // (`src/config.zig` builtinProvider) is labelle-bgfx 0.20.0, the lowest + // bgfx that survives core v1.32.0's `MaterialEffect.pixel_water`. Its + // HARD core floor is 1.28.0 (`core.BackendTextureId`, a struct field + // type, bgfx >= 0.15.0) — 0.20.0 does compile against core 1.28.0, + // verified standalone; the v1.32.0 `PixelWaterDraw` / `PIXEL_WATER_*` + // names it takes from `backend_contract` are reached only through + // `@hasField(MaterialEffect, "pixel_water")`-gated paths or lazy + // top-level aliases. 1.32.0 is a CURATED floor like the gfx 1.28 one: it + // is the core bgfx 0.20.0 was released against (its own build.zig.zon), + // the only core on which the pixel_water effect the default backend + // ships is reachable, and the pairing labelle-bgfx's own examples run + // on. A `labelle init --backend=bgfx` scaffold should not pair a backend + // with a core older than the backend's own pin. `src/init_cmd.zig`'s + // "scaffold core default pairs with the builtin bgfx provider" test + // asserts both floors. // Bump all three together when moving the engine default (their // build.zig.zon pins/floors must agree — read them from the tags), and // keep `src/init_cmd.zig`'s "scaffold pins a MUTUALLY COMPATIBLE trio" // test satisfied — it encodes the floors below as assertions. - const core_version: []const u8 = b.option([]const u8, "core_version", "Default core library version") orelse "1.28.0"; + const core_version: []const u8 = b.option([]const u8, "core_version", "Default core library version") orelse "1.32.0"; const engine_version: []const u8 = b.option([]const u8, "engine_version", "Default engine library version") orelse "2.12.2"; const gfx_version: []const u8 = b.option([]const u8, "gfx_version", "Default gfx library version") orelse "1.30.1"; // Version this assembler binary stamps into a freshly scaffolded diff --git a/docs/showcase-plan.md b/docs/showcase-plan.md index e190b7b8..297fa976 100644 --- a/docs/showcase-plan.md +++ b/docs/showcase-plan.md @@ -89,10 +89,13 @@ generate+build CI assertion above. ### Games 6–9 (labelle-assembler#611) — released runtime, in-tree assembler -Games 6–9 pin the **released RUNTIME** package set (core 1.26.0 / engine -2.6.0 / gfx 1.28.1 / cli 1.58.0) + released backends, but — like every -sibling example in this repo — pin the **assembler at `local:../../`** (the -in-tree source). Two reasons the assembler is in-tree, not a release +Games 6–9 pin a **released RUNTIME** package set + released backends — +tile-explorer and sprite-runner on core 1.26.0 / engine 2.6.0 / gfx 1.28.1, +the two bgfx games (pack-city, material-demo) on core 1.32.0 / engine 2.12.2 +/ gfx 1.30.1 because their bgfx 0.20.0 pin has a hard core floor of 1.28.0 +and was released against 1.32.0 (see their `project.labelle`), all on cli +1.58.0 — but, like every sibling example in this repo, pin the **assembler +at `local:../../`** (the in-tree source). Two reasons the assembler is in-tree, not a release number: 1. `examples/` exist to validate the assembler *under test*. Numbered diff --git a/examples/material-demo/README.md b/examples/material-demo/README.md index 47576170..5f99484e 100644 --- a/examples/material-demo/README.md +++ b/examples/material-demo/README.md @@ -44,16 +44,16 @@ sources. ## Scope note — the two halves of gfx#305 -gfx#305 shipped TWO surfaces. This demo exercises the **post-fx** half, -which is wired end-to-end for games in the released engine (2.6.0). The -**per-entity material** half (`palette_swap` / `flash` / `dissolve` / -`outline`) is plumbed through core + gfx (`SpriteVisual.material`) but the -released engine `2.6.0` exposes **no game-facing authoring surface** for -it — there is no `Sprite.material` field and no `setMaterial` on the game, -so a game cannot drive per-entity materials yet. That engine bridge is the -tracked follow-up ([labelle-engine#789](https://github.com/labelle-toolkit/labelle-engine/issues/789)); -when it lands, this demo grows a second scene that flashes / dissolves -individual orbs. +gfx#305 shipped TWO surfaces. This demo exercises the **post-fx** half +only, wired end-to-end for games since engine 2.6.0 (the demo's original +pin). The **per-entity material** half (`palette_swap` / `flash` / +`dissolve` / `outline`) is plumbed through core + gfx +(`SpriteVisual.material`), and the engine now pinned here (`2.12.2`) does +expose `game.setMaterial(entity, material)` (`src/game/visuals.zig`) — it +was `2.6.0` that had no game-facing authoring surface for it. This demo +still does **not** drive per-entity materials; the second scene that +flashes / dissolves individual orbs is the tracked follow-up +([labelle-engine#789](https://github.com/labelle-toolkit/labelle-engine/issues/789)). Files: @@ -70,19 +70,28 @@ The **runtime** packages + bgfx backend pin the **released** set; the see the pins note in `tile-explorer/README.md`): ```zig -.core_version = "1.26.0", .engine_version = "2.6.0", .gfx_version = "1.28.1", +.core_version = "1.32.0", .engine_version = "2.12.2", .gfx_version = "1.30.1", .labelle_version = "1.58.0", .assembler_version = "local:../../", .backend_package = .{ .name = "bgfx", … .version = "0.20.0" }, ``` -> **Backend pin bumped to `0.20.0` (labelle-core v1.32.0).** That core -> appended `MaterialEffect.pixel_water`; every bgfx release before -> `0.20.0` switches exhaustively over that enum with no arm for the new -> tag, so it stops compiling against it. `0.20.0` implements the effect -> behind `@hasField(MaterialEffect, "pixel_water")` probes and builds -> against old and new core alike. CI clones `labelle-core` at `main` as a -> sibling checkout and the assembler builds that sibling in preference to -> the `.core_version` pin above, so the old backend pin failed there. +> **Backend pin `0.20.0`, and the runtime trio raised with it.** +> labelle-core v1.32.0 appended `MaterialEffect.pixel_water`; every bgfx +> release before `0.20.0` switches exhaustively over that enum with no arm +> for the new tag, so it stops compiling against that core — and CI clones +> `labelle-core` at `main` as a sibling checkout that the assembler builds +> in preference to the `.core_version` pin. `0.20.0` implements the effect +> behind `@hasField(MaterialEffect, "pixel_water")` probes. +> +> The core pin follows the backend. bgfx `0.20.0` has a **hard** core floor +> of `1.28.0` (`core.BackendTextureId`, a struct field type), so the earlier +> `core_version = "1.26.0"` pairing (#731) failed a **standalone** `labelle +> build` inside the backend (`labelle-bgfx/src/gfx/types.zig:17:38: … no +> member named 'BackendTextureId'`) — CI never saw it because the sibling +> core is `main` and the bgfx games are generate-only there (#732). The pin +> is `1.32.0`, the core bgfx `0.20.0` was released against and the one the +> curated `labelle init` trio carries; the trio here is that curated set. +> `project.labelle` spells out the reasoning. On the fully-released `labelle` path this game builds on assembler `0.94.0` (bgfx takes the generic desktop `unifyCoreDiamond` codegen, diff --git a/examples/material-demo/project.labelle b/examples/material-demo/project.labelle index ca108a45..f32efb02 100644 --- a/examples/material-demo/project.labelle +++ b/examples/material-demo/project.labelle @@ -13,27 +13,53 @@ // in a single screenshot. // // SCOPE NOTE (precise): gfx#305 has TWO halves. This demo exercises the -// POST-FX half, which is wired end-to-end for games in the released engine -// (2.6.0). The PER-ENTITY MATERIAL half (palette_swap / flash / dissolve / -// outline) is plumbed through core + gfx (`SpriteVisual.material`) but the -// released engine 2.6.0 exposes NO game-facing authoring surface for it -// (no `Sprite.material`, no `setMaterial`) — so a game cannot drive -// per-entity materials yet. Tracked as the material-demo follow-up; see -// README.md. +// POST-FX half only. The PER-ENTITY MATERIAL half (palette_swap / flash / +// dissolve / outline) is plumbed through core + gfx (`SpriteVisual.material`) +// and the pinned engine 2.12.2 DOES expose `game.setMaterial(entity, m)` +// (labelle-engine `src/game/visuals.zig`) — it was engine 2.6.0, this +// demo's original pin, that had no authoring surface. The demo still does +// not drive per-entity materials; that is the follow-up in README.md. // // VERSION PINS: the RUNTIME packages (core/engine/gfx/cli) + the backend // pin the RELEASED set — this game proves the released runtime path (see -// tile-explorer's note). bgfx is pinned explicitly to 0.20.0: 0.13.1 first -// carried the post-fx shaders this demo needs (the registry enum default -// predates them), but labelle-core 1.32.0 appended +// tile-explorer's note). bgfx is pinned explicitly (the registry enum +// default predates the post-fx shaders this demo needs, which 0.13.1 first +// carried), and that pin is 0.20.0: labelle-core 1.32.0 appended // `MaterialEffect.pixel_water` and every bgfx release before 0.20.0 // switches EXHAUSTIVELY over that enum with no arm for it. CI clones -// labelle-core at `main` as a sibling checkout and the assembler's monorepo -// auto-discovery builds that sibling INSTEAD of the `.core_version` pin -// below, so a pre-0.20.0 bgfx compiles against the NEW core and fails sema. -// 0.20.0 IMPLEMENTS pixel_water behind a -// `@hasField(MaterialEffect, "pixel_water")` gate, so it builds against -// both the pinned 1.26.0 and core `main`. +// labelle-core at `main` as a sibling checkout and the assembler's +// monorepo auto-discovery builds that sibling INSTEAD of the +// `.core_version` pin below, so a pre-0.20.0 bgfx failed sema there. +// +// The core pin FOLLOWS the backend pin, not the other way round. The +// assembler unifies every package onto `.core_version`, and released bgfx +// 0.20.0 has a HARD core floor of 1.28.0: `src/gfx/types.zig` types a +// struct field as `core.BackendTextureId` (core#328 phase 3, bgfx >= +// 0.15.0), which is analyzed eagerly. So on a STANDALONE checkout — no +// sibling labelle-core, i.e. the documented `labelle build` path — bgfx +// 0.20.0 over the previous core 1.26.0 pin died inside the backend: +// labelle-bgfx/src/gfx/types.zig:17:38: error: root source file struct +// 'root' has no member named 'BackendTextureId' +// #731 shipped that pairing claiming it "builds against the pinned +// 1.26.0"; CI never saw it because the sibling core at `main` is >= 1.32.0 +// AND the bgfx games are generate-only there (labelle-assembler#732). The +// v1.32.0 names bgfx 0.20.0 takes from `backend_contract` (`PixelWaterDraw`, +// `PIXEL_WATER_*`) are NOT a compile floor: they are reached only through +// `@hasField(MaterialEffect, "pixel_water")`-gated paths or lazily-analyzed +// top-level aliases, so 0.20.0 does compile against core 1.28.0 (proven +// standalone with a `labelle init --backend=bgfx` scaffold). +// The pin is 1.32.0 anyway: the core labelle-bgfx 0.20.0 was released +// against (its own build.zig.zon), the one on which the pixel_water effect +// the backend ships is reachable at all, and the core the curated `labelle +// init` trio now carries — so this demo and a fresh scaffold prove ONE +// pairing (core 1.32.0 / engine 2.12.2 / gfx 1.30.1), at or above +// labelle-bgfx's own `examples/bgfx-android` pins (core 1.32.0 / engine +// 2.11.0 / gfx 1.30.0). It satisfies every trio floor in build.zig / +// `src/init_cmd.zig:checkTrioFloors` (engine >= 2.11.0 → core >= 1.27.0 + +// gfx >= 1.28.0; gfx >= 1.30.0 → core >= 1.28.0 + engine >= 2.12.1). +// Verified STANDALONE (scratch LABELLE_HOME, registry tarballs at these +// exact tags, no sibling checkouts): install → generate → full `zig build`, +// exit 0. // The ASSEMBLER is `local:../../` (in-tree source, the examples // convention). On the fully-RELEASED `labelle` path this game builds on // assembler 0.94.0 — bgfx takes the generic desktop `unifyCoreDiamond` @@ -70,9 +96,9 @@ // Subtle CRT: slight curve + scanlines + shadow mask. .{ .crt = .{ .curvature = 0.08, .scanline = 0.35, .mask = 0.25, .aberration = 0.12 } }, }, - .core_version = "1.26.0", - .engine_version = "2.6.0", - .gfx_version = "1.28.1", + .core_version = "1.32.0", + .engine_version = "2.12.2", + .gfx_version = "1.30.1", .labelle_version = "1.58.0", // In-tree assembler (examples convention). Fully-released path: 0.94.0. .assembler_version = "local:../../", diff --git a/examples/pack-city/README.md b/examples/pack-city/README.md index 332f528f..142e0e4d 100644 --- a/examples/pack-city/README.md +++ b/examples/pack-city/README.md @@ -53,19 +53,28 @@ to the current release, which carries the material/post-fx work; the registry enum default predates it: ```zig -.core_version = "1.26.0", .engine_version = "2.6.0", .gfx_version = "1.28.1", +.core_version = "1.32.0", .engine_version = "2.12.2", .gfx_version = "1.30.1", .labelle_version = "1.58.0", .assembler_version = "local:../../", .backend_package = .{ .name = "bgfx", … .version = "0.20.0" }, ``` -> **Backend pin bumped to `0.20.0` (labelle-core v1.32.0).** That core -> appended `MaterialEffect.pixel_water`; every bgfx release before -> `0.20.0` switches exhaustively over that enum with no arm for the new -> tag, so it stops compiling against it. `0.20.0` implements the effect -> behind `@hasField(MaterialEffect, "pixel_water")` probes and builds -> against old and new core alike. CI clones `labelle-core` at `main` as a -> sibling checkout and the assembler builds that sibling in preference to -> the `.core_version` pin above, so the old backend pin failed there. +> **Backend pin `0.20.0`, and the runtime trio raised with it.** +> labelle-core v1.32.0 appended `MaterialEffect.pixel_water`; every bgfx +> release before `0.20.0` switches exhaustively over that enum with no arm +> for the new tag, so it stops compiling against that core — and CI clones +> `labelle-core` at `main` as a sibling checkout that the assembler builds +> in preference to the `.core_version` pin. `0.20.0` implements the effect +> behind `@hasField(MaterialEffect, "pixel_water")` probes. +> +> The core pin follows the backend. bgfx `0.20.0` has a **hard** core floor +> of `1.28.0` (`core.BackendTextureId`, a struct field type), so the earlier +> `core_version = "1.26.0"` pairing (#731) failed a **standalone** `labelle +> build` inside the backend (`labelle-bgfx/src/gfx/types.zig:17:38: … no +> member named 'BackendTextureId'`) — CI never saw it because the sibling +> core is `main` and the bgfx games are generate-only there (#732). The pin +> is `1.32.0`, the core bgfx `0.20.0` was released against and the one the +> curated `labelle init` trio carries; the trio here is that curated set. +> `project.labelle` spells out the reasoning. On the fully-released `labelle` path this game builds on assembler `0.94.0` (bgfx takes the generic desktop `unifyCoreDiamond` codegen, diff --git a/examples/pack-city/project.labelle b/examples/pack-city/project.labelle index 59a7bc38..9c307bd5 100644 --- a/examples/pack-city/project.labelle +++ b/examples/pack-city/project.labelle @@ -18,11 +18,35 @@ // every bgfx release before 0.20.0 switches EXHAUSTIVELY over that enum // with no arm for it. CI clones labelle-core at `main` as a sibling // checkout and the assembler's monorepo auto-discovery builds that sibling -// INSTEAD of the `.core_version` pin below, so a pre-0.20.0 bgfx compiles -// against the NEW core and fails sema. 0.20.0 IMPLEMENTS the effect behind -// a `@hasField(MaterialEffect, "pixel_water")` gate, so it builds against -// both the pinned 1.26.0 and core `main`. The ASSEMBLER is -// `local:../../` (in-tree source, the examples convention). On the +// INSTEAD of the `.core_version` pin below, so a pre-0.20.0 bgfx failed +// sema there. +// +// The core pin FOLLOWS the backend pin, not the other way round. The +// assembler unifies every package onto `.core_version`, and released bgfx +// 0.20.0 has a HARD core floor of 1.28.0: `src/gfx/types.zig` types a +// struct field as `core.BackendTextureId` (core#328 phase 3, bgfx >= +// 0.15.0), which is analyzed eagerly. So on a STANDALONE checkout — no +// sibling labelle-core, i.e. the documented `labelle build` path — bgfx +// 0.20.0 over the previous core 1.26.0 pin died inside the backend: +// labelle-bgfx/src/gfx/types.zig:17:38: error: root source file struct +// 'root' has no member named 'BackendTextureId' +// #731 shipped that pairing claiming it "builds against the pinned +// 1.26.0"; CI never saw it because the sibling core at `main` is >= 1.32.0 +// AND the bgfx games are generate-only there (labelle-assembler#732). The +// v1.32.0 names bgfx 0.20.0 takes from `backend_contract` (`PixelWaterDraw`, +// `PIXEL_WATER_*`) are NOT a compile floor — they sit behind +// `@hasField(MaterialEffect, "pixel_water")`-gated paths or lazy top-level +// aliases, and 0.20.0 does compile against core 1.28.0 — but the pin is +// 1.32.0 anyway: the core labelle-bgfx 0.20.0 was released against (its +// own build.zig.zon), the one on which its pixel_water effect is reachable +// at all, and the core the curated `labelle init` trio now carries. So this +// game and a fresh scaffold prove ONE pairing (core 1.32.0 / engine 2.12.2 +// / gfx 1.30.1), at or above labelle-bgfx's own `examples/bgfx-android` +// pins (core 1.32.0 / engine 2.11.0 / gfx 1.30.0), satisfying every floor +// in build.zig / `src/init_cmd.zig:checkTrioFloors`. Verified STANDALONE +// (scratch LABELLE_HOME, registry tarballs at these exact tags, no sibling +// checkouts): install → generate → full `zig build`, exit 0. The ASSEMBLER +// is `local:../../` (in-tree source, the examples convention). On the // fully-RELEASED `labelle` path this game builds on assembler 0.94.0 — // bgfx takes the generic desktop `unifyCoreDiamond` codegen, unaffected by // the sokol-desktop byte-anchor gap #611 fixes. @@ -48,9 +72,9 @@ .{ .name = "buildings", .repo = "@packs/buildings" }, .{ .name = "traffic", .repo = "@packs/traffic" }, }, - .core_version = "1.26.0", - .engine_version = "2.6.0", - .gfx_version = "1.28.1", + .core_version = "1.32.0", + .engine_version = "2.12.2", + .gfx_version = "1.30.1", .labelle_version = "1.58.0", // In-tree assembler (examples convention). Fully-released path: 0.94.0. .assembler_version = "local:../../", diff --git a/src/config.zig b/src/config.zig index d9f3d4b7..86328789 100644 --- a/src/config.zig +++ b/src/config.zig @@ -1222,7 +1222,7 @@ pub const ProjectConfig = struct { /// just a *shorthand* for a provider. `.backend = .` transparently /// resolves to the fetched package (`isExternal()` ⇒ true) with no /// project-config change. - fn builtinProvider(backend: Backend) ?PluginDep { + pub fn builtinProvider(backend: Backend) ?PluginDep { return switch (backend) { // Extracted out-of-tree (#386 Phase 6c) — `.backend = .` resolves // to the provider package, not a bundled slot. @@ -1254,13 +1254,23 @@ pub const ProjectConfig = struct { // FLOOR NOTE: this is a LATEST-as-lowest-fixed bump, not a bump for // its own sake — there is no older bgfx with the fix, because the // fix shipped the same day as the core release. It does raise the - // implicit runtime floor for a project that takes this default: - // bgfx >= 0.15.0 consumes core's typed `BackendTextureId` - // (core#328 phase 3, core >= v1.28.0), and — unlike labelle-sokol, - // which comptime-gates every v1.32.0 decl it names — bgfx names - // `PixelWaterDraw` / `PIXEL_WATER_*` from `backend_contract` - // ungated, so pair this default with core >= v1.32.0. A project - // pinned to an older core that must stay there should pin + // implicit runtime floor for a project that takes this default. + // The HARD floor is core >= v1.28.0: bgfx >= 0.15.0 types a struct + // field as core's `BackendTextureId` (core#328 phase 3), analyzed + // eagerly, so an older core fails inside the backend ("root source + // file struct 'root' has no member named 'BackendTextureId'" — + // how the #731 examples on core 1.26.0 failed standalone). The + // v1.32.0 `PixelWaterDraw` / `PIXEL_WATER_*` names bgfx takes from + // `backend_contract` are NOT a compile floor: unlike labelle-sokol, + // which comptime-gates each decl at its use, bgfx aliases them at + // top level — but every path that reaches them sits behind + // `@hasField(MaterialEffect, "pixel_water")` and an unreferenced + // alias is never analyzed, so 0.20.0 compiles against core 1.28.0 + // (verified standalone). The CURATED pairing is still core >= + // v1.32.0: the core 0.20.0 was released against and the only one + // on which its pixel_water effect is reachable. `build.zig`'s + // scaffold trio carries it and `src/init_cmd.zig` asserts it. A + // project pinned to an older core that must stay there should pin // `.backend_package` explicitly rather than ride the default. .bgfx => .{ .name = "bgfx", .repo = "github.com/labelle-toolkit/labelle-bgfx", .version = "0.20.0" }, .wgpu => .{ .name = "wgpu", .repo = "github.com/labelle-toolkit/labelle-wgpu", .version = "0.3.0" }, diff --git a/src/init_cmd.zig b/src/init_cmd.zig index 02d9ab30..646c6358 100644 --- a/src/init_cmd.zig +++ b/src/init_cmd.zig @@ -115,9 +115,166 @@ pub fn cmdInit(allocator: std.mem.Allocator, io: std.Io, args: *std.process.Args std.process.exit(2); }; + // #736 review: refuse a backend/core pairing that cannot compile BEFORE + // a single file is written. Only the hard floor is fatal; a curated + // floor warns and scaffolds, since the user asked for that core + // explicitly and it does build. The version default never trips this + // (the tests pin `config.CORE_VERSION` against the real provider). + if (backendCoreFloorViolation(opts.backend, opts.core_version) catch |err| { + std.log.err("labelle-assembler init: --core-version={s}: {s}", .{ opts.core_version, @errorName(err) }); + std.process.exit(2); + }) |v| { + var buf: [512]u8 = undefined; + switch (v.severity) { + .compile_break => { + std.log.err("labelle-assembler init: {s}", .{v.describe(&buf)}); + std.process.exit(2); + }, + .curated => std.log.warn("labelle-assembler init: {s}", .{v.describe(&buf)}), + } + } + try scaffold(allocator, io, opts); } +// ── backend ⇄ core floor validation ────────────────────────────────── +// +// PRODUCTION check, not a test helper (#736 review): `checkTrioFloors` +// below guards the DEFAULTS `build.zig` bakes in, but `init` also takes +// `--backend=` and `--core-version=` from the user, and nothing stopped +// `labelle init --backend=bgfx --core-version=1.26.0` from writing a +// project that dies inside the backend at the first `labelle build` — the +// exact standalone failure #731 shipped in its examples, reachable through +// the front door. The table is the ONE place the bgfx floors live; the +// test helper further down exercises this same code, so the two cannot +// drift. + +const FloorSeverity = enum { + /// An older core fails to COMPILE inside the backend. + compile_break, + /// The pairing builds, but is not the one the backend was released + /// against — the scaffold default never picks it, an explicit + /// `--core-version` gets a warning. + curated, +}; + +/// One floor the builtin bgfx provider puts on the project's core. +const BackendCoreFloor = struct { + backend_at_least: []const u8, + core_at_least: []const u8, + severity: FloorSeverity, + /// Why, in the words the diagnostic prints. + why: []const u8, +}; + +/// Strictest first. `providerCoreFloorViolation` reports the hard floor +/// when one is violated (that is the actionable one), else the strictest +/// curated floor; either way the diagnostic recommends the strictest core. +/// +/// bgfx >= 0.15.0 floors core >= 1.28.0 — a COMPILE break: the backend's +/// `src/gfx/types.zig` types a struct field as `core.BackendTextureId` +/// (core#328 phase 3), which is analyzed eagerly, so an older core dies +/// inside the backend with "root source file struct 'root' has no member +/// named 'BackendTextureId'". That is exactly how #731's examples (core +/// 1.26.0 + bgfx 0.20.0) failed a standalone `labelle build`. +/// +/// bgfx >= 0.20.0 floors core >= 1.32.0 — a CURATED floor, like the gfx +/// 1.28 one in `checkTrioFloors`: 0.20.0 does compile against core 1.28.0 +/// (its v1.32.0 `PixelWaterDraw` / `PIXEL_WATER_*` names are reached only +/// through `@hasField(MaterialEffect, "pixel_water")`-gated paths or lazy +/// top-level aliases — verified standalone), but 1.32.0 is the core it was +/// released against (its build.zig.zon) and the only core on which the +/// pixel_water effect the default backend ships is reachable. The scaffold +/// pairs the default backend with the core it was released against, not +/// merely one it happens to compile on. +const bgfx_core_floors = [_]BackendCoreFloor{ + .{ + .backend_at_least = "0.20.0", + .core_at_least = "1.32.0", + .severity = .curated, + .why = "its pixel_water effect is unreachable on an older core", + }, + .{ + .backend_at_least = "0.15.0", + .core_at_least = "1.28.0", + .severity = .compile_break, + .why = "the backend types a struct field as `core.BackendTextureId`, which an older core lacks — it fails to compile", + }, +}; + +/// A backend/core pairing that violates a floor, with everything the +/// user-facing diagnostic names. +pub const FloorViolation = struct { + backend: []const u8, + backend_version: []const u8, + core_version: []const u8, + core_floor: []const u8, + /// The strictest floor for this backend — what to pass instead. + recommended_core: []const u8, + severity: FloorSeverity, + why: []const u8, + + /// The diagnostic, rendered into `buf`. Names the backend and its real + /// provider version, the floor violated, the core requested, why, and + /// the core to pass instead. + pub fn describe(self: FloorViolation, buf: []u8) []const u8 { + // "requires" only when it is a compile break; a curated floor that + // still builds says what it is, so the warning does not contradict + // the scaffold that follows it. + const verb: []const u8 = switch (self.severity) { + .compile_break => "requires", + .curated => "is released against", + }; + return std.fmt.bufPrint( + buf, + "{s} {s} {s} labelle-core >= {s}; got {s} ({s}). Pass --core-version={s}, the core it was released against.", + .{ self.backend, self.backend_version, verb, self.core_floor, self.core_version, self.why, self.recommended_core }, + ) catch "backend/core version floor violated (diagnostic too long to render)"; + } +}; + +/// The floor the BUILTIN provider for `backend_name` puts on `core_version`, +/// or null when the pairing is fine. Reads the real `builtinProvider` +/// default, so a provider bump moves the check with it. A backend name the +/// enum does not know, a backend with no versioned provider, and a core pin +/// that is not a release version (`local:…`, resolved elsewhere) are all +/// "fine" here — they are other checks' business. +pub fn backendCoreFloorViolation(backend_name: []const u8, core_version: []const u8) error{UnparsableVersionPin}!?FloorViolation { + const backend = std.meta.stringToEnum(config.Backend, backend_name) orelse return null; + const provider = config.ProjectConfig.builtinProvider(backend) orelse return null; + return providerCoreFloorViolation(backend, provider.version, core_version); +} + +/// `backendCoreFloorViolation` with the provider version supplied — the +/// seam the table tests use, so the floors are exercised by exactly the +/// code `init` runs. +fn providerCoreFloorViolation(backend: config.Backend, backend_version: []const u8, core_version: []const u8) error{UnparsableVersionPin}!?FloorViolation { + const floors: []const BackendCoreFloor = switch (backend) { + .bgfx => &bgfx_core_floors, + else => return null, + }; + if (!config.isSemverVersion(core_version)) return null; + + var worst: ?FloorViolation = null; + for (floors) |f| { + if (!try pinAtLeast(backend_version, f.backend_at_least)) continue; + if (try pinAtLeast(core_version, f.core_at_least)) continue; + const v: FloorViolation = .{ + .backend = @tagName(backend), + .backend_version = backend_version, + .core_version = core_version, + .core_floor = f.core_at_least, + .recommended_core = floors[0].core_at_least, + .severity = f.severity, + .why = f.why, + }; + // A compile break beats a curated floor; among equals, keep the + // strictest (first). + if (worst == null or (v.severity == .compile_break and worst.?.severity != .compile_break)) worst = v; + } + return worst; +} + /// Materialize a new project directory from `opts`. Exits the process /// non-zero on a filesystem failure (the CLI propagates the exit code). /// Split out from `cmdInit` so it can be unit-tested without building an @@ -493,7 +650,8 @@ test "curated-trio floors reject each incoherent combination — #683 review" { // The floors themselves, against synthetic trios: the test above can // only ever see whatever `build.zig` defaults to today, so without // these a floor could be silently dropped and still go green. - try checkTrioFloors("1.28.0", "2.12.2", "1.30.1"); // the current curated set + try checkTrioFloors("1.32.0", "2.12.2", "1.30.1"); // the current curated set + try checkTrioFloors("1.28.0", "2.12.2", "1.30.1"); // the pre-#731 curated set — still coherent // gfx >= 1.30.0 needs core >= 1.28.0 and engine >= 2.12.1. try std.testing.expectError(error.TestUnexpectedResult, checkTrioFloors("1.27.0", "2.12.2", "1.30.1")); @@ -508,6 +666,77 @@ test "curated-trio floors reject each incoherent combination — #683 review" { try checkTrioFloors("1.24.0", "2.7.0", "1.27.0"); } +/// The builtin backend providers are curated WITH the trio (#731 review): +/// a `labelle init --backend=bgfx` scaffold pairs `src/config.zig`'s bgfx +/// default with the core default above. The floors themselves live in +/// `bgfx_core_floors` (see the rationale there); this helper is a thin +/// test-shaped wrapper over the PRODUCTION validator `init` runs, so the +/// table the tests pin is the table the user hits (#736 review). +fn checkBgfxProviderFloors(core_version: []const u8, bgfx_version: []const u8) !void { + if (try providerCoreFloorViolation(.bgfx, bgfx_version, core_version)) |_| return error.TestUnexpectedResult; +} + +test "scaffold core default pairs with the builtin bgfx provider — #731 review" { + // Read the REAL default from `builtinProvider`, not a copy of its version + // string, so a provider bump that forgets the trio fails here. + const bgfx = config.ProjectConfig.builtinProvider(.bgfx) orelse return error.TestUnexpectedResult; + try std.testing.expect(config.isSemverVersion(bgfx.version)); + try checkBgfxProviderFloors(config.CORE_VERSION, bgfx.version); +} + +test "bgfx-provider floors reject the #731 pairings" { + try checkBgfxProviderFloors("1.32.0", "0.20.0"); // the curated pairing + // What #731 shipped: the bgfx 0.20.0 default over the core 1.28.0 scaffold default. + try std.testing.expectError(error.TestUnexpectedResult, checkBgfxProviderFloors("1.28.0", "0.20.0")); + // What #731's examples pinned: bgfx 0.20.0 over core 1.26.0 — the compile break. + try std.testing.expectError(error.TestUnexpectedResult, checkBgfxProviderFloors("1.26.0", "0.20.0")); + try std.testing.expectError(error.TestUnexpectedResult, checkBgfxProviderFloors("1.26.0", "0.15.0")); + // Below every floor, nothing is asserted — old coherent pairings stay legal. + try checkBgfxProviderFloors("1.26.0", "0.13.1"); +} + +test "init refuses --backend=bgfx --core-version=1.26.0 with a named diagnostic, accepts 1.32.0 — #736 review" { + const bgfx = config.ProjectConfig.builtinProvider(.bgfx) orelse return error.TestUnexpectedResult; + var buf: [512]u8 = undefined; + var want_buf: [128]u8 = undefined; + + // The front-door failure: the hard floor is the one reported (it is the + // actionable one), the diagnostic names backend + REAL provider version, + // the floor, the requested core, and recommends the curated core. + const v = (try backendCoreFloorViolation("bgfx", "1.26.0")) orelse return error.TestUnexpectedResult; + try std.testing.expectEqual(FloorSeverity.compile_break, v.severity); + try std.testing.expectEqualStrings("1.28.0", v.core_floor); + try std.testing.expectEqualStrings("1.32.0", v.recommended_core); + const msg = v.describe(&buf); + const want = try std.fmt.bufPrint(&want_buf, "bgfx {s} requires labelle-core >= 1.28.0; got 1.26.0 (", .{bgfx.version}); + try std.testing.expect(std.mem.startsWith(u8, msg, want)); + try std.testing.expect(std.mem.indexOf(u8, msg, "BackendTextureId") != null); + try std.testing.expect(std.mem.indexOf(u8, msg, "--core-version=1.32.0") != null); + + // What #731's scaffold stamped: builds, so it is a WARNING, not a refusal. + const c = (try backendCoreFloorViolation("bgfx", "1.28.0")) orelse return error.TestUnexpectedResult; + try std.testing.expectEqual(FloorSeverity.curated, c.severity); + try std.testing.expectEqualStrings("1.32.0", c.core_floor); + const cmsg = c.describe(&buf); + try std.testing.expect(std.mem.indexOf(u8, cmsg, "is released against labelle-core >= 1.32.0; got 1.28.0") != null); + try std.testing.expect(std.mem.indexOf(u8, cmsg, "requires") == null); + + // Accepted: the curated core, the scaffold's own default, and anything newer. + try std.testing.expect((try backendCoreFloorViolation("bgfx", "1.32.0")) == null); + try std.testing.expect((try backendCoreFloorViolation("bgfx", config.CORE_VERSION)) == null); + try std.testing.expect((try backendCoreFloorViolation("bgfx", "1.40")) == null); + + // Not this validator's business: other backends, a non-release core pin, + // a backend name the enum does not know (init's own parser rejects it). + try std.testing.expect((try backendCoreFloorViolation("raylib", "1.26.0")) == null); + try std.testing.expect((try backendCoreFloorViolation("null", "1.0.0")) == null); + try std.testing.expect((try backendCoreFloorViolation("bgfx", "local:../labelle-core")) == null); + try std.testing.expect((try backendCoreFloorViolation("klingon", "1.0.0")) == null); + + // A dotted-but-unparsable core pin surfaces as the named error, not a crash. + try std.testing.expectError(error.UnparsableVersionPin, backendCoreFloorViolation("bgfx", "1.2.3.4")); +} + test "pinAtLeast normalizes the abbreviated `X.Y` pin form, and names a bad one — #683 review" { // `config.isSemverVersion` admits both of these, so `zig build test // -Dgfx_version=1.30` used to reach `catch unreachable` and crash with