diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9955fae..7716a9b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ env: jobs: # Whether anything beyond documentation changed; the builds skip when not. changes: - uses: Rawframe-Project/.github/.github/workflows/changes.yml@8414d388d2116b581c5c5d8f6641e1da4d863244 # v0.0.42 + uses: Rawframe-Project/.github/.github/workflows/changes.yml@972ea41c6abf2652d3b067e57bf0713f1661cf0e # v0.0.51 permissions: pull-requests: read @@ -52,12 +52,12 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - - uses: Rawframe-Project/.github/.github/actions/docs-check@8414d388d2116b581c5c5d8f6641e1da4d863244 # v0.0.42 + - uses: Rawframe-Project/.github/.github/actions/docs-check@972ea41c6abf2652d3b067e57bf0713f1661cf0e # v0.0.51 with: exclude: tools/ucd zizmor: - uses: Rawframe-Project/.github/.github/workflows/zizmor.yml@8414d388d2116b581c5c5d8f6641e1da4d863244 # v0.0.42 + uses: Rawframe-Project/.github/.github/workflows/zizmor.yml@972ea41c6abf2652d3b067e57bf0713f1661cf0e # v0.0.51 permissions: contents: read @@ -134,20 +134,17 @@ jobs: run: python3 tools/table_budget.py out/table-sizes.txt # On the Release preset, where the assertions compile out, and with # them live: each build hides findings the other shows. The header - # filter is given on the command line: clang-tidy-20 takes it from the - # configuration of the directory above the one it runs in, which has - # none, and would drop every finding in our headers as non-user code. - # It is anchored at the source directory the build tree records, - # regex-escaped, so it also leaves out the headers of dependencies - # fetched under out/. + # filter comes from tools/tidy_filter.sh: clang-tidy-20 run from the + # checkout reports no header finding without one, and the filter is + # anchored at the source directory the build tree records, so the + # headers of dependencies under out/ stay out. - name: clang-tidy run: | for build in Release Debug; do echo "clang-tidy, $build" cmake --preset linux-clang-20 -B "out/tidy-$build" -DCMAKE_BUILD_TYPE=$build \ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -D${PREFIX}_BUILD_TESTS=OFF -D${PREFIX}_HARFBUZZ=ON - root=$(sed -n 's/^CMAKE_HOME_DIRECTORY:INTERNAL=//p' "out/tidy-$build/CMakeCache.txt") - filter="^$(printf '%s' "$root" | sed 's/[][\.*^$+?(){}|]/\\&/g')/(src|include)/" + filter=$(tools/tidy_filter.sh "out/tidy-$build") for f in src/*.c; do clang-tidy-20 -p "out/tidy-$build" --quiet --header-filter="$filter" \ --warnings-as-errors='*' "$f" @@ -174,7 +171,7 @@ jobs: - { preset: linux-i686-gcc-14 } - { preset: macos-clang, runner: macos-15 } - { preset: windows-clang-cl, runner: windows-2025 } - uses: Rawframe-Project/.github/.github/workflows/cpp.yml@8414d388d2116b581c5c5d8f6641e1da4d863244 # v0.0.42 + uses: Rawframe-Project/.github/.github/workflows/cpp.yml@972ea41c6abf2652d3b067e57bf0713f1661cf0e # v0.0.51 permissions: contents: read with: @@ -456,7 +453,7 @@ jobs: prune-caches: needs: [build, fuzz] if: always() && github.event_name != 'pull_request' && github.ref == 'refs/heads/main' - uses: Rawframe-Project/.github/.github/workflows/cache-prune.yml@8414d388d2116b581c5c5d8f6641e1da4d863244 # v0.0.42 + uses: Rawframe-Project/.github/.github/workflows/cache-prune.yml@972ea41c6abf2652d3b067e57bf0713f1661cf0e # v0.0.51 permissions: actions: write diff --git a/.github/workflows/pr-gate.yml b/.github/workflows/pr-gate.yml index 19e2da6..3965ed2 100644 --- a/.github/workflows/pr-gate.yml +++ b/.github/workflows/pr-gate.yml @@ -14,7 +14,7 @@ permissions: {} jobs: gate: - uses: Rawframe-Project/.github/.github/workflows/pr-gate.yml@8414d388d2116b581c5c5d8f6641e1da4d863244 # v0.0.42 + uses: Rawframe-Project/.github/.github/workflows/pr-gate.yml@972ea41c6abf2652d3b067e57bf0713f1661cf0e # v0.0.51 permissions: pull-requests: write issues: read diff --git a/.github/workflows/pr-title.yml b/.github/workflows/pr-title.yml index 1dbb7b3..ae40746 100644 --- a/.github/workflows/pr-title.yml +++ b/.github/workflows/pr-title.yml @@ -11,7 +11,7 @@ permissions: {} jobs: title: - uses: Rawframe-Project/.github/.github/workflows/pr-title.yml@da9f20a2a9bf9d40cecbeaf39569d05321f83a37 # v0.0.45 + uses: Rawframe-Project/.github/.github/workflows/pr-title.yml@972ea41c6abf2652d3b067e57bf0713f1661cf0e # v0.0.51 permissions: pull-requests: read with: diff --git a/AGENTS.md b/AGENTS.md index 6b59b4f..a34874c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,7 +28,7 @@ Run the `pr` and `full` tiers through the workspace's heavy-job wrapper on the s - **Killing by pattern.** Never `pkill -f`, `pgrep | kill` or `kill` a PID found by matching a name or path: other sessions build and fuzz on the same machine with the same binary names. Kill only a PID you started. - **Endless fuzzing.** A libFuzzer target runs until it finds something. Always pass `-max_total_time`, and stop what you started. - **Editing generated or external files.** `src/generated/` is written by `tools/munigen`; `tools/ucd/` holds the Unicode files exactly as published, with their SHA-256 in `tools/ucd/README.md`, which `tools/check_ucd.py` checks. Change the generator, or replace the data as [development.md](docs/operations/development.md#a-new-unicode-version) describes. -- **Editing shared family files.** These are synced from the canonical copies in the organization's `.github` repository and fail CI when they differ: `docs/internals/maul-conventions.md`; `.clang-format`, `.clang-tidy`, `.editorconfig`, `.gitattributes`, `.gitignore`; `cmake/CompilerFlags.cmake`, `cmake/Install.cmake`, `cmake/Library.cmake`, `cmake/ReadVersion.cmake`, `cmake/Testing.cmake`, `cmake/Config.cmake.in`, `cmake/pkg-config.pc.in`, `cmake/HeaderCheck/CMakeLists.txt`; `bench/CMakeLists.txt`; `test/test_harness.h`; and `tools/build_docs.py`, `check_docs.py`, `check_exports.py`, `check_guide.py`, `check_lengths.py`, `check_modules.py`, `check_source.py`, `check_test_hygiene.py`, `gen_api.py`, `fuzz_target.sh`, `run_quarantine.sh`, and the fixtures in `tools/test_hygiene/`. Change the canonical copy first. +- **Editing shared family files.** These are synced from the canonical copies in the organization's `.github` repository and fail CI when they differ: `docs/internals/maul-conventions.md`; `.clang-format`, `.clang-tidy`, `.editorconfig`, `.gitattributes`, `.gitignore`; `cmake/CompilerFlags.cmake`, `cmake/Install.cmake`, `cmake/Library.cmake`, `cmake/ReadVersion.cmake`, `cmake/Testing.cmake`, `cmake/Config.cmake.in`, `cmake/pkg-config.pc.in`, `cmake/HeaderCheck/CMakeLists.txt`; `bench/CMakeLists.txt`; `test/test_harness.h`; and `tools/build_docs.py`, `check_docs.py`, `check_exports.py`, `check_guide.py`, `check_lengths.py`, `check_modules.py`, `check_source.py`, `check_test_hygiene.py`, `gen_api.py`, `fuzz_target.sh`, `run_quarantine.sh`, `tidy_filter.sh`, and the fixtures in `tools/test_hygiene/`. Change the canonical copy first. ## Invariants diff --git a/docs/internals/maul-conventions.md b/docs/internals/maul-conventions.md index 37be11a..80f34b2 100644 --- a/docs/internals/maul-conventions.md +++ b/docs/internals/maul-conventions.md @@ -95,6 +95,7 @@ The libraries are one product family: a program may link several of them, and a - Getters that cannot fail take ids and return values. Out-parameters end in `Out`. - Strings cross the API as UTF-8 with an explicit byte length, never as NUL-terminated text alone, so text from hostile sources is never measured by searching for a terminator. - A library's root object is a typed opaque pointer. Every object a root owns is named by a typed value id, a 1-based slot index (0 is null) and a generation, one struct per kind. A stale id is refused with a typed result, never dereferenced, which costs one array read and a compare. Platform handles are never identities. +- State a caller holds is checked by how a caller can get it wrong. Opaque state that only the library writes is checked by its cookie, and its ABI version where it records one, and by every index, slot and length in it being in bounds, so that no bit pattern makes a call read outside the state or the data it names; a forged one is outside the contract, and its fields' consistency with each other is not checked again. A value struct with documented fields that a call takes back from the caller, which the caller may copy and assign, carries no raw pointer and size into the library's data: it names the object or data it came from and an index, checked on each call, so that a field copied from another value cannot reach memory outside what it names. A struct the library only fills in for the caller, such as a label it points at, may point into its data. So may one the header also documents as input a caller builds, such as an accessibility update, provided the call that takes it reads through it as through any buffer argument: only within the ranges its own pointers and counts state, checking every count, offset and index it reads there as untrusted, and relying on nothing the library validated earlier. Its pointers and counts are then the caller's to keep valid. Caller memory a call keeps "as checked", such as a bound message, must stay unchanged as documented, and whatever it becomes, a later call reads only within the extents it recorded. - An operation that finishes later is a request: it returns its status at once and, accepted, a typed request id that exactly one completion record answers, in the owner's notification queue, with a typed outcome. Callbacks that deliver completions fire at unsafe points and leave re-entrancy rules to every caller; a queue does not. ## Floating point and determinism @@ -154,7 +155,7 @@ The libraries are one product family: a program may link several of them, and a | Rule | Tool | |---|---| | Formatting, include order, braces, `const` placement | `clang-format` at the version CI pins | -| Naming of locals, parameters and fields; cognitive complexity | `clang-tidy` | +| Naming of locals, parameters and fields; cognitive complexity | `clang-tidy`, over the library's sources and its own headers: every call passes `--header-filter` from `tools/tidy_filter.sh`, anchored at the build tree's `CMAKE_HOME_DIRECTORY`, since clang-tidy run from the checkout reports no header finding without one, and a changed header tidies every source | | Public API C17, C23 and C++17 clean; MSVC header build | header tests (`cmake/Testing.cmake`, `cmake/HeaderCheck/`) | | SPDX lines, comment style, history markers, banned calls, mutable file-scope state, `` allow list, file names, shell scripts through shellcheck | `tools/check_source.py` | | Module graph | `tools/check_modules.py` | diff --git a/tools/check.sh b/tools/check.sh index b4d8be3..ba93ae9 100755 --- a/tools/check.sh +++ b/tools/check.sh @@ -119,27 +119,21 @@ all_c_files() { git ls-files 'include/*.h' 'src/*.[ch]' 'test/*.[ch]' 'tools/*.[ # clang-tidy on the files given, as CI's checks job runs it: on the # Release preset, where the assertions compile out, and with them live. -# Each hides findings the other shows. The header filter is given on the -# command line: clang-tidy-20 takes it from the configuration of the -# directory above the one it runs in, which has none, and would drop every -# finding in our headers as non-user code. It is anchored at the source -# directory the build tree records, the path its compile commands name, so -# it also leaves out the headers of dependencies fetched under out/. -header_filter() { # build directory - local root - root=$(sed -n 's/^CMAKE_HOME_DIRECTORY:INTERNAL=//p' "$1/CMakeCache.txt") - printf '^%s/(src|include)/' "$(printf '%s' "$root" | sed 's/[][\.*^$+?(){}|]/\\&/g')" -} - +# Each hides findings the other shows. The header filter comes from +# tools/tidy_filter.sh: clang-tidy-20 run from the checkout reports no +# header finding without one, and the filter is anchored at the source +# directory the build tree records, so the headers of dependencies under +# out/ stay out. tidy() { # files - local build + local build filter for build in Release Debug; do step "clang-tidy, $build, on $# files" cmake --preset linux-clang-20 -B "$out/check-tidy-$build" -DCMAKE_BUILD_TYPE=$build \ -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DMAUL_UNICODE_BUILD_TESTS=OFF -DMAUL_UNICODE_HARFBUZZ=ON \ >"$out/check-tidy-$build.log" + filter=$(tools/tidy_filter.sh "$out/check-tidy-$build") printf '%s\n' "$@" | xargs -P "$jobs" -n 1 clang-tidy-20 -p "$out/check-tidy-$build" --quiet \ - --header-filter="$(header_filter "$out/check-tidy-$build")" --warnings-as-errors='*' 2>/dev/null + --header-filter="$filter" --warnings-as-errors='*' 2>/dev/null done } diff --git a/tools/tidy_filter.sh b/tools/tidy_filter.sh new file mode 100755 index 0000000..d61928e --- /dev/null +++ b/tools/tidy_filter.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# SPDX-License-Identifier: MIT +# Copyright (c) 2026 Sirac Ozmen +# +# Prints clang-tidy's --header-filter for a build tree: this library's +# headers under src/ and include/, anchored at the source directory the +# tree was configured from. CMake writes the include paths through that +# directory as it was given, so in a checkout reached through a symbolic +# link neither pwd nor pwd -P need match them; the tree's +# CMAKE_HOME_DIRECTORY does. The directory is escaped, so that a dot or +# a plus in it matches only itself. +# +# A library whose own sources also sit in subdirectories with their own +# src/ and include/ (such as rhi/ or window/) names them after the tree, +# and their headers are kept too. +# +# Assign the output to a variable of its own before passing it on, so +# that a failure here stops the caller instead of leaving an empty +# filter: +# +# filter=$(tools/tidy_filter.sh out/tidy) +# clang-tidy-20 -p out/tidy --header-filter="$filter" ... +# +# usage: tidy_filter.sh [subdirectory ...] +set -euo pipefail + +if [ $# -lt 1 ]; then + echo "usage: tidy_filter.sh [subdirectory ...]" >&2 + exit 2 +fi + +escape() { + printf '%s' "$1" | sed 's/[][\\.^$*+?(){}|]/\\&/g' +} + +home=$(sed -n 's/^CMAKE_HOME_DIRECTORY:INTERNAL=//p' "$1/CMakeCache.txt") +if [ -z "$home" ]; then + echo "no CMAKE_HOME_DIRECTORY in $1/CMakeCache.txt" >&2 + exit 1 +fi +shift + +prefix="" +if [ $# -gt 0 ]; then + names="" + for dir in "$@"; do + names="${names:+$names|}$(escape "$dir")" + done + prefix="(($names)/)?" +fi +printf '^%s/%s(src|include)/\n' "$(escape "$home")" "$prefix"