Skip to content

ci(macos-amd64): flip checked-in gate from must-stay-broken to must-pass #103

ci(macos-amd64): flip checked-in gate from must-stay-broken to must-pass

ci(macos-amd64): flip checked-in gate from must-stay-broken to must-pass #103

Workflow file for this run

name: build and test (macos-amd64)
on:
push:
branches: [thirdparty-macos-amd64]
pull_request:
branches: [thirdparty-macos-amd64]
workflow_dispatch: {}
jobs:
build:
runs-on: macos-15-intel
steps:
# thirdparty/tccbin_tests (the shared cross-platform conformance
# suite - see its README), thirdparty/libgc/include (gc.h), and
# thirdparty/build_scripts/thirdparty-macos-amd64_tcc.sh (the
# official rebuild recipe vlang/v's own update_tccbin.yml uses for
# this exact platform) all live in the main v repo, not here.
# vlib/v/compiler_errors_test.v is a sentinel file the build
# script checks for, to confirm it's being run from a real v repo
# checkout.
- name: checkout v (for thirdparty/libgc, tccbin_tests, and the build script)
uses: actions/checkout@v4
with:
repository: vlang/v
ref: c82d3f08271e9324d6fb8de3c251e9c0e1a9154b
sparse-checkout: |
thirdparty/libgc
thirdparty/tccbin_tests
thirdparty/build_scripts
vlib/v/compiler_errors_test.v
sparse-checkout-cone-mode: false
path: work
# this branch, checked out where the build script expects to find
# itself (work/thirdparty/tcc), matching a normal local checkout
# layout.
- name: checkout this branch
uses: actions/checkout@v4
with:
path: work/thirdparty/tcc
# Codex pullrequestreview-4780178390 on vlang/tccbin#74 (P1): the
# steps below replace this branch's checked-in tcc.exe with a
# rebuild from tinycc `mob`, and later replace libgc.a with a
# rebuild from bdwgc `master`, before ever testing either - so a
# future PR that commits a broken tcc.exe/libgc.a pair here would
# still get a passing CI result, since the suite would only ever
# exercise the unrelated freshly-rebuilt artifacts. Test the
# checked-out distribution BEFORE any rebuild step touches it.
#
# vlang/v#27982 (deliverable A of the macos-amd64 libgc-dylib-pairing
# plan) has landed: vlang/v's update_tccbin.yml now rebuilds
# libgc.dylib in lockstep with every tcc.exe rebuild, mirroring
# macos-arm64's already-proven dylib+rpath pattern, instead of
# silently preserving a stale libgc.a across every rebuild. This
# branch's checked-in pair is therefore NO LONGER expected to stay
# broken - this is deliberately the required baseline going forward
# (dylib blocking, no-GC blocking, static libgc.a XFAIL-only - the
# exact same shape already proven below for the freshly-rebuilt
# copy), not a relaxation: the old "must stay broken, error if it
# starts passing" assertion only ever existed because the ancient,
# pre-A checked-in v0.9.27 tcc.exe/libgc.a pair genuinely could not
# pass. An UNEXPECTED failure shape from a future PR that changes
# these binaries still fails the job for real instead of being
# silently absorbed.
- name: verify the checked-in tcc.exe is executable (as distributed, before rebuild)
working-directory: work
run: |
# A PR that accidentally committed tcc.exe with the wrong
# (non-executable) file mode would still pass a check that
# silently repaired it first - but a real consumer checking out
# this exact commit gets the actual committed mode, not this CI
# job's repaired one (Codex pullrequestreview-4783470598 on
# vlang/tccbin#74). Assert the committed mode is already correct
# instead of silently fixing it.
if [ ! -x thirdparty/tcc/tcc.exe ]; then
echo "::error::thirdparty/tcc/tcc.exe is not committed as executable - a consumer checking out this exact commit would get a non-executable file. This is a real regression in what this PR proposes to commit, not something CI should silently repair before testing."
exit 1
fi
otool -l thirdparty/tcc/tcc.exe | grep -A3 "LC_VERSION_MIN_MACOSX\|LC_BUILD_VERSION" || echo "(no LC_VERSION_MIN_MACOSX/LC_BUILD_VERSION load command found)"
# Independently re-verify the checked-in libgc.dylib itself - never
# trust that whatever produced it (vlang/v's update_tccbin.yml) got
# this right; same "don't just trust it ran" principle already
# applied on the producer side in
# thirdparty-macos-amd64_bdwgc_validate.sh. A prior version of the
# macos-arm64 bundle shipped libgc.dylib as a plain, dereferenced
# file instead of a real symlink to a versioned target (confirmed
# via the GitHub API) - a real, already-happened silent failure mode
# this check exists to catch on this platform too.
- name: verify the checked-in libgc.dylib (as distributed, before rebuild)
working-directory: work
run: |
if [ ! -L thirdparty/tcc/lib/libgc.dylib ]; then
echo "::error::thirdparty/tcc/lib/libgc.dylib is not committed as a symlink - expected a symlink to a versioned file (e.g. libgc.1.dylib). A consumer checking out this exact commit would get a plain, dereferenced file instead of the real libtool-managed layout."
exit 1
fi
libgc_dylib_target=$(readlink thirdparty/tcc/lib/libgc.dylib)
if [ ! -f "thirdparty/tcc/lib/$libgc_dylib_target" ]; then
echo "::error::thirdparty/tcc/lib/libgc.dylib points at '$libgc_dylib_target', but that file is not committed alongside it."
exit 1
fi
libgc_install_name=$(otool -D "thirdparty/tcc/lib/$libgc_dylib_target" | tail -n 1)
case "$libgc_install_name" in
@rpath/*) echo "libgc.dylib -> $libgc_dylib_target, install name $libgc_install_name (both confirmed)" ;;
*) echo "::error::install name of $libgc_dylib_target is '$libgc_install_name', not an @rpath/-relative path."; exit 1 ;;
esac
# Validate the checked-in libgc.a archive directly via ar/nm before
# ever treating a link failure against it as the known tcc archive-
# parsing limitation (same reasoning as the static XFAIL lane below,
# applied to the checked-in copy). Unlike the legacy checked-in
# archive this replaces (a FAT x86_64+arm64 universal binary from an
# older, cross-built pipeline, confirmed via a real CI run's `file`
# output), a pair published through vlang/v's amd64-native rebuild
# is expected to be a thin x86_64-only archive - detect via
# `lipo -archs` (which works uniformly on fat or thin input) and
# only extract a slice with `-thin` when the file is actually fat,
# since `lipo -thin` fails outright on an already-thin input
# regardless of whether its single architecture matches what was
# requested.
- name: verify the checked-in libgc.a is a well-formed x86_64 archive (as distributed, before rebuild)
working-directory: work
run: |
archs="$(lipo -archs thirdparty/tcc/lib/libgc.a)"
echo "thirdparty/tcc/lib/libgc.a architectures: $archs"
case " $archs " in
*" x86_64 "*) ;;
*)
echo "::error::thirdparty/tcc/lib/libgc.a does not contain an x86_64 slice (architectures: $archs) - a real regression (wrong/missing architecture) in the checked-in archive this PR proposes to commit."
exit 1
;;
esac
if [ "$(printf '%s' "$archs" | wc -w)" -gt 1 ]; then
lipo -thin x86_64 thirdparty/tcc/lib/libgc.a -output /tmp/checkedin_libgc_x86_64.a
else
cp thirdparty/tcc/lib/libgc.a /tmp/checkedin_libgc_x86_64.a
fi
if ! ar t /tmp/checkedin_libgc_x86_64.a >/dev/null 2>&1; then
echo "::error::thirdparty/tcc/lib/libgc.a's x86_64 slice is not a well-formed archive (ar t failed) - a real regression in the checked-in archive this PR proposes to commit."
exit 1
fi
# `grep -q` stops reading as soon as it finds a match, which
# can make `nm` receive SIGPIPE while still writing the rest
# of a large archive's symbol list - under GitHub's bash
# (pipefail enabled), that SIGPIPE-killed `nm` makes the WHOLE
# pipeline report failure even though grep itself found the
# match (Codex pullrequestreview-4783470598 on vlang/tccbin#74).
# Capture the full output first and grep the captured text
# without -q.
checkedin_gc_symbols=$(nm -g /tmp/checkedin_libgc_x86_64.a 2>&1) || true
checkedin_gc_init_defined=$(printf '%s\n' "$checkedin_gc_symbols" | grep -E '(^| )[Tt] _?GC_init$') || true
if [ -z "$checkedin_gc_init_defined" ]; then
echo "::error::thirdparty/tcc/lib/libgc.a's x86_64 slice does not define GC_init - a real regression in the checked-in archive this PR proposes to commit (missing/incomplete symbols)."
exit 1
fi
- name: run shared conformance tests against the checked-in pair (libgc.dylib, dynamic - blocking)
id: checkedin_dylib_test
working-directory: work
run: |
chmod +x thirdparty/tccbin_tests/run.sh
./thirdparty/tccbin_tests/run.sh thirdparty/tcc/tcc.exe macos -- \
-DGC_BUILTIN_ATOMIC=1 -DMPROTECT_VDB=1 \
-I thirdparty/libgc/include \
"$PWD/thirdparty/tcc/lib/libgc.dylib" \
-Wl,-rpath,"$PWD/thirdparty/tcc/lib" \
-ldl -lpthread
- name: verify no-GC fallback path against the checked-in pair (plain tcc, no libgc linked)
id: checkedin_nogc_test
if: ${{ !cancelled() }}
working-directory: work
run: |
thirdparty/tcc/tcc.exe thirdparty/tccbin_tests/shared/crash.c -o /tmp/crash_checkedin_nogc.exe
if [ ! -x /tmp/crash_checkedin_nogc.exe ]; then
echo "expected tcc to produce a runnable /tmp/crash_checkedin_nogc.exe, but it's missing or not executable - tcc likely exited 0 without actually producing a working binary" >&2
exit 1
fi
set +e
/tmp/crash_checkedin_nogc.exe
code=$?
set -e
if [ "$code" -ne 139 ]; then
echo "expected exit 139 (killed by SIGSEGV) from an unguarded null-pointer dereference, got $code" >&2
exit 1
fi
echo "no-GC compile+crash check passed against the checked-in pair (killed by SIGSEGV as expected)"
cat > /tmp/nogc_trivial_checkedin.c <<'TRIVIALEOF'
int main(void) { return 0; }
TRIVIALEOF
thirdparty/tcc/tcc.exe /tmp/nogc_trivial_checkedin.c -o /tmp/nogc_trivial_checkedin.exe
if [ ! -x /tmp/nogc_trivial_checkedin.exe ]; then
echo "expected tcc to produce a runnable trivial no-GC binary, but it's missing or not executable" >&2
exit 1
fi
set +e
/tmp/nogc_trivial_checkedin.exe
trivial_code=$?
set -e
if [ "$trivial_code" -ne 0 ]; then
echo "expected the trivial no-GC program to exit 0, got $trivial_code - the no-GC toolchain path may be broken even though the crash.c check above happened to pass" >&2
exit 1
fi
echo "trivial no-GC success check passed against the checked-in pair (exit=0)"
# tcc's Mach-O archive-parsing limitation (documented on macos-arm64,
# and re-confirmed against a freshly-rebuilt archive below) is a
# property of tcc itself vs. a modern-toolchain-built static archive
# - independent of whether that archive is checked-in or freshly
# rebuilt, and not expected to go away here. This is a temporary,
# explicit XFAIL, not a blanket continue-on-error, so an unexpected
# failure shape (or an unexpected full pass, meaning tcc's archive
# parsing got fixed) still fails the job for real.
- name: run shared conformance tests against the checked-in pair (libgc.a, static - XFAIL)
id: checkedin_static_xfail_test
working-directory: work
if: ${{ !cancelled() }}
run: |
set +e
output=$(./thirdparty/tccbin_tests/run.sh ./thirdparty/tcc/tcc.exe macos -- \
-DGC_BUILTIN_ATOMIC=1 -DMPROTECT_VDB=1 \
-I ./thirdparty/libgc/include \
./thirdparty/tcc/lib/libgc.a \
-ldl -lpthread 2>&1)
code=$?
set -e
echo "$output"
if [ "$code" -eq 0 ]; then
echo "::error::Static libgc.a linking now succeeds against the checked-in pair on macOS amd64 - this XFAIL special-casing should be removed and folded back into a single blocking lane, it's no longer needed."
exit 1
fi
# The pinned run.sh (c82d3f0..., predates vlang/v#27935's
# stderr-capture fix) only ever prints "(compile error)" with no
# detail - compile both failing tests directly to capture their
# real tcc stderr and assert each is specifically the known
# unresolved-GC-symbol error, the same reasoning already applied
# to the freshly-rebuilt archive's XFAIL lane below.
set +e
direct_err_gc_alloc=$(./thirdparty/tcc/tcc.exe thirdparty/tccbin_tests/shared/gc_alloc.c \
-DGC_BUILTIN_ATOMIC=1 -DMPROTECT_VDB=1 \
-I ./thirdparty/libgc/include \
./thirdparty/tcc/lib/libgc.a \
-ldl -lpthread \
-o /tmp/gc_alloc_checkedin_xfail_probe 2>&1)
direct_code_gc_alloc=$?
direct_err_hello=$(./thirdparty/tcc/tcc.exe thirdparty/tccbin_tests/shared/hello.c \
-DGC_BUILTIN_ATOMIC=1 -DMPROTECT_VDB=1 \
-I ./thirdparty/libgc/include \
./thirdparty/tcc/lib/libgc.a \
-ldl -lpthread \
-o /tmp/hello_checkedin_xfail_probe 2>&1)
direct_code_hello=$?
set -e
is_known_gc_init() {
# $1 = captured stderr text, $2 = the probe's own exit code.
# Same reasoning as the freshly-rebuilt archive's equivalent
# check below: reject a signal-terminated exit (128+signal)
# and reject exit 0 (a changed tcc.exe could print this same
# diagnostic as noise while still reporting overall success).
if [ "$2" -eq 0 ] || [ "$2" -gt 128 ]; then
return 1
fi
case "$1" in
*"unresolved reference to '_GC_init'"*|*"unresolved reference to 'GC_init'"*) ;;
*) return 1 ;;
esac
other=$(printf '%s\n' "$1" | grep -E '^tcc: error:' | grep -v -E "unresolved reference to '_?GC_[A-Za-z0-9_]+'")
[ -z "$other" ]
}
case "$output" in
*"PASS crash"*"FAIL gc_alloc (compile error)"*"FAIL hello (compile error)"*)
if is_known_gc_init "$direct_err_gc_alloc" "$direct_code_gc_alloc" && is_known_gc_init "$direct_err_hello" "$direct_code_hello"; then
echo "known XFAIL: static libgc.a still can't link gc_alloc.c/hello.c against the checked-in pair (unresolved GC_init, the known tcc archive-parsing bug), as expected - not blocking CI"
else
echo "::error::libgc.a lane failed with the known summary shape against the checked-in pair, but at least one of gc_alloc.c/hello.c's direct compile errors does NOT mention an unresolved GC_init reference (or exited abnormally) - this looks like a different, unexpected regression:"
echo "gc_alloc.c: exit=$direct_code_gc_alloc: $direct_err_gc_alloc"
echo "hello.c: exit=$direct_code_hello: $direct_err_hello"
exit 1
fi
;;
*)
echo "::error::Static libgc.a lane failed against the checked-in pair, but NOT with the known/expected summary (PASS crash, FAIL gc_alloc/hello with compile error) - this looks like a different, unexpected problem and should not be silently treated as the known XFAIL."
exit 1
;;
esac
- name: install build dependencies
# automake/libtool are needed by bdwgc's autogen.sh (aclocal,
# libtoolize) for the libgc rebuild step below - not required
# by the tcc build itself.
run: brew install make automake libtool
- name: configure git identity
# the build script commits the rebuilt binaries inside
# $TCC_FOLDER as its last step; it needs an identity to do
# that even though we never push this commit anywhere.
run: |
git config --global user.name tccbin-ci
git config --global user.email tccbin-ci@users.noreply.github.com
# This branch's tcc.exe is stale (version 0.9.27, no
# build_source_hash.txt/build_version.txt provenance tracking at
# all - unlike every other actively-maintained platform branch),
# and its bundled lib/libc.dylib is a symlink to
# /System/DriverKit/usr/lib/libSystem.dylib - an intentional
# workaround for macOS Big Sur (see the official build script's
# own "## needed for Big Sur" comment), but that DriverKit path
# no longer exists on current macOS (confirmed via otool: "No
# such file or directory" on this macos-15-intel runner). No CI
# flag can fix a broken symlink baked into the committed binary -
# rebuilding from current tinycc (mob) with the official script
# is the actual fix, reusing this branch's already-committed
# libgc.a (the script's own rsync step preserves it, no GC
# rebuild needed).
# MACOSX_DEPLOYMENT_TARGET pinned to 10.13 (High Sierra, 2017) -
# without it, clang stamps rebuilt binaries with this CI runner's
# own macOS 15 minimum, so the uploaded generic macOS-amd64
# distribution silently couldn't run on older Intel macOS releases
# the existing prebuilt distribution supports (Codex
# pullrequestreview-4780918023 on vlang/tccbin#74). 10.13 is a
# conservative, widely-used baseline for Intel-only distributions;
# the "verify the checked-in tcc.exe" step above now also surfaces
# the CURRENT binary's actual load-command minimum via otool, to
# refine this value against real data rather than a guess.
- name: build tcc.exe from current tinycc (mob)
working-directory: work
env:
TCC_COMMIT: mob
TCC_FOLDER: thirdparty/tcc
CC: clang
MACOSX_DEPLOYMENT_TARGET: "10.13"
run: |
set -eu
# The official script's own carry-forward step
# (`rsync -a thirdparty/tcc.original/lib/build* $TCC_FOLDER/lib/`)
# expects prior lib/build_*.txt provenance files to exist -
# true for every other platform, since they've already been
# through this pipeline at least once. This branch never has
# (confirmed: no build_source_hash.txt/build_version.txt at
# all before this), so the glob matches nothing and rsync
# errors "No such file or directory" on its first-ever run
# through the modern pipeline. Seed one placeholder so the
# glob has something to match; the script overwrites the real
# build_*.txt files with accurate content right after anyway.
touch thirdparty/tcc/lib/build_bootstrap_marker.txt
# repo.or.cz's HTTPS endpoint has been observed to fail
# intermittently in CI (confirmed independently, including from
# a non-CI network). An earlier version of this step fell back
# to the unauthenticated, unencrypted git:// transport when
# HTTPS failed - but that fallback is itself the vulnerability:
# an active on-path attacker can simply block HTTPS to FORCE
# the fallback, then feed tampered tinycc source into a
# compile-and-run pipeline whose output gets uploaded as a
# build artifact. A warning in the log doesn't mitigate that -
# the tampered source still gets built and run either way
# (Codex pullrequestreview-4780817355 on vlang/tccbin#74,
# tightening pullrequestreview-4780178390's earlier retry-then-
# fall-back mitigation). No downgrade path exists anymore - if
# HTTPS is genuinely unreachable, the job fails for real.
#
# A lightweight `git ls-remote` probe succeeding beforehand
# does NOT guarantee the heavier `git clone` the official
# script performs moments later will also succeed - confirmed
# by a real CI failure (run 30200458603): the probe passed on
# retry, but the script's own clone then failed anyway with
# "Failed to connect to repo.or.cz port 443... Couldn't
# connect to server", and with the fallback removed there was
# nothing left to recover with. Retry the ACTUAL build script
# invocation instead of a decoupled proxy check - but only
# when its failure specifically matches a network-connection
# signature, so a genuine build bug (a real gmake failure, a
# configure error) fails fast instead of wasting 3 attempts on
# something retrying will never fix. The script itself does
# `rm -rf tinycc/` unconditionally as its first action, and
# the network operation (git clone) happens before any step
# that mutates $TCC_FOLDER, so retrying the whole script after
# a network failure is safe - no partial state to clean up.
attempt=0
while true; do
attempt=$((attempt + 1))
set +e
build_output=$(bash thirdparty/build_scripts/thirdparty-macos-amd64_tcc.sh 2>&1)
build_code=$?
set -e
echo "$build_output"
if [ "$build_code" -eq 0 ]; then
break
fi
case "$build_output" in
*"Failed to connect"*|*"Couldn't connect to server"*|*"Could not resolve host"*|*"Connection reset by peer"*|*"Recv failure"*|*"Connection timed out"*)
if [ "$attempt" -ge 3 ]; then
echo "::error::repo.or.cz remained unreachable after $attempt attempts (network-connection failure) - failing rather than falling back to the unauthenticated git:// transport. Re-run the job; if this persists, investigate repo.or.cz's HTTPS availability directly."
exit 1
fi
echo "tinycc build attempt $attempt failed due to a network-connection error reaching repo.or.cz - retrying ($attempt/3)..."
sleep 10
;;
*)
echo "::error::tinycc build failed for a reason unrelated to repo.or.cz connectivity - not retrying, since retrying wouldn't fix a real build error."
exit 1
;;
esac
done
thirdparty/tcc/tcc.exe --version
thirdparty/tcc/tcc.exe -v -v
# The official script's own "## needed for Big Sur" symlink
# (thirdparty/tcc/lib/libc.dylib -> /System/DriverKit/usr/lib/
# libSystem.dylib) no longer resolves on current macOS -
# confirmed via otool ("No such file or directory") on this
# macos-15-intel runner. /usr/lib/libSystem.B.dylib is the one
# system library Apple has kept as a real on-disk compatibility
# shim (unlike individual frameworks, which only exist inside
# dyld's shared cache) - repoint at that instead.
ls -la /usr/lib/libSystem.B.dylib || ls -la /usr/lib/ | head -20
rm -f thirdparty/tcc/lib/libc.dylib
ln -s /usr/lib/libSystem.B.dylib thirdparty/tcc/lib/libc.dylib
ls -la thirdparty/tcc/lib/libc.dylib
# There is no official thirdparty-macos-amd64_bdwgc.sh in vlang/v
# (unlike every other actively-maintained platform) - adapted
# directly from thirdparty-macos-arm64_bdwgc.sh, which is
# otherwise architecture-generic (produces whatever architecture
# the host clang natively targets - amd64 here, arm64 there).
# Needed because the branch's old bundled libgc.a is from a much
# older tcc/toolchain and the freshly-rebuilt tcc.exe can't even
# parse it ("unrecognized file type") - the same class of
# old-lib-vs-new-compiler mismatch as windows-amd64's original
# tinyc_getbp issue earlier this session, just without an
# existing community fix to reuse here.
- name: rebuild libgc.a from current bdwgc source
working-directory: work
env:
CC: clang
TCC_FOLDER: thirdparty/tcc
LIBGC_COMMIT: master
# See the "build tcc.exe" step's comment - same deployment-
# target gap Codex flagged applies to this build too (Codex
# pullrequestreview-4780918023 on vlang/tccbin#74).
MACOSX_DEPLOYMENT_TARGET: "10.13"
run: |
set -eu
rm -rf bdwgc/
git clone --quiet https://github.com/ivmai/bdwgc
cd bdwgc/
git checkout --quiet "$LIBGC_COMMIT"
LIBGC_COMMIT_FULL_HASH="$(git rev-parse HEAD)"
# libatomic_ops is cloned from its moving default branch and
# gets compiled into the resulting archive, but the provenance
# below previously recorded only bdwgc's own commit - two
# artifacts could carry an identical libgc_build_source_hash.txt
# while containing different libatomic_ops code, making the
# archive untraceable (Codex pullrequestreview-4780918023 on
# vlang/tccbin#74). Record its resolved commit too.
git clone --quiet https://github.com/bdwgc/libatomic_ops
LIBATOMIC_OPS_COMMIT_FULL_HASH="$(git -C libatomic_ops rev-parse HEAD)"
./autogen.sh
CC="$CC" CFLAGS="-Os -mtune=generic -fPIC" LDFLAGS="-Os -fPIC" ./configure \
--disable-dependency-tracking \
--disable-docs \
--enable-static=yes \
--enable-shared=yes \
--enable-single-obj-compilation \
--enable-gc-debug \
--enable-thread-local-alloc \
--enable-large-config \
--enable-cplusplus \
--with-libatomic-ops=check \
--enable-sigrt-signals
make
cd .libs/
for dname in *.dylib; do
install_name_tool -id "@rpath/${dname}" "$dname"
otool -D "$dname"
done
cd ../..
date > "$TCC_FOLDER/lib/libgc_build_on_date.txt"
echo "$LIBGC_COMMIT_FULL_HASH" > "$TCC_FOLDER/lib/libgc_build_source_hash.txt"
echo "$LIBATOMIC_OPS_COMMIT_FULL_HASH" > "$TCC_FOLDER/lib/libgc_build_libatomic_ops_source_hash.txt"
uname -a > "$TCC_FOLDER/lib/libgc_build_machine_uname.txt"
# The checked-in lib/libgc_build_cmd.txt describes the OLD
# archive's build (a universal x86_64/arm64 configure
# invocation) - if left untouched it would keep claiming that
# for the archive THIS step just produced with a materially
# different, amd64-only configure line, making every uploaded
# artifact's provenance false (Codex pullrequestreview-
# 4780817355 on vlang/tccbin#74). Overwrite it with the actual
# command used above - including MACOSX_DEPLOYMENT_TARGET,
# which materially affects dylib/archive compatibility and
# would otherwise be missing from anyone trying to reproduce
# this exact build from the recorded command (Codex
# pullrequestreview-4781470066 on vlang/tccbin#74).
echo "MACOSX_DEPLOYMENT_TARGET=\"$MACOSX_DEPLOYMENT_TARGET\" CC=\"$CC\" CFLAGS=\"-Os -mtune=generic -fPIC\" LDFLAGS=\"-Os -fPIC\" ./configure --disable-dependency-tracking --disable-docs --enable-static=yes --enable-shared=yes --enable-single-obj-compilation --enable-gc-debug --enable-thread-local-alloc --enable-large-config --enable-cplusplus --with-libatomic-ops=check --enable-sigrt-signals" \
> "$TCC_FOLDER/lib/libgc_build_cmd.txt"
rsync -a bdwgc/.libs/ "$TCC_FOLDER/lib/"
ls -la "$TCC_FOLDER/lib/"
# V's own builder comment ("macOS amd64 tccbin only ships libgc.a -
# no .dylib") describes the OLD, ancient-toolchain-built archive,
# which apparently linked fine under the OLD tcc.exe. Diagnosed
# directly against THIS freshly-rebuilt archive: `nm -g` shows
# `_GC_init` present as a proper defined global symbol, and `ar t`/
# `file` show a well-formed archive with a valid `__.SYMDEF SORTED`
# index - yet tcc still reports "unresolved reference to '_GC_init'"
# linking against it. That rules out a build misconfiguration on our
# side; it's the same tcc archive-parsing limitation already
# documented on macos-arm64 ("tcc on macOS arm64 can leave the
# bundled GC archive symbols unresolved"), just newly surfaced here
# because rebuilding the archive with a modern toolchain produces an
# ar/symdef layout tcc's reader doesn't handle - where the old,
# untouched archive happened to be in a layout it did handle. Same
# fix as arm64: link the dynamic libgc.dylib (produced by the
# `--enable-shared=yes` configure flag above) with an rpath instead,
# and keep the static libgc.a path as an explicit, signature-checked
# XFAIL rather than silently dropping it.
- name: run shared conformance tests (libgc.dylib, dynamic - blocking)
id: dylib_test
working-directory: work
run: |
chmod +x thirdparty/tccbin_tests/run.sh
./thirdparty/tccbin_tests/run.sh thirdparty/tcc/tcc.exe macos -- \
-DGC_BUILTIN_ATOMIC=1 \
-DMPROTECT_VDB=1 \
-I thirdparty/libgc/include \
"$PWD/thirdparty/tcc/lib/libgc.dylib" \
-Wl,-rpath,"$PWD/thirdparty/tcc/lib" \
-ldl -lpthread
# The GC-linked lane above always passes libgc.dylib/rpath to every
# shared test, including crash.c - which doesn't touch the GC at
# all. That never exercises this branch's other documented
# supported path: a plain tcc invocation with no GC library on the
# link line whatsoever. Check that directly, mirroring macos-arm64's
# own equivalent step.
- name: verify no-GC fallback path (plain tcc, no libgc linked)
id: nogc_test
if: ${{ !cancelled() }}
working-directory: work
run: |
thirdparty/tcc/tcc.exe thirdparty/tccbin_tests/shared/crash.c -o /tmp/crash_nogc.exe
if [ ! -x /tmp/crash_nogc.exe ]; then
echo "expected tcc to produce a runnable /tmp/crash_nogc.exe, but it's missing or not executable - tcc likely exited 0 without actually producing a working binary" >&2
exit 1
fi
set +e
/tmp/crash_nogc.exe
code=$?
set -e
# Any nonzero exit isn't enough: if the executable can't even
# start (missing loader, an unresolved dynamic library) the
# shell also reports a nonzero status, which this check would
# wrongly accept as "the expected crash" (Codex
# pullrequestreview-4780817355 on vlang/tccbin#74). crash.c's
# null-pointer dereference is expected to be killed by SIGSEGV
# specifically, which a POSIX shell reports as exit code
# 128+11=139 - verify that exact signal-terminated signature.
if [ "$code" -ne 139 ]; then
echo "expected exit 139 (killed by SIGSEGV) from an unguarded null-pointer dereference, got $code" >&2
exit 1
fi
echo "no-GC compile+crash check passed (killed by SIGSEGV as expected)"
# crash.c exiting 139 only proves SOME crash happened - if a
# broken libtcc1.a/crt or startup path made EVERY program
# segfault immediately (before ever reaching the intentional
# null-pointer dereference), this check would still see exit
# 139 and wrongly call it "expected" (Codex pullrequestreview-
# 4781468264 on the sibling freebsd-amd64 workflow, vlang/
# tccbin#75, same class of gap here). The GC-backed lanes
# can't provide this positive check either, since the static
# one is expected to fail at link time. Compile and run a
# trivial, non-crashing program with the same no-GC flags,
# requiring it to actually complete and exit 0 - proving the
# toolchain can still produce a genuinely working binary, not
# just one that crashes regardless of source.
cat > /tmp/nogc_trivial.c <<'TRIVIALEOF'
int main(void) { return 0; }
TRIVIALEOF
thirdparty/tcc/tcc.exe /tmp/nogc_trivial.c -o /tmp/nogc_trivial.exe
if [ ! -x /tmp/nogc_trivial.exe ]; then
echo "expected tcc to produce a runnable trivial no-GC binary, but it's missing or not executable" >&2
exit 1
fi
set +e
/tmp/nogc_trivial.exe
trivial_code=$?
set -e
if [ "$trivial_code" -ne 0 ]; then
echo "expected the trivial no-GC program to exit 0, got $trivial_code - the no-GC toolchain path may be broken (e.g. crashing at startup) even though the crash.c check above happened to pass" >&2
exit 1
fi
echo "trivial no-GC success check passed (exit=0)"
# tcc cannot yet link this freshly-rebuilt static libgc.a at all
# (see the comment above the dylib lane) - this is a temporary,
# explicit XFAIL, not a blanket continue-on-error, so an unexpected
# failure shape (or an unexpected full pass, meaning tcc's archive
# parsing got fixed) still fails the job for real instead of being
# silently absorbed.
- name: run shared conformance tests (libgc.a, static - XFAIL)
id: static_xfail_test
working-directory: work
if: ${{ !cancelled() }}
run: |
# If the rebuild produced a structurally valid but empty or
# incomplete libgc.a (a real, different regression from a
# broken bdwgc build), both direct probes below would still
# show only "unresolved reference to '_GC_init'" text - the
# exact same shape as tcc's own known archive-parsing
# limitation - and KNOWN_ISSUES.txt would claim the symbol is
# "present and well-formed" even though nothing here ever
# checked that (Codex pullrequestreview-4783389496 on vlang/
# tccbin#74). Validate the rebuilt archive directly via ar/nm
# before treating any link failure against it as the known,
# harmless tcc limitation.
if ! ar t ./thirdparty/tcc/lib/libgc.a >/dev/null 2>&1; then
echo "::error::thirdparty/tcc/lib/libgc.a is not a well-formed archive (ar t failed) right after being rebuilt - this is a real regression in the bdwgc build, not the known tcc archive-parsing limitation."
exit 1
fi
# `grep -q` stops reading as soon as it finds a match, which
# can make `nm` receive SIGPIPE while still writing the rest
# of a large archive's symbol list - under GitHub's bash
# (pipefail enabled), that SIGPIPE-killed `nm` makes the WHOLE
# pipeline report failure even though grep itself found the
# match, so this reported "does not define GC_init" on every
# run regardless of the archive's actual contents (Codex
# pullrequestreview-4783470598 on vlang/tccbin#74 - the exact
# bug that broke run 30234854073, the CI run right after this
# check was first added). Capture the full output first
# (command substitution fully drains the producer, no early
# exit possible) and grep the captured text without -q.
rebuilt_gc_symbols=$(nm -g ./thirdparty/tcc/lib/libgc.a 2>&1) || true
rebuilt_gc_init_defined=$(printf '%s\n' "$rebuilt_gc_symbols" | grep -E '(^| )[Tt] _?GC_init$') || true
if [ -z "$rebuilt_gc_init_defined" ]; then
echo "::error::thirdparty/tcc/lib/libgc.a does not define GC_init right after being rebuilt - this is a real regression in the bdwgc build (missing/incomplete symbols), not the known tcc archive-parsing limitation."
exit 1
fi
set +e
output=$(./thirdparty/tccbin_tests/run.sh ./thirdparty/tcc/tcc.exe macos -- \
-DGC_BUILTIN_ATOMIC=1 -DMPROTECT_VDB=1 \
-I ./thirdparty/libgc/include \
./thirdparty/tcc/lib/libgc.a \
-ldl -lpthread 2>&1)
code=$?
set -e
echo "$output"
if [ "$code" -eq 0 ]; then
echo "::error::Static libgc.a linking now succeeds on macOS amd64 - this lane's special-casing should be removed and folded back into a single blocking lane, this workaround is no longer needed."
exit 1
fi
# The pinned run.sh (c82d3f0..., predates vlang/v#27935's
# stderr-capture fix) only ever prints "(compile error)" with no
# detail, so the summary-line match above can't tell "the known
# GC_init archive-parsing bug" apart from a DIFFERENT static-link
# regression that happens to fail the same two tests (a
# corrupted/wrong-architecture archive, an unrelated compiler
# bug) - Codex pullrequestreview-4780178390 on vlang/tccbin#74.
# Compile BOTH failing tests directly to capture their real tcc
# stderr and assert each is specifically the known unresolved-
# GC-symbol error - checking only one of the two (e.g.
# gc_alloc.c) would let an unrelated regression isolated to the
# OTHER test (hello.c) hide behind this same summary shape and
# pass unnoticed (same class of gap as Codex pullrequestreview-
# 4780815399 on the sibling freebsd-amd64 workflow, vlang/tccbin#75).
set +e
direct_err_gc_alloc=$(./thirdparty/tcc/tcc.exe thirdparty/tccbin_tests/shared/gc_alloc.c \
-DGC_BUILTIN_ATOMIC=1 -DMPROTECT_VDB=1 \
-I ./thirdparty/libgc/include \
./thirdparty/tcc/lib/libgc.a \
-ldl -lpthread \
-o /tmp/gc_alloc_xfail_probe 2>&1)
direct_code_gc_alloc=$?
direct_err_hello=$(./thirdparty/tcc/tcc.exe thirdparty/tccbin_tests/shared/hello.c \
-DGC_BUILTIN_ATOMIC=1 -DMPROTECT_VDB=1 \
-I ./thirdparty/libgc/include \
./thirdparty/tcc/lib/libgc.a \
-ldl -lpthread \
-o /tmp/hello_xfail_probe 2>&1)
direct_code_hello=$?
set -e
# The known bug is that tcc can't parse this WHOLE archive, so
# every GC_* symbol the test references legitimately shows up
# as unresolved together (gc_alloc.c: _GC_init, _GC_malloc,
# _GC_noop1; hello.c: _GC_init, _GC_noop1) - checking for one
# exact symbol name alone isn't enough to also catch a
# DIFFERENT regression that adds a non-GC unresolved symbol
# alongside the known ones (Codex pullrequestreview-4780907186
# on the sibling freebsd-amd64 workflow, vlang/tccbin#75, same
# class of gap). Require GC_init specifically to confirm the
# known root cause, AND reject if any unresolved-reference line
# does NOT match the GC_-prefixed pattern the archive-parsing
# bug produces.
is_known_gc_init() {
# $1 = captured stderr text, $2 = the probe's own exit code.
# A regression could make tcc print the expected GC_init
# diagnostic and then crash/abort rather than exiting
# cleanly with its normal compile-error status - the earlier
# `|| true` discarded that exit code entirely (Codex
# pullrequestreview-4781865117 on the sibling freebsd-amd64
# workflow, vlang/tccbin#75, same class of gap here). A
# POSIX shell reports a signal-terminated process as exit
# code 128+signal; reject those instead of treating any
# nonzero exit as "the expected compile-error status". Also
# reject exit 0: a changed tcc.exe could start printing this
# same diagnostic as noise while still reporting overall
# success - the text alone isn't proof the compile actually
# failed (Codex pullrequestreview-4783389496 on vlang/
# tccbin#74).
if [ "$2" -eq 0 ] || [ "$2" -gt 128 ]; then
return 1
fi
case "$1" in
*"unresolved reference to '_GC_init'"*|*"unresolved reference to 'GC_init'"*) ;;
*) return 1 ;;
esac
# Filtering only "unresolved reference to '...'" lines and
# checking their NAMES misses a wholly DIFFERENT kind of tcc
# error (invalid object, relocation error, etc.) riding
# alongside the known one - such a line simply wouldn't match
# the "unresolved reference to" pattern at extraction, so
# `other` would stay empty and this would wrongly accept the
# XFAIL (Codex pullrequestreview-4781468264 on the sibling
# freebsd-amd64 workflow, vlang/tccbin#75, same class of gap
# here). Match on tcc's actual error-line prefix instead, so
# ANY tcc error line that isn't specifically a GC_-prefixed
# unresolved reference gets caught, not just a differently-
# named one.
other=$(printf '%s\n' "$1" | grep -E '^tcc: error:' | grep -v -E "unresolved reference to '_?GC_[A-Za-z0-9_]+'")
[ -z "$other" ]
}
case "$output" in
*"PASS crash"*"FAIL gc_alloc (compile error)"*"FAIL hello (compile error)"*)
if is_known_gc_init "$direct_err_gc_alloc" "$direct_code_gc_alloc" && is_known_gc_init "$direct_err_hello" "$direct_code_hello"; then
echo "known XFAIL: static libgc.a still can't link gc_alloc.c/hello.c (unresolved GC_init, the known tcc archive-parsing bug), as expected - not blocking CI"
else
echo "::error::libgc.a lane failed with the known summary shape, but at least one of gc_alloc.c/hello.c's direct compile errors does NOT mention an unresolved GC_init reference (or exited abnormally) - this looks like a different, unexpected regression:"
echo "gc_alloc.c: exit=$direct_code_gc_alloc: $direct_err_gc_alloc"
echo "hello.c: exit=$direct_code_hello: $direct_err_hello"
exit 1
fi
;;
*)
echo "::error::Static libgc.a lane failed, but NOT with the known/expected summary (PASS crash, FAIL gc_alloc/hello with compile error) - this looks like a different, unexpected problem and should not be silently treated as the known XFAIL."
exit 1
;;
esac
# This artifact bundles thirdparty/tcc/lib/libgc.a even though the
# XFAIL lane above proves this tcc.exe can't link it - V's existing
# tinyc+boehm path for macOS amd64 still selects that static
# archive (vlib/builtin/builtin_d_gcboehm.c.v), so anyone who
# downloads and installs this as the distribution would hit
# working GC builds turning into silent link failures, despite the
# dynamic dylib lane passing (Codex pullrequestreview-4780918023
# on vlang/tccbin#74). Not pulling libgc.a out of the archive -
# some future consumer may still want it for reference/debugging,
# and V's builder isn't switched to the dylib yet (see this PR's
# earlier reply on that sequencing) - but a downloader must not be
# able to miss this caveat.
# `!cancelled()` stays true after an ORDINARY step failure (it only
# excludes a cancelled run), so a failure in the blocking dylib
# lane or the no-GC check still reached this step - publishing an
# artifact whose bundled KNOWN_ISSUES.txt unconditionally claimed
# "verified working in this build's own CI run" even when this
# run's own validation didn't pass (Codex pullrequestreview-
# 4781470066 on vlang/tccbin#74). Keep publishing on failure too -
# a maintainer debugging a red run still gets a downloadable build
# to reproduce against (mirroring macos-arm64's own rationale for
# `!cancelled()` here) - but make the claim match what THIS run
# actually verified, using the two prior steps' real outcomes.
# The static-link claim below (that libgc.a can't be linked by
# this tcc.exe) was, until now, ALSO printed unconditionally - if
# the static XFAIL step above failed with an unexpected signature,
# or unexpectedly passed, this artifact would still assert the
# known archive-parsing diagnosis as if this run had confirmed it
# (Codex pullrequestreview-4782525602 on vlang/tccbin#74, same
# class of gap as the dylib/no-GC claims fixed just below it).
- name: package rebuilt tcc for upload
if: ${{ !cancelled() }}
working-directory: work
env:
DYLIB_TEST_OUTCOME: ${{ steps.dylib_test.outcome }}
NOGC_TEST_OUTCOME: ${{ steps.nogc_test.outcome }}
STATIC_XFAIL_TEST_OUTCOME: ${{ steps.static_xfail_test.outcome }}
run: |
if [ "$STATIC_XFAIL_TEST_OUTCOME" = "success" ]; then
static_claim="lib/libgc.a (static archive) CANNOT be linked by this tcc.exe - confirmed via a real CI run: tcc reports \"unresolved reference to '_GC_init'\" etc. even though the symbol is present and well-formed in the archive (a tcc Mach-O archive-parsing limitation, the same one already documented on macos-arm64)."
else
static_claim="THIS BUILD'S OWN CI VALIDATION OF THE STATIC libgc.a LINK FAILURE DID NOT CONFIRM THE EXPECTED SIGNATURE (static XFAIL test outcome: $STATIC_XFAIL_TEST_OUTCOME) - do not assume lib/libgc.a's link status from this specific artifact without checking the workflow run's log."
fi
if [ "$DYLIB_TEST_OUTCOME" = "success" ] && [ "$NOGC_TEST_OUTCOME" = "success" ]; then
dylib_claim="Use lib/libgc.dylib with -Wl,-rpath,<path-to-lib> instead; that link path is verified working in this build's own CI run."
else
dylib_claim="THIS BUILD'S OWN CI VALIDATION DID NOT PASS (dylib test: $DYLIB_TEST_OUTCOME, no-GC test: $NOGC_TEST_OUTCOME) - do not treat lib/libgc.dylib as a verified-working link path from this specific artifact. Check the workflow run's log before relying on it."
fi
cat > thirdparty/tcc/KNOWN_ISSUES.txt <<EOF
This is a CI-verification build, not a drop-in replacement for the
committed tccbin-macos-amd64 distribution.
$static_claim $dylib_claim
V's existing builtin_d_gcboehm.c.v for macOS amd64+tinyc still
selects the static libgc.a - do not switch it to the dylib until a
build like this one is committed to the actual distribution branch.
EOF
tar --exclude=.git -czf tccbin-macos-amd64-current.tar.gz thirdparty/tcc
- name: upload rebuilt binaries
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
with:
name: tccbin-macos-amd64-current
path: work/tccbin-macos-amd64-current.tar.gz