Skip to content

ci: add macos-arm64 automation baseline (#91) #122

ci: add macos-arm64 automation baseline (#91)

ci: add macos-arm64 automation baseline (#91) #122

name: build and test (macos-arm64)
on:
push:
branches: [thirdparty-macos-arm64]
pull_request:
branches: [thirdparty-macos-arm64]
permissions:
contents: read
env:
CONTRACT_REPOSITORY: vlang/v
CONTRACT_SHA: 7545e515b434cd399333d43659238427d72e22e7
VC_SHA: dfc458a13ba8923ebc249e262c331f8169aa728b
PUBLISH: 'false'
TCCBIN_PUBLISH: 'false'
concurrency:
group: build-and-test-macos-arm64-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
if: >-
github.repository == 'vlang/tccbin' &&
(
(github.event_name == 'pull_request' &&
github.base_ref == 'thirdparty-macos-arm64') ||
(github.event_name == 'push' &&
github.ref == 'refs/heads/thirdparty-macos-arm64')
)
runs-on: macos-15
timeout-minutes: 30
steps:
# thirdparty/tccbin_tests (the shared cross-platform conformance
# suite - see its README) and thirdparty/libgc/include (gc.h,
# needed to compile the shared tests even though this branch
# ships prebuilt libgc.a/libgc.dylib) both live in the main v
# repo, not here. vlib/v/compiler_errors_test.v is a sentinel
# file build.sh checks for, to confirm it's being run from a
# real v repo checkout. Only thirdparty/libgc/include is needed:
# this branch reuses its own prebuilt libgc.a/libgc.dylib rather
# than compiling thirdparty/libgc/gc.c from source.
- name: checkout v (for thirdparty/libgc and thirdparty/tccbin_tests)
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
repository: vlang/v
ref: ${{ env.CONTRACT_SHA }}
path: work
fetch-depth: 0
persist-credentials: false
- name: checkout immutable VC bootstrap snapshot
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
repository: vlang/vc
ref: ${{ env.VC_SHA }}
path: .tccbin-bootstrap-vc
fetch-depth: 0
persist-credentials: false
- name: bootstrap contract-bound validator
shell: bash
run: |
set -euo pipefail
validator_work="$RUNNER_TEMP/tccbin-validator-work"
test ! -e "$validator_work"
git -C work config --local core.autocrlf false
git -C .tccbin-bootstrap-vc config --local core.autocrlf false
work/thirdparty/tccbin_automation/bootstrap/bootstrap.sh \
"$GITHUB_WORKSPACE/work" "$CONTRACT_REPOSITORY" "$CONTRACT_SHA" \
"$GITHUB_WORKSPACE/.tccbin-bootstrap-vc" "$validator_work"
{
echo "TCCBIN_VALIDATOR_CLI=$validator_work/tccbin-automation"
echo "TCCBIN_VALIDATOR_CONTRACT_ROOT=$validator_work/contract-source"
} >> "$GITHUB_ENV"
# this branch, checked out where build.sh expects to find itself
# (work/thirdparty/tcc), matching a normal local checkout layout.
- name: checkout this branch
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: ${{ github.sha }}
path: work/thirdparty/tcc
fetch-depth: 0
persist-credentials: false
- name: validate baseline manifest and immutable contract binding
shell: bash
run: |
set -euo pipefail
git -C work/thirdparty/tcc config --local core.autocrlf false
bash work/thirdparty/tcc/automation/probes/baseline-contract.sh \
"$TCCBIN_VALIDATOR_CLI" "$TCCBIN_VALIDATOR_CONTRACT_ROOT" \
"$GITHUB_WORKSPACE/work/thirdparty/tcc" macos-arm64 \
thirdparty-macos-arm64 "$CONTRACT_SHA" "$GITHUB_SHA" \
"$GITHUB_WORKSPACE/.tccbin-bootstrap-vc"
# This branch DISTRIBUTES the checked-in tcc.exe/libgc.a/libgc.dylib
# directly - that's what V's own builder and anyone else using this
# branch actually downloads and runs. The "build tcc.exe" step below
# REBUILDS tcc.exe from source (a different check: can we still
# produce a working compiler from source), which overwrites the
# checked-in binary before anything else touches it - so a corrupted
# or broken committed tcc.exe/libtcc1.a could still pass CI as long
# as the *rebuild* happens to work. Test the actual checked-in
# binary here first, before build.sh overwrites it.
- name: verify the checked-in tcc.exe (as distributed, before rebuild)
working-directory: work
run: |
thirdparty/tccbin_tests/run.sh thirdparty/tcc/tcc.exe macos -- \
-DGC_BUILTIN_ATOMIC=1 \
-DGC_THREADS=1 \
-DMPROTECT_VDB=1 \
-I thirdparty/libgc/include \
"$PWD/thirdparty/tcc/lib/libgc.dylib" \
-Wl,-rpath,"$PWD/thirdparty/tcc/lib" \
-ldl -lpthread
# Pin the rebuild to the tinycc revision this branch already
# records as having produced its committed tcc.exe, instead of
# floating on build.sh's own unpinned "mob" default - otherwise
# this is a reproducibility check in name only, since a run
# today and a run tomorrow could rebuild against two different
# upstream commits.
- name: resolve pinned TCC revision
working-directory: work
run: echo "TCC_COMMIT=$(cat thirdparty/tcc/build_source_hash.txt)" >> "$GITHUB_ENV"
- name: install build dependencies
run: brew install make
- name: configure git identity
# build.sh 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
- name: build tcc.exe (reuses this branch's already-built libgc.a/libgc.dylib)
working-directory: work
env:
TCC_FOLDER: thirdparty/tcc
CC: clang
run: bash thirdparty/tcc/build.sh
# TCC on this platform was configured with a *relative* runtime
# library path (`thirdparty/tcc/lib`, resolved against the
# process's cwd at invocation time, not against tcc.exe's own
# location - confirmed by running `tcc.exe -v -v` and by the
# fact that V's own builder (vlib/v/builder/cc.v) os.chdir()s to
# vroot before invoking bundled tcc for exactly this reason).
# So every tcc invocation below runs with cwd=work and refers to
# the compiler as the relative path `thirdparty/tcc/tcc.exe` -
# an absolute path from a different cwd silently fails to find
# libtcc1.a (tcc still exits 0, so this is easy to miss without
# checking the actual test output).
#
# The extra -D/-l flags below (GC_THREADS, MPROTECT_VDB, -ldl,
# -lpthread) mirror what V's own builder actually adds for this
# exact platform (vlib/builtin/builtin_d_gcboehm.c.v) - without
# them this suite would validate a different, thread/mprotect-
# unaware build configuration than what real `v -gc boehm`
# programs get here.
- name: run shared conformance tests (libgc.dylib, dynamic - blocking)
working-directory: work
run: |
thirdparty/tccbin_tests/run.sh thirdparty/tcc/tcc.exe macos -- \
-DGC_BUILTIN_ATOMIC=1 \
-DGC_THREADS=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 (README.md: "limited to programs compiled with
# `-gc none`"): a plain tcc invocation with no GC library on the
# link line whatsoever. Check that directly, since a regression
# there could go unnoticed while every existing lane stays green.
- name: verify no-GC fallback path (plain tcc, no libgc linked)
# Run even if the lane above failed: this checks a genuinely
# independent code path (no libgc at all), so a failure there
# shouldn't hide whether *this* path still works, and vice
# versa. !cancelled() (rather than always()) still skips on a
# cancelled run, but the job's overall conclusion stays
# "failure" if an earlier non-continue-on-error step failed,
# regardless of what runs after it.
if: ${{ !cancelled() }}
working-directory: work
run: |
thirdparty/tcc/tcc.exe thirdparty/tccbin_tests/shared/crash.c -o /tmp/crash_nogc.exe
# tcc can exit 0 without leaving a runnable binary (see the
# relative-libtcc1.a-lookup comment above) - `if /tmp/crash_nogc.exe`
# alone can't tell "ran and exited nonzero as expected" apart from
# "doesn't exist/isn't executable", since bash reports both as a
# nonzero if-condition and this script would silently print success
# either way. Check the binary actually exists first.
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
if [ "$code" -eq 0 ]; then
echo "expected a nonzero exit from an unguarded null-pointer dereference, got 0" >&2
exit 1
fi
echo "no-GC compile+crash check passed (exit=$code)"
- name: run shared conformance tests (libgc.a, static - XFAIL)
working-directory: work
# TCC on macOS arm64 cannot yet link the Clang-built static
# libgc.a (see this branch's README.md) - only the dynamic
# libgc.dylib path above is supported today. This lane is a
# temporary, explicit XFAIL - but a blanket continue-on-error
# would accept ANY failure here, including run.sh going missing,
# a bad checkout, or a wholly different compile/link error (this
# actually happened once already in this workflow's own history:
# a missing working-directory made this step fail with "No such
# file or directory" for two full CI runs, and continue-on-error
# silently absorbed it as if it were the known XFAIL). So instead
# of continue-on-error, this captures the output and asserts it
# matches the *specific* known failure signature before accepting
# it - an unexpected full pass (static linking got fixed) or any
# other failure shape both fail the job for real.
if: ${{ !cancelled() }}
run: |
set +e
output=$(thirdparty/tccbin_tests/run.sh thirdparty/tcc/tcc.exe macos -- \
-DGC_BUILTIN_ATOMIC=1 \
-DGC_THREADS=1 \
-DMPROTECT_VDB=1 \
-I thirdparty/libgc/include \
"$PWD/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 arm64 - this lane's special-casing should be removed and folded back into a single blocking lane, this workaround is no longer needed."
exit 1
elif [ "$code" -ne 1 ]; then
echo "::error::Static libgc.a XFAIL returned exit $code; expected TCC compile-error exit 1 exactly."
exit 1
fi
set +e
direct_err_gc_alloc=$(thirdparty/tcc/tcc.exe thirdparty/tccbin_tests/shared/gc_alloc.c \
-DGC_BUILTIN_ATOMIC=1 \
-DGC_THREADS=1 \
-DMPROTECT_VDB=1 \
-I thirdparty/libgc/include \
"$PWD/thirdparty/tcc/lib/libgc.a" \
-ldl -lpthread \
-o /tmp/gc_alloc_arm64_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 \
-DGC_THREADS=1 \
-DMPROTECT_VDB=1 \
-I thirdparty/libgc/include \
"$PWD/thirdparty/tcc/lib/libgc.a" \
-ldl -lpthread \
-o /tmp/hello_arm64_xfail_probe 2>&1)
direct_code_hello=$?
set -e
is_known_gc_init() {
# TCC's compile-error exit is exactly 1. Every nonempty stderr
# line must be one complete unresolved GC_* diagnostic; require
# GC_init specifically so an unrelated missing symbol cannot
# masquerade as the known archive-parsing limitation.
if [ "$2" -ne 1 ]; then
return 1
fi
stderr_lines=$(printf '%s\n' "$1" | sed '/^$/d')
[ -n "$stderr_lines" ] || return 1
unexpected=$(printf '%s\n' "$stderr_lines" | grep -Ev "^tcc: error: unresolved reference to '_?GC_[A-Za-z0-9_]+'$" || true)
[ -z "$unexpected" ] || return 1
printf '%s\n' "$stderr_lines" | grep -E "^tcc: error: unresolved reference to '_?GC_init'$" >/dev/null
}
pass_count=$(printf '%s\n' "$output" | grep -Ec '^PASS ' || true)
fail_count=$(printf '%s\n' "$output" | grep -Ec '^FAIL ' || true)
final_summary=$(printf '%s\n' "$output" | sed '/^$/d' | tail -n 1)
case "$output" in
*"PASS crash"*"FAIL gc_alloc (compile error:"*"FAIL hello (compile error:"*)
if [ "$pass_count" -ne 1 ] || [ "$fail_count" -ne 2 ] || [ "$final_summary" != '1 passed, 2 failed' ]; then
echo "::error::Static libgc.a summary counts/final line differ from the exact known XFAIL (PASS=1, FAIL=2, final='1 passed, 2 failed')."
exit 1
fi
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/hello (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 signature (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
tccbin-branch-gate:
name: tccbin-branch-gate
if: >-
always() &&
github.repository == 'vlang/tccbin' &&
(
(github.event_name == 'pull_request' &&
github.base_ref == 'thirdparty-macos-arm64') ||
(github.event_name == 'push' &&
github.ref == 'refs/heads/thirdparty-macos-arm64')
)
needs: [build]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- name: require successful baseline validation
shell: bash
env:
BUILD_RESULT: ${{ needs.build.result }}
run: test "$BUILD_RESULT" = success