-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathJustfile
More file actions
284 lines (251 loc) · 15.9 KB
/
Copy pathJustfile
File metadata and controls
284 lines (251 loc) · 15.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
# Tests read `.env` themselves, but a mutants job runs in a gitignore-filtered copy that `.env`
# never reaches, so IDAKIT_CORPUS_MANIFEST has to arrive through the environment instead.
set dotenv-load := true
# What every mutants recipe hands the kernel harness. `registered` drops the corpus fan-out, which
# would otherwise run every check against every fixture once per mutant. The worker cap is the job
# that `--test-threads 4` used to do under nextest: concurrency nests, so without it each of
# `--jobs` mutants runs a full core-count pool of live kernels at ~0.85 GiB apiece.
mutants_env := "IDAKIT_TEST_SCOPE=registered IDAKIT_TEST_WORKERS=4"
# The TUs clang-tidy and the pedantic sweep lint, from compile_commands.json rather than a glob so
# the generated `gen_<domain>_bodies.cc` are covered too.
lint_tus := "jq -r '.[].file' crates/idakit-sys/compile_commands.json"
# Formatting stays on the hand-written sources; generated output is never formatted in place.
facade_sources := "crates/idakit-sys/facade/*.cpp crates/idakit-sys/facade/*.h"
default:
@just --list
# One-stop gate mirroring CI: a clean run here means CI will very likely pass.
check: fmt-check actionlint zizmor clippy cross tidy (doc "hermetic") readme-check test
build:
cargo build --workspace
# nextest runs the suite; the kernel-touching integration tests are serialized by
# .config/nextest.toml and skip without their preconditions. The `kernel` binary is its own runner
# (`test = false`, so nextest skips it): it reports every registered test and every corpus case
# individually while executing them across one pool of warm workers, which nextest cannot do because
# it spawns one process per listed test. Doctests run separately, since nextest doesn't cover them.
test:
cargo nextest run --workspace --all-features
cargo test -p idakit --all-features --test kernel
cargo test --workspace --all-features --doc
fmt: fmt-rust fmt-cpp
fmt-rust:
cargo fmt --all
fmt-cpp:
clang-format -i {{ facade_sources }}
fmt-check: fmt-rust-check fmt-cpp-check
fmt-rust-check:
cargo fmt --all --check
fmt-cpp-check:
clang-format --dry-run --Werror {{ facade_sources }}
# clang-tidy runs one TU at a time; xargs -P fans the facade files across cores so wall time
# tracks the slowest file, not their sum. In CI, CTCACHE_DIR routes through clang-tidy-cache
# (content-hash keyed on compile command + .clang-tidy + source) to skip an untouched facade;
# local runs without it use plain clang-tidy.
tidy:
#!/usr/bin/env bash
set -euo pipefail
IDAKIT_EMIT_COMPILE_COMMANDS=1 cargo build -q -p idakit-sys
# clang-tidy replays compile_commands.json, which records only the flags cc passes -- not
# the clang driver's implicit macOS -isysroot. Without it clang-tidy can't find the SDK's
# system headers (stdlib.h), and the broken parse then misfires other checks, so supply it.
extra_args=()
if [ "$(uname -s)" = Darwin ]; then
extra_args=(--extra-arg=-isysroot --extra-arg="$(xcrun --show-sdk-path)")
fi
tidy_cmd=(clang-tidy)
if [ -n "${CTCACHE_DIR:-}" ] && command -v clang-tidy-cache >/dev/null 2>&1; then
tidy_cmd=(clang-tidy-cache "$(command -v clang-tidy)")
fi
nproc_val="$(nproc 2>/dev/null || sysctl -n hw.ncpu)"
# --config-file, not discovery: clang-tidy walks up from the source file, and a generated body
# under target/ never passes through crates/idakit-sys/, so it would pass with no checks run.
{{ lint_tus }} | xargs -P "$nproc_val" -I{} "${tidy_cmd[@]}" \
--config-file=crates/idakit-sys/.clang-tidy -p crates/idakit-sys "${extra_args[@]}" {}
# Advisory, not part of `check`: Clang's `-Weverything` minus pure-noise categories -- C++98
# compat (the facade is C++17), buffer-hardening (it does deliberate raw-pointer work over the
# SDK's C-style API and the ELF GOT-rewrite trap), and padding notices. What's left (old-style
# casts, switch-enum, sign-conversion, ...) is real signal to triage by hand.
pedantic:
#!/usr/bin/env bash
set -uo pipefail
IDAKIT_EMIT_COMPILE_COMMANDS=1 cargo build -q -p idakit-sys
sdk_include="$(jq -r '.[0].arguments[(.[0].arguments | index("-isystem")) + 1]' crates/idakit-sys/compile_commands.json)"
out_include="$(jq -r '.[0].arguments | map(select(startswith("-I") and (endswith("/facade") | not))) | first' crates/idakit-sys/compile_commands.json)"
extra_args=(-isystem "$sdk_include" "$out_include")
if [ "$(uname -s)" = Darwin ]; then
extra_args+=(-isysroot "$(xcrun --show-sdk-path)")
fi
# `-Weverything` shifts across Clang releases; prefer a `clang++` matching clang-tidy's
# major over the ambient one, which may be older and reject (then warn about) these flags.
clang_major="$(clang-tidy --version | grep -oE 'version [0-9]+' | grep -oE '[0-9]+')"
clangxx="clang++"
command -v "clang++-$clang_major" >/dev/null 2>&1 && clangxx="clang++-$clang_major"
nproc_val="$(nproc 2>/dev/null || sysctl -n hw.ncpu)"
{{ lint_tus }} | xargs -P "$nproc_val" -I{} \
"$clangxx" -std=c++17 -Icrates/idakit-sys/facade "${extra_args[@]}" -D__EA64__ -D__LINUX__ \
-Weverything -Wno-c++98-compat -Wno-c++98-compat-local-type-template-args \
-Wno-unsafe-buffer-usage -Wno-unsafe-buffer-usage-in-libc-call -Wno-padded \
-fsyntax-only {}
# ASan+UBSan (facade too) or ThreadSanitizer against the real kernel across the FFI boundary;
# nightly-only (needs -Z build-std). `mode` is "address" or "thread"; UBSan rides only on the
# C++ side (rustc has no `-Zsanitizer=undefined`, and its runtime is ASan xor TSan). Leak
# detection is off -- IDA's kernel is a process-lifetime singleton, so LSan's exit findings all
# sit in libida.so, not real leaks. Thread mode carries a suppressions file for the same
# reason: see crates/idakit-sys/tsan-suppressions.txt.
#
# Covers the facade's guarded<> boundary plus every binary that marshals a string or buffer
# across it, which since the harness merge means `traps` plus the whole `kernel` suite.
# `--test-threads=1` is mandatory for `traps`: unlike nextest, plain `cargo test` shares one
# process across a binary's tests, and the kernel singleton hard-errors on a second concurrent
# claim. `kernel` schedules its own workers and ignores the flag.
sanitize mode="address":
#!/usr/bin/env bash
set -euo pipefail
if [ "{{ mode }}" = thread ]; then
export IDAKIT_SANITIZE=thread
export RUSTFLAGS="-Zsanitizer=thread -Cdebuginfo=1"
export TSAN_OPTIONS="suppressions=$(pwd)/crates/idakit-sys/tsan-suppressions.txt"
else
export IDAKIT_SANITIZE=address,undefined
export RUSTFLAGS="-Zsanitizer=address -Cdebuginfo=1"
export ASAN_OPTIONS=detect_leaks=0
fi
tests=(-p idakit -p idakit-sys --test traps --test kernel)
cargo +nightly test -Z build-std --target x86_64-unknown-linux-gnu "${tests[@]}" -- --test-threads=1
clippy:
cargo clippy --workspace --all-targets --all-features -- -D warnings
# Lints the matrix's other targets from a Linux box, catching `#[cfg]`-gated breakage a host build
# cannot see. Scoped to the crates with no native IDA dependency, which is where nearly all the
# platform-specific code lives; the rest needs a per-target C++ toolchain for `cxx`'s build script.
cross:
#!/usr/bin/env bash
set -euo pipefail
for target in x86_64-pc-windows-msvc aarch64-apple-darwin; do
rustup target add "$target"
DOCS_RS=1 cargo clippy -p idakit-runner -p idakit-runner-macros \
--all-targets --all-features --target "$target" -- -D warnings
done
# Line coverage over the workspace, written to coverage/ (gitignored); needs cargo-llvm-cov.
# Every step carries the cfg, else the `coverage(off)` exceptions go inert and count against the
# total. Doctests are left out: `--doctests` is still incomplete. Advisory, not part of `check`.
#
# `llvm-cov nextest` alone would miss the `kernel` binary entirely, since nextest never lists a
# `test = false` target: its ~900 cases would contribute nothing while the total still read as
# whole-suite coverage. `show-env` exports the instrumentation into the shell instead, so both
# runners write into one profile set. Warm workers are covered for free: LLVM_PROFILE_FILE carries
# `%p`, so each worker process lands in its own raw file.
coverage:
#!/usr/bin/env bash
set -euo pipefail
export RUSTFLAGS="--cfg coverage_nightly"
source <(cargo +nightly llvm-cov show-env --export-prefix)
cargo +nightly llvm-cov clean --workspace
cargo +nightly nextest run --workspace --all-features --no-fail-fast --hide-progress-bar
cargo +nightly test -p idakit --all-features --test kernel
mkdir -p coverage
cargo +nightly llvm-cov report --html --output-dir coverage/html
cargo +nightly llvm-cov report --lcov --output-path coverage/lcov.info
cargo +nightly llvm-cov report --json --output-path coverage/coverage.json
# `just coverage`, then open the HTML report.
coverage-open: coverage
cargo +nightly llvm-cov report --html --output-dir coverage/html --open
# Build API docs, warnings-as-errors (broken links, bad code blocks, bare URLs, invalid HTML
# tags in example doc comments all fail). Default scrapes example call-sites onto each item
# (nightly + real runtime); `hermetic` skips scraping and builds under DOCS_RS, so no IDA
# runtime -- CI and `check` use it. Both pass --examples so example `//!` doc comments are
# linted too, not just the library crates.
doc mode="scrape":
RUSTDOCFLAGS="-D warnings" {{ if mode == "hermetic" { "DOCS_RS=1 cargo doc --workspace --all-features --no-deps --examples" } else { "cargo +nightly doc --workspace --all-features --no-deps --examples -Z rustdoc-scrape-examples" } }}
# Lint the GitHub Actions workflows (auto-discovers .github/workflows/).
actionlint:
actionlint
# Audits the workflows. Needs a token: the pin audits resolve `uses:` SHAs against GitHub and are
# silently skipped offline.
zizmor:
GH_TOKEN="$(gh auth token)" zizmor .
# Each crate's README is its crate-level `//!` doc run through cargo-rdme so the two can't drift;
# intra-doc links resolve to live docs.rs URLs, which needs the pinned nightly
# `cargo rdme install-rust-toolchain-for-intralinks` installs. DOCS_RS=1 skips the native IDA
# link, same as `doc hermetic`. The root README.md is idakit's; idakit-sys keeps its own.
readme:
DOCS_RS=1 cargo rdme --manifest-path crates/idakit/Cargo.toml --heading-base-level 1 --force
DOCS_RS=1 cargo rdme --manifest-path crates/idakit-sys/Cargo.toml --heading-base-level 1 --force
readme-check:
DOCS_RS=1 cargo rdme --manifest-path crates/idakit/Cargo.toml --heading-base-level 1 --check
DOCS_RS=1 cargo rdme --manifest-path crates/idakit-sys/Cargo.toml --heading-base-level 1 --check
# Like `test`, but spells --no-fail-fast rather than leaning on the nextest profile for it. In CI
# the fetch-corpus step exports IDAKIT_CORPUS_MANIFEST, so the dedicated tests source the same
# host-independent canonical fixture the corpus matrix uses.
ci-test:
cargo nextest run --workspace --all-features --no-fail-fast
cargo test -p idakit --all-features --test kernel
cargo test --workspace --all-features --doc
# Advisory, not part of `check`. Records a CPU flamegraph of the nextest half of the suite under
# samply, pinned to one core (taskset) so it never fights a foreground workload: slower, but
# single-core removes contention noise and the ratios are what matter. nextest runs every test in
# its own process; samply follows the tree and `--reuse-threads` merges the per-process threads, so
# the profile aggregates them into one flamegraph rather than 200 disjoint ones. The same run emits
# JUnit timing, so it doubles as the "which test is slow" view. It does not cover `kernel`, which
# nextest does not list; that binary reports its own per-case timings already. Writes a
# profile file; open it with `samply load <out>`. Needs perf_event_paranoid <= 1 (samply prints the
# sudo one-liner) and the corpus. Single-core, so it takes several minutes; that is the point.
profile out="target/profile.json.gz":
#!/usr/bin/env bash
set -euo pipefail
cargo nextest run --no-run --workspace --all-features
taskset -c 0 samply record --save-only --reuse-threads --rate 999 -o {{ out }} -- \
cargo nextest run --workspace --all-features --profile ci --test-threads=1
python3 tools/profile_junit.py target/nextest/ci/junit.xml
printf '\nCPU flamegraph: samply load %s\n' {{ out }} >&2
# Advisory. Records a single-core CPU flamegraph of one test binary (or one exact test) under
# samply. `test` is a test-binary name (e.g. `decompile_cache`), `filter` an exact test name; drop
# it to profile every test in the binary in one process. Drilldown twin of `profile`; opens the
# Firefox profiler. Needs perf_event_paranoid <= 1 (samply prints the sudo one-liner).
profile-cpu test filter="":
#!/usr/bin/env bash
set -euo pipefail
cargo test -q --no-run --test {{ test }}
bin="$(ls -t target/debug/deps/{{ test }}-* | grep -vE '\.d$' | head -1)"
exact=(); [ -n "{{ filter }}" ] && exact=(--exact {{ filter }})
taskset -c 0 samply record --rate 999 -- "$bin" "${exact[@]}" --test-threads=1
# Refuses to start a mutants run that cannot check anything. Without a corpus the kernel tests
# skip, pass, and leave every mutant MISSED, which reads exactly like a real result: the whole run
# then reports a coverage hole that does not exist, and buries the ones that do.
[private]
require-corpus:
#!/usr/bin/env bash
if [ -z "${IDAKIT_CORPUS_MANIFEST:-}" ] || [ ! -f "${IDAKIT_CORPUS_MANIFEST:-}" ]; then
echo "IDAKIT_CORPUS_MANIFEST is unset or names no file; set it in .env." >&2
echo "Without it the kernel tests skip and every mutant reports MISSED against a run that checked nothing." >&2
exit 1
fi
# Mutation-tests the modules scoped in .cargo/mutants.toml against the unit tests and the kernel
# harness. `jobs` stays well under the core count: concurrency nests, since every job runs its own
# build plus a pool of workers, each holding a live ~0.85 GiB kernel (capped by `mutants_env`).
mutants jobs="3": require-corpus
{{ mutants_env }} cargo mutants -p idakit --jobs {{ jobs }}
# Like `mutants`, but skips what a previous run already caught or found unviable (accumulated in
# mutants.out/previously_caught.txt). A heuristic: confirm with a full `just mutants` before
# trusting a clean result, since it assumes new tests never reduce coverage elsewhere.
mutants-iterate jobs="3": require-corpus
{{ mutants_env }} cargo mutants -p idakit --jobs {{ jobs }} --iterate
# Only the mutants touching lines changed since `base`, including uncommitted edits. `base`
# takes any revision git resolves, including relative ones (`HEAD~5`), with no hash-typing
# required. Diffs against the working tree, not `HEAD`, so staged and unstaged changes count too.
mutants-diff base="master": require-corpus
git diff {{ base }} > /tmp/idakit-mutants.diff
{{ mutants_env }} cargo mutants -p idakit --in-diff /tmp/idakit-mutants.diff
# One shard of N for CI fan-out, e.g. `just mutants-shard 0/8`.
mutants-shard shard: require-corpus
{{ mutants_env }} cargo mutants -p idakit --shard {{ shard }}
# Only one file's mutants, e.g. `just mutants-file crates/idakit/src/name.rs`, for checking a
# specific fix without paying for the whole tree. Goes through `require-corpus` like the rest:
# a bare `cargo mutants` misses this Justfile's dotenv-load and `mutants_env`, leaving the kernel
# tests without a corpus to skip against (so every mutant reports MISSED against a run that checked
# nothing) and running the whole corpus fan-out per mutant if it did find one.
#
# Filters with `--re`, not `--file`: mutants.toml's `examine_globs` silently overrides `--file`,
# so the obvious spelling runs the entire tree while reporting that it scoped to one file. `--re`
# matches the `--list` line, which is `<path>:<line>:<col>: <mutant>`, so anchoring on the path
# scopes it. Dots in `file` stay unescaped -- they match themselves in any real path.
mutants-file file jobs="3": require-corpus
{{ mutants_env }} cargo mutants -p idakit --jobs {{ jobs }} --re '{{ file }}:'