Skip to content

toolchain: absolutize relative -F/-iframework framework dirs at exec time (macOS) - #1

Closed
DhashS wants to merge 1 commit into
sysroot-feature-pinnedfrom
ds/macos-framework-abspath
Closed

DhashS wants to merge 1 commit into
sysroot-feature-pinnedfrom
ds/macos-framework-abspath

Conversation

@DhashS

@DhashS DhashS commented Aug 17, 2026 •

Copy link
Copy Markdown
Owner

The in-code comment in zig-wrapper.zig is deliberately terse (a few lines); the full rationale lives in this description to keep the source diff minimal (+41, one file).

Absolutize relative -F / -iframework framework search dirs at exec time (macOS)

Stacks on the external-sysroot feature (uber#263); base
here is that feature pinned at 01d6714 so this diff is only the framework-dir
fix.

The bug it fixes

zig cc's self-hosted Mach-O linker resolves a relative -L/-l search
path fine — the wrapper already relies on this in resolveColonLibraries,
where cwd is the Bazel execroot — but it does not resolve a relative
-F framework search dir. A framework dir has to be absolute for the
linker to search it. So a relative -Fexternal/…/System/Library/Frameworks
(exactly what Bazel emits) makes any -framework Foo fail with:

unable to find framework 'Foo'. searched paths:  none

Neither obvious alternative helps:

  • -isysroot <sdk> alone does not add the sysroot's
    System/Library/Frameworks to the framework search path (zig's linker,
    unlike Apple ld64, doesn't auto-search it);
  • passing the dir through the linker directly — -Wl,-F / -Wl,-syslibroot —
    is rejected by zig as an unsupported linker arg.

An absolute -F is the only lever that works.

Why it has to live in the wrapper

Bazel only ever emits execroot-relative paths (for cache-correctness and
remote-execution portability), and nothing at analysis time — not even a
custom Starlark rule — can produce an absolute execroot path
, because the
execroot isn't known until the action runs. Only a runtime wrapper, executing
in the execroot, can absolutize it. So absolutizeFrameworkDirs rewrites
relative -F/-iframework dirs to <cwd-abs>/<dir> at exec time. This is the
framework-dir analog of the existing relative--L handling in
resolveColonLibraries.

Scope / safety

  • macOS-scoped in practice: -F/-iframework only appear on Apple-framework
    links, so non-macOS compiles are untouched.
  • Only rewrites dirs that actually exist relative to cwd; an already-absolute
    path, or one that doesn't exist, is left as-is (no behavior change).
  • Handles both the separate ("-F" "dir") and fused ("-Fdir") spellings, for
    -F and -iframework.
  • Cache-safe: this runs inside the action, so Bazel's action key still sees the
    original relative flags.

Minimal repro

A cc_binary linking an Apple framework against a vendored SDK exposed with a
relative -F, built with this toolchain as the host/exec cc toolchain:

# BUILD.bazel
cc_binary(
    name = "cf_smoke",
    srcs = ["cf_smoke.c"],  # #include <CoreFoundation/CoreFoundation.h>; calls CFStringCreateWithCString
    linkopts = ["-framework", "CoreFoundation"],
    target_compatible_with = ["@platforms//os:macos"],
)

with the sysroot feature pointing zig cc at the SDK via a relative
-Fexternal/…/System/Library/Frameworks. Before this change the link fails
unable to find framework 'CoreFoundation'. searched paths: none; after it,
it links and runs (exit 0). Same fix unblocks protobuf/Abseil's -framework CoreFoundation, a Cocoa #import chain, and rules_swift's compile worker.

…time (macOS)

zig cc's Mach-O linker resolves relative -L/-l (see resolveColonLibraries) but
not a relative -F framework dir, so `-framework Foo` against a sysroot passed
with a relative -F fails 'unable to find framework ...: searched paths:  none'.
Rewrite relative -F/-iframework dirs to absolute at exec time (cwd = execroot),
the framework-dir analog of the existing relative -L handling. macOS-scoped;
existing dirs only; cache-safe (action key still sees the relative flags).
Full rationale + repro: #1.
@DhashS
DhashS force-pushed the ds/macos-framework-abspath branch from 0388d21 to f947d66 Compare August 17, 2026 04:41
@DhashS

DhashS commented Aug 17, 2026

Copy link
Copy Markdown
Owner Author

Closing this — the premise is wrong. zig cc's Mach-O linker resolves relative -F dirs just fine; the flag was never reaching the linker at all, and the consumer side can fix it with one character.

What actually happens

zig's clang option table (src/clang_options_data.zig, 0.15.2) lists the MSVC/clang-cl joined options before the generic -F, and matches them in plain cc mode too:

-Fa -Fc -Fd -Fe -Fi -Fm -Fo -Fp -Fr -FA -FI -FR -FU   ->  .zig_equivalent = .other   (discarded)
-F                                                     ->  .zig_equivalent = .framework_dir

So -Fexternal/+http_archive+macos_sdk/System/Library/Frameworks is parsed as -Fe (clang-cl "name the executable") carrying the value xternal/..., and dropped. create_module.framework_dirs ends up empty, which is exactly what searched paths: none means — accessFrameworkPath appends every candidate it probes to checked_paths, so an empty list is the only way to print nothing at all. strace confirms zero filesystem probes in the failing case: the dir is never even stat'd.

Verified letter map (zig c++ -F<letter>zz ... -framework CoreFoundation, zig 0.15.2):

first char of the glued path result
a c d e i m o p r (and A I R U) dropped silently
everything else, incl. . / s b x parsed

Absolute paths only ever worked because they start with /. A relative path links fine, unpatched, whenever its first character isn't one of those letters — e.g. -Fsdkplain/System/Library/Frameworks resolves CoreFoundation today. The separate spelling -F <dir> is also unaffected. -L and -I have no such shadowing entries; -F is the only flag with this problem.

So the rule this patch encodes ("relative -F is ignored, absolutize it") is not the actual behavior, and absolutizeFrameworkDirs fixes the Bazel case only by accident — it happens to prepend /, which sidesteps the -Fe match.

The fix, on the consumer side

Prefix the glued path with ./:

linkopts = [
    "-F./" + _MACOS_SDK_PATH + "/System/Library/Frameworks",
    "-L./" + _MACOS_SDK_PATH + "/usr/lib",
]

Keeps the path relative (cache/RBE correct) and single-token (so rules_rust's trailing -oso_prefix ${pwd}/ can't be mis-split by rust-lld), with no toolchain patch at all.

Verified end to end against 01d67148 — this PR's base, i.e. the sysroot tag from uber#263 without this commit:

check result
bazel build //... (Linux) 96 targets, 4937 actions
bazel test //... (Linux) 27/27
cc_binary linking -framework CoreFoundation, cross-compiled --platforms=@zig_sdk//platform:darwin_arm64 links, Mach-O 64-bit arm64 executable

Control: same base, unprefixed linkopts -> unable to find framework 'CoreFoundation'. searched paths: none.

uber#263 (the sysroot tag) is unaffected and still needed — this only retires the wrapper patch stacked on top of it.

The real bug belongs upstream in zig: the clang-cl joined options should be gated to cl mode rather than matched in zig cc. Closest existing report is ziglang/zig#16235, a different symptom whose repro already happens to use -F./.

@DhashS DhashS closed this Aug 17, 2026
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.

1 participant