Skip to content

fix(nix): Fix for macOS build for nixpkgs#1316

Merged
cjpais merged 2 commits intocjpais:mainfrom
xilec:fix/swiftc-parse-as-library
Apr 27, 2026
Merged

fix(nix): Fix for macOS build for nixpkgs#1316
cjpais merged 2 commits intocjpais:mainfrom
xilec:fix/swiftc-parse-as-library

Conversation

@xilec
Copy link
Copy Markdown
Contributor

@xilec xilec commented Apr 20, 2026

Before Submitting This PR

Please confirm you have done the following:

Human Written Description

While debugging the Handy macOS build for nixpkgs it turned out that swiftc in build.rs, when invoked without -parse-as-library, emits its own _main symbol into the Apple Intelligence bridge object file, and the open-source ld64 used by nixpkgs' Darwin stdenv picks that _main over Rust's — so the Rust runtime is never entered and the app silently exits with code 0. Adding the flag keeps swiftc in library mode and the binary works. Production releases (Xcode's linker) mask this because Xcode's ld prefers Rust's _main.

A second related fix: the Apple Intelligence bridge build (build.rs) invoked xcrun directly to locate the SDK and swiftc, which is unavailable in non-Xcode toolchains (nixpkgs uses standalone apple-sdk_* + swift). The build now honors SDKROOT and SWIFTC env vars when set, falling back to xcrun otherwise — Apple-toolchain behavior is unchanged. Both code paths verified on macos-26: https://github.com/xilec/Handy/actions/runs/24940175357

Related Issues/Discussions

  • NixOS/nixpkgs#507754 — philocalyst reports "the generated binary on Darwin does nothing, has no typical Rust main entrypoint, 50 KiB stub of a stub"
  • Discussion #1242 — same observation ("it will quit and exit cleanly, without executing anything")
  • philocalyst/nixpkgs#1current Nix packaging PR (feeds into handy: init at 0.8.3 NixOS/nixpkgs#507754). src is pinned to this PR's HEAD, so it's the easiest end-to-end test of the fix on Darwin under Nix. macOS users wanting to validate this PR under Nix should build from there: nix-build https://github.com/xilec/nixpkgs/archive/handy-cross-platform-deps.tar.gz -A handy works as-is on aarch64-darwin.

Community Feedback

Bug reported by philocalyst (nixpkgs maintainer working on the Handy package) in NixOS/nixpkgs#507754 and in discussion #1242.

Testing

Reproduced and fixed in a Nix flake that mirrors the Darwin build used by nixpkgs. Matrix CI on macos-latest (aarch64-darwin) across four variants — full run: https://github.com/xilec/Handy/actions/runs/24674832551

Variant _main address Behaviour on launch Job
baseline (current main) 0x100031180 5-insn stub, exits in ~50 ms job 72155708502
minimal swift stub (no import Foundation, no typealias) 0x100031340 still stub — script mode emits _main regardless of file content job 72155708494
+ apple-sdk_26 (real apple_intelligence.swift with FoundationModels) 0x100031200 still stub job 72155708487
-parse-as-library (this PR) 0x100007b10 real Rust main, GUI event loop runs until killed by 5 s timeout job 72155708525

In each job the collapsed "Diagnose binary (stub check)" step contains the ::group::_main disassembly, ::group::_main symbol, and ::group::Try to run (…) sections with raw otool -tV output and wall-clock timings. The "Compare pre-bundle vs bundled binary" step confirms that the stub _main is already present in src-tauri/target/<arch>/release/handy before cargo tauri bundle runs — i.e., the bug is at Rust/link time, not in bundling.

Baseline _main (5-inst no-op return 0):

_main:
    stp x29, x30, [sp, #-0x10]!
    mov x29, sp
    mov w0, #0x0               ; return 0
    ldp x29, x30, [sp], #0x10
    ret

After the fix (_main saves callee-saved registers and calls into rustc's lang_start_internal via blr x8 — real Rust entry):

_main:
    stp x22, x21, [sp, #0x90]
    stp x20, x19, [sp, #0xa0]
    stp x29, x30, [sp, #0xb0]
    add x29, sp, #0xb0
    adrp x0, ...
    ldr x8, [x0]
    blr x8                      ; call lang_start
    cbnz x8, ...
    ...

Production macOS CI (macos-26 runner, Xcode's linker) is unaffected — Xcode's ld was already ignoring Swift's _main, the flag just stops emitting that unused symbol.

References on swiftc's script-mode _main emission:

Debug branch used for reproduction: https://github.com/xilec/Handy/tree/debug/darwin-flake

Screenshots/Videos (if applicable)

N/A — one-line build-config change, no UI impact.

AI Assistance

  • No AI was used in this PR
  • AI was used (please describe below)

If AI was used:

  • Tools used: Claude Code (claude.ai/code)
  • How extensively: used for researching swiftc script-mode behaviour and ld64 differences (Swift forums, GitHub issues, blog posts), drafting the build.rs comment, and running/analyzing the matrix-CI reproduction. The fix itself is one flag; the investigation was AI-assisted but validated against Swift compiler docs and primary sources cited above.

Without this flag swiftc compiles a single-file input in script mode
and emits a synthetic `_main` into the object file. Packaged into
libapple_intelligence.a and linked alongside Rust's `_main`, Apple's
open-source ld64 (used by nixpkgs' Darwin stdenv) picks Swift's main,
leaving the app with a 5-instruction no-op that returns 0 immediately.

The binary looks complete — full Rust code, Metal, Swift runtime,
onnxruntime rpath — but launching it exits cleanly with code 0, no
output. Production CI masks the issue because Xcode's linker happens
to prefer Rust's `_main`.

`-parse-as-library` keeps swiftc in library mode so no `_main` is
emitted. The @_cdecl exports used by the Rust FFI are unaffected.
@xilec xilec mentioned this pull request Apr 20, 2026
13 tasks
xilec added a commit to xilec/nixpkgs that referenced this pull request Apr 21, 2026
Without this swiftc flag the app exits with 0 immediately on nixpkgs
Darwin stdenv, because the open-source ld64 picks Swift's synthetic
`_main` over Rust's. The fix is one line in src-tauri/build.rs.

Cargo.lock, package.json and bun.lock are unchanged between the old
and new src rev, so cargoHash and frontendDepsHashes remain valid.
xilec added a commit to xilec/nixpkgs that referenced this pull request Apr 21, 2026
Hunk 3 of the patch failed to apply because cjpais/Handy#1316 added
`-parse-as-library` (with explanatory comment) right where the patch
expected `-target` to follow `"swiftc"`. Regenerated against current
src; the substantive change (drop xcrun wrapper, call swiftc directly)
is unchanged.
@philocalyst
Copy link
Copy Markdown

@cjpais CAN confirm this fixes the packaging issues on Nix for Darwin! This would be HUGE to get in :)

@xilec xilec changed the title fix(macos): pass -parse-as-library to swiftc for Apple Intelligence bridge fix(macos): pass -parse-as-library to swiftc Apr 21, 2026
@xilec xilec changed the title fix(macos): pass -parse-as-library to swiftc fix(nix): Fix for macOS build for nixpkgs Apr 21, 2026
@cjpais
Copy link
Copy Markdown
Owner

cjpais commented Apr 22, 2026

Will need some time to merge! I will test it soon, I'm in some deep work right now and not able to context switch. I need to test this on my Mac before I release

@philocalyst
Copy link
Copy Markdown

Got it! Thanks for your time :) @cjpais -- let me know if you need supporting evidence, because it has proved to be an elusive bug, so I'm worried you won't see a difference.

@xilec
Copy link
Copy Markdown
Contributor Author

xilec commented Apr 25, 2026

macOS testers wanted

If you're on macOS, a second pair of eyes on this fix would help. Two ways to test:

Plain build (no Nix, Xcode toolchain)

gh pr checkout 1316
bun install
bun run tauri build
open src-tauri/target/release/bundle/macos/Handy.app

The flag should be a no-op for the Xcode toolchain (Xcode's ld was already ignoring Swift's _main), so we're mostly looking for "no regression". Production CI on macos-26 already passes; cross-checking on a developer machine with a different Xcode version would be a useful sanity check.

Under Nix (the build path this PR fixes)

The current Nix packaging in philocalyst/nixpkgs#1 pins src to this PR's HEAD, so a single command builds and runs:

nix-build https://github.com/xilec/nixpkgs/archive/handy-cross-platform-deps.tar.gz -A handy
./result/bin/handy

Already independently verified on aarch64-darwin by @totoroot — see his comment in NixOS/nixpkgs#507754. More confirmations welcome.

x86_64-darwin currently throws on the frontendDeps hash (GH Actions retired the free Intel runner pool). If anyone has an Intel Mac and is willing to compute the hash, a note on philocalyst/nixpkgs#1 would be much appreciated.

xcrun is unavailable in non-Xcode setups (e.g. nixpkgs uses
apple-sdk_* plus a standalone swift compiler). Honor SDKROOT and
SWIFTC if set; fall back to xcrun otherwise so Apple-toolchain
behavior is unchanged.

Also invoke swiftc directly via the resolved path rather than via
`xcrun swiftc`.
xilec added a commit to xilec/nixpkgs that referenced this pull request Apr 25, 2026
The SDKROOT/SWIFTC env-var fallbacks in build.rs are now part of
cjpais/Handy#1316 alongside -parse-as-library, so the local patch
is no longer needed. Bump src.rev to the new HEAD of NixOS#1316.
xilec added a commit to xilec/nixpkgs that referenced this pull request Apr 25, 2026
The SDKROOT/SWIFTC env-var fallbacks in build.rs are now part of
cjpais/Handy#1316 alongside -parse-as-library, so the local patch
is no longer needed. Bump src.rev to the new HEAD of NixOS#1316.
@totoroot
Copy link
Copy Markdown

@xilec Yeah, that matches what I tested.

My validation was on aarch64-darwin only. I built Handy from source under Nix with the -parse-as-library patch applied, plus the Nix-specific SDKROOT/SWIFTC handling needed for the sandbox. The build completed successfully and produced both Handy.app and bin/handy and I am happily using it since.
I also verified the onnxruntime rpath was present.

I have not tested the plain Xcode/Bun path yet, and I do not have an x86_64-darwin machine available to compute the Intel frontendDeps hash so cannot help with testing any further.

@cjpais
Copy link
Copy Markdown
Owner

cjpais commented Apr 26, 2026

Thanks for confirmation, hopefully I have a chance tomorrow to test on my machine and pull in, if you leave a comment I'll be sure to see it in the morning :)

@xilec
Copy link
Copy Markdown
Contributor Author

xilec commented Apr 26, 2026

@cjpais Morning ping :)

@cjpais
Copy link
Copy Markdown
Owner

cjpais commented Apr 27, 2026

Tested on my Mac, no issues. Just fixing the CI error from before and will pull in after it builds

@github-actions
Copy link
Copy Markdown

🧪 Test Build Ready

Build artifacts for PR #1316 are available for testing.

Download artifacts from workflow run

Artifacts expire after 30 days.

@cjpais
Copy link
Copy Markdown
Owner

cjpais commented Apr 27, 2026

Works for me end to end

@cjpais cjpais merged commit 8346bc2 into cjpais:main Apr 27, 2026
2 checks passed
@xilec
Copy link
Copy Markdown
Contributor Author

xilec commented Apr 27, 2026

🎉 thank you @cjpais!

If it's not too much trouble, would an intermediate v0.8.3 release be possible anytime soon? It would let us tie our nixpkgs package to it.

@cjpais
Copy link
Copy Markdown
Owner

cjpais commented Apr 27, 2026

Let me check, most likely yes

@xilec
Copy link
Copy Markdown
Contributor Author

xilec commented Apr 28, 2026

@cjpais thanks for cutting v0.8.3 so promptly! Really appreciate the fast turnaround. 🎉

@cjpais
Copy link
Copy Markdown
Owner

cjpais commented Apr 28, 2026

no problem, meant to do it earlier but had to get my laptop repaired

xilec added a commit to xilec/nixpkgs that referenced this pull request Apr 28, 2026
cjpais/Handy#1316 (-parse-as-library swiftc fix + SDKROOT/SWIFTC
env-var fallbacks) shipped in v0.8.3, so revert `src` from the
in-flight rev pin back to `tag = "v${finalAttrs.version}"`.

bun.lock unchanged between v0.8.2 and v0.8.3, so frontendDepsHashes
remain valid; cargoHash bumped due to the version field in
src-tauri/Cargo.toml.
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.

4 participants