Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ on:
permissions:
contents: read

# A newer push to the same pull request supersedes this run: it is cancelled
# rather than finished for a result nobody reads. A run for anything else (a
# push to main, the schedule, a dispatch) is its own group and is never
# cancelled: each merge commit is checked by its own result.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
Expand Down
101 changes: 98 additions & 3 deletions .github/workflows/code-map.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ on:
permissions:
contents: read

# A newer push to the same pull request supersedes this run: it is cancelled
# rather than finished for a result nobody reads. A run for anything else (a
# push to main, a dispatch) is its own group and is never cancelled: each
# merge commit is checked by its own result.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
Expand Down Expand Up @@ -76,14 +84,69 @@ jobs:
test -d "$ANDROID_HOME/ndk/$NDK_VERSION/toolchains/llvm/prebuilt"
echo "ANDROID_NDK_HOME=$ANDROID_HOME/ndk/$NDK_VERSION" >> "$GITHUB_ENV"

# The expensive half of this job (the fixture run, the three indexes and
# the mutation cases, about seventeen of its twenty minutes) reads code,
# configuration and this workflow: never a pin and never prose. The
# other half (the map's tables, its check, the intent manifest, every
# pin, the pins' own tests) runs on every commit. So the expensive half
# is kept under a key of everything it can read: every tracked file
# except `*.md` and specs/requirements/INTENT_PINS.tsv, and the runner
# image. The intent manifest stays in the key, because the mutation
# cases read the map its root questions shape. A commit that changes
# only pins or prose finds the indexes and fixture run of a commit whose
# every other byte is the same, verified there, and skips that half.
# Any other change misses and rebuilds it. The key matches exactly or
# not at all: there are no restore-keys, so a near match is a miss.
- name: The key of what the expensive half reads
id: key
run: |
inputs="$(git ls-tree -r HEAD | awk -F'\t' '$2 !~ /\.md$/ && $2 != "specs/requirements/INTENT_PINS.tsv"' | sha256sum | cut -d' ' -f1)"
echo "key=code-map-v1-${ImageOS}-${ImageVersion}-${inputs}" >> "$GITHUB_OUTPUT"

# Besides the indexes and the tables beside them, the map reads one
# thing from ./target: each build script's `output` (what it printed to
# cargo), which the indexes' cargo runs leave under target/debug/build
# and target/<triple>/debug/build. A shipped crate's build script that
# sets a cfg is refused from it, so it is kept with them.
- name: The indexes and fixture run of a commit with the same key
id: kept
uses: actions/cache/restore@v4
with:
key: ${{ steps.key.outputs.key }}
path: |
target/requirement-map/android.scip
target/requirement-map/android.log
target/requirement-map/node.scip
target/requirement-map/node.log
target/requirement-map/tests.scip
target/requirement-map/tests.log
target/requirement-map/android-features.txt
target/requirement-map/android-features-indexed.txt
target/requirement-map/android-packages.txt
target/requirement-map/node-packages.txt
target/requirement-map/node-features.txt
target/requirement-map/node-features-indexed.txt
target/requirement-map/fixture
target/debug/build/*/output
target/*/debug/build/*/output

- name: The map reads its fixture as expected
# Dispatch through a type argument, a qualified path and a value, and
# a type only named: tools/requirement_map/fixture/expected.tsv.
id: fixture
if: steps.kept.outputs.cache-hit != 'true'
run: make requirement-map-fixture MAP=target/requirement-map

- name: Index
id: indexes
if: steps.kept.outputs.cache-hit != 'true'
run: make requirement-map-indexes MAP=target/requirement-map

- name: Map
# From the indexes, built above or kept; it refuses an index whose
# analyzer log shows a failure either way.
id: map
run: make requirement-map MAP=target/requirement-map
run: make requirement-map-tables MAP=target/requirement-map

- name: The map holds no contradiction and keeps its committed facts
# Contradictions in its own tables; sentinels, entry points and
Expand All @@ -110,10 +173,42 @@ jobs:
- name: Every mutation case moves what it names and nothing else
# tools/requirement_map/fixture/mutations.toml: fixture changes, the
# app's declarations, a real-tree re-index, and faults planted in
# copies of the maps, each in a temporary copy.
if: ${{ !cancelled() && steps.map.outcome == 'success' }}
# copies of the maps, each in a temporary copy. With the indexes kept,
# every input of every case is what it was where they passed.
id: mutations
if: ${{ !cancelled() && steps.map.outcome == 'success' && steps.kept.outputs.cache-hit != 'true' }}
run: make requirement-map-mutations MAP=target/requirement-map

- name: Keep the indexes and fixture run for a commit with the same key
# Only once this run built them and every step that verifies them
# passed: the fixture, the map, its check and every mutation case. The
# intent manifest and the pins need not hold: a commit re-pinning the
# rows this one left stale is exactly the one that reuses them.
if: >-
${{ !cancelled() && steps.kept.outputs.cache-hit != 'true'
&& steps.fixture.outcome == 'success' && steps.indexes.outcome == 'success'
&& steps.map.outcome == 'success' && steps.check.outcome == 'success'
&& steps.mutations.outcome == 'success' }}
uses: actions/cache/save@v4
with:
key: ${{ steps.key.outputs.key }}
path: |
target/requirement-map/android.scip
target/requirement-map/android.log
target/requirement-map/node.scip
target/requirement-map/node.log
target/requirement-map/tests.scip
target/requirement-map/tests.log
target/requirement-map/android-features.txt
target/requirement-map/android-features-indexed.txt
target/requirement-map/android-packages.txt
target/requirement-map/node-packages.txt
target/requirement-map/node-features.txt
target/requirement-map/node-features-indexed.txt
target/requirement-map/fixture
target/debug/build/*/output
target/*/debug/build/*/output

- name: Keep the map
if: always()
uses: actions/upload-artifact@v7
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,14 @@ on:
permissions:
contents: read

# A newer push to the same pull request supersedes this run: it is cancelled
# rather than finished for a result nobody reads. A run for anything else (a
# push to main, the schedule, a dispatch) is its own group and is never
# cancelled: each merge commit is checked by its own result.
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
analyze:
name: CodeQL (${{ matrix.language }})
Expand Down
24 changes: 18 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -354,18 +354,21 @@ MAP_INPUTS := ci/requirement_map.android.rust-analyzer.json ci/requirement_map.n
# The NDK Gradle pins; the Android index is built for the real target with it.
NDK_PIN := $(shell sed -n 's/.*ndkVersion = "\([^"]*\)".*/\1/p' dsm_client/android/app/build.gradle.kts | head -1)
NDK_BIN = $(ANDROID_NDK_HOME)/toolchains/llvm/prebuilt/$(shell uname -s | tr A-Z a-z)-x86_64/bin
requirement-map: ## Code map (MAP=dir): every source file hashed; each shipped build's call graph (Android: aarch64-linux-android; storage node: Linux, built on Linux only) and the host test build, each definition reached, dead or indeterminate with a reason code; MAP/code-map.html
requirement-map: requirement-map-indexes requirement-map-tables ## Code map (MAP=dir): every source file hashed; each shipped build's call graph (Android: aarch64-linux-android; storage node: Linux, built on Linux only) and the host test build, each definition reached, dead or indeterminate with a reason code; MAP/code-map.html

.PHONY: requirement-map-indexes requirement-map-tables
# The map in two halves. The indexes read code and configuration only, never
# specs/; they are the expensive half, and CI keeps them between commits whose
# code and configuration are the same (.github/workflows/code-map.yml). The
# tables read the indexes and the intent manifest's root questions, and run
# every time.
requirement-map-indexes: ## The code map's indexes (MAP=dir): each build's features and packages, and a rust-analyzer index of each build
@test -n "$(ANDROID_NDK_HOME)" || { echo "requirement-map: set ANDROID_NDK_HOME to the NDK Gradle pins ($(NDK_PIN)): the Android index is built for the real aarch64-linux-android target"; exit 1; }
@test -x "$(NDK_BIN)/aarch64-linux-android23-clang" || { echo "requirement-map: no aarch64-linux-android23-clang in $(NDK_BIN)"; exit 1; }
rustup component add rust-analyzer --toolchain $(RUST_PIN)
rustup target add aarch64-linux-android --toolchain $(RUST_PIN)
mkdir -p $(MAP)
rm -f $(MAP)/node.scip $(MAP)/node.log $(MAP)/node-features.txt $(MAP)/node-features-indexed.txt
git ls-files --cached --others --exclude-standard -- $(SOURCES) > $(MAP)/sources.txt
rustup run $(RUST_PIN) rust-analyzer --version > $(MAP)/analyzer.txt
printf '%s\n' $(MAP_INPUTS) $(MAP)/analyzer.txt > $(MAP)/inputs.txt
rustup run $(RUST_PIN) cargo build --locked --release -p requirement_map
target/release/requirement_map fingerprint --root . --files $(MAP)/sources.txt --inputs $(MAP)/inputs.txt --out $(MAP)/tree
# Each build's features, and the ones the index resolves (cargo metadata
# unifies dev-dependencies): the difference is what the index compiles that
# the build does not.
Expand All @@ -384,6 +387,15 @@ requirement-map: ## Code map (MAP=dir): every source file hashed; each shipped b
rustup run $(RUST_PIN) rust-analyzer scip . --config-path ci/requirement_map.node.rust-analyzer.json --output $(MAP)/node.scip > $(MAP)/node.log 2>&1; \
fi
rustup run $(RUST_PIN) rust-analyzer scip . --config-path ci/requirement_map.tests.rust-analyzer.json --output $(MAP)/tests.scip > $(MAP)/tests.log 2>&1

requirement-map-tables: ## The code map's tables from its indexes (after make requirement-map-indexes): the tree's fingerprint, the intent manifest's root questions, every definition's reading; MAP/code-map.html
rustup component add rust-analyzer --toolchain $(RUST_PIN)
mkdir -p $(MAP)
git ls-files --cached --others --exclude-standard -- $(SOURCES) > $(MAP)/sources.txt
rustup run $(RUST_PIN) rust-analyzer --version > $(MAP)/analyzer.txt
printf '%s\n' $(MAP_INPUTS) $(MAP)/analyzer.txt > $(MAP)/inputs.txt
rustup run $(RUST_PIN) cargo build --locked --release -p requirement_map
target/release/requirement_map fingerprint --root . --files $(MAP)/sources.txt --inputs $(MAP)/inputs.txt --out $(MAP)/tree
# The intent manifest's root questions, answered by the map itself.
python3 ci/intent_comparator.py root-queries --manifest specs/requirements/INTENT_MANIFEST.tsv > $(MAP)/root-queries.txt
target/release/requirement_map index --root . --files $(MAP)/sources.txt --inputs $(MAP)/inputs.txt --fingerprint $(MAP)/tree \
Expand Down
Loading