diff --git a/docs/superpowers/evidence/flutter-git-env-wrapper-2026-07-26.json b/docs/superpowers/evidence/flutter-git-env-wrapper-2026-07-26.json new file mode 100644 index 00000000..4ffea676 --- /dev/null +++ b/docs/superpowers/evidence/flutter-git-env-wrapper-2026-07-26.json @@ -0,0 +1,58 @@ +{ + "intent": "Make it impossible for the Flutter SDK to misidentify itself when invoked from a git hook, so Flutter gates in any repo stop failing with 'The current Flutter SDK version is 0.0.0-unknown'.", + "scope": [ + "linux_configuration/scripts/single_use/fixes/fix_flutter_git_env.sh (new)", + "Installs to ~/.local/share/flutter-git-env/wrapper.sh with ~/.local/bin/{flutter,dart} symlinks", + "Non-goal: patching /opt/flutter/bin/internal/shared.sh — pacman-owned by flutter-bin, an update would silently restore the bug", + "Non-goal: CI runners, which do not set GIT_DIR because they call flutter from workflow steps rather than hooks" + ], + "changes": [ + "Root cause: git exports GIT_DIR to its hooks, and `git -C \"$FLUTTER_ROOT\" rev-parse HEAD` in shared.sh does not beat it — `-C` only changes directory while GIT_DIR overrides repository discovery. flutter therefore reads the repo being committed as its own SDK, reports that repo's HEAD as the framework revision, resolves the SDK as 0.0.0-unknown, and caches the wrong answer in bin/cache/flutter.version.json where it poisons later callers in other repos.", + "Adds a PATH wrapper ahead of /usr/bin that strips GIT_DIR and the other git hook variables, then execs the real binary. Covers flutter and dart (both are aur_* shims into /opt/flutter).", + "The wrapper removes its own directory from PATH before exec, so the Flutter entry point's internal `which flutter` lookup cannot loop back into it.", + "Ships --check (re-verify, e.g. after a flutter update) and --uninstall. Install is idempotent.", + "Verification is built in: the installer creates a throwaway git repo, runs flutter with GIT_DIR pointed at it, and fails unless flutter still reports github.com/flutter/flutter." + ], + "verification": [ + { + "command": "GIT_DIR=/home/kuhy/diet-guard/.git git -C /opt/flutter rev-parse HEAD", + "result": "reproduced", + "evidence": "84bd5503316568914ed93b9451c397ff517a09d4 (diet-guard's HEAD) vs 058e0af2c2... without GIT_DIR — confirms `git -C` loses to GIT_DIR" + }, + { + "command": "GIT_DIR=/home/kuhy/diet-guard/.git flutter --version # before the fix", + "result": "reproduced", + "evidence": "Flutter 3.47.0-0.1.pre • channel main • https://github.com/kuhyx/diet-guard — the SDK reporting the project as itself" + }, + { + "command": "cd /home/kuhy/diet-guard/app && GIT_DIR=/home/kuhy/diet-guard/.git flutter analyze # after the fix, no per-call env -u", + "result": "pass", + "evidence": "Analyzing app... No issues found! (ran in 1.2s) — previously failed with 'url_launcher_platform_interface ^2.3.2 is forbidden' against 0.0.0-unknown" + }, + { + "command": "GIT_DIR=... dart --version; flutter --version; command -v flutter dart", + "result": "pass", + "evidence": "Dart SDK version: 3.12.2 (stable); Flutter 3.44.8 • channel stable • github.com/flutter/flutter; both resolve to /home/kuhy/.local/bin — normal use unaffected, no exec loop" + }, + { + "command": "bash fix_flutter_git_env.sh --check && bash fix_flutter_git_env.sh", + "result": "pass", + "evidence": "verified: Flutter 3.44.8 • channel stable • https://github.com/flutter/flutter.git; reinstall is idempotent" + }, + { + "command": "shellcheck linux_configuration/scripts/single_use/fixes/fix_flutter_git_env.sh", + "result": "pass", + "evidence": "no findings" + } + ], + "risks": [ + "Shadows /usr/bin/flutter and /usr/bin/dart on PATH for this user. Mitigated by exec'ing the real binary by absolute path and by the installer refusing to overwrite a non-symlink at either location.", + "If ~/.local/bin ever stops being on PATH the wrapper silently stops applying; the installer and --check both fail loudly on that, but only when run.", + "Per-call `env -u GIT_DIR` remains in screen-locker's and diet-guard's hooks on purpose: those repos may be cloned onto machines without this wrapper." + ], + "rollback": [ + "bash linux_configuration/scripts/single_use/fixes/fix_flutter_git_env.sh --uninstall", + "Confirm `command -v flutter` is /usr/bin/flutter again and `flutter --version` reports 3.x on channel stable", + "Expect Flutter gates run from git hooks to start failing with 0.0.0-unknown again unless the per-call env -u fixes are present" + ] +} diff --git a/linux_configuration/scripts/single_use/fixes/fix_flutter_git_env.sh b/linux_configuration/scripts/single_use/fixes/fix_flutter_git_env.sh new file mode 100755 index 00000000..a754ad8a --- /dev/null +++ b/linux_configuration/scripts/single_use/fixes/fix_flutter_git_env.sh @@ -0,0 +1,183 @@ +#!/bin/bash + +# ============================================================================ +# Stop the Flutter SDK misreading YOUR repository as itself inside git hooks. +# +# `git commit` / `git push` export GIT_DIR (and GIT_WORK_TREE, +# GIT_INDEX_FILE) to every hook they run. The flutter tool shells out to git +# to identify its own SDK, and `git -C "$FLUTTER_ROOT" rev-parse HEAD` +# (bin/internal/shared.sh) does NOT beat an inherited GIT_DIR -- `-C` only +# changes directory, while GIT_DIR overrides repository discovery outright: +# +# $ git -C /opt/flutter rev-parse HEAD +# 058e0af2c2... <- Flutter's HEAD +# $ GIT_DIR=~/some-repo/.git git -C /opt/flutter rev-parse HEAD +# 84bd550331... <- YOUR repo's HEAD +# +# So inside a hook, `flutter --version` reports your project as the SDK, pub +# resolves the SDK as "0.0.0-unknown" and every version constraint becomes +# unsatisfiable ("... is forbidden"). Worse, flutter caches that answer in +# bin/cache/flutter.version.json, so the corruption outlives the hook and +# follows the next caller into an unrelated repository -- which is why the +# same command passes when you re-run it by hand and fails again next push. +# +# Upstream already unsets these variables in bin/internal/content_aware_hash.sh +# and bin/internal/update_engine_version.sh, but not in the version path. +# +# THE FIX: a wrapper ahead of /usr/bin on PATH that strips the git-hook +# variables and hands off to the real binary. Per-call `env -u GIT_DIR ...` in +# each hook works too, but only for as long as every future hook author +# remembers; this makes the failure impossible for every caller in every repo. +# +# Deliberately NOT patching /opt/flutter/bin/internal/shared.sh: that file is +# owned by the flutter-bin package and the next update would silently restore +# the bug. +# +# Usage: fix_flutter_git_env.sh [--check | --uninstall] +# ============================================================================ + +set -euo pipefail + +SCRIPT_NAME="$(basename "$0")" +readonly SCRIPT_NAME +readonly BIN_DIR="${HOME}/.local/bin" +readonly LIB_DIR="${HOME}/.local/share/flutter-git-env" +readonly WRAPPER="${LIB_DIR}/wrapper.sh" +# Every SDK entry point that shells out to git to identify itself. +readonly TOOLS=(flutter dart) + +log() { echo "$SCRIPT_NAME: $*"; } + +fail() { + echo "$SCRIPT_NAME: $*" >&2 + exit 1 +} + +write_wrapper() { + mkdir -p "$LIB_DIR" "$BIN_DIR" + cat > "$WRAPPER" <<'WRAPPER_EOF' +#!/bin/bash + +# Strip the git-hook environment, then hand off to the real SDK binary. +# Installed by linux_configuration/scripts/single_use/fixes/fix_flutter_git_env.sh +# -- see that script for why this exists. Invoked through a symlink named +# after the tool it fronts (flutter, dart). + +set -euo pipefail + +tool="$(basename "$0")" +real="/usr/bin/${tool}" + +[[ -x $real ]] || { + echo "flutter-git-env: $real is missing or not executable" >&2 + exit 127 +} + +# Drop our own directory so a PATH lookup inside the real tool cannot find +# this wrapper again. The Flutter entry point does exactly such a lookup. +clean_path="" +IFS=':' read -r -a path_parts <<< "$PATH" +for part in "${path_parts[@]}"; do + [[ $part == "$HOME/.local/bin" ]] && continue + clean_path="${clean_path:+$clean_path:}$part" +done + +# GIT_DIR is the one that actually bites; the rest are exported by git in the +# same breath and would mislead the SDK the same way. +exec env \ + -u GIT_DIR \ + -u GIT_WORK_TREE \ + -u GIT_INDEX_FILE \ + -u GIT_COMMON_DIR \ + -u GIT_OBJECT_DIRECTORY \ + -u GIT_ALTERNATE_OBJECT_DIRECTORIES \ + -u GIT_NAMESPACE \ + -u GIT_PREFIX \ + PATH="$clean_path" \ + "$real" "$@" +WRAPPER_EOF + chmod +x "$WRAPPER" +} + +link_tools() { + local tool link + for tool in "${TOOLS[@]}"; do + link="${BIN_DIR}/${tool}" + if [[ -e $link && ! -L $link ]]; then + fail "$link exists and is not a symlink — refusing to overwrite it" + fi + ln -sfn "$WRAPPER" "$link" + log "linked $link -> $WRAPPER" + done +} + +check_path() { + case ":$PATH:" in + *":$BIN_DIR:"*) ;; + *) fail "$BIN_DIR is not on PATH — the wrapper would never be used" ;; + esac + local resolved + for tool in "${TOOLS[@]}"; do + resolved="$(command -v "$tool" || true)" + [[ $resolved == "${BIN_DIR}/${tool}" ]] || + fail "$tool resolves to '$resolved', not the wrapper — check PATH order" + done +} + +# The real test: does the SDK still know what it is with GIT_DIR poisoned? +verify() { + local probe_repo probe_version + probe_repo="$(mktemp -d)" + git -C "$probe_repo" init -q + git -C "$probe_repo" -c user.email=x@y -c user.name=x commit -q --allow-empty -m probe + + probe_version="$(GIT_DIR="$probe_repo/.git" flutter --version 2>&1 | head -1 || true)" + rm -rf "$probe_repo" + + if grep -q '0\.0\.0-unknown' <<< "$probe_version"; then + fail "still broken: flutter reports an unknown SDK under GIT_DIR" + fi + if ! grep -q 'flutter/flutter' <<< "$probe_version"; then + fail "still broken: flutter reports '$probe_version' under GIT_DIR" + fi + log "verified: $probe_version" +} + +uninstall() { + local tool link + for tool in "${TOOLS[@]}"; do + link="${BIN_DIR}/${tool}" + if [[ -L $link && "$(readlink -f "$link")" == "$WRAPPER" ]]; then + rm -f "$link" + log "removed $link" + fi + done + rm -rf "$LIB_DIR" + log "uninstalled" +} + +main() { + case "${1:-}" in + --uninstall) + uninstall + return + ;; + --check) + [[ -x $WRAPPER ]] || fail "wrapper is not installed" + check_path + verify + return + ;; + "") ;; + *) fail "unknown option: $1 (expected --check or --uninstall)" ;; + esac + + command -v flutter >/dev/null 2>&1 || fail "flutter is not on PATH" + write_wrapper + link_tools + check_path + verify + log "done — flutter/dart now ignore an inherited GIT_DIR" +} + +main "$@"