diff --git a/test/tool/devfs_sweep_test.dart b/test/tool/devfs_sweep_test.dart index 472a435da..b8450d213 100644 --- a/test/tool/devfs_sweep_test.dart +++ b/test/tool/devfs_sweep_test.dart @@ -74,7 +74,11 @@ List namesIn(Directory home) { '${home.path}/Library/Developer/CoreSimulator/Devices/DEV-1/data' '/Containers/Data/Application/APP-1/tmp', ); - return tmp.listSync().map((e) => e.path.split('/').last).toList()..sort(); + return tmp + .listSync() + .map((e) => e.path.split(Platform.pathSeparator).last) + .toList() + ..sort(); } void main() { diff --git a/tool/check.sh b/tool/check.sh index c8fb54ab9..3582a6679 100755 --- a/tool/check.sh +++ b/tool/check.sh @@ -65,9 +65,10 @@ step 'codegen is up to date' # build_info.g.dart is excluded because it can never match: it holds the hash # of the commit it is committed in. generated_hashes() { + require_sha256 || return git ls-files -co --exclude-standard -- '*.g.dart' '*.freezed.dart' | grep -v '^lib/core/build_info\.g\.dart$' | - LC_ALL=C sort | tr '\n' '\0' | xargs -0 shasum -a 256 + LC_ALL=C sort | tr '\n' '\0' | xargs -0 "${SHA256[@]}" } codegen_check() { diff --git a/tool/check/intent_localization.py b/tool/check/intent_localization.py index 05d78e793..b2c570705 100644 --- a/tool/check/intent_localization.py +++ b/tool/check/intent_localization.py @@ -17,7 +17,7 @@ def validate(root): """Return actionable violations; locales come from the Xcode project.""" errors = [] - project = (root / "ios/Runner.xcodeproj/project.pbxproj").read_text() + project = (root / "ios/Runner.xcodeproj/project.pbxproj").read_text(encoding="utf-8") regions = re.search(r"knownRegions\s*=\s*\((.*?)\);", project, re.S) development = re.search(r"developmentRegion\s*=\s*([^;]+);", project) if not regions or not development: @@ -37,7 +37,7 @@ def validate(root): model = plistlib.loads(definition.read_bytes()) # Use the definition's stem, matching Apple's metadata loader. catalog_path = definition.with_suffix(".xcstrings") - catalog = json.loads(catalog_path.read_text()) + catalog = json.loads(catalog_path.read_text(encoding="utf-8")) except (OSError, ValueError, plistlib.InvalidFileException) as error: errors.append(f"{label}: cannot load matching Intent/catalog ({error})") continue diff --git a/tool/check/tooling.sh b/tool/check/tooling.sh index 28d015072..ceaf69dab 100755 --- a/tool/check/tooling.sh +++ b/tool/check/tooling.sh @@ -66,7 +66,8 @@ for wf in .github/workflows/*.yml; do ) done -# Every script in tool/, checked as a script. +# Every executable workflow in tool/, checked as a script. Shared libraries are +# parsed too, but are not launched directly. # # `tool/run.sh` runs this at launch, so these are the things worth catching # before anything else happens: a script that does not parse, one that is not @@ -75,7 +76,10 @@ done while IFS= read -r script; do bash -n "$script" 2>/dev/null || report "$script: does not parse" - [[ -x $script ]] || report "$script: is not executable (chmod +x)" + # `_lib.sh` is sourced by workflows, not launched as one. It deliberately has + # no shebang, so do not require an executable bit for this shared library. + [[ $script == tool/dev/_lib.sh || -x $script ]] || + report "$script: is not executable (chmod +x)" head -1 "$script" | grep -q '^#!/usr/bin/env bash$' || [[ $script == tool/dev/_lib.sh ]] || diff --git a/tool/dev/_lib.sh b/tool/dev/_lib.sh index 8f8f376a9..7a757d92d 100755 --- a/tool/dev/_lib.sh +++ b/tool/dev/_lib.sh @@ -10,6 +10,14 @@ set -euo pipefail # The repo root, whatever directory the script was invoked from. repo_root() { cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd; } +# Git Bash, MSYS2 or Cygwin on Windows. +is_windows() { + case "$(uname -s)" in + MINGW* | MSYS* | CYGWIN*) return 0 ;; + *) return 1 ;; + esac +} + # Refuses to go on without mise, and without mise owning the toolchain. # # A bare `flutter` is not merely discouraged here, it is forbidden — because the @@ -80,7 +88,21 @@ EOF # is identical either way. local owned owned="$(cd "$root" && mise where flutter 2>/dev/null || true)" - if [[ -z $owned || $resolved != "$owned"/* ]]; then + + # Git Bash receives Windows paths with backslashes from mise. Normalize both + # sides before checking containment; otherwise a valid child path is rejected + # because the POSIX glob below only matches `/` separators. + local resolved_norm="${resolved//\\//}" + local owned_norm="${owned//\\//}" + resolved_norm="${resolved_norm%/}" + owned_norm="${owned_norm%/}" + # Windows paths are case-insensitive, and mise and PATH need not agree on + # the drive letter's case. Elsewhere a case difference is a different path. + if is_windows; then + resolved_norm="$(printf '%s' "$resolved_norm" | tr '[:upper:]' '[:lower:]')" + owned_norm="$(printf '%s' "$owned_norm" | tr '[:upper:]' '[:lower:]')" + fi + if [[ -z $owned || $resolved_norm != "$owned_norm"/* ]]; then printf '\n flutter resolves to %s\n' "$resolved" >&2 printf ' which is not inside mise (%s).\n\n' "${owned:-not installed}" >&2 printf ' Run `mise install` from %s.\n\n' "$root" >&2 @@ -139,15 +161,37 @@ step() { printf '\n\033[1m▸ %s\033[0m\n' "$*" >&2; } # clean` cannot reach it, and it is per-clone. cache_dir() { printf '%s/dpip-checks' "$(git rev-parse --git-dir)"; } +# The SHA-256 command, as an array rather than a function so that xargs can +# run it: a shell function does not exist in xargs' child process. macOS and +# Linux have `shasum`; Git Bash on Windows may have only `sha256sum`. Both +# print ` `. +if command -v shasum >/dev/null 2>&1; then + SHA256=(shasum -a 256) +elif command -v sha256sum >/dev/null 2>&1; then + SHA256=(sha256sum) +else + SHA256=() +fi + +# Fails when there is no SHA-256 command: with an empty SHA256, `xargs` would +# run `echo` instead and every cache key would silently be a list of names. +require_sha256() { + ((${#SHA256[@]})) && return 0 + printf 'error: no SHA-256 utility found (shasum or sha256sum)\n' >&2 + return 127 +} + # A hash of every tracked and every untracked-but-not-ignored file under the # given paths — contents *and* names, so a rename or a deletion also invalidates. -# Measured at 0.09 s over 793 files. +# One xargs batch, not one process per file: measured at 0.18 s over 1,265 +# files, where a process per file took 12 s on macOS (`shasum` is Perl). cache_key() { + require_sha256 || return local files files="$(git ls-files -co --exclude-standard -- "$@" | LC_ALL=C sort)" [[ -z $files ]] && { printf 'empty'; return; } - printf '%s' "$files" | tr '\n' '\0' | xargs -0 shasum -a 256 | - shasum -a 256 | cut -d' ' -f1 + printf '%s' "$files" | tr '\n' '\0' | xargs -0 "${SHA256[@]}" | + "${SHA256[@]}" | cut -d' ' -f1 } # What each cached step reads. Anything a step's result depends on has to be in diff --git a/tool/gen/widget_township_resources.py b/tool/gen/widget_township_resources.py index ba5f5aba8..ffc8fa802 100644 --- a/tool/gen/widget_township_resources.py +++ b/tool/gen/widget_township_resources.py @@ -75,11 +75,18 @@ def main() -> int: outputs = generated_outputs() if args.check: - stale = [ - path - for path, expected in outputs.items() - if not path.is_file() or path.read_bytes() != expected - ] + stale = [] + for path, expected in outputs.items(): + if not path.is_file(): + stale.append(path) + continue + + actual = path.read_bytes() + if path == DIRECTORY_OUTPUT: + actual = actual.replace(b"\r\n", b"\n") + + if actual != expected: + stale.append(path) if stale: for path in stale: print(