Skip to content

builtin: use the paired libgc.dylib for macOS amd64 + tinyc, not libgc.a - #28008

Closed
quaesitor-scientiam wants to merge 1 commit into
vlang:masterfrom
quaesitor-scientiam:c-selector-dylib
Closed

builtin: use the paired libgc.dylib for macOS amd64 + tinyc, not libgc.a#28008
quaesitor-scientiam wants to merge 1 commit into
vlang:masterfrom
quaesitor-scientiam:c-selector-dylib

Conversation

@quaesitor-scientiam

@quaesitor-scientiam quaesitor-scientiam commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Deliverable C of the macOS-amd64 libgc pairing plan, and the last piece of it.

tcc cannot reliably link the bundled static GC archive on macOS: its Mach-O archive reader leaves the GC symbols unresolved against an archive produced by a modern toolchain, even though nm -g shows them defined. arm64 has linked the dynamic library with an rpath for exactly that reason since vlang/tccbin#70. amd64 kept selecting libgc.a only because tccbin shipped no .dylib for it - which is what #27982 fixed, rebuilding libgc.dylib in lockstep with every tcc.exe rebuild.

-						$if arm64 {
-							#flag @VEXEROOT/thirdparty/tcc/lib/libgc.dylib
-							#flag -Wl,-rpath,"@VEXEROOT/thirdparty/tcc/lib"
-						} $else {
-							// macOS amd64 tccbin only ships libgc.a (no .dylib).
-							#flag @VEXEROOT/thirdparty/tcc/lib/libgc.a
-						}
+						#flag @VEXEROOT/thirdparty/tcc/lib/libgc.dylib
+						#flag -Wl,-rpath,"@VEXEROOT/thirdparty/tcc/lib"

Why it's safe to land now

This was deliberately held back until the dylib was actually distributed - pointing the selector at a file that isn't in the bundle would break every macOS amd64 -cc tcc -gc boehm build. Both preconditions are now met:

  • Published: vlang/tccbin thirdparty-macos-amd64 is at da8ac5a4, which ships lib/libgc.dylib as a genuine symlink (git mode 120000) to libgc.1.dylib, alongside libgc.a.
  • Gate green: vlang/tccbin#83's converted must-pass gate is green on that exact commit (run 30692570864).

Two things that would otherwise have broken

Both found by searching for anything depending on the old behaviour, rather than only changing the selector:

vlib/v/builder/gc_flags_test.v asserted libgc.a is on the link line, and its guards ($if !macos { return } / $if arm64 { return }) mean it runs on exactly the platform this changes. It now asserts the dylib and rpath are present and that no libgc.a token remains - the absence is what actually proves the selector collapsed, rather than the dylib merely appearing alongside it.

.github/workflows/update_tccbin.yml patched this very selector block in memory, so the macos-amd64 smoke test could exercise the dylib pairing before this change existed. That patch is now both redundant and fatal: it asserted its search text occurred exactly once, and that text no longer exists, so the job would fail outright on its next run. Removed. The smoke test's own expectations (libgc.dylib present, no libgc.a token - already written that way) are unchanged and are now satisfied by the real selector rather than a simulated one, which is strictly better fidelity.

Deliberately left alone after checking: vlib/v/tests/gnu_make_tcc_fallback_test.v's libgc.a assertion is inside a $if !linux { return } test; vlib/v/ast/cflags_test.v uses the path only as a flag-parsing string fixture; the FreeBSD/OpenBSD/Windows/Linux libgc.a selections are untouched.

What CI here can and cannot prove

Worth stating plainly so a green tick isn't over-read: vlang/v CI has no macOS amd64 job that runs the self-test suite. macos_ci.yml (which runs self_tests) is macos-14 only, i.e. arm64; macos-15-intel appears solely in release_ci.yml, vup_works.yml and update_tccbin.yml. Since test_macos_tcc_boehm_uses_bundled_libgc() returns early under $if arm64, the test updated here will skip in every CI job - it is a guard for developers on Intel Macs, not a gate that runs on this PR.

So CI here proves no regression elsewhere (arm64 macOS, formatting, the YAML lint, every other platform), but it does not exercise the changed path.

What does exercise it is #27982's smoke test on a real macos-15-intel runner: real V, real tcc.exe, real libgc.dylib, asserting the staged compiler was the one invoked, exactly one libgc.dylib token, exactly one matching -rpath, and no libgc.a token anywhere on the link line. That test ran against a selector patched in memory to do precisely what this PR now does for real - so the link path in this PR is already validated end-to-end; this PR is what makes it the committed default.

Test plan

  • v fmt -verify clean on both changed .v files.
  • v vet clean on both changed .v files.
  • update_tccbin.yml parses, and passes the repo's Prettier lint (npx prettier@3.9.6 --check **.yml from the repo root - the same invocation workflow_lint.yml runs).
  • Swept the tree for other assertions/docs depending on macOS amd64 selecting libgc.a; the two that did are updated above, the rest verified out of scope.
  • gc_flags_test.v on a macOS amd64 runner - this is the direct end-to-end check, and it now requires the published bundle, which is exactly the precondition verified above.

The dylib link path itself was already validated end-to-end on macos-15-intel during #27982: real V + real tcc.exe + real libgc.dylib, asserting the staged compiler ran, -rpath was present, and libgc.a never appeared on the link line.

Deliverable C of the macOS-amd64 libgc pairing plan, and the last piece.

tcc cannot reliably link the bundled *static* GC archive on macOS: its
Mach-O archive reader leaves the GC symbols unresolved against an archive
produced by a modern toolchain, even though `nm -g` shows them defined.
arm64 has linked the dynamic library with an rpath for exactly that reason
since vlang/tccbin#70. amd64 kept selecting libgc.a only because tccbin
shipped no .dylib for it - which is what vlang#27982 fixed, rebuilding
libgc.dylib in lockstep with every tcc.exe rebuild.

That pair is now published (vlang/tccbin thirdparty-macos-amd64 da8ac5a4,
libgc.dylib -> libgc.1.dylib as a real symlink), and vlang/tccbin#83's
converted gate is green on it, so the selector can finally point at it.

Also updated, both of which would otherwise break:

- vlib/v/builder/gc_flags_test.v asserted the old behaviour, and runs on
  exactly the platform this changes (macOS, non-arm64). It now asserts the
  dylib and rpath are present AND that no libgc.a token remains - the
  absence is what actually proves the selector collapsed, rather than the
  dylib merely appearing alongside it.
- update_tccbin.yml's isolated-workspace step patched this selector block
  in memory so its smoke test could exercise the dylib pairing before this
  change existed. That patch is now both redundant and fatal: it asserted
  its search text occurred exactly once, and that text no longer exists.
  Removed - the smoke test's own expectations (libgc.dylib present, no
  libgc.a token) are unchanged and are now satisfied by the real selector
  instead of a simulated one, which is strictly better fidelity.

Co-Authored-By: WOZCODE <contact@withwoz.com>
@quaesitor-scientiam

Copy link
Copy Markdown
Contributor Author

All three points are correct - I've verified each against the tree rather than just taking them. Standing down on Part C; closing this so it can't be mistaken for mergeable.

Over-broad selector. Right, and this is the one I'd defend least. The $else I deleted covered every non-arm64 arch reaching that branch, not just amd64 - the enclosing guard admits amd64 || arm64 || i386 || arm32 || rv64. I collapsed on the assumption macOS is only ever arm64/amd64, which is true in practice but is exactly the kind of unexamined widening I shouldn't have slipped in: I validated amd64, arm64 was already dylib, and i386/arm32/rv64 were neither validated nor considered. Preserving the libgc.a fallback outside arm64/amd64 is the correct shape.

Test gaps. All three real:

  • No -gc boehm. The command relies on the default, which makes the test's coverage implicit for a file that only compiles under boehm.
  • No -no-rsp. This one is worse than it looks: with a response file in play, the negative assertion (!contains('libgc.a')) passes vacuously, because the flags never appear in -showcc output at all. So the assertion I added specifically to prove the collapse happened is the one a response file silently disarms.
  • contains('-rpath') is far too weak - it doesn't check the rpath points at the bundled lib dir. The exact-token matching in update_tccbin.yml's smoke test is the right standard, and I should have matched it here.

The C0 comma-path prerequisite. This is the one I missed outright, and it's the sharpest of the three. fixup_tcc_macos_comma_path_flags in cc.v only ever fired for arm64, because amd64 emitted no -Wl,-rpath, flag and no dylib at all. Part C routes every amd64 tcc+boehm build straight through that helper, so it promotes a known-unsafe workaround from an arm64-only edge case to the nominal amd64 path. Fixing it first is plainly the right order.

Worth noting why I missed it, since it generalises: my impact analysis searched for things referencing what I removed (libgc.a), which found the two dependants I did fix. It never asked the inverse question - what newly fires when amd64 starts emitting -Wl,-rpath, and a dylib. That second question is where C0 lives.


One piece here that must not get dropped, whoever lands Part C:

.github/workflows/update_tccbin.yml's isolated-workspace step patches the selector block in memory (so the macos-amd64 smoke test could exercise the dylib pairing before Part C existed), guarded by:

assert count == 1, f"expected exactly 1 occurrence of the selector block to patch, found {count}"

The moment the selector changes, that search text stops existing and the assert hard-fails, taking the macos-amd64 rebuild job with it. It needs removing in the same PR as the selector change - it's independent of the three points above, and it isn't optional. The smoke test's own expectations (libgc.dylib present, no libgc.a token) are already written that way, so deleting the patch makes it exercise the real selector instead of a simulated one, which is strictly better.

The commit is 8b09e79 if it's easier to lift than rewrite; the branch stays up. Happy to help with the macOS smoke test or review C0 if useful - otherwise I'll leave Part C with you and stay on the FreeBSD side.

@quaesitor-scientiam

Copy link
Copy Markdown
Contributor Author

Closing as superseded - Part C is being taken over with the C0 comma-path prerequisite and a dedicated macOS smoke test. Branch c-selector-dylib stays up for reference; see the note above re: the update_tccbin.yml patch removal, which is needed regardless of who lands the selector change.

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.

2 participants