From f04672f350d01a57808d50328643597da75188ae Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Mon, 17 Aug 2026 12:05:06 +0200 Subject: [PATCH] build(make): prove setup converged instead of trusting build_runner's exit code `make setup` ran build_runner and reported success whether it produced the 39 files a fresh clone needs or almost nothing. `flutter test`'s only guard checked a single sentinel file, which is why the 2026-08-09 4-of-46 gap still read as 26 passed / 17 failed instead of a missing build step. ci/check_codegen.sh walks every `part` directive under lib/ and confirms the sibling file it names exists, then wires into both `setup` (fail loud right after codegen if it under-produced) and `test` (fail loud, exit 69, if nobody ran setup at all). Replaces the one-file guard, which would have missed 44 of the 45 directives that exist today. --- Makefile | 26 +++++++++++++++++++++-- ci/check_codegen.sh | 50 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+), 2 deletions(-) create mode 100755 ci/check_codegen.sh diff --git a/Makefile b/Makefile index cff2284..999d1f8 100644 --- a/Makefile +++ b/Makefile @@ -16,15 +16,20 @@ help: ## Show this help setup: ## Fetch dependencies and generate code (run this after a fresh clone) flutter pub get $(MAKE) generate + @ci/check_codegen.sh .PHONY: generate generate: ## Regenerate freezed/json_serializable/riverpod sources dart run build_runner build --delete-conflicting-outputs +.PHONY: check-codegen +check-codegen: ## Prove every part directive has a generated file on disk + @ci/check_codegen.sh + .PHONY: test test: ## Run the widget and unit tests - @test -f lib/core/auth/user_preferences.freezed.dart \ - || { echo "FAIL — generated sources missing; run: make setup"; exit 69; } + @ci/check_codegen.sh \ + || { echo "FAIL — generated sources missing or stale; run: make setup"; exit 69; } flutter test # Why the guard above: *.freezed.dart and lib/**/*.g.dart are gitignored, so a @@ -33,6 +38,23 @@ test: ## Run the widget and unit tests # looks like broken tests and is actually a missing build step. After # generating, the same suite is 452 passed. A test run that cannot compile # should say so in those words. +# +# `setup` and `test` both call ci/check_codegen.sh rather than one calling +# the other's target, because `setup`'s job is "make the tree usable" (fails +# loud if codegen silently produced less than the tree needs) and `test`'s +# job is "is the tree usable right now" (fails loud if nobody ran setup at +# all, or ran it before a source file changed). Same check, two different +# questions, so a shared script rather than a shared Make target — a Make +# target can only be reused by depending on it, which would make `test` +# imply `flutter pub get` and `build_runner`, both slow, every run. +# +# The check walks every `part '.g.dart'`/`part '.freezed.dart'` +# directive under lib/ and confirms the named sibling file exists — not one +# sentinel file (the previous guard checked only +# user_preferences.freezed.dart, which would have missed 44 of the 45 +# directives that exist today). See ci/check_codegen.sh for why this is +# preferred over `flutter analyze`: cheaper, and it targets exactly the +# generated/ungenerated distinction rather than static analysis in general. .PHONY: lint lint: ## Static analysis (analysis_options.yaml at the repo root) diff --git a/ci/check_codegen.sh b/ci/check_codegen.sh new file mode 100755 index 0000000..9ac8d95 --- /dev/null +++ b/ci/check_codegen.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +# Prove that generated sources exist for every part directive that requires +# one, after `make generate` has run. Lives here rather than inline in the +# Makefile so it can be read, run by hand (`make check-codegen`), and changed +# under review — same reasoning as ci/secrets.sh (D-27). +set -euo pipefail + +cd "$(git rev-parse --show-toplevel)" + +# *.freezed.dart and lib/**/*.g.dart are gitignored, so a fresh or stale +# checkout can silently have some but not all of the files a `part` +# directive names. `dart run build_runner build` exits 0 whether it produced +# everything the tree needs or almost nothing — exit code is not evidence +# (T-47). What IS evidence: every `part '';` directive in lib/ names a +# sibling file, and that file either exists or it doesn't. This walks every +# directive and checks its target directly, rather than trusting a single +# sentinel file (the old `test` guard checked one file, +# user_preferences.freezed.dart, and would have missed 44 other gaps). +# +# On 2026-08-09 this exact condition was 4 generated files present where 46 +# were needed. `flutter analyze` would also catch it, but slower and later — +# this check is the cheapest thing that proves the same fact. + +missing=0 +checked=0 + +while IFS=: read -r file part_line; do + # grep -H prefixes exactly one "file:" — no line numbers, so a colon + # inside the match (there is none here, but be safe) can't split wrong. + # part_line looks like: part 'auth_state.freezed.dart'; + target=$(printf '%s' "$part_line" | sed -E "s/^part '([^']+)';.*/\1/") + dir=$(dirname "$file") + checked=$((checked + 1)) + if [ ! -f "$dir/$target" ]; then + echo "MISSING generated file: $dir/$target (required by 'part' directive in $file)" >&2 + missing=$((missing + 1)) + fi +done < <(grep -rH "^part '" lib --include='*.dart') + +if [ "$checked" -eq 0 ]; then + echo "FAIL check-codegen — found zero 'part' directives under lib/; the check itself is broken, not the tree." >&2 + exit 1 +fi + +if [ "$missing" -gt 0 ]; then + echo "FAIL check-codegen — $missing of $checked generated files are missing. Run: make generate" >&2 + exit 1 +fi + +echo "check-codegen — $checked/$checked generated files present."