ci(freebsd-amd64): accept 'unresolved reference to' wording in etext/end XFAIL matcher #106
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: build and test (freebsd-amd64) | |
| on: | |
| push: | |
| branches: [thirdparty-freebsd-amd64] | |
| pull_request: | |
| branches: [thirdparty-freebsd-amd64] | |
| workflow_dispatch: {} | |
| jobs: | |
| build: | |
| runs-on: ubuntu-latest | |
| # `github.sha` and `github.event.repository.clone_url` are used | |
| # uniformly across all 3 trigger event types, and correctly cover | |
| # each: | |
| # - pull_request: `github.sha` is GitHub's own synthesized MERGE | |
| # commit (what the target branch would actually look like if | |
| # this PR were merged right now), not the PR branch's own head - | |
| # testing only the head tree in isolation could pass a PR whose | |
| # OWN commit works fine, while the actual merge result (combined | |
| # with whatever the target branch has since gained) is broken | |
| # (Codex pullrequestreview-4781865117 on vlang/tccbin#75). That | |
| # merge commit exists ONLY as an object in the base repo | |
| # (vlang/tccbin, exposed via `refs/pull/<n>/merge`) - never in a | |
| # contributor's fork - so this must clone from | |
| # `github.event.repository.clone_url` (the base repo), not the | |
| # fork's URL, regardless of where the PR originated. | |
| # - push/workflow_dispatch: `github.sha`/`github.event.repository` | |
| # are simply the pushed/dispatched commit and the repo it lives | |
| # in - already exactly what should be tested. | |
| # In every case, `github.sha` is fixed for this run's entire | |
| # lifetime once the triggering event fires (a later push creates a | |
| # brand new run with its own new `github.sha`, it doesn't mutate | |
| # this one) - so this is exactly as race-safe against a | |
| # mid-queue push as the earlier head.sha-based approach was (Codex | |
| # pullrequestreview-4780048339 on vlang/tccbin#75), while also | |
| # testing the actual merge result instead of the head tree alone. | |
| # Set at job level (matching update_tccbin.yml's own working BSD | |
| # job env, which only ever references github.*/secrets.*, never | |
| # steps.* - step-level env on the "Start VM" step was tried first | |
| # and empirically did NOT get forwarded into the VM by | |
| # environment_variables, unlike this job-level placement). | |
| env: | |
| TCCBIN_CLONE_URL: ${{ github.event.repository.clone_url }} | |
| TCCBIN_CLONE_SHA: ${{ github.sha }} | |
| steps: | |
| # cross-platform-actions/action boots a real FreeBSD VM inside this | |
| # Linux runner - the same action vlang/v's own update_tccbin.yml | |
| # uses to rebuild this branch's binaries. Cloning both repos | |
| # directly inside the VM (rather than actions/checkout on the | |
| # runner + sync_files) mirrors that proven-working pattern instead | |
| # of relying on an unverified file-permission-preserving sync. | |
| # Pinned to the v1.3.0 tag's commit (resolved via the GitHub API) | |
| # rather than the mutable tag itself, so a retag upstream can't | |
| # silently swap in different third-party code here without a | |
| # review-visible diff (Codex pullrequestreview-4780048339 on | |
| # vlang/tccbin#75). | |
| - name: Start freebsd-amd64 VM | |
| uses: cross-platform-actions/action@4347c661239bf5dcd0cd77708061488d907343d6 # v1.3.0 | |
| with: | |
| operating_system: freebsd | |
| version: '15.1' | |
| memory: 4G | |
| shell: sh | |
| sync_files: runner-to-vm | |
| environment_variables: TCCBIN_CLONE_URL TCCBIN_CLONE_SHA | |
| - name: fetch shared conformance suite and this branch's binaries | |
| shell: cpa.sh {0} | |
| run: | | |
| set -eu | |
| sudo pkg install -y bash git | |
| # Fetch the exact commit that triggered this run, not a branch | |
| # name - `git clone --branch <name>` would resolve whatever | |
| # that name points to *at clone time*, which can race a push | |
| # that lands after this run started (Codex pullrequestreview- | |
| # 4780048339 on vlang/tccbin#75). | |
| mkdir -p thirdparty/tcc && cd thirdparty/tcc | |
| git init -q | |
| git remote add origin "$TCCBIN_CLONE_URL" | |
| git fetch --depth=1 origin "$TCCBIN_CLONE_SHA" | |
| git checkout -q FETCH_HEAD | |
| cd ../.. | |
| # thirdparty/libgc/include (gc.h) and thirdparty/tccbin_tests | |
| # (the shared cross-platform conformance suite) both live in | |
| # the main v repo, not here. A plain `git clone --depth=1` | |
| # only ever contains the CURRENT tip commit's objects - once | |
| # vlang/v advances past this pinned SHA, that historical commit | |
| # object won't exist in a fresh shallow clone and this checkout | |
| # would fail (Codex P1, pullrequestreview-4780048339 on | |
| # vlang/tccbin#75, line 60). Fetch that exact SHA directly | |
| # instead of cloning HEAD and hoping it's still there. | |
| mkdir -p vsrc && cd vsrc | |
| git init -q | |
| git remote add origin https://github.com/vlang/v.git | |
| git sparse-checkout init --no-cone | |
| git sparse-checkout set thirdparty/libgc thirdparty/tccbin_tests | |
| git fetch --filter=blob:none --depth=1 origin c82d3f08271e9324d6fb8de3c251e9c0e1a9154b | |
| git checkout -q FETCH_HEAD | |
| cd .. | |
| chmod +x thirdparty/tcc/tcc.exe | |
| # tcc on FreeBSD cannot currently link the bundled libgc.a: its | |
| # linker only ever defines the glibc-style _etext/_edata/_end | |
| # (see tccelf.c's tcc_add_linker_symbols), never the plain BSD- | |
| # style etext/edata/end that BDWGC's FreeBSD data-segment-scanning | |
| # code references directly - so any GC-dependent program fails | |
| # with "undefined symbol 'etext'"/"'end'" (or, on the tcc build | |
| # currently on this branch, "unresolved reference to 'etext'"/ | |
| # "'end'" - same underlying bug, tcc has used both diagnostic | |
| # wordings for an unresolved-symbol linker error across versions). | |
| # Root-caused and a fix | |
| # verified against a real FreeBSD VM (patched tcc successfully | |
| # builds+runs shared/hello.c against this same libgc.a); the fix | |
| # is being submitted upstream to tinycc-devel but isn't in any | |
| # shipped tcc.exe yet, so it can't be relied on here. | |
| # | |
| # crash.c doesn't touch the GC at all, so it's checked directly | |
| # (bypassing run.sh, which would otherwise apply the same GC | |
| # flags to every test) as the real, blocking regression check for | |
| # this platform's tcc/libtcc1.a/crt pairing. | |
| - name: run shared conformance tests (crash only, no GC - blocking) | |
| shell: cpa.sh {0} | |
| run: | | |
| set -eu | |
| thirdparty/tcc/tcc.exe vsrc/thirdparty/tccbin_tests/shared/crash.c \ | |
| -o /tmp/crash_nogc | |
| set +e | |
| /tmp/crash_nogc | |
| code=$? | |
| set -e | |
| # Any nonzero exit isn't enough: an exec-format error, a | |
| # missing dynamic loader, or an unrelated miscompilation that | |
| # just exit(1)s would also satisfy that and silently mask a | |
| # real regression, since the GC-backed suite below is | |
| # non-blocking. 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, not just "nonzero". | |
| 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 vlang/tccbin#75). The GC-backed lane below | |
| # can't provide this positive check either, since it's | |
| # 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 | |
| set +e | |
| /tmp/nogc_trivial | |
| 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)" | |
| # continue-on-error alone would also swallow an unrelated failure | |
| # (a corrupted libgc.a, a bad checkout, run.sh going missing) as if | |
| # it were the known etext/end failure, and the blocking crash-only | |
| # step above never references libgc.a at all - so a real | |
| # regression in the archive itself could pass CI unnoticed (Codex | |
| # pullrequestreview-4780048339 on vlang/tccbin#75, line 107). | |
| # Assert the exact known failure signature instead, mirroring | |
| # macos-arm64's own libgc.a XFAIL lane: any other failure shape | |
| # (or an unexpected full pass, meaning the etext/end fix landed) | |
| # fails the job for real. | |
| - name: run shared conformance tests (libgc.a - XFAIL until the etext/end tcc fix ships) | |
| shell: cpa.sh {0} | |
| run: | | |
| set +e | |
| output=$(bash vsrc/thirdparty/tccbin_tests/run.sh "$PWD/thirdparty/tcc/tcc.exe" freebsd -- \ | |
| -DGC_BUILTIN_ATOMIC=1 -DBUS_PAGE_FAULT=T_PAGEFLT -DALL_INTERIOR_POINTERS=1 \ | |
| -I "$PWD/vsrc/thirdparty/libgc/include" \ | |
| "$PWD/thirdparty/tcc/lib/libgc.a" \ | |
| -lpthread 2>&1) | |
| code=$? | |
| set -e | |
| echo "$output" | |
| if [ "$code" -eq 0 ]; then | |
| echo "expected the known etext/end unresolved-symbol failure, but this lane fully passed - the upstream tcc fix must have landed; remove this XFAIL special-casing and fold it back into a single blocking lane." >&2 | |
| exit 1 | |
| fi | |
| # The pinned run.sh only ever prints "(compile error)" with no | |
| # detail, so the summary-line match above can't tell "the known | |
| # etext/end bug" apart from a DIFFERENT static-link regression | |
| # that happens to fail the same two tests - e.g. a PR that | |
| # replaces libgc.a with a valid-but-incomplete archive lacking | |
| # the GC objects entirely: crash.c still compiles (it never | |
| # references GC symbols) while gc_alloc/hello both report | |
| # "compile error", matching this same summary shape for an | |
| # unrelated reason (Codex pullrequestreview-4780219278 on | |
| # vlang/tccbin#75). Compile BOTH failing tests directly to | |
| # capture their real tcc stderr and assert each is specifically | |
| # the known etext/end unresolved-symbol error - checking only | |
| # one of the two (e.g. hello.c) would let an unrelated | |
| # regression isolated to the OTHER test (gc_alloc.c) hide | |
| # behind this same summary shape and pass unnoticed (Codex | |
| # pullrequestreview-4780815399 on vlang/tccbin#75). | |
| set +e | |
| direct_err_hello=$(thirdparty/tcc/tcc.exe vsrc/thirdparty/tccbin_tests/shared/hello.c \ | |
| -DGC_BUILTIN_ATOMIC=1 -DBUS_PAGE_FAULT=T_PAGEFLT -DALL_INTERIOR_POINTERS=1 \ | |
| -I vsrc/thirdparty/libgc/include \ | |
| thirdparty/tcc/lib/libgc.a \ | |
| -lpthread \ | |
| -o /tmp/hello_xfail_probe 2>&1) | |
| direct_code_hello=$? | |
| direct_err_gc_alloc=$(thirdparty/tcc/tcc.exe vsrc/thirdparty/tccbin_tests/shared/gc_alloc.c \ | |
| -DGC_BUILTIN_ATOMIC=1 -DBUS_PAGE_FAULT=T_PAGEFLT -DALL_INTERIOR_POINTERS=1 \ | |
| -I vsrc/thirdparty/libgc/include \ | |
| thirdparty/tcc/lib/libgc.a \ | |
| -lpthread \ | |
| -o /tmp/gc_alloc_xfail_probe 2>&1) | |
| direct_code_gc_alloc=$? | |
| set -e | |
| # Unlike macos-amd64's whole-archive-unparseable bug, this | |
| # FreeBSD bug is narrow: tcc's linker only ever fails to define | |
| # the two plain BSD-style symbols (etext, end) that BDWGC | |
| # references directly - nothing else in the archive is | |
| # affected. So an exact match here is the correct check: reject | |
| # if any unresolved-reference line names a symbol OTHER than | |
| # etext/end, since that would mean a genuinely different, | |
| # additional regression rode along with the known bug (Codex | |
| # pullrequestreview-4780907186 on vlang/tccbin#75). | |
| is_known_etext_end() { | |
| # $1 = captured stderr text, $2 = the probe's own exit code. | |
| # A regression could make tcc print the expected etext/end | |
| # diagnostic and then crash/abort rather than exiting | |
| # cleanly with its normal compile-error status - the earlier | |
| # `|| true` discarded that exit code entirely, so a shell | |
| # "Segmentation fault" or similar abnormal termination | |
| # alongside the expected text would still pass (Codex | |
| # pullrequestreview-4781865117 on vlang/tccbin#75). 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 the sibling | |
| # macos-amd64 workflow, vlang/tccbin#74, same class of gap | |
| # here). | |
| if [ "$2" -eq 0 ] || [ "$2" -gt 128 ]; then | |
| return 1 | |
| fi | |
| # Run 30421217468 (2026-07-29) is a confirmed matcher false | |
| # negative, not a real regression: this branch's tcc now | |
| # reports "unresolved reference to 'etext'"/"'end'" instead of | |
| # the "undefined symbol '...'" wording this check originally | |
| # matched - same known bug, different tcc diagnostic wording | |
| # (tcc has used both phrasings for an unresolved-symbol linker | |
| # error across versions/builds). Accept either wording rather | |
| # than replacing one with the other, so a differently-built | |
| # tcc.exe landing here later isn't a second false negative. | |
| case "$1" in | |
| *"undefined symbol 'etext'"*|*"undefined symbol 'end'"*|*"unresolved reference to 'etext'"*|*"unresolved reference to 'end'"*) ;; | |
| *) return 1 ;; | |
| esac | |
| # Filtering only the known-wording 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 either wording | |
| # pattern at extraction, so `other` would stay empty and this | |
| # would wrongly accept the XFAIL (Codex pullrequestreview- | |
| # 4781468264 on vlang/tccbin#75). Match on tcc's actual | |
| # error-line prefix instead, so ANY tcc error line that isn't | |
| # specifically etext/end (under either wording) gets caught, | |
| # not just a differently-named undefined symbol. | |
| other=$(printf '%s\n' "$1" | grep -E '^tcc: error:' | grep -v -E "undefined symbol '(etext|end)'|unresolved reference to '(etext|end)'") | |
| [ -z "$other" ] | |
| } | |
| case "$output" in | |
| *"PASS crash"*"FAIL gc_alloc (compile error)"*"FAIL hello (compile error)"*) | |
| if is_known_etext_end "$direct_err_hello" "$direct_code_hello" && is_known_etext_end "$direct_err_gc_alloc" "$direct_code_gc_alloc"; then | |
| echo "known XFAIL: etext/end still unresolved on FreeBSD tcc for both gc_alloc.c and hello.c, as expected - not blocking CI" | |
| else | |
| echo "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 etext/end symbol (or exited abnormally) - this looks like a different, unexpected regression:" >&2 | |
| echo "hello.c: exit=$direct_code_hello: $direct_err_hello" >&2 | |
| echo "gc_alloc.c: exit=$direct_code_gc_alloc: $direct_err_gc_alloc" >&2 | |
| exit 1 | |
| fi | |
| ;; | |
| *) | |
| echo "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." >&2 | |
| exit 1 | |
| ;; | |
| esac |