ci(freebsd-amd64): flip libgc.a lane from XFAIL to blocking (etext/end fix validated) #108
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 | |
| # HISTORY: tcc on FreeBSD used to be unable to link the bundled | |
| # libgc.a at all - its linker only ever defined 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 failed with "undefined symbol 'etext'"/ | |
| # "'end'" (later builds reworded this to "unresolved reference | |
| # to ..."). That is FIXED upstream as of tinycc 384614a, "tccelf: | |
| # provide complete ELF boundary symbols", which gives the plain | |
| # etext/edata/end names PROVIDE semantics on every ELF target. | |
| # | |
| # The GC lane below is therefore a normal BLOCKING lane now, not an | |
| # XFAIL - see the comment above it. | |
| # | |
| # 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 an independent, blocking regression check | |
| # for this platform's tcc/libtcc1.a/crt pairing - it still catches a | |
| # broken toolchain even if the GC lane were to fail for its own | |
| # unrelated reasons. | |
| - 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. 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). Keep this positive check | |
| # independent of the GC lane below rather than relying on it: | |
| # this lane's whole purpose is to stay meaningful on its own, | |
| # covering the plain no-GC toolchain path that a GC-linked test | |
| # never exercises. 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)" | |
| # BLOCKING as of tinycc 384614a ("tccelf: provide complete ELF | |
| # boundary symbols"), which is what this branch's tcc.exe must now | |
| # be built from or later. This lane was previously a | |
| # signature-checked XFAIL asserting that gc_alloc.c/hello.c MUST | |
| # still fail on unresolved etext/end - correct while the bug was | |
| # live, but it would hard-fail the moment a fixed tcc.exe is | |
| # published here (its own "unexpected full pass" guard fired on | |
| # exactly that), breaking CI for the opposite reason. | |
| # | |
| # This STRENGTHENS the gate rather than relaxing it: a full pass of | |
| # all three shared tests is now the required baseline, where before | |
| # only a very specific failure was accepted. Nothing here is | |
| # downgraded to a warning, and no failure shape is tolerated. | |
| # | |
| # Verified on a real FreeBSD 15.1 VM before flipping: with this | |
| # branch's EXACT libgc.a held byte-identical, the shipped tcc.exe | |
| # (85ba3ae8, predating the fix) fails on unresolved etext/end and | |
| # nothing else, while a tcc.exe rebuilt from tinycc 2be0218b (which | |
| # contains 384614a) reports "3 passed, 0 failed". tcc.exe was the | |
| # only variable between the two. | |
| - name: run shared conformance tests (libgc.a - blocking) | |
| shell: cpa.sh {0} | |
| run: | | |
| set -eu | |
| 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 | |
| # Positive proof of the MECHANISM, not just the outcome. The lane | |
| # above passing means BDWGC linked, but it does not by itself prove | |
| # WHY - a future tcc.exe could conceivably satisfy the suite while | |
| # regressing the boundary symbols BDWGC's FreeBSD data-segment scan | |
| # depends on, or provide them with a nonsensical range. Reference | |
| # all three directly from C: if the linker fails to provide them, | |
| # this simply does not link, which is precisely the original bug. | |
| # | |
| # Deliberately NOT an `nm` symbol-table check - that was tried while | |
| # validating this flip and is the wrong instrument: tcc emits no | |
| # full .symtab by default, and linker-PROVIDEd symbols need not | |
| # appear in the output binary's symbol table at all, so `nm` | |
| # reported all three "absent" for a binary that had just linked and | |
| # run correctly. | |
| - name: verify the linker provides etext/edata/end (blocking) | |
| shell: cpa.sh {0} | |
| if: ${{ !cancelled() }} | |
| run: | | |
| set -eu | |
| cat > /tmp/boundary_probe.c <<'PROBEEOF' | |
| #include <stdio.h> | |
| /* The plain BSD-style names BDWGC's FreeBSD data-segment scan | |
| references, and that tcc did not provide before 384614a. */ | |
| extern char etext[], edata[], end[]; | |
| int main(void) { | |
| printf("etext=%p edata=%p end=%p\n", | |
| (void *)etext, (void *)edata, (void *)end); | |
| /* Sanity-check the advertised range, not merely the | |
| symbols' existence: 384614a is specifically about | |
| boundary symbols being finalized from the ordered | |
| loadable sections, so provided-but-wrong is a distinct | |
| failure mode from not-provided. */ | |
| if (!((void *)etext < (void *)end)) { | |
| fprintf(stderr, "bad boundary range: etext is not below end\n"); | |
| return 1; | |
| } | |
| return 0; | |
| } | |
| PROBEEOF | |
| thirdparty/tcc/tcc.exe /tmp/boundary_probe.c -o /tmp/boundary_probe | |
| /tmp/boundary_probe | |
| echo "confirmed: the linker provides etext/edata/end, and the advertised range is ordered sanely" |