Skip to content

Polish the sum types. #214

Polish the sum types.

Polish the sum types. #214

Workflow file for this run

# 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