fix(ios): give the generator a runtimeDir option, and run its tests in CI - #108
Merged
Conversation
✅ Binary Size Report
Size limits
|
✅ Binary load timeWhat this measures
Both binaries are measured interleaved on this runner and compared by |
…n CI Four findings from the seam review, all reproduced before being fixed. **The suite depended on the developer's shell.** `init()` read `CRAFT_IOS_RUNTIME` straight from `process.env`, with nothing to override it, so `bun test src/index.test.ts` gave 12 passing in a clean shell and 8 passing with 4 failing when the documented dev-loop variable happened to be exported. `InitOptions.runtimeDir` is the escape hatch CLAUDE.md already describes for the sibling variable — `AppConfig.craftPath` is to `CRAFT_BIN` what this is to `CRAFT_IOS_RUNTIME` — and `null` means "no runtime, whatever the environment says". Every `init()` in the suite now says so, and the result is identical with the variable set and unset. **And nothing ran that suite.** No CI job touched `packages/ios`, which is how the above survived. `ios-builder` runs it with `CRAFT_IOS_RUNTIME` explicitly empty — the job that would notice if the dependency came back — and `publish-commit` gates on it. **`installRuntime` destroyed a working install before validating the source.** `rmSync(Runtime/)` ran first and the per-SDK "none of these archives" check ran after, so pointing at an incomplete runtime directory deleted the archives already in place on the way to throwing, leaving `project.yml` linking `-lcraft-ios` against a directory that no longer existed. Every SDK is resolved before anything is removed. **A single simulator slice was copied silently.** `RUNTIME_ARCHIVES`' own comment calls a single-slice archive the break that "only shows up on someone else's laptop", and then the code shipped one without a word. It still copies — a one-architecture dev loop is legitimate — but it says which slice is missing and that the project will not link on the other architecture. **Re-running init without a runtime left the archives behind.** The link settings came out of `project.yml` and `Runtime/` stayed on disk, so the directory and the project disagreed. Whatever a run decides, the tree now agrees with it. The two-slice `lipo` path is not unit-tested: it needs genuine Mach-O input and `lipo` does not exist on the Linux runner this job uses. It is covered by building a real generated app, which is how the fat archive was verified when the seam landed.
…king a stale copy `installRuntime` had exactly one caller: `init`. `build` never touched `Runtime/`, and `run` just calls `build`. Since the archives are copied rather than symlinked, the monorepo dev loop this whole option exists for did not work: edit `packages/zig/src`, run `zig build build-ios-all`, then `craft ios run`, and xcodebuild relinks the archive copied when the project was first generated. The build succeeds, the app launches, and the change is absent — the worst shape a stale artefact takes, because there is no error to chase. `build` now refreshes a runtime the project already links, taking the same `runtimeDir` override `init` does. Refresh only: never installs, never removes. A project with no `Runtime/` was generated without one and its `project.yml` carries no link settings, so copying archives in would leave them unreferenced — that decision is `init`'s. A project that does link one keeps the archives it has when no runtime directory is configured, because a shell that forgot the variable should not quietly turn the runtime off. Both are pinned by tests.
chrisbbreuer
force-pushed
the
fix/ios-generator-runtime-dir
branch
from
September 3, 2026 18:30
b59f0b3 to
19d071f
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Four findings from the #103 seam review, each reproduced before being fixed.
The suite depended on the developer's shell
init()readCRAFT_IOS_RUNTIMEstraight fromprocess.envwith no way to override it:CRAFT_IOS_RUNTIMEbun test src/index.test.tsInitOptions.runtimeDiris the escape hatch CLAUDE.md already documents for the sibling variable —AppConfig.craftPathis toCRAFT_BINwhat this is toCRAFT_IOS_RUNTIME— andnullmeans no runtime, whatever the environment says. Everyinit()in the suite now declares it, and the result is identical either way: 22 pass, 0 fail.And nothing ran that suite
No CI job touched
packages/ios. That is how the above survived. The newios-builderjob runs it withCRAFT_IOS_RUNTIMEexplicitly empty — so it is the job that notices if the dependency returns — andpublish-commitgates on it.installRuntime destroyed a working install before validating the source
rmSync(Runtime/)ran first; the "none of these archives" check ran after. Pointing at an incomplete runtime directory deleted the archives already in place on the way to throwing, leavingproject.ymllinking-lcraft-iosagainst a directory that no longer existed. Every SDK is resolved before anything is removed, and there is a test that installs, then fails an install, then asserts the first one survived.A single simulator slice was copied silently
RUNTIME_ARCHIVES' own comment calls a single-slice archive the break that "only shows up on someone else's laptop" — and the code then shipped one without a word. It still copies, because a one-architecture dev loop is legitimate, but it now names the missing slice and says the project will not link on the other architecture.Re-running init without a runtime orphaned the archives
The link settings came out of
project.ymlandRuntime/stayed on disk, so the directory and the project disagreed. Whatever a run decides, the tree agrees with it.build() relinked a stale runtime
installRuntimehad one caller:init.buildnever touchedRuntime/, andrunjust callsbuild. Because the archives are copied rather than symlinked, the dev loop this option exists for did not work — edit Zig,zig build build-ios-all,craft ios run, and xcodebuild relinks the copy from init. The build succeeds, the app launches, the change is absent, and there is no error to chase.buildnow refreshes a runtime the project already links, taking the sameruntimeDiroverride. Refresh only: it never installs into a project that links none (those archives would be unreferenced — that isinit's decision) and never removes one when the variable is simply unset. Both pinned by tests.Not covered
The two-slice
lipopath needs genuine Mach-O input andlipodoes not exist on the Linux runner, so it is not unit-tested. It is covered by building a real generated app, which is how the fat archive was verified when the seam landed.