ci: probe the EBADARCH architectures, after the arch-flag hypothesis failed - #25
Shepdesign wants to merge 5 commits into
Conversation
The traced build was compiling arm64 + x86_64 while the tracer's libtrace.dylib is arm64 only. An arm64 library cannot be inserted into the x86_64 slice's compiler, so dyld refused it and everything that process then loaded died with EBADARCH — which is why #Preview stopped expanding inside KeyboardShortcuts and the build exited 65. Nothing in project.yml or the xcconfigs sets ARCHS, so the Release configurations take ARCHS_STANDARD. ONLY_ACTIVE_ARCH=YES on the two CodeQL xcodebuild calls leaves one architecture and nothing the tracer cannot host. Scoped to this workflow. ci.yml keeps building universal, because that is what ships and it is untraced, so it never had this problem. CodeQL loses nothing: the database holds Swift source, and the second architecture only compiled the same files twice. This is a hypothesis with a mechanism behind it, not a verified fix, and it cannot be verified anywhere but on the runner. Two things to watch on the first green run. The EBADARCH failure is intermittent — zero occurrences in one of the two runs that motivated this — so one pass does not prove the flag did anything. And a second, consistent failure sits underneath it: four "'self' is immutable" errors in KeyboardShortcuts/ViewModifiers.swift, present in both runs and only under tracing. If those were knock-on from the arch mismatch they go now; if they were independent they are what turns the job red next, and this changes nothing about them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UuHYhiqbmF4dT7sq8a6AYm
…tead ONLY_ACTIVE_ARCH=YES did exactly what it claimed — run 36093785096 compiled pure arm64, no x86_64 slice anywhere — and "Bad CPU type" still occurred. The flag is reverted rather than left in place looking like a fix. The theory behind it is dead on its own terms too: libtrace.dylib is a FAT binary carrying both x86_64 and arm64, read out of the 2.27.1 osx64 bundle's Mach-O fat header. It was never missing a slice for the build. I inferred that mismatch from a tool-cache path and a "SwiftCompile normal x86_64" line and never measured it, which is the same mistake as the 2.27.0 version claim. In exchange the run settled the other question. There is ONE blocker, not two: @State is itself a macro, the log names it failing to expand — external macro implementation type 'SwiftUIMacros.StateMacro' could not be found for macro 'State()'; compiler plugin '…/swift-plugin-server' could not be loaded: Bad CPU type in executable — and the "'self' is immutable" errors are what that looks like downstream. They need no separate fix and the banner no longer implies they might. What replaces the flag is a probe, not another guess: one step that prints lipo -archs for swift-plugin-server, swift-frontend and the inserted dylib. The remaining candidate is an architecture on neither list, arm64e being the obvious one, and that is measurable on the runner rather than arguable from here. continue-on-error, so it can never fail the job. The build command is byte-identical to main's again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UuHYhiqbmF4dT7sq8a6AYm
The probe measured the failure instead of inferring it, and it is neither arm64e nor a missing tracer slice: runner (uname -m): x86_64 <- the job's shell runs under Rosetta swift-plugin-server: arm64 <- arm64 ONLY swift-frontend: arm64 <- arm64 ONLY libtrace.dylib: x86_64 arm64 Xcode 27 dropped the x86_64 slices from its toolchain binaries, while the Actions runner's shell is still translated x86_64. Untraced this does not matter — the kernel picks the arm64 slice, which is why ci.yml has been green throughout. Under the tracer it does: DYLD_INSERT_LIBRARIES makes dyld hold a child to its parent's architecture so the inserted library matches, and an arm64-only helper has no x86_64 slice to be held to. Hence compiler plugin '.../swift-plugin-server' could not be loaded: Bad CPU type in executable and hence @State never expanding. Running xcodebuild through `arch -arm64` makes the whole tree arm64, which the arm64-only toolchain and the fat libtrace both satisfy. It also explains the intermittency that earlier notes called deterministic and then called intermittent without explaining: it depends which runner the job lands on. Every premise here was read off the runner rather than reasoned about, which is the difference between this and the two attempts before it. The step prints its parent architecture so the next reader can confirm the mechanism still holds rather than trusting this message. The probe stays for the same reason. Scoped to this workflow; ci.yml is untraced and unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UuHYhiqbmF4dT7sq8a6AYm
… worked
The arm64 parent fixed what it targeted. Controlled rather than hopeful: the
probe printed `runner: x86_64` in both runs, so the translated shell was
unchanged and the wrapper was the only difference.
c5d7d6a c2c0166 (arch -arm64)
Bad CPU type 2 0
could not be loaded 2 0
'self' is immutable 4 0
The macros expand, KeyboardShortcuts compiles, and the build reaches this
project's own target for the first time. EBADARCH is settled.
What is red now is a different failure, and in our code rather than a
dependency:
error: the following command failed with exit code 0 but produced no further
output
SwiftCompile normal arm64 (in target 'NetRelish' from project 'NetRelish')
Twice, once per flavour, surrounded by warnings only — no Swift error, no BUILD
FAILED line, no crash, and an exit code of 0 that xcodebuild treats as a failure
regardless. Cause not established, and recorded as not established.
So this adds an instrument rather than a fix: a failure-only step that dumps the
tracer log and the extractor's diagnostics, neither of which reaches the job log
because `analyze` is skipped when the build fails. Measuring first is what turned
the last round from two wrong guesses into a working change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UuHYhiqbmF4dT7sq8a6AYm
I reported this as fixed one commit ago. It was not. It was a fail-open, and
the only reason it did not ship as a green CodeQL job with an empty database is
that `database finalize` refuses one:
Error: CodeQL could not process any code written in Swift.
/usr/bin/arch is SIP-protected and dyld purges DYLD_* across a restricted exec,
so `arch -arm64 xcodebuild` ran with no libtrace inserted. Every symptom I read
as success follows from that:
c5d7d6a arch -arm64 (untraced)
Bad CPU type 2 0
'self' is immutable 4 0
build fail SUCCEEDED
code extracted - NONE
Three of those four look like a fix. The fourth is explained by nothing except
the tracer being absent, and an untraced build passing is exactly what ci.yml
does on every push — which I had already written down as the reason ci.yml is
green, and then failed to apply to my own result.
The "controlled comparison" I claimed was not one. Holding `runner: x86_64`
fixed while changing the wrapper did not isolate the architecture; it removed
tracing, which changes every signal at once.
So: build command back to byte-identical with main, job back to red on the known
EBADARCH failure rather than green on nothing, and the banner now warns the next
reader off this specific trap instead of recommending it.
The probe grows three lines that measure the purge directly — DYLD_INSERT_LIBRARIES
outside arch, under arch, and re-set past the arch boundary — because whether an
arm64 parent is reachable at all now depends on that, and it is measurable rather
than arguable.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UuHYhiqbmF4dT7sq8a6AYm
Correction: I reported this fixed. It was a fail-open.
Three of those four read as a fix. The fourth is explained by nothing except the tracer being absent — and an untraced build passing is precisely what The "controlled comparison" I leaned on was not one. Holding What nearly shippedHad That is the fourth distinct way this mechanism has been one step from reporting clean on nothing, and the first where I was the one holding the step. Where this leaves the PR
Still true, and not affected by any of this
Also worth flagging: the Generated by Claude Code |
Handoff — one measurement in flight, and how to read itI'm standing down on this PR. Run 36096284681 ( In the
And if the second line is Verify before trusting any future green here. The lesson of State of both PRsNeither is required for anything on
What is established, all measured rather than argued
All of it is in the workflow banner, so nobody has to repeat today. Generated by Claude Code |
Based on #24, not
main. Retarget when #24 merges.What the failed experiment established
ONLY_ACTIVE_ARCH=YESworked exactly as specified — the only architecture compiled wasarm64, no x86_64 slice anywhere in the log — andBad CPU typestill occurred. So the arch mismatch was not the cause.The theory is dead on its own terms too. I read the Mach-O fat header of
tools/osx64/libtrace.dylibout of the 2.27.1osx64bundle:It was never missing a slice for the build. I inferred that mismatch from the
…/CodeQL/2.27.1/arm64/…tool-cache path plus aSwiftCompile normal x86_64line, and never measured it — the same mistake as the 2.27.0 version claim in #24, one step further along.What it settled, which is the actual win
There is one blocker, not two. #24 recorded the
'self' is immutableerrors as possibly independent, cause unestablished. This run named the cause outright:@Stateis itself a macro, expanded by the same plugin server. When the plugin will not load,@Statedoes not apply, so itsnonmutating setnever exists — andtriggerRefresh.toggle()at :114 and the assignment at :117 become "cannot use mutating member on immutable value". They are downstream of the one failure and need no separate fix. That removes the "second blocker" from the picture entirely.932b589universal6682385universal8ad8ad4arm64-onlyBad CPU type'self' is immutableWhat this PR now does
Replaces the flag with one
continue-on-errorstep that printslipo -archsforswift-plugin-server,swift-frontendand$DYLD_INSERT_LIBRARIES. The build command is byte-identical tomain's again.It is an instrument, not a fix. The remaining candidate is an architecture on neither list — arm64e, since dyld refuses an arm64 library inserted into an arm64e process, and Xcode 27 moving helpers to arm64e would explain why this began when the image moved. That is measurable on the runner in one cycle, and I am not going to argue it from here, because arguing it from here has now been wrong twice.
The probe also records in the workflow what is already ruled out, so nobody re-tests it: libtrace carries both slices, and building one architecture does not help.
Demo
The probe's output in the next
Analyze (swift)log is the deliverable.Analyze (swift)will still fail — the probe does not fix anything and cannot, by design.Rule check
Non-negotiable 5 — serves its enforcement by trying to make the gate able to run at all. No app behaviour, no endpoint,
Sources/untouched,contents: readunchanged. The probe only reads binary architectures already on the runner; it sends nothing anywhere.Brand check
None. No UI, no assets, no tokens.
🤖 Generated with Claude Code
https://claude.ai/code/session_01UuHYhiqbmF4dT7sq8a6AYm