Repository navigation
Polish the sum types. #214
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
| # This file is flagrantly "vibe-coded". It may not be up to the standards of most libCat code. | |
| name: CI | |
| on: | |
| workflow_dispatch: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| permissions: | |
| contents: read | |
| actions: write | |
| jobs: | |
| install-toolchain: | |
| # Prime the mise cache once so parallel jobs do not all download and extract | |
| # the Clang package set on a cold cache in the same workflow run. | |
| runs-on: ubuntu-24.04 | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/setup-toolchain | |
| lscpu: | |
| # Log host CPU and ISA up front so a CPU-dependent failure | |
| # anywhere in the matrix is diagnosable from the run log. The | |
| # `ubuntu-24.04` label does not pin a microarchitecture. In | |
| # practice based on previous runs, the Azure SKU rotates | |
| # across Intel Ice Lake-SP (Xeon Platinum 8370C) and AMD | |
| # EPYC (Milan-era 7763, Genoa-era 9V74) within the same | |
| # hour, with no advance notice and with materially different | |
| # feature flags (AVX-512, AVX-VNNI, GFNI-on-VEX, etc.). | |
| runs-on: ubuntu-24.04 | |
| needs: install-toolchain | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/setup-toolchain | |
| - name: System specs | |
| run: | | |
| set -eu | |
| flags=$(awk -F: '/^flags/ { gsub(/^ +/, "", $2); print $2; exit }' /proc/cpuinfo) | |
| avx512_flags=$(printf '%s\n' ${flags} \ | |
| | grep -Ex 'avx512.*' \ | |
| | sort -u \ | |
| | tr '\n' ' ' || true) | |
| if [ -n "${avx512_flags}" ]; then | |
| printf 'AVX-512: **yes** (%s)\n' "${avx512_flags}" | |
| else | |
| echo 'AVX-512: **no**' | |
| fi | |
| echo "::group::OS / kernel" | |
| if [ -r /etc/os-release ]; then | |
| . /etc/os-release | |
| printf 'distro: %s %s (%s)\n' \ | |
| "${NAME:-?}" "${VERSION:-?}" "${VERSION_CODENAME:-?}" | |
| fi | |
| printf 'kernel: %s\n' "$(uname -srv)" | |
| printf 'arch: %s\n' "$(uname -m)" | |
| echo "::endgroup::" | |
| echo "::group::CPU (lscpu)" | |
| lscpu | grep -E \ | |
| '^(Architecture|Byte Order|Vendor ID|Model name|CPU family|Model|Stepping|CPU\(s\)|Thread|Socket|Core|CPU max MHz)' \ | |
| || true | |
| echo "::endgroup::" | |
| echo "::group::ISA extensions (/proc/cpuinfo flags)" | |
| show() { | |
| label="$1"; pat="$2"; matches= | |
| matches=$(printf '%s\n' ${flags} \ | |
| | grep -Ex "${pat}" \ | |
| | sort -u \ | |
| | tr '\n' ' ' || true) | |
| printf '%-10s %s\n' "${label}:" "${matches:--}" | |
| } | |
| show 'SIMD' 'sse|sse2|pni|ssse3|sse4_1|sse4_2|avx|avx2|avx512.*|f16c|fma|xop|3dnow.*' | |
| show 'Bitmanip' 'bmi1|bmi2|abm|tbm|lzcnt|popcnt|movbe|adx' | |
| show 'Crypto' 'aes|vaes|sha_ni|sha512|pclmulqdq|vpclmulqdq|gfni|rdrand|rdseed' | |
| show 'Other' 'rdtscp|clflushopt|clwb|fsgsbase|cldemote|movdiri|movdir64b|waitpkg|serialize' | |
| echo "::endgroup::" | |
| echo "::group::Clang -march=native predefined feature macros" | |
| # Clang's source of truth for codegen. Cross-check against | |
| # /proc/cpuinfo above when a feature appears in one but not | |
| # the other (e.g. AVX-512 masked off by clang's CPU model). | |
| clang++ -dM -E -x c++ -march=native /dev/null \ | |
| | grep -E '^#define __(SSE|AVX|F16C|FMA|XOP|BMI|LZCNT|POPCNT|MOVBE|AES|VAES|SHA|PCLMUL|VPCLMULQDQ|GFNI|ADX|RDRND|RDSEED|CLFLUSHOPT|CLWB|FSGSBASE|MWAIT|MWAITX|CLDEMOTE|RDTSCP|MOVDIR|WAITPKG|SERIALIZE)' \ | |
| | sort | |
| echo "::endgroup::" | |
| detect-changes: | |
| runs-on: ubuntu-24.04 | |
| outputs: | |
| compile: ${{ steps.compile.outputs.compile }} | |
| gdb_symlink: ${{ steps.gdb-symlink.outputs.gdb_symlink }} | |
| arithmetic_negative: ${{ steps.arithmetic-negative.outputs.arithmetic_negative }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Resolve diff base and changed files | |
| # Materialize the changed-files list once so each detect-* step | |
| # below is a small, self-contained grep against | |
| # `${CAT_CHANGED_FILES}`. `CAT_FORCE_RUN=true` short-circuits | |
| # all detection whenever the diff base cannot be resolved | |
| # (initial push, force-push, shallow fetch, missing 0000... base), | |
| # preserving the original "run everything" fallback. | |
| run: | | |
| set -eu | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| base="${{ github.event.pull_request.base.sha }}" | |
| else | |
| base="${{ github.event.before }}" | |
| fi | |
| changed_files="${RUNNER_TEMP}/cat-changed-files" | |
| if git rev-parse --verify -q "${base}^{commit}" >/dev/null 2>&1; then | |
| force="false" | |
| git diff --name-only "${base}" "${{ github.sha }}" > "${changed_files}" | |
| printf 'changed files (%d):\n' "$(wc -l < "${changed_files}")" | |
| cat "${changed_files}" | |
| else | |
| force="true" | |
| echo "::warning title=Missing diff base::Could not resolve base commit '${base}'; running all detect-* steps." | |
| fi | |
| { | |
| echo "CAT_CHANGED_FILES=${changed_files}" | |
| echo "CAT_FORCE_RUN=${force}" | |
| } >> "${GITHUB_ENV}" | |
| - name: Detect compile changes | |
| id: compile | |
| run: | | |
| set -eu | |
| if [ "${CAT_FORCE_RUN}" = "true" ] || grep -Eq \ | |
| '(^|/)CMakeLists\.txt$|^cmake/|^src/|^tests/|^examples/' \ | |
| "${CAT_CHANGED_FILES}" | |
| then | |
| compile="true" | |
| else | |
| compile="false" | |
| fi | |
| echo "compile=${compile}" >> "${GITHUB_OUTPUT}" | |
| echo "compile changed: ${compile}" | |
| - name: Detect gdb_symlink changes | |
| id: gdb-symlink | |
| run: | | |
| set -eu | |
| if [ "${CAT_FORCE_RUN}" = "true" ] || grep -qx \ | |
| 'cmake/gdb_symlink.cmake' "${CAT_CHANGED_FILES}" | |
| then | |
| gdb_symlink="true" | |
| else | |
| gdb_symlink="false" | |
| fi | |
| echo "gdb_symlink=${gdb_symlink}" >> "${GITHUB_OUTPUT}" | |
| echo "gdb_symlink changed: ${gdb_symlink}" | |
| - name: Detect arithmetic-negative changes | |
| id: arithmetic-negative | |
| run: | | |
| set -eu | |
| if [ "${CAT_FORCE_RUN}" = "true" ] || grep -Eq \ | |
| '^src/libraries/(meta|arithmetic)/|^cmake/cat_arithmetic_neg(ative|_cases|_matrix)\.cmake$|^tests/CMakeLists\.txt$|^\.github/workflows/ci\.yml$' \ | |
| "${CAT_CHANGED_FILES}" | |
| then | |
| arithmetic_negative="true" | |
| else | |
| arithmetic_negative="false" | |
| fi | |
| echo "arithmetic_negative=${arithmetic_negative}" >> "${GITHUB_OUTPUT}" | |
| echo "arithmetic negative changed: ${arithmetic_negative}" | |
| lint: | |
| # Lightweight static checks on the source tree. Runs in parallel | |
| # with the heavier compile / test jobs to gate invariants that don't | |
| # require compiling. | |
| runs-on: ubuntu-24.04 | |
| needs: install-toolchain | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/setup-toolchain | |
| - name: Set lint build directory | |
| run: echo "CAT_LINT_BUILD_DIR=${RUNNER_TEMP}/cat-build-lint" >> "${GITHUB_ENV}" | |
| - name: Verify unique basenames under src/ | |
| # Every file under `src/` must have a unique name. This makes searching | |
| # the codebase easier and is important for the `cat-ir` target. | |
| run: | | |
| set -eu | |
| dups=$(find src -type f \ | |
| ! -name 'CMakeLists.txt' \ | |
| ! -name '*.ld' \ | |
| -printf '%f\n' \ | |
| | sort | uniq -d) | |
| if [ -n "${dups}" ]; then | |
| echo 'WARNING: duplicate basenames under src/:' | |
| while IFS= read -r name; do | |
| printf '\n %s:\n' "${name}" | |
| find src -type f -name "${name}" -printf ' %p\n' | |
| done <<< "${dups}" | |
| echo "::warning title=Duplicate source basenames::Duplicate basenames under src. See this step log for the full file list." | |
| fi | |
| count=$(find src -type f \ | |
| ! -name 'CMakeLists.txt' \ | |
| ! -name '*.ld' \ | |
| | wc -l) | |
| printf 'OK: %d files under src/ have unique basenames\n' "${count}" | |
| - name: Verify src files are listed in src/CMakeLists.txt | |
| run: | | |
| set -eu | |
| missing="${RUNNER_TEMP:-/tmp}/cat-missing-src-cmake-files" | |
| : > "${missing}" | |
| find src -type f -print | sort | while IFS= read -r file; do | |
| if [ "${file#src/libraries/}" != "${file}" ]; then | |
| needle="${file#src/libraries/}" | |
| else | |
| needle="${file#src/}" | |
| fi | |
| if ! grep -F -q -- "${needle}" src/CMakeLists.txt; then | |
| printf '%s\n' "${file}" >> "${missing}" | |
| fi | |
| done | |
| if [ -s "${missing}" ]; then | |
| echo 'FAIL: files under src/ missing from src/CMakeLists.txt:' | |
| while IFS= read -r file; do | |
| printf ' %s\n' "${file}" | |
| done < "${missing}" | |
| exit 1 | |
| fi | |
| count=$(find src -type f -print | wc -l) | |
| printf 'OK: %d files under src/ are mentioned in src/CMakeLists.txt\n' "${count}" | |
| - name: Configure CMake lint targets | |
| # `CAT_BUILD_UNIT_TESTS=OFF` skips `tests/CMakeLists.txt` entirely. | |
| # The lint job only consumes format / comment-style targets and | |
| # never invokes anything from `tests/`, so the test surface is | |
| # dead weight here. | |
| run: | | |
| cmake -S . -B "${CAT_LINT_BUILD_DIR}" -G Ninja \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DCAT_BUILD_ALL_EXAMPLES=OFF \ | |
| -DCAT_BUILD_UNIT_TESTS=OFF \ | |
| -DCAT_PCH=OFF | |
| - name: cat-format-check | |
| # Advisory. Emits a warning annotation instead of gating the merge. Fix | |
| # locally with | |
| # `just format "${CAT_LINT_BUILD_DIR}"`. | |
| run: | | |
| if ! just format-check "${CAT_LINT_BUILD_DIR}" | |
| then | |
| echo "::warning title=cat-format-check::Formatting drift detected! Run the `cat-format` target to fix it." | |
| fi | |
| - name: cat-restyle-comments-check | |
| # Same advisory policy as `cat-format-check` above. | |
| run: | | |
| if ! just restyle-comments-check "${CAT_LINT_BUILD_DIR}" | |
| then | |
| echo "::warning title=cat-restyle-comments-check::Comment styling drift detected! Run the `cat-restyle-comments` target to fix it." | |
| fi | |
| debug: | |
| # Debug build, run twice: once with sanitizers and once without. The | |
| # `build-debug` artifact (sanitizer-instrumented) is uploaded for the | |
| # `gdb-pretty-printers` job so that job can reuse this configuration and PCH. | |
| # `if: ${{ !cancelled() }}` on the nosan step preserves the matrix-style | |
| # behaviour: a failure in the san variant still surfaces results from | |
| # the nosan variant. | |
| runs-on: ubuntu-24.04 | |
| needs: [install-toolchain, detect-changes] | |
| if: ${{ needs.detect-changes.outputs.compile == 'true' }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/setup-toolchain | |
| - name: Configure and build (Debug, sanitizers=ON) | |
| run: | | |
| cmake -S . -B build-debug -G Ninja \ | |
| -DCMAKE_BUILD_TYPE=Debug \ | |
| -DCAT_USE_SANITIZERS=ON \ | |
| -DCAT_GDB_EXECUTABLE=/usr/bin/gdb | |
| cmake --build build-debug | |
| - name: Test (Debug, sanitizers=ON) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| ctest --test-dir build-debug -R '^UnitTests$' --verbose --output-on-failure \ | |
| --timeout 150 | |
| - name: Run example binaries (Debug, sanitizers=ON) | |
| if: ${{ !cancelled() }} | |
| # End-to-end smoke test of the example binaries on top of the | |
| # `_start.cpp` / `<cat/cpuid>` plumbing the unit tests don't exercise: | |
| # `hello` runs the `NO_ARGC_ARGV` + `CAT_NO_STATIC_CONSTRUCTORS` + | |
| # `CAT_NO_CPUID` minimal `_start` path; `echo` runs the | |
| # naked-`_start` argv pickup off the kernel-supplied stack. Same | |
| # check is repeated for the nosan and Release configurations so any | |
| # one of those breaking surfaces here instead of in users' hands. | |
| run: | | |
| set -eu | |
| out=$(build-debug/examples/hello) | |
| printf 'hello (Debug+ASan) output: %s\n' "${out}" | |
| if [ "${out}" != "Meow, world!" ]; then | |
| echo 'FAIL: hello (Debug+ASan) did not produce the expected greeting.' | |
| exit 1 | |
| fi | |
| echo 'OK: hello (Debug+ASan)' | |
| out=$(build-debug/examples/echo foo moooo boo) | |
| printf 'echo foo moooo boo (Debug+ASan) output: %s\n' "${out}" | |
| if [ "${out}" != "foo moooo boo" ]; then | |
| echo 'FAIL: echo (Debug+ASan) output did not match "foo moooo boo".' | |
| exit 1 | |
| fi | |
| echo 'OK: echo foo moooo boo (Debug+ASan)' | |
| - name: Configure and build (Debug, sanitizers=OFF) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| cmake -S . -B build-debug-nosan -G Ninja \ | |
| -DCMAKE_BUILD_TYPE=Debug \ | |
| -DCAT_USE_SANITIZERS=OFF \ | |
| -DCAT_GDB_EXECUTABLE=/usr/bin/gdb | |
| cmake --build build-debug-nosan | |
| - name: Test (Debug, sanitizers=OFF) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| ctest --test-dir build-debug-nosan -R '^UnitTests$' --verbose --output-on-failure \ | |
| --timeout 150 | |
| - name: Run example binaries (Debug, sanitizers=OFF) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| set -eu | |
| out=$(build-debug-nosan/examples/hello) | |
| printf 'hello (Debug, nosan) output: %s\n' "${out}" | |
| if [ "${out}" != "Meow, world!" ]; then | |
| echo 'FAIL: hello (Debug, nosan) did not produce the expected greeting.' | |
| exit 1 | |
| fi | |
| echo 'OK: hello (Debug, nosan)' | |
| out=$(build-debug-nosan/examples/echo foo moooo boo) | |
| printf 'echo foo moooo boo (Debug, nosan) output: %s\n' "${out}" | |
| if [ "${out}" != "foo moooo boo" ]; then | |
| echo 'FAIL: echo (Debug, nosan) output did not match "foo moooo boo".' | |
| exit 1 | |
| fi | |
| echo 'OK: echo foo moooo boo (Debug, nosan)' | |
| - name: Upload Debug build for GDB pretty printers | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: build-debug | |
| path: build-debug | |
| include-hidden-files: true | |
| retention-days: 1 | |
| - name: Verify .gdbinit symlink (Debug, Ninja generator non-multi) | |
| if: ${{ needs.detect-changes.outputs.gdb_symlink == 'true' }} | |
| run: | | |
| set -eu | |
| if [ ! -f build-debug/.gdbinit ]; then | |
| echo "FAIL: missing build-debug/.gdbinit" | |
| ls -la build-debug/ || true | |
| exit 1 | |
| fi | |
| echo "OK: build-debug/.gdbinit" | |
| release: | |
| # Release build and tests. The build dir is uploaded for the | |
| # `gdb-pretty-printers` job so that job can reuse this configuration and PCH. | |
| runs-on: ubuntu-24.04 | |
| needs: [install-toolchain, detect-changes] | |
| if: ${{ needs.detect-changes.outputs.compile == 'true' }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/setup-toolchain | |
| - name: Configure and build (Release) | |
| run: | | |
| cmake -S . -B build-release -G Ninja \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DCAT_GDB_EXECUTABLE=/usr/bin/gdb | |
| cmake --build build-release | |
| - name: Test (Release) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| ctest --test-dir build-release -R '^UnitTests$' --verbose --output-on-failure \ | |
| --timeout 150 | |
| - name: Run example binaries (Release) | |
| if: ${{ !cancelled() }} | |
| # See the Debug job's matching step for what these exercise. | |
| run: | | |
| set -eu | |
| out=$(build-release/examples/hello) | |
| printf 'hello (Release) output: %s\n' "${out}" | |
| if [ "${out}" != "Meow, world!" ]; then | |
| echo 'FAIL: hello (Release) did not produce the expected greeting.' | |
| exit 1 | |
| fi | |
| echo 'OK: hello (Release)' | |
| out=$(build-release/examples/echo foo moooo boo) | |
| printf 'echo foo moooo boo (Release) output: %s\n' "${out}" | |
| if [ "${out}" != "foo moooo boo" ]; then | |
| echo 'FAIL: echo (Release) output did not match "foo moooo boo".' | |
| exit 1 | |
| fi | |
| echo 'OK: echo foo moooo boo (Release)' | |
| - name: Upload Release build for GDB pretty printers | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: build-release | |
| path: build-release | |
| include-hidden-files: true | |
| retention-days: 1 | |
| - name: Verify `hello` is 231 bytes | |
| if: ${{ !cancelled() }} | |
| # Tripwire on the size of the canonical libCat size-demo example after | |
| # `sstrip`. `hello` is the most-stripped libCat binary (no `argc`/ | |
| # `argv`, no `.init_array`, no cpuid init, single `cat::print`), so any | |
| # byte-level regression in the `_start.cpp` plumbing or the linker | |
| # flag set in `CAT_LINK_OPTIONS_RELEASE` shows up here. Runs after the | |
| # GDB artifact upload so the unstripped `hello` is preserved for the | |
| # downstream pretty-printers job. If a libCat change intentionally | |
| # changes this size, bump `CAT_HELLO_SSTRIPPED_SIZE` below. | |
| run: | | |
| set -eu | |
| CAT_HELLO_SSTRIPPED_SIZE=231 | |
| just strip release hello | |
| actual=$(wc -c < build-release/examples/hello) | |
| if [ "${actual}" -ne "${CAT_HELLO_SSTRIPPED_SIZE}" ]; then | |
| echo "::error title=hello size regression::expected ${CAT_HELLO_SSTRIPPED_SIZE} bytes, got ${actual}" | |
| echo 'If this change is intentional, update CAT_HELLO_SSTRIPPED_SIZE in .github/workflows/ci.yml.' | |
| exit 1 | |
| fi | |
| echo "OK: hello sstripped size = ${actual} bytes." | |
| - name: Verify `echo` is 335 bytes | |
| if: ${{ !cancelled() }} | |
| run: | | |
| set -eu | |
| CAT_ECHO_SSTRIPPED_SIZE=335 | |
| just strip release echo | |
| actual=$(wc -c < build-release/examples/echo) | |
| if [ "${actual}" -ne "${CAT_ECHO_SSTRIPPED_SIZE}" ]; then | |
| echo "::error title=echo size regression::expected ${CAT_ECHO_SSTRIPPED_SIZE} bytes, got ${actual}" | |
| echo 'If this change is intentional, update CAT_ECHO_SSTRIPPED_SIZE in .github/workflows/ci.yml.' | |
| exit 1 | |
| fi | |
| echo "OK: echo sstripped size = ${actual} bytes." | |
| - name: Verify .gdbinit symlink (Release, Ninja generator non-multi) | |
| if: ${{ needs.detect-changes.outputs.gdb_symlink == 'true' }} | |
| run: | | |
| set -eu | |
| if [ ! -f build-release/.gdbinit ]; then | |
| echo "FAIL: missing build-release/.gdbinit" | |
| ls -la build-release/ || true | |
| exit 1 | |
| fi | |
| echo "OK: build-release/.gdbinit" | |
| - name: Configure and build (Release, sanitizers=ON) | |
| if: ${{ !cancelled() }} | |
| # Release + ASan is *the* combo that exercises Release-only link | |
| # options on a dynamic-PIE executable. | |
| run: | | |
| cmake -S . -B build-release-asan -G Ninja \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DCAT_USE_SANITIZERS=ON \ | |
| -DCAT_GDB_EXECUTABLE=/usr/bin/gdb | |
| cmake --build build-release-asan | |
| - name: Verify Release+ASan executable has PT_INTERP | |
| if: ${{ !cancelled() }} | |
| run: | | |
| set -eu | |
| if ! readelf -l build-release-asan/tests/unit_tests \ | |
| | grep -q INTERP; then | |
| echo 'FAIL: Release+ASan unit_tests is missing PT_INTERP.' | |
| echo 'Likely cause: a Release-only link option (e.g. omagic' | |
| echo '`-Wl,-N`) is being applied to dynamic-PIE binaries.' | |
| readelf -l build-release-asan/tests/unit_tests | |
| exit 1 | |
| fi | |
| echo 'OK: Release+ASan unit_tests has PT_INTERP.' | |
| - name: Test (Release, sanitizers=ON) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| ctest --test-dir build-release-asan -R '^UnitTests$' --verbose --output-on-failure \ | |
| --timeout 150 | |
| - name: Run example binaries (Release, sanitizers=ON) | |
| if: ${{ !cancelled() }} | |
| # Same example smoke as the Debug+ASan and Release jobs. The | |
| # `_start.cpp` + `<cat/cpuid>` plumbing crosses the most config | |
| # combinations here (Release link option set, asan runtime | |
| # loaded by `ld.so`, `BIND_NOW` eager binding active), so any | |
| # regression in that surface shows up in this step in addition | |
| # to the unit-tests step above. | |
| run: | | |
| set -eu | |
| out=$(build-release-asan/examples/hello) | |
| printf 'hello (Release+ASan) output: %s\n' "${out}" | |
| if [ "${out}" != "Meow, world!" ]; then | |
| echo 'FAIL: hello (Release+ASan) did not produce the expected greeting.' | |
| exit 1 | |
| fi | |
| echo 'OK: hello (Release+ASan)' | |
| out=$(build-release-asan/examples/echo foo moooo boo) | |
| printf 'echo foo moooo boo (Release+ASan) output: %s\n' "${out}" | |
| if [ "${out}" != "foo moooo boo" ]; then | |
| echo 'FAIL: echo (Release+ASan) output did not match "foo moooo boo".' | |
| exit 1 | |
| fi | |
| echo 'OK: echo foo moooo boo (Release+ASan)' | |
| gdb-pretty-printers: | |
| # GDB checks run after Debug and Release so they can reuse those build dirs | |
| # and their PCH outputs instead of configuring separate GDB-only builds. | |
| runs-on: ubuntu-24.04 | |
| needs: [install-toolchain, detect-changes, debug, release] | |
| if: ${{ needs.detect-changes.outputs.compile == 'true' }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/setup-toolchain | |
| - name: Download Debug build | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: build-debug | |
| path: build-debug | |
| - name: Download Release build | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: build-release | |
| path: build-release | |
| - name: GDB pretty printers (Debug) | |
| run: cmake --build build-debug --target check-gdb-pretty-printers | |
| - name: GDB pretty printers (Release) | |
| run: cmake --build build-release --target check-gdb-pretty-printers | |
| - name: Delete downloaded build artifacts | |
| if: always() | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const artifactNames = new Set(['build-debug', 'build-release']); | |
| const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| run_id: context.runId, | |
| per_page: 100, | |
| }); | |
| await Promise.all( | |
| artifacts | |
| .filter((artifact) => artifactNames.has(artifact.name)) | |
| .map((artifact) => github.rest.actions.deleteArtifact({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| artifact_id: artifact.id, | |
| })), | |
| ); | |
| arithmetic-negative: | |
| # Must-reject arithmetic probes are expensive. Run them only when the | |
| # arithmetic/meta surface or the negative-test machinery changes. | |
| runs-on: ubuntu-24.04 | |
| needs: [install-toolchain, detect-changes] | |
| if: ${{ needs.detect-changes.outputs.arithmetic_negative == 'true' }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/setup-toolchain | |
| - name: Run arithmetic type check | |
| run: just test release arithmetic -v | |
| shared: | |
| # `libcat.so` build smoke: configure, build, run unit tests and the | |
| # `hello`/`echo` examples against the shared library. | |
| runs-on: ubuntu-24.04 | |
| needs: [install-toolchain, detect-changes] | |
| if: ${{ needs.detect-changes.outputs.compile == 'true' }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/setup-toolchain | |
| - name: Configure and build (CAT_USE_SHARED=ON, Debug) | |
| # `CAT_USE_SHARED=ON` swaps `cat`'s INTERFACE-linked impl to | |
| # `cat-impl-shared`. | |
| run: | | |
| set -eu | |
| cmake -S . -B build-shared -G Ninja \ | |
| -DCMAKE_BUILD_TYPE=Debug \ | |
| -DCAT_USE_SANITIZERS=ON \ | |
| -DCAT_USE_SHARED=ON | |
| cmake --build build-shared | |
| - name: Test (CAT_USE_SHARED=ON, Debug) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| set -eu | |
| ctest --test-dir build-shared -R '^UnitTests$' --verbose --output-on-failure \ | |
| --timeout 150 | |
| - name: Run example binaries (CAT_USE_SHARED=ON, Debug) | |
| if: ${{ !cancelled() }} | |
| # Same example smoke as the Debug and Release jobs, run here so a | |
| # `CAT_USE_SHARED=ON` regression in `_start.cpp`, the dynamic-loader | |
| # `%fs` handoff, or the argv pickup surfaces in this job too. | |
| run: | | |
| set -eu | |
| out=$(build-shared/examples/hello) | |
| printf 'hello (Shared+ASan) output: %s\n' "${out}" | |
| if [ "${out}" != "Meow, world!" ]; then | |
| echo 'FAIL: hello (Shared+ASan) did not produce the expected greeting.' | |
| exit 1 | |
| fi | |
| echo 'OK: hello (Shared+ASan)' | |
| out=$(build-shared/examples/echo foo moooo boo) | |
| printf 'echo foo moooo boo (Shared+ASan) output: %s\n' "${out}" | |
| if [ "${out}" != "foo moooo boo" ]; then | |
| echo 'FAIL: echo (Shared+ASan) output did not match "foo moooo boo".' | |
| exit 1 | |
| fi | |
| echo 'OK: echo foo moooo boo (Shared+ASan)' | |
| # Headline artifact for this configuration. | |
| test -f build-shared/libcat.so | |
| - name: Verify libcat.so does not export libc/libm symbols | |
| if: ${{ !cancelled() }} | |
| # `libcat.so` must not export libCat's libc / libm | |
| # substitutes, or it would interpose for any process that | |
| # loads it. Pins the `[[gnu::visibility("hidden")]]` | |
| # behaviour in `src/libraries/{string,math}/implementations/*.cpp`. | |
| run: | | |
| set -eu | |
| if nm -D --defined-only build-shared/libcat.so \ | |
| | grep -E ' T (memset|memcpy|memmove|memcmp|strlen|sqrt|sqrtf|pow|powf)$' | |
| then | |
| echo 'FAIL: libcat.so is exporting libc/libm symbols (would interpose at runtime).' | |
| exit 1 | |
| fi | |
| tools: | |
| # Tooling-adjacent invariants: | |
| # * Multi-Config `.gdbinit` symlink layout. | |
| # * `clang-repl-libcat` wrapper across the {Debug, Release} | |
| # x {ASan, no ASan} matrix. | |
| # The SDE-pinned `cpuid` run (commented out below) | |
| # also lands here so the cached toolchain is reused. | |
| runs-on: ubuntu-24.04 | |
| needs: [install-toolchain, detect-changes] | |
| if: ${{ needs.detect-changes.outputs.compile == 'true' }} | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - uses: ./.github/actions/setup-toolchain | |
| - name: Configure and minimal build (Ninja Multi-Config, Debug and Release) | |
| if: ${{ needs.detect-changes.outputs.gdb_symlink == 'true' }} | |
| # Smallest surface that still pulls in `cat-gdb-tests` and | |
| # `cat-gdb-examples` so we can symlink `.gdbinit`. | |
| run: | | |
| cmake -S . -B build-multi -G 'Ninja Multi-Config' \ | |
| -DCAT_BUILD_UNIT_TESTS=OFF \ | |
| -DCAT_BUILD_ALL_EXAMPLES=OFF \ | |
| -DCAT_BUILD_EXAMPLE_HELLO=ON | |
| cmake --build build-multi --config Debug --target test_dummy hello | |
| cmake --build build-multi --config Release --target test_dummy hello | |
| - name: Verify .gdbinit symlinks (Ninja generator multi-config) | |
| if: ${{ needs.detect-changes.outputs.gdb_symlink == 'true' }} | |
| # Multi-config layout: per-subdirectory per-config files, no | |
| # top-level. Single-config equivalents are checked in the | |
| # Debug and Release jobs. | |
| run: | | |
| set -eu | |
| check_file() { | |
| path="$1" | |
| if [ ! -f "${path}" ]; then | |
| echo "FAIL: missing ${path}" | |
| ls -la "$(dirname "${path}")" || true | |
| exit 1 | |
| fi | |
| echo "OK: ${path}" | |
| } | |
| check_file build-multi/tests/Debug/.gdbinit | |
| check_file build-multi/examples/Debug/.gdbinit | |
| if [ -e build-multi/.gdbinit ]; then | |
| echo "FAIL: build-multi/.gdbinit should NOT exist in multi-config builds!" | |
| exit 1 | |
| fi | |
| - name: Configure and build (cat-repl Debug, sanitizers=ON) | |
| # Smoke test the `clang-repl-libcat` wrapper across the | |
| # {Debug, Release} x {ASan, no ASan} matrix. Catches | |
| # regressions in the `%lib` injection, the | |
| # `compile_commands.json` flag extractor, and the optional | |
| # ASan runtime preload all at once. A final snippet argument | |
| # runs libCat code one-shot, so we can grep | |
| # stdout for the marker the snippet prints. | |
| # | |
| # Was a `repl:` matrix job; folded in here as four configure- | |
| # then-test pairs so the cached toolchain isn't restored on | |
| # four extra runners. `if: ${{ !cancelled() }}` on every | |
| # subsequent step preserves the original `fail-fast: false` | |
| # behaviour: a failure in one variant still surfaces | |
| # results from the rest. Each variant gets its own build | |
| # dir to avoid CMake reconfiguration churn between runs. | |
| run: | | |
| set -eu | |
| cmake -S . -B build-repl-debug-asan -G Ninja \ | |
| -DCMAKE_BUILD_TYPE=Debug \ | |
| -DCAT_BUILD_SHARED=ON \ | |
| -DCAT_USE_SANITIZERS=ON \ | |
| -DCAT_BUILD_UNIT_TESTS=OFF \ | |
| -DCAT_BUILD_ALL_EXAMPLES=OFF | |
| cmake --build build-repl-debug-asan --target cat-impl-shared | |
| - name: Test (cat-repl Debug, sanitizers=ON) | |
| if: ${{ !cancelled() }} | |
| # `|| rc=$?` keeps `clang-repl-libcat`'s exit code without letting | |
| # `set -e` abort the step at the assignment, so the captured | |
| # stdout / stderr (`[clang-repl-libcat] preloading ...`, ASan | |
| # diagnostics, segfault output, ...) gets surfaced in the job log | |
| # even when the JIT'd snippet crashes or the wrapper bails. | |
| run: | | |
| set -eu | |
| rc=0 | |
| out=$(./build-repl-debug-asan/clang-repl-libcat \ | |
| '#include <cat/string> | |
| auto _ = cat::println("Meow world!");' \ | |
| 2>&1) || rc=$? | |
| printf 'cat-repl output (exit=%d):\n%s\n' "${rc}" "${out}" | |
| if [ "${rc}" -ne 0 ] || ! printf '%s\n' "${out}" | grep -qF 'Meow world!'; then | |
| echo 'FAIL: cat-repl Debug+ASan did not produce the expected marker.' | |
| exit 1 | |
| fi | |
| echo 'OK: cat-repl Debug+ASan produced "Meow world!"' | |
| - name: Configure and build (cat-repl Debug, sanitizers=OFF) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| set -eu | |
| cmake -S . -B build-repl-debug -G Ninja \ | |
| -DCMAKE_BUILD_TYPE=Debug \ | |
| -DCAT_BUILD_SHARED=ON \ | |
| -DCAT_USE_SANITIZERS=OFF \ | |
| -DCAT_BUILD_UNIT_TESTS=OFF \ | |
| -DCAT_BUILD_ALL_EXAMPLES=OFF | |
| cmake --build build-repl-debug --target cat-impl-shared | |
| - name: Test (cat-repl Debug, sanitizers=OFF) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| set -eu | |
| rc=0 | |
| out=$(./build-repl-debug/clang-repl-libcat \ | |
| '#include <cat/string> | |
| auto _ = cat::println("Meow world!");' \ | |
| 2>&1) || rc=$? | |
| printf 'cat-repl output (exit=%d):\n%s\n' "${rc}" "${out}" | |
| if [ "${rc}" -ne 0 ] || ! printf '%s\n' "${out}" | grep -qF 'Meow world!'; then | |
| echo 'FAIL: cat-repl Debug (no sanitizers) did not produce the expected marker.' | |
| exit 1 | |
| fi | |
| echo 'OK: cat-repl Debug (no sanitizers) produced "Meow world!"' | |
| - name: Configure and build (cat-repl Release, sanitizers=ON) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| set -eu | |
| cmake -S . -B build-repl-release-asan -G Ninja \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DCAT_BUILD_SHARED=ON \ | |
| -DCAT_USE_SANITIZERS=ON \ | |
| -DCAT_BUILD_UNIT_TESTS=OFF \ | |
| -DCAT_BUILD_ALL_EXAMPLES=OFF | |
| cmake --build build-repl-release-asan --target cat-impl-shared | |
| - name: Test (cat-repl Release, sanitizers=ON) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| set -eu | |
| rc=0 | |
| out=$(./build-repl-release-asan/clang-repl-libcat \ | |
| '#include <cat/string> | |
| auto _ = cat::println("Meow world!");' \ | |
| 2>&1) || rc=$? | |
| printf 'cat-repl output (exit=%d):\n%s\n' "${rc}" "${out}" | |
| if [ "${rc}" -ne 0 ] || ! printf '%s\n' "${out}" | grep -qF 'Meow world!'; then | |
| echo 'FAIL: cat-repl Release+ASan did not produce the expected marker.' | |
| exit 1 | |
| fi | |
| echo 'OK: cat-repl Release+ASan produced "Meow world!"' | |
| - name: Configure and build (cat-repl Release, sanitizers=OFF) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| set -eu | |
| cmake -S . -B build-repl-release -G Ninja \ | |
| -DCMAKE_BUILD_TYPE=Release \ | |
| -DCAT_BUILD_SHARED=ON \ | |
| -DCAT_USE_SANITIZERS=OFF \ | |
| -DCAT_BUILD_UNIT_TESTS=OFF \ | |
| -DCAT_BUILD_ALL_EXAMPLES=OFF | |
| cmake --build build-repl-release --target cat-impl-shared | |
| - name: Test (cat-repl Release, sanitizers=OFF) | |
| if: ${{ !cancelled() }} | |
| run: | | |
| set -eu | |
| rc=0 | |
| out=$(./build-repl-release/clang-repl-libcat \ | |
| '#include <cat/string> | |
| auto _ = cat::println("Meow world!");' \ | |
| 2>&1) || rc=$? | |
| printf 'cat-repl output (exit=%d):\n%s\n' "${rc}" "${out}" | |
| if [ "${rc}" -ne 0 ] || ! printf '%s\n' "${out}" | grep -qF 'Meow world!'; then | |
| echo 'FAIL: cat-repl Release (no sanitizers) did not produce the expected marker.' | |
| exit 1 | |
| fi | |
| echo 'OK: cat-repl Release (no sanitizers) produced "Meow world!"' | |
| # SDE-pinned Skylake run for `unit_tests_cpuid`. SDE intercepts | |
| # CPUID so the emulated CPU is always Skylake regardless of | |
| # the underlying hardware, which the test's | |
| # `__builtin_cpu_is("skylake")` check requires. | |
| # | |
| # TODO: Once `tests/unit_tests.cpp`'s runner accepts a test-name | |
| # filter (or the cpuid baseline lives behind a runtime guard in | |
| # `src/libraries/cpuid/cat/cpuid`), drop the standalone | |
| # `unit_tests_cpuid` binary and just | |
| # `sde64 -skl -- unit_tests --gtest-style-filter cpuid`. | |
| # - name: Install xz-utils for SDE tarball | |
| # run: sudo apt-get install -y xz-utils | |
| # | |
| # - name: Download Intel SDE 9.58.0 | |
| # # Pinning the URL by version + SHA256 keeps CI reproducible | |
| # # and aborts on a swapped tarball before any binary runs. | |
| # run: | | |
| # set -eu | |
| # sde_url=https://downloadmirror.intel.com/859732/sde-external-9.58.0-2025-06-16-lin.tar.xz | |
| # sde_sha256=f849acecad4c9b108259c643b2688fd65c35723cd23368abe5dd64b917cc18c0 | |
| # wget -q -O sde.tar.xz "${sde_url}" | |
| # echo "${sde_sha256} sde.tar.xz" | sha256sum --check --status | |
| # mkdir -p "${RUNNER_TEMP}/sde" | |
| # tar -xJf sde.tar.xz -C "${RUNNER_TEMP}/sde" --strip-components=1 | |
| # echo "CAT_SDE_EXECUTABLE=${RUNNER_TEMP}/sde/sde64" >> "${GITHUB_ENV}" | |
| # "${RUNNER_TEMP}/sde/sde64" -version | head -3 | |
| # | |
| # - name: Configure, build, and run UnitTestsCpuid under SDE | |
| # # Sanitizers and sccache off: ASan and Pin-based binary | |
| # # instrumentation do not coexist cleanly. | |
| # run: | | |
| # cmake -S . -B build-sde -G Ninja \ | |
| # -DCMAKE_BUILD_TYPE=Release \ | |
| # -DCMAKE_CXX_COMPILER=clang++ \ | |
| # -DCAT_USE_SANITIZERS=OFF \ | |
| # -DCAT_USE_SCCACHE=OFF \ | |
| # -DCAT_SDE_EXECUTABLE="${CAT_SDE_EXECUTABLE}" | |
| # cmake --build build-sde --target unit_tests | |
| # ctest --test-dir build-sde -R '^UnitTestsCpuid$' --verbose --output-on-failure |