diff --git a/.github/workflows/batch-check.yml b/.github/workflows/batch-check.yml index 17548259..5a31b6ba 100644 --- a/.github/workflows/batch-check.yml +++ b/.github/workflows/batch-check.yml @@ -627,6 +627,21 @@ jobs: run: | & tests\selfapps_warnfix_venv_repair.ps1 + # CLAUDE.md Active Backlog Item 39: the EXE fast path's freshness check switched from + # mtime-only to a content-hash comparison, so a genuinely changed source file whose + # mtime is backdated (the ZIP/xcopy/robocopy delivery scenario) is no longer silently + # treated as fresh. uv lane only (non-gating for first landing -- the isolated + # tools/fast_check.ps1 script is already verified locally via real pwsh, but this is + # the first time the full :try_fast_exe -> :run_entry_smoke -> :success -> + # :write_fast_hash cycle runs end-to-end on real Windows CI). See + # selfapps_fastpath_hash.ps1's own header comment for the full mechanism. + - name: "Self-test: EXE fast path detects a backdated-mtime content change (uv lane only)" + if: ${{ matrix.mode == 'uv' }} + continue-on-error: true + shell: pwsh + run: | + & tests\selfapps_fastpath_hash.ps1 + # REQ-005.11: PEP 723 header write-back (uv add --script). v1-scoped to HP_ENV_MODE=uv, # so wired to the dedicated uv lane only (non-gating) for this first CI pass rather than # the gating real lane -- lets these brand-new scenarios prove out in real Windows CI @@ -1893,6 +1908,16 @@ jobs: tests\~selftest_exefastpath\~exefastpath_run2.log tests/~selftest_exefastpath/~setup.log tests\~selftest_exefastpath\~setup.log + tests/~selftest_fastpath_hash/~fastpath_hash_run1.log + tests\~selftest_fastpath_hash\~fastpath_hash_run1.log + tests/~selftest_fastpath_hash/~fastpath_hash_run2.log + tests\~selftest_fastpath_hash\~fastpath_hash_run2.log + tests/~selftest_fastpath_hash/~setup.log + tests\~selftest_fastpath_hash\~setup.log + tests/~selftest_fastpath_hash/~run.out.txt + tests\~selftest_fastpath_hash\~run.out.txt + tests/~selftest_fastpath_hash/~fast_check.hash.txt + tests\~selftest_fastpath_hash\~fast_check.hash.txt tests/~pandas_excel/~pandas_excel.log tests\~pandas_excel\~pandas_excel.log tests/~pipgap/~pipgap.log diff --git a/CLAUDE.md b/CLAUDE.md index 0783f4b5..0f80a1a4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -865,6 +865,96 @@ but several represent real gaps worth closing before calling the path fully rele **Coverage gap to close in the same slice**: no scenario backdates a source file's mtime below the EXE's to test this. Add one. + **Implemented: freshness is now a content-hash comparison, closing both exposures at once.** + `HP_FAST_CHECK` (canonical source `tools/fast_check.ps1`, previously inline-only with no + `tools/` file) now hashes the SAME `*.py` file set already scanned (unchanged filter), extended + to include `requirements.txt`/`pyproject.toml`/`runtime.txt` when present -- a combined SHA256 + over each file's own `relpath|filehash` line, sorted by path for determinism. This structurally + cannot be fooled by a preserved/backdated mtime (exposure a) and now sees dependency-file + changes regardless of mtime (exposure b), closing both suggested fix directions at once rather + than picking one. Two modes via a second positional arg: `check` (default, called from + `:try_fast_exe`, unchanged call site) prints `fresh` iff a stored `~fast_check.hash.txt` exists + and matches; a missing stored hash (first run under this fix, or an EXE built by an older + `run_setup.bat`) is a safe, one-time-cost "not fresh" default, never a false "fresh." `write` + (new subroutine `:write_fast_hash`, called from `:success`) unconditionally (re)writes the + hash after a genuine fresh build attempt. Uses `[System.Security.Cryptography.SHA256]` + directly, not `Get-FileHash`, per this repo's own "Prefer raw .NET types over Utility-module + cmdlets" lesson (Utility-module auto-load is not guaranteed in the `-File` invocation shape + embedded helpers run under on real Windows PowerShell 5.1, invisible to local `pwsh`/Linux + testing). + + **A real bug in the first-shipped `write` gate, found by CodeRabbit's review of this same + PR (#460) before landing.** The original gate was `if not defined HP_FASTPATH_USED call + :write_fast_hash` -- but `HP_FASTPATH_USED` being unset does not prove a build actually + SUCCEEDED this run; a skipped or FAILED rebuild also leaves it unset, and if a stale + `dist\.exe` from an EARLIER successful run was still sitting there untouched, this gate + would write the CURRENT (changed) sources' hash paired against that OLD binary -- the next + run would then wrongly trust the stale EXE as "fresh," silently reintroducing the exact class + of bug this item exists to fix, just relocated one layer down. Fixed with a new, more precise + flag, `HP_FRESH_BUILD_OK` -- reset once per fresh build attempt (alongside + `HP_NUITKA_FALLBACK_USED` in `:run_entry_smoke`'s own init block) and set ONLY in the 4 + genuine build-success branches (PyInstaller producing `dist\%ENVNAME%.exe` cleanly, or Tier + A/Nuitka succeeding after PyInstaller didn't, across all 3 of its own failure sub-branches) -- + never on any `:warn_build_incomplete` path. `HP_FRESH_BUILD_OK` being set already implies the + fast path was not taken (a build can only be attempted after the fast path declines to fire), + so this is a strictly more precise REPLACEMENT for the old `HP_FASTPATH_USED` check, not an + additional condition alongside it -- `:success` now gates on `if defined HP_FRESH_BUILD_OK` + alone. A later repair-loop rebuild (`:hidden_import_recover`/`:dll_bundle_recover`) failing + after the initial build already succeeded does NOT unset the flag -- reasoned as correct, + since those loops fix BUNDLING issues, not SOURCE CONTENT, so the already-built EXE from the + same run still genuinely reflects the current sources even if a later repair attempt fails; + `:write_fast_hash`'s own pre-existing `if not exist "dist\%ENVNAME%.exe" exit /b 0` guard + remains the correct backstop for the rarer case where a repair-loop failure actually removes + the file. + + **Coverage gap closed**: `tests/selfapps_fastpath_hash.ps1` (uv lane, non-gating for first + landing) is the literal scenario this item asked for -- run 1 builds the EXE printing a V1 + token, and asserts `~fast_check.hash.txt` was genuinely written before proceeding (a + CodeRabbit-review addition: without this, a completely broken write side would be + indistinguishable from a working one, since a missing hash file also safely forces a rebuild); + entry.py is rewritten to print V2 and its mtime backdated to 2001-09-09 (well before the EXE's + own mtime) before run 2; asserts run 2's captured EXE stdout shows V2 (not V1) and the "Fast + path: reusing" line is absent, proving a genuine rebuild happened rather than the old + mtime-only check wrongly reusing the stale EXE. `tests/test_fast_check.py` unit-tests the + isolated script directly via real `pwsh` (6 scenarios: no stored hash, write-then-fresh, + backdated-mtime content change, dependency-file-only change, rewrite-after-change, missing + EXE) plus a `PayloadSync` byte-equality check against `tools/fast_check.ps1`. + `tests/harness.ps1`'s `batch.fastpath.hash_write` statically guards the `:write_fast_hash` + wiring -- label exists, called from `:success` with the `HP_FRESH_BUILD_OK` gate, invokes the + payload in `write` mode -- via SCOPED body extraction (bounded by each subroutine's own next + label, another CodeRabbit-review fix: the original check used whole-file `-match`, which could + pass even if the call or the write invocation were removed, as long as matching text happened + to exist anywhere else in the file). See `docs/agent-ndjson.md` for the full row registry entry. + + **A second CodeRabbit review round on the same PR caught four more findings, all fixed before + landing.** (1) A defensive `set "HP_FRESH_BUILD_OK="` was added at the very top of the file + (alongside the pre-existing `HP_FASTPATH_USED=` reset, before the first `:try_fast_exe` call of + the whole run) -- `:run_entry_smoke`'s own per-build-attempt reset sits past a preflight-failure + early-return (`HP_PREFLIGHT_FAILED`), so a provider-cascade re-entry hitting that early return + would skip the in-subroutine reset; no concrete exploit was confirmed against the current + single-invocation call graph (every write site pairs a genuinely fresh build with unchanged + sources even across this path), but the fix is a zero-cost, unconditional close of the whole + class of risk regardless. (2) `:write_fast_hash` now captures the PowerShell write call's own + exit code and logs `[WARN] Fast-path hash write failed; the next run will rebuild.` on a nonzero + result, instead of silently deleting the helper and returning success either way -- matches this + repo's own "Bootstrap must fail fast and explicitly -- no silent fallbacks unless explicitly + logged" principle; still safe either way (a failed write leaves no stored hash, forcing exactly + one more rebuild), just no longer silent. (3) `tests/selfapps_fastpath_hash.ps1`'s + `hashWrittenAfterRun1` check was `Test-Path`-only (existence, not content) -- a truncated or + malformed write would pass the same way a working one would, since a broken file is just as + "not fresh" as a missing one to the read side. Now reads the file and validates it matches + `tools/fast_check.ps1`'s own canonical format (a 64-char lowercase hex SHA256 digest, no + trailing newline) via `hashContentValid`, folded into `$pass`. (4) The same test never confirmed + its own core precondition -- that `entry.py`'s backdated mtime assignment actually took, and is + genuinely older than the built EXE's own mtime -- before trusting run 2's outcome as evidence the + NEW hash-based path (not the OLD mtime-only path, coincidentally rebuilding for an unrelated + reason) did the detecting. Added `mtimePreconditionHolds` (compares `entry.py`'s and the built + EXE's `LastWriteTimeUtc`), also folded into `$pass`. `.github/workflows/batch-check.yml`'s + "Upload test logs" step gained both path-style variants for `~fast_check.hash.txt` under + `tests/~selftest_fastpath_hash/` (a new observable artifact this PR introduced, per this repo's + own coding guideline that every new artifact needs both slash-style upload paths) -- previously + only the run logs/`~setup.log`/`~run.out.txt` were wired. + - **Item 42: console output is verbose across all log levels, and every fresh build ends with two unexplained Y/N prompts -- both plausibly overwhelming for the actual target audience (beginners with no setup experience).** Confirmed reasoned-from-source and CI capture. New; not diff --git a/docs/agent-interconnect.md b/docs/agent-interconnect.md index 01c3178e..159b3949 100644 --- a/docs/agent-interconnect.md +++ b/docs/agent-interconnect.md @@ -1184,6 +1184,51 @@ individual test file pins it locally -- see the "Accepted gap" entry in path is completely provider-independent. The test correctly validates the EXE fast path, not the env-state or uv venv fast path. +**`HP_FAST_CHECK`'s own freshness signal (CLAUDE.md Active Backlog Item 39): content-hash, not +mtime -- read side and write side are two separate call sites, must stay paired.** `:try_fast_exe` +(read/`check` mode, unchanged call site) compares a stored `~fast_check.hash.txt` against a +composite SHA256 over the same non-infra `*.py` files already scanned, extended to +`requirements.txt`/`pyproject.toml`/`runtime.txt` when present. **The write side is a SEPARATE +subroutine, `:write_fast_hash`, called from `:success`** (not inlined there -- a set-then-read of +the same var inside one parenthesized block would hit the classic parse-time-`%VAR%`-expansion +trap, see `docs/agent-lessons-learned.md`), gated on `if defined HP_FRESH_BUILD_OK`. + +**`HP_FRESH_BUILD_OK`, not `HP_FASTPATH_USED`, is the correct gate -- a real bug CodeRabbit's +review caught before this shipped.** The first-shipped gate was `if not defined HP_FASTPATH_USED`, +reasoning "skip the redundant re-hash when the fast path already proved nothing changed." But +`HP_FASTPATH_USED` being unset only proves the fast path was NOT taken -- it does NOT prove a +build SUCCEEDED. A skipped or genuinely FAILED rebuild also leaves it unset, and if a stale +`dist\.exe` from an EARLIER successful run was still sitting there untouched, that gate would +write the CURRENT (changed) sources' hash paired against the OLD binary -- the next run would then +wrongly trust the stale EXE as "fresh," reintroducing Item 39's own bug one layer down. Fixed with +`HP_FRESH_BUILD_OK`: reset once per fresh build attempt in `:run_entry_smoke`'s own init block +(alongside `HP_NUITKA_FALLBACK_USED`), set ONLY in the 4 genuine build-success branches inside that +subroutine's PyInstaller/Tier-A build block (PyInstaller producing `dist\%ENVNAME%.exe` cleanly, or +Tier A/Nuitka succeeding after PyInstaller didn't, across all 3 of ITS OWN failure sub-branches) -- +never on any `:warn_build_incomplete` path. Since a build can only be ATTEMPTED after the fast path +has already declined to fire, `HP_FRESH_BUILD_OK` being set strictly implies `HP_FASTPATH_USED` was +never set either -- so this is a precision REPLACEMENT for the old gate, not an additional +condition layered on top of it; `:success` now checks `HP_FRESH_BUILD_OK` alone. + +**A later repair-loop rebuild failing does NOT need to unset the flag, by design.** +`:hidden_import_recover`/`:dll_bundle_recover` can each rebuild `dist\.exe` again AFTER the +initial build already succeeded and set `HP_FRESH_BUILD_OK=1` -- but these loops repair BUNDLING +issues (a missing DLL, a missing hidden import), not SOURCE CONTENT, so even if a later repair +attempt fails, the EXE from the initial successful build in the SAME run still genuinely reflects +the current source snapshot the hash is computed from. `:write_fast_hash`'s own pre-existing +`if not exist "dist\%ENVNAME%.exe" exit /b 0` guard remains the correct backstop for the rarer case +where a repair-loop failure actually removes the file rather than merely leaving it imperfect. + +**Any future change to either the scanned file set or the hash algorithm must update BOTH +`:try_fast_exe`'s check invocation and `:write_fast_hash`'s write invocation identically** -- they +share the single `tools/fast_check.ps1` script (mode-dispatched via a second positional arg), so a +change to the shared script's file enumeration or hashing logic automatically stays in sync between +the two call sites; only a change to the CALL SITES themselves (e.g. adding a new mode, or passing +different exe/cwd context) risks the two drifting apart. A missing stored hash (first run after +upgrading to this fix, or an +EXE built by an older `run_setup.bat` with no hash file at all) is a safe "not fresh" default -- +forces exactly one rebuild, never a false "fresh." + ### ~dependency_installed.txt: pip freeze output and its consumers `~dependency_installed.txt` is written after install via `pip freeze` (run_setup.bat lines 1122-1134): diff --git a/docs/agent-ndjson.md b/docs/agent-ndjson.md index 0beaa7e3..d1472c48 100644 --- a/docs/agent-ndjson.md +++ b/docs/agent-ndjson.md @@ -197,6 +197,7 @@ batch.conda.probe.deferred, uv.python.preference.configured, batch.script_root.trailing_backslash, batch.progress.conda_create, batch.progress.pyi_build, batch.progress.dep_install, +batch.fastpath.hash_write, batch.pyi.collect.precheck, batch.pyi.hidden_import.recover, batch.smoke.kill_warn, @@ -991,6 +992,37 @@ Non-gating for its first landing, matching this repo's established graduation pa self.exe.warnfix.venv_repair ``` +## selfapps-fastpath-hash NDJSON rows (selfapps_fastpath_hash.ps1, uv lane only, non-gating) + +CLAUDE.md Active Backlog Item 39: the EXE fast path's freshness check (`HP_FAST_CHECK`, +`tools/fast_check.ps1`) switched from mtime-only over `*.py` files to a content-hash +comparison over the same file set, extended to `requirements.txt`/`pyproject.toml`/ +`runtime.txt`. Closes two exposures: (a) a timestamp-preserving delivery method (a ZIP, +xcopy, robocopy) carrying a genuinely changed file whose mtime still predates the built +EXE was previously silently treated as fresh; (b) a dependency-file-only change was +previously invisible to the scan regardless of mtime. `:write_fast_hash` (called from +`:success`, gated on `HP_FASTPATH_USED` being unset so the already-fast reuse case never +pays a redundant re-hash pass) writes the stored hash after a genuine fresh build attempt. + +This test is the coverage-gap Item 39 itself named ("no scenario backdates a source file's +mtime below the EXE's to test this"): run 1 builds the EXE from an entry file printing a V1 +token; entry.py is then rewritten to print a V2 token and its mtime is set to 2001-09-09 +(well before `dist\.exe`'s own mtime) before run 2. Asserts run 2's captured EXE +stdout (`~run.out.txt`, written by both the fast-path-reuse launch and the fresh-build +smokerun) shows the V2 token, not V1 -- direct evidence a genuine rebuild happened, not +just that some log line says "rebuilding" -- and that the "Fast path: reusing" log line is +absent. The isolated `tools/fast_check.ps1` script itself is unit-tested directly via real +`pwsh` in `tests/test_fast_check.py` (6 scenarios: no stored hash, write-then-fresh, +backdated-mtime content change, dependency-file-only change, rewrite-after-change, missing +EXE) -- this selfapps test is what proves the full `:try_fast_exe -> :run_entry_smoke -> +:success -> :write_fast_hash` cycle wires correctly end-to-end on real Windows CI, which +`test_fast_check.py` alone cannot. Non-gating for its first landing, matching this repo's +established graduation pattern (see CLAUDE.md's "CI lane gating maturity" periodic check). + +``` +self.fastpath.hash.backdated_mtime +``` + ## selfapps-cache-selfheal NDJSON rows (test_ci_cache_selfheal.ps1, `real` lane only, GATING) Item 19 follow-on (docs/agent-closed-backlog.md): the cache-lane self-heal logic diff --git a/run_setup.bat b/run_setup.bat index ea9859f4..948b3966 100644 --- a/run_setup.bat +++ b/run_setup.bat @@ -652,6 +652,13 @@ call :compute_interactive_run rem --- Very top EXE fast path: reuse dist\%ENVNAME%.exe when sources are unchanged --- set "HP_FASTPATH_USED=" +rem CLAUDE.md Active Backlog Item 39 (CodeRabbit review, PR #460): defensive reset. Every real +rem write site for HP_FRESH_BUILD_OK lives inside :run_entry_smoke, whose own per-build-attempt +rem reset sits past a preflight-failure early-return (HP_PREFLIGHT_FAILED) -- a provider-cascade +rem re-entry that hits that early return would skip the in-subroutine reset. Clearing it here too, +rem before the very first :try_fast_exe call of the whole run, closes that gap unconditionally so +rem :write_fast_hash below can never fire off a value this run never genuinely earned. +set "HP_FRESH_BUILD_OK=" rem REQ-016: start clean so an inherited env var can never trigger a false "EXE rem unverified" caveat; :run_exe_smokerun sets this only on a real non-zero EXE exit. set "HP_EXE_VERIFY_FAILED=" @@ -2186,6 +2193,16 @@ if not defined HP_FASTPATH_USED ( rem other console output. call :print_fastpath_ambiguous_note ) +rem CLAUDE.md Active Backlog Item 39: gated on HP_FRESH_BUILD_OK, not merely "HP_FASTPATH_USED +rem unset" (CodeRabbit review, PR #460, caught a real bug in the original gate: a skipped or +rem failed rebuild also leaves HP_FASTPATH_USED unset, but a stale dist\%ENVNAME%.exe left over +rem from an earlier successful run would still be sitting there -- writing the CURRENT sources' +rem hash against that OLD binary would make the next run wrongly trust it as "fresh"). Set ONLY +rem in :run_entry_smoke's own genuine build-success branches -- see the reset/set sites there. +rem HP_FRESH_BUILD_OK being set already implies the fast path was not taken (a build can only be +rem attempted after the fast path declined to fire), so this alone is a strictly more precise +rem replacement for the old HP_FASTPATH_USED check, not an additional condition alongside it. +if defined HP_FRESH_BUILD_OK call :write_fast_hash call :release_lock rem REQ-016: retain terminal window on success so user can read the output. if not defined HP_CI_LANE ( @@ -3572,6 +3589,33 @@ if not "%HP_SMOKE_RC%"=="0" if not defined HP_PROBE_EXCEEDED ( set "HP_SMOKE_RC=" ) exit /b 0 +:write_fast_hash +rem derived requirement (CLAUDE.md Active Backlog Item 39): called only when the caller's own +rem HP_FRESH_BUILD_OK gate confirms a genuine build succeeded THIS run (see :run_entry_smoke's +rem reset + 4 success-branch sets) -- never for a fast-path reuse (nothing changed, stored hash +rem already correct) and never for a skipped/failed rebuild (would pair the CURRENT sources' +rem hash with a stale leftover EXE from an earlier run -- a real bug CodeRabbit's review caught +rem on PR #460 in an earlier, less precise HP_FASTPATH_USED-only gate). (Re)writes the +rem composite source-content hash the EXE fast path's freshness check compares against on the +rem next run. A dedicated subroutine, not inlined at :success, specifically so no line here +rem sits inside a parenthesized block -- see docs/agent-lessons-learned.md's "Parse-time +rem vs. runtime variable expansion" entry for why a set-then-read-in-the-same-block +rem pattern is unsafe, and this repo's own house preference for goto/call-based dispatch +rem over inlined blocks wherever a payload emit + invoke sequence is involved. +if not exist "dist\%ENVNAME%.exe" exit /b 0 +set "HP_FAST_CHECK_PS=~fast_check.ps1" +if exist "%HP_FAST_CHECK_PS%" del "%HP_FAST_CHECK_PS%" >nul 2>&1 +call :emit_from_base64 "%HP_FAST_CHECK_PS%" HP_FAST_CHECK +if errorlevel 1 exit /b 0 +powershell -NoProfile -ExecutionPolicy Bypass -File "%HP_FAST_CHECK_PS%" "dist\%ENVNAME%.exe" write >> "%LOG%" 2>&1 +set "HP_FAST_HASH_RC=%ERRORLEVEL%" +rem derived requirement (CodeRabbit review, PR #460): a write failure here is not fatal -- the +rem next run's missing-hash default safely forces exactly one rebuild -- but a silent failure +rem could repeat unnoticed across many runs. Log it explicitly rather than swallowing it. +if not "%HP_FAST_HASH_RC%"=="0" call :log "[WARN] Fast-path hash write failed; the next run will rebuild." +if exist "%HP_FAST_CHECK_PS%" del "%HP_FAST_CHECK_PS%" >nul 2>&1 +set "HP_FAST_HASH_RC=" +exit /b 0 :run_entry_smoke call :record_chosen_entry "%HP_ENTRY%" rem CLAUDE.md Active Backlog Item 45: a failed env-create can fall through :die (exit /b @@ -3729,6 +3773,17 @@ if not defined HP_BUILD_OK ( rem before this check -- warnfix can still fire and even rebuild over the Nuitka-built EXE, see rem the HP_NUITKA_FALLBACK_USED clear a few dozen lines below for how that case is handled. set "HP_NUITKA_FALLBACK_USED=" + rem CLAUDE.md Active Backlog Item 39 (CodeRabbit review, PR #460): a real correctness bug -- + rem gating :write_fast_hash on "HP_FASTPATH_USED unset" alone does not prove a build actually + rem SUCCEEDED this run; a stale dist\%ENVNAME%.exe left over from an EARLIER successful run + rem would still be sitting there if THIS run's rebuild is skipped/fails, and the freshness + rem hash would get (re)written for the CURRENT (changed) sources paired with that OLD binary + rem -- the next run would then wrongly trust the stale EXE as "fresh." HP_FRESH_BUILD_OK is + rem set ONLY in the genuine-success branches below -- PyInstaller producing + rem dist\%ENVNAME%.exe, or Tier A/Nuitka succeeding after it didn't -- never on a + rem warn_build_incomplete path. + rem Reset once per fresh build attempt, same reasoning as HP_NUITKA_FALLBACK_USED above. + set "HP_FRESH_BUILD_OK=" rem REQ-009/REQ-005.10 (cascade-vs-postexec fix): reset the "this provider's dependencies rem look incomplete" flag at the start of every fresh build attempt, not just when warnfix rem happens to run again -- a provider that needs no warnfix repair at all must not inherit @@ -3778,6 +3833,7 @@ if not defined HP_BUILD_OK ( call :warn_build_incomplete "[WARN] PyInstaller execution failed; will verify your code directly via Python instead. reason=test_forced_fail" ) else ( set "HP_NUITKA_FALLBACK_USED=1" + set "HP_FRESH_BUILD_OK=1" ) ) else ( "%HP_PY%" -m PyInstaller -y --onefile --clean --log-level WARN %HP_PYI_EXPAT% %HP_PYI_COLLECT% --name "%ENVNAME%" "%HP_ENTRY%" >> "%LOG%" 2>&1 @@ -3793,6 +3849,7 @@ if not defined HP_BUILD_OK ( call :warn_build_incomplete "[WARN] PyInstaller execution failed; will verify your code directly via Python instead. reason=build_error" ) else ( set "HP_NUITKA_FALLBACK_USED=1" + set "HP_FRESH_BUILD_OK=1" ) ) else ( if defined HP_TEST_FORCE_OUTPUT_VANISH if exist "dist\%ENVNAME%.exe" ( @@ -3807,9 +3864,11 @@ if not defined HP_BUILD_OK ( call :warn_build_incomplete "[WARN] PyInstaller did not produce dist\%ENVNAME%.exe; will verify your code directly via Python instead. reason=missing_output" ) else ( set "HP_NUITKA_FALLBACK_USED=1" + set "HP_FRESH_BUILD_OK=1" ) ) else ( call :log "[INFO] PyInstaller produced dist\%ENVNAME%.exe" + set "HP_FRESH_BUILD_OK=1" ) ) ) @@ -5082,17 +5141,16 @@ set "HP_PYPROJ_DEPS=IiIicHlwcm9qX2RlcHMgKEhQX1BZUFJPSl9ERVBTKSAtLSBleHRyYWN0cyBb set "HP_CONDARC=Y2hhbm5lbHM6CiAgLSBjb25kYS1mb3JnZQpjaGFubmVsX3ByaW9yaXR5OiBzdHJpY3QKc2hvd19jaGFubmVsX3VybHM6IHRydWUK" set "HP_DETECT_PY=IiIiZGV0ZWN0X3B5dGhvbiAoSFBfREVURUNUX1BZKSAtLSBSRVEtMDA0IFRpZXIgMS8yIHJlcXVpcmVzLXB5dGhvbiBkZXRlY3Rvci4KClJ1biBmcm9tIHRoZSBhcHBsaWNhdGlvbiBkaXJlY3Rvcnk7IHByaW50cyBhIG5vcm1hbGl6ZWQgY29uZGEtc3ludGF4CiJweXRob248b3A+PHZlcj5bLC4uLl0iIGNvbnN0cmFpbnQgdG8gc3Rkb3V0LCBvciBhbiBlbXB0eSBsaW5lIGlmIG5laXRoZXIKdGllciBmb3VuZCBhbnl0aGluZyAodGhlIGNhbGxlciB0aGVuIGxldHMgdGhlIHNlbGVjdGVkIHByb3ZpZGVyIHBpY2sgdGhlCmxhdGVzdCBhdmFpbGFibGUgUHl0aG9uIC0tIFJFUS0wMDQgVGllciAzLCBoYW5kbGVkIG91dHNpZGUgdGhpcyBoZWxwZXIpLgpEZXRlY3Rpb24gb3JkZXI6CgogIDEuIHJ1bnRpbWUudHh0IC0tIGEgYmFyZSAicHl0aG9uLVguWVsuWl0iIG9yICJYLllbLlpdIiBsaW5lIHBpbnMgYW4gZXhhY3QKICAgICBtaW5vciB2ZXJzaW9uIChweXRob249WC5ZOyB0aGUgcGF0Y2ggY29tcG9uZW50LCBpZiBwcmVzZW50LCBpcyBub3QKICAgICBmb3J3YXJkZWQgLS0gcHJvdmlkZXJzIHBpbiBieSBtaW5vciBvbmx5KS4KICAyLiBweXByb2plY3QudG9tbCdzIFtwcm9qZWN0XSByZXF1aXJlcy1weXRob24gLS0gYSBQRVAgNDQwIHNwZWNpZmllcgogICAgIChlLmcuICI+PTMuMTAsPDQiLCAifj0zLjExIikgaXMgcGFyc2VkIGFuZCBleHBhbmRlZCBpbnRvIG9uZSBvciBtb3JlCiAgICAgY29uZGEtc3ludGF4IGNsYXVzZXMgdmlhIHBlcDQ0MF90b19jb25kYSgpLgoKcGVwNDQwX3RvX2NvbmRhKCkgYWxzbyBkb3VibGVzIGFzIGEgc3RhbmRhbG9uZSBDTEkgdXRpbGl0eSAoc2VlIG1haW4oKSkgZm9yCnRyYW5zbGF0aW5nIGFyYml0cmFyeSBQRVAgNDQwIHNwZWNpZmllcnMsIGluY2x1ZGluZyB0aGUgIn49IiBjb21wYXRpYmxlLXJlbGVhc2UKb3BlcmF0b3IsIHdoaWNoIGNvbmRhIGhhcyBubyBuYXRpdmUgZXF1aXZhbGVudCBmb3IgYW5kIG11c3QgYmUgZXhwYW5kZWQgaW50bwphbiBleHBsaWNpdCA+PS88IHJhbmdlLgoKVGhpcyBpcyB0aGUgY2Fub25pY2FsIHNvdXJjZSBmb3IgdGhlIEhQX0RFVEVDVF9QWSBiYXNlNjQgcGF5bG9hZCBlbWJlZGRlZCBpbgpydW5fc2V0dXAuYmF0LiBBZnRlciBlZGl0aW5nLCByZS1lbmNvZGUgYW5kIHBhc3RlIGl0IGludG8gdGhlCmBzZXQgIkhQX0RFVEVDVF9QWT0uLi4iYCBsaW5lOyB0ZXN0cy90ZXN0X2RldGVjdF9weXRob24ucHkgYXNzZXJ0cyB0aGUKZW1iZWRkZWQgcGF5bG9hZCBtYXRjaGVzIHRoaXMgZmlsZS4KIiIiCl9fdmVyc2lvbl9fID0gImRldGVjdF9weXRob24gdjIgKDIwMjUtMDktMjQpIgpfX2FsbF9fID0gWyJwZXA0NDBfdG9fY29uZGEiLCAiZGV0ZWN0X3JlcXVpcmVzX3B5dGhvbiIsICJtYWluIl0KT1JERVIgPSB7Ij09IjogMCwgIiE9IjogMSwgIj49IjogMiwgIj4iOiAzLCAiPD0iOiA0LCAiPCI6IDV9CgppbXBvcnQgb3MKaW1wb3J0IHJlCmltcG9ydCBzeXMKCiMgSGVscGVyIGltcGxlbWVudHMgdGhlIFJFQURNRSBib290c3RyYXAgY29udHJhY3QuIFBFUCA0NDAgZGV0YWlsczoKIyBodHRwczovL3BlcHMucHl0aG9uLm9yZy9wZXAtMDQ0MC8KCkNEID0gb3MuZ2V0Y3dkKCkKUlVOVElNRV9QQVRIID0gb3MucGF0aC5qb2luKENELCAicnVudGltZS50eHQiKQpQWVBST0pFQ1RfUEFUSCA9IG9zLnBhdGguam9pbihDRCwgInB5cHJvamVjdC50b21sIikKUFlQUk9KRUNUX1JFID0gcmUuY29tcGlsZSgicmVxdWlyZXMtcHl0aG9uXFxzKj1cXHMqWydcIl0oW14nXCJdKylbJ1wiXSIsIHJlLklHTk9SRUNBU0UpClNQRUNfUEFUVEVSTiA9IHJlLmNvbXBpbGUocicofj18PT18IT18Pj18PD18Pnw8KVxzKihbMC05XSsoPzpcLlswLTldKykqKScpCgoKZGVmIHZlcnNpb25fa2V5KHRleHQ6IHN0cik6CiAgICAiIiJSZXR1cm4gYSB0dXBsZSB1c2FibGUgZm9yIG51bWVyaWMgb3JkZXJpbmcgb2YgZG90dGVkIHZlcnNpb25zLiIiIgogICAgcGFydHMgPSBbXQogICAgZm9yIGNodW5rIGluIHRleHQuc3BsaXQoJy4nKToKICAgICAgICB0cnk6CiAgICAgICAgICAgIHBhcnRzLmFwcGVuZChpbnQoY2h1bmspKQogICAgICAgIGV4Y2VwdCBWYWx1ZUVycm9yOgogICAgICAgICAgICBwYXJ0cy5hcHBlbmQoMCkKICAgIHJldHVybiB0dXBsZShwYXJ0cykKCgpkZWYgYnVtcF9mb3JfY29tcGF0aWJsZSh2ZXJzaW9uOiBzdHIpIC0+IHN0cjoKICAgICIiIlRyYW5zbGF0ZSB0aGUgUEVQIDQ0MCBjb21wYXRpYmxlIHJlbGVhc2UgdXBwZXIgYm91bmQuIiIiCiAgICBwaWVjZXMgPSBbaW50KGl0ZW0pIGZvciBpdGVtIGluIHZlcnNpb24uc3BsaXQoJy4nKSBpZiBpdGVtLmlzZGlnaXQoKV0KICAgIGlmIG5vdCBwaWVjZXM6CiAgICAgICAgcmV0dXJuIHZlcnNpb24KICAgIGlmIGxlbihwaWVjZXMpID49IDM6CiAgICAgICAgcmV0dXJuIGYie3BpZWNlc1swXX0ue3BpZWNlc1sxXSArIDF9IgogICAgaWYgbGVuKHBpZWNlcykgPT0gMjoKICAgICAgICByZXR1cm4gZiJ7cGllY2VzWzBdICsgMX0uMCIKICAgIHJldHVybiBzdHIocGllY2VzWzBdICsgMSkKCgpkZWYgZXhwYW5kX2NsYXVzZShvcDogc3RyLCB2ZXJzaW9uOiBzdHIpOgogICAgaWYgb3AgPT0gIn49IjoKICAgICAgICB1cHBlciA9IGJ1bXBfZm9yX2NvbXBhdGlibGUodmVyc2lvbikKICAgICAgICByZXR1cm4gWygiPj0iLCB2ZXJzaW9uKSwgKCI8IiwgdXBwZXIpXQogICAgcmV0dXJuIFsob3AsIHZlcnNpb24pXQoKCmRlZiBwZXA0NDBfdG9fY29uZGEoc3BlYzogc3RyKSAtPiBzdHI6CiAgICAiIiJSZXR1cm4gInB5dGhvbiIgY29uc3RyYWludHMgZXhwYW5kZWQgZnJvbSBhIHJlcXVpcmVzLXB5dGhvbiBzcGVjLiIiIgogICAgY2xhdXNlcyA9IFtdCiAgICBmb3IgcmF3IGluIHNwZWMuc3BsaXQoJywnKToKICAgICAgICByYXcgPSByYXcuc3RyaXAoKQogICAgICAgIGlmIG5vdCByYXc6CiAgICAgICAgICAgIGNvbnRpbnVlCiAgICAgICAgbWF0Y2ggPSBTUEVDX1BBVFRFUk4ubWF0Y2gocmF3KQogICAgICAgIGlmIG5vdCBtYXRjaDoKICAgICAgICAgICAgY29udGludWUKICAgICAgICBvcCwgdmVyc2lvbiA9IG1hdGNoLmdyb3VwcygpCiAgICAgICAgY2xhdXNlcy5leHRlbmQoZXhwYW5kX2NsYXVzZShvcCwgdmVyc2lvbikpCiAgICBpZiBub3QgY2xhdXNlczoKICAgICAgICByZXR1cm4gIiIKICAgIGRlZHVwID0ge30KICAgIGZvciBvcCwgdmVyc2lvbiBpbiBjbGF1c2VzOgogICAgICAgIGRlZHVwWyhvcCwgdmVyc2lvbildID0gKG9wLCB2ZXJzaW9uKQogICAgb3JkZXJlZCA9IHNvcnRlZChkZWR1cC52YWx1ZXMoKSwga2V5PWxhbWJkYSBpdGVtOiAoT1JERVIuZ2V0KGl0ZW1bMF0sIDk5KSwgdmVyc2lvbl9rZXkoaXRlbVsxXSkpKQogICAgcmV0dXJuICJweXRob24iICsgIiwiLmpvaW4oZiJ7b3B9e3ZlcnNpb259IiBmb3Igb3AsIHZlcnNpb24gaW4gb3JkZXJlZCkKCgpkZWYgcmVhZF9ydW50aW1lX3NwZWMoKSAtPiBzdHI6CiAgICBpZiBub3Qgb3MucGF0aC5leGlzdHMoUlVOVElNRV9QQVRIKToKICAgICAgICByZXR1cm4gIiIKICAgIHdpdGggb3BlbihSVU5USU1FX1BBVEgsICdyJywgZW5jb2Rpbmc9J3V0Zi04JywgZXJyb3JzPSdpZ25vcmUnKSBhcyBoYW5kbGU6CiAgICAgICAgdGV4dCA9IGhhbmRsZS5yZWFkKCkKICAgIG1hdGNoID0gcmUuc2VhcmNoKHInKD86cHl0aG9uWy09XSk/XHMqKFswLTldKyg/OlwuWzAtOV0rKXswLDJ9KScsIHRleHQpCiAgICBpZiBub3QgbWF0Y2g6CiAgICAgICAgcmV0dXJuICIiCiAgICBwYXJ0cyA9IG1hdGNoLmdyb3VwKDEpLnNwbGl0KCcuJykKICAgIG1ham9yX21pbm9yID0gJy4nLmpvaW4ocGFydHNbOjJdKQogICAgcmV0dXJuIGYncHl0aG9uPXttYWpvcl9taW5vcn0nCgoKZGVmIHJlYWRfcHlwcm9qZWN0X3NwZWMoKSAtPiBzdHI6CiAgICBpZiBub3Qgb3MucGF0aC5leGlzdHMoUFlQUk9KRUNUX1BBVEgpOgogICAgICAgIHJldHVybiAiIgogICAgd2l0aCBvcGVuKFBZUFJPSkVDVF9QQVRILCAncicsIGVuY29kaW5nPSd1dGYtOCcsIGVycm9ycz0naWdub3JlJykgYXMgaGFuZGxlOgogICAgICAgIHRleHQgPSBoYW5kbGUucmVhZCgpCiAgICBtYXRjaCA9IFBZUFJPSkVDVF9SRS5zZWFyY2godGV4dCkKICAgIGlmIG5vdCBtYXRjaDoKICAgICAgICByZXR1cm4gIiIKICAgIHJldHVybiBwZXA0NDBfdG9fY29uZGEobWF0Y2guZ3JvdXAoMSkpCgoKZGVmIGRldGVjdF9yZXF1aXJlc19weXRob24oKSAtPiBzdHI6CiAgICAiIiJSZXR1cm4gYmVzdC1lZmZvcnQgcmVxdWlyZXMtcHl0aG9uIGNvbnN0cmFpbnQgZm9yIHRoZSBjdXJyZW50IHByb2plY3QuIiIiCiAgICBydW50aW1lX3NwZWMgPSByZWFkX3J1bnRpbWVfc3BlYygpCiAgICBpZiBydW50aW1lX3NwZWM6CiAgICAgICAgcmV0dXJuIHJ1bnRpbWVfc3BlYwogICAgcmV0dXJuIHJlYWRfcHlwcm9qZWN0X3NwZWMoKQoKCmRlZiBtYWluKGFyZ3Y9Tm9uZSkgLT4gTm9uZToKICAgICIiIkNMSSBlbnRyeSBwb2ludCB0aGF0IHByaW50cyBub3JtYWxpemVkIHJlcXVpcmVzLXB5dGhvbiBjb25zdHJhaW50cy4iIiIKICAgIGFyZ3MgPSBsaXN0KHN5cy5hcmd2WzE6XSBpZiBhcmd2IGlzIE5vbmUgZWxzZSBhcmd2KQogICAgaWYgYXJncyBhbmQgYXJnc1swXSA9PSAiLS1zZWxmLXRlc3QiOgogICAgICAgIGZvciBzYW1wbGUgaW4gKCJ+PTMuMTAiLCAifj0zLjguMSIpOgogICAgICAgICAgICBzeXMuc3Rkb3V0LndyaXRlKHBlcDQ0MF90b19jb25kYShzYW1wbGUpICsgIlxuIikKCiAgICAgICAgcmV0dXJuCiAgICBpZiBhcmdzOgogICAgICAgIGZvciBpdGVtIGluIGFyZ3M6CiAgICAgICAgICAgIHN5cy5zdGRvdXQud3JpdGUocGVwNDQwX3RvX2NvbmRhKGl0ZW0pICsgIlxuIikKCiAgICAgICAgcmV0dXJuCiAgICBzeXMuc3Rkb3V0LndyaXRlKGRldGVjdF9yZXF1aXJlc19weXRob24oKSArICJcbiIpCgoKCmlmIF9fbmFtZV9fID09ICJfX21haW5fXyI6CiAgICBtYWluKCkK" set "HP_PRINT_PYVER=aW1wb3J0IHN5cwoKcHJpbnQoZiJweXRob24te3N5cy52ZXJzaW9uX2luZm9bMF19LntzeXMudmVyc2lvbl9pbmZvWzFdfS57c3lzLnZlcnNpb25faW5mb1syXX0iKQo=" -rem HP_FAST_CHECK decoded content: -rem $exe = $args[0] -rem if (-not $exe) { $exe = $env:HP_FAST_EXE } -rem $infraPattern = '(?i)(^|[/\\])(\.git|\.github|dist|\.venv|\.uv_env|__pycache__|\.conda)([/\\]|$)' -rem $sources = Get-ChildItem -Recurse -File -Filter '*.py' | Where-Object { $_.FullName -notmatch $infraPattern -and $_.Name -notlike '~*.py' } -rem if (-not $sources) { exit 1 } -rem $latest = ($sources | Sort-Object -Property LastWriteTimeUtc -Descending | Select-Object -First 1).LastWriteTimeUtc -rem $exeTime = (Get-Item -LiteralPath $exe).LastWriteTimeUtc -rem if ($exeTime -ge $latest) { 'fresh' } +rem HP_FAST_CHECK decoded content (CLAUDE.md Active Backlog Item 39): content-hash freshness +rem check over the same *.py file set (unchanged filter), extended to requirements.txt/ +rem pyproject.toml/runtime.txt. Two modes via the second positional arg: 'check' (default, +rem called from :try_fast_exe) prints 'fresh' iff a stored ~fast_check.hash.txt exists and +rem matches the current source hash; 'write' (called from :success) unconditionally +rem (re)writes it. See tools/fast_check.ps1 (the canonical source) for the full script and +rem docs/agent-lessons-learned.md's "Prefer raw .NET types over Utility-module cmdlets" +rem entry for why this uses [System.Security.Cryptography.SHA256] directly, not Get-FileHash. rem If HP_FAST_CHECK changes, update this decoded comment block to match the base64 payload. -set "HP_FAST_CHECK=JGV4ZSA9ICRhcmdzWzBdCmlmICgtbm90ICRleGUpIHsgJGV4ZSA9ICRlbnY6SFBfRkFTVF9FWEUgfQokaW5mcmFQYXR0ZXJuID0gJyg/aSkoXnxbL1xcXSkoXC5naXR8XC5naXRodWJ8ZGlzdHxcLnZlbnZ8XC51dl9lbnZ8X19weWNhY2hlX198XC5jb25kYSkoWy9cXF18JCknCiRzb3VyY2VzID0gR2V0LUNoaWxkSXRlbSAtUmVjdXJzZSAtRmlsZSAtRmlsdGVyICcqLnB5JyB8IFdoZXJlLU9iamVjdCB7ICRfLkZ1bGxOYW1lIC1ub3RtYXRjaCAkaW5mcmFQYXR0ZXJuIC1hbmQgJF8uTmFtZSAtbm90bGlrZSAnfioucHknIH0KaWYgKC1ub3QgJHNvdXJjZXMpIHsgZXhpdCAxIH0KJGxhdGVzdCA9ICgkc291cmNlcyB8IFNvcnQtT2JqZWN0IC1Qcm9wZXJ0eSBMYXN0V3JpdGVUaW1lVXRjIC1EZXNjZW5kaW5nIHwgU2VsZWN0LU9iamVjdCAtRmlyc3QgMSkuTGFzdFdyaXRlVGltZVV0YwokZXhlVGltZSA9IChHZXQtSXRlbSAtTGl0ZXJhbFBhdGggJGV4ZSkuTGFzdFdyaXRlVGltZVV0YwppZiAoJGV4ZVRpbWUgLWdlICRsYXRlc3QpIHsgJ2ZyZXNoJyB9Cg==" +set "HP_FAST_CHECK=IyBBU0NJSSBvbmx5LiBDTEFVREUubWQgQWN0aXZlIEJhY2tsb2cgSXRlbSAzOTogdGhlIEVYRSBmYXN0IHBhdGgncyBmcmVzaG5lc3MgY2hlY2sgd2FzCiMgbXRpbWUtb25seSBvdmVyICoucHkgZmlsZXMsIHNvIChhKSBhIHRpbWVzdGFtcC1wcmVzZXJ2aW5nIGRlbGl2ZXJ5IG1ldGhvZCAoYSBaSVAsIHhjb3B5LAojIHJvYm9jb3B5KSBjb3VsZCBjYXJyeSBhIGdlbnVpbmVseSBjaGFuZ2VkIGZpbGUgd2hvc2UgbXRpbWUgc3RpbGwgcHJlZGF0ZXMgdGhlIGJ1aWx0IEVYRSwKIyBzaWxlbnRseSByZXVzaW5nIHN0YWxlIGxvZ2ljIHdpdGggbm8gc2lnbmFsIHRvIHRoZSB1c2VyLCBhbmQgKGIpIGEgcmVxdWlyZW1lbnRzLnR4dC8KIyBweXByb2plY3QudG9tbC9ydW50aW1lLnR4dCBjaGFuZ2Ugd2FzIGludmlzaWJsZSB0byB0aGUgc2NhbiBlbnRpcmVseSwgbXRpbWUgb3Igbm90LiBGaXhlcwojIGJvdGg6IGZyZXNobmVzcyBpcyBub3cgYSBjb250ZW50LWhhc2ggY29tcGFyaXNvbiAobmV2ZXIgZm9vbGVkIGJ5IGEgcHJlc2VydmVkL2JhY2tkYXRlZAojIG10aW1lKSBvdmVyIHRoZSBTQU1FICoucHkgZmlsZSBzZXQgYWxyZWFkeSBzY2FubmVkLCBleHRlbmRlZCB0byBpbmNsdWRlIHRob3NlIHRocmVlCiMgZGVwZW5kZW5jeSBmaWxlcy4KIwojIFR3byBtb2Rlcywgc2VsZWN0ZWQgYnkgdGhlIHNlY29uZCBwb3NpdGlvbmFsIGFyZ3VtZW50IChkZWZhdWx0ICdjaGVjaycpOgojICAgY2hlY2sgIChydW4gZnJvbSA6dHJ5X2Zhc3RfZXhlKTogcHJpbnQgJ2ZyZXNoJyBpZmYgYSBzdG9yZWQgaGFzaCBleGlzdHMgQU5EIG1hdGNoZXMgdGhlCiMgICAgICAgICAgaGFzaCBvZiB0aGUgQ1VSUkVOVCBzb3VyY2Ugc2V0LiBQcmludHMgbm90aGluZyAoZmFsbHMgdGhyb3VnaCB0byBhIHJlYnVpbGQpIG9uIGFueQojICAgICAgICAgIG1pc21hdGNoLCBvciB3aGVuIG5vIHN0b3JlZCBoYXNoIGV4aXN0cyB5ZXQgKGUuZy4gdGhlIGZpcnN0IHJ1biB1bmRlciB0aGlzIGZpeCwKIyAgICAgICAgICBvciBkaXN0XDxlbnY+LmV4ZSB3YXMgYnVpbHQgYnkgYW4gb2xkZXIgcnVuX3NldHVwLmJhdCB3aXRoIG5vIGhhc2ggZmlsZSBhdCBhbGwpIC0tCiMgICAgICAgICAgYSBzYWZlLCBvbmUtdGltZS1jb3N0IGRlZmF1bHQgdGhhdCBuZXZlciBtaXMtcmVwb3J0cyBzdGFsZW5lc3MgYXMgZnJlc2huZXNzLgojICAgd3JpdGUgIChydW4gZnJvbSA6c3VjY2Vzcywgb25seSB3aGVuIGEgZnJlc2ggYnVpbGQgYXR0ZW1wdCBqdXN0IGhhcHBlbmVkIC0tIHNlZSB0aGUKIyAgICAgICAgICBjYWxsIHNpdGUncyBvd24gSFBfRkFTVFBBVEhfVVNFRCBnYXRlIGZvciB3aHkgdGhlIGZhc3QtcmV1c2UgY2FzZSBza2lwcyB0aGlzIGFuZAojICAgICAgICAgIGF2b2lkcyBhIHJlZHVuZGFudCBzZWNvbmQgaGFzaCBwYXNzKTogdW5jb25kaXRpb25hbGx5IChyZSl3cml0ZSB0aGUgaGFzaCBvZiB0aGUKIyAgICAgICAgICBDVVJSRU5UIHNvdXJjZSBzZXQuIEFsd2F5cyBydW4gZnJvbSB0aGUgYXBwIHJvb3QgYXMgQ1dEIChib3RoIGNhbGwgc2l0ZXMgYWxyZWFkeQojICAgICAgICAgIGd1YXJhbnRlZSB0aGlzKSwgc28gcmVsYXRpdmUgcGF0aHMgY2FwdHVyZWQgdmlhIFJlc29sdmUtUGF0aCAtUmVsYXRpdmUgbWF0Y2gKIyAgICAgICAgICBiZXR3ZWVuIHRoZSB3cml0ZSBhbmQgYSBsYXRlciBjaGVjay4KIwojIFVzZXMgW1N5c3RlbS5TZWN1cml0eS5DcnlwdG9ncmFwaHkuU0hBMjU2XSBkaXJlY3RseSwgbm90IEdldC1GaWxlSGFzaCAtLSBzZWUKIyBkb2NzL2FnZW50LWxlc3NvbnMtbGVhcm5lZC5tZCdzICJQcmVmZXIgcmF3IC5ORVQgdHlwZXMgb3ZlciBVdGlsaXR5LW1vZHVsZSBjbWRsZXRzIiBlbnRyeToKIyBNaWNyb3NvZnQuUG93ZXJTaGVsbC5VdGlsaXR5IGlzIG5vdCBndWFyYW50ZWVkIHRvIGF1dG8tbG9hZCBpbiB0aGUgZm9yL2YtYmFja3RpY2sgYW5kCiMgLUZpbGUgaW52b2NhdGlvbiBzaGFwZXMgdGhpcyByZXBvJ3MgZW1iZWRkZWQgaGVscGVycyBydW4gdW5kZXIgb24gcmVhbCBXaW5kb3dzIFBvd2VyU2hlbGwKIyA1LjEsIGFuZCB0aGlzIGNsYXNzIG9mIGdhcCBpcyBpbnZpc2libGUgdG8gbG9jYWwgcHdzaCAoTGludXgsIFBvd2VyU2hlbGwgNykgdGVzdGluZy4KIwojIFRoaXMgaXMgdGhlIGNhbm9uaWNhbCBzb3VyY2UgZm9yIHRoZSBIUF9GQVNUX0NIRUNLIGJhc2U2NCBwYXlsb2FkIGVtYmVkZGVkIGluCiMgcnVuX3NldHVwLmJhdC4gQWZ0ZXIgZWRpdGluZywgcmUtc3luYyB2aWE6CiMgICBweXRob24gdG9vbHMvc3luY19wYXlsb2FkLnB5IEhQX0ZBU1RfQ0hFQ0sgdG9vbHMvZmFzdF9jaGVjay5wczEKIyB0ZXN0cy90ZXN0X2Zhc3RfY2hlY2sucHkgYXNzZXJ0cyB0aGUgZW1iZWRkZWQgcGF5bG9hZCBtYXRjaGVzIHRoaXMgZmlsZSBieXRlLWZvci1ieXRlCiMgKGFmdGVyIENSTEYvTEYgbm9ybWFsaXphdGlvbiwgc2luY2UgdGhpcyBmaWxlIGlzIGVvbD1jcmxmIGJ1dCB3YXMgYXV0aG9yZWQgb24gTEYpLgoKJGV4ZSA9ICRhcmdzWzBdCmlmICgtbm90ICRleGUpIHsgJGV4ZSA9ICRlbnY6SFBfRkFTVF9FWEUgfQokbW9kZSA9ICRhcmdzWzFdCmlmICgtbm90ICRtb2RlKSB7ICRtb2RlID0gJ2NoZWNrJyB9CgokaGFzaEZpbGUgPSAnfmZhc3RfY2hlY2suaGFzaC50eHQnCiRpbmZyYVBhdHRlcm4gPSAnKD9pKShefFsvXFxdKShcLmdpdHxcLmdpdGh1YnxkaXN0fFwudmVudnxcLnV2X2VudnxfX3B5Y2FjaGVfX3xcLmNvbmRhKShbL1xcXXwkKScKJHB5ID0gQChHZXQtQ2hpbGRJdGVtIC1SZWN1cnNlIC1GaWxlIC1GaWx0ZXIgJyoucHknIHwgV2hlcmUtT2JqZWN0IHsgJF8uRnVsbE5hbWUgLW5vdG1hdGNoICRpbmZyYVBhdHRlcm4gLWFuZCAkXy5OYW1lIC1ub3RsaWtlICd+Ki5weScgfSkKJGRlcE5hbWVzID0gQCgncmVxdWlyZW1lbnRzLnR4dCcsICdweXByb2plY3QudG9tbCcsICdydW50aW1lLnR4dCcpCiRkZXBzID0gQCgkZGVwTmFtZXMgfCBXaGVyZS1PYmplY3QgeyBUZXN0LVBhdGggLUxpdGVyYWxQYXRoICRfIC1QYXRoVHlwZSBMZWFmIH0gfCBGb3JFYWNoLU9iamVjdCB7IEdldC1JdGVtIC1MaXRlcmFsUGF0aCAkXyB9KQokc291cmNlcyA9IEAoJHB5KSArIEAoJGRlcHMpCgppZiAoLW5vdCAkc291cmNlcykgewogIGlmICgkbW9kZSAtZXEgJ2NoZWNrJykgeyBleGl0IDEgfSBlbHNlIHsgZXhpdCAwIH0KfQoKJHNoYSA9IFtTeXN0ZW0uU2VjdXJpdHkuQ3J5cHRvZ3JhcGh5LlNIQTI1Nl06OkNyZWF0ZSgpCiRsaW5lcyA9IGZvcmVhY2ggKCRmIGluICgkc291cmNlcyB8IFNvcnQtT2JqZWN0IC1Qcm9wZXJ0eSBGdWxsTmFtZSkpIHsKICAkcmVsUGF0aCA9IFJlc29sdmUtUGF0aCAtTGl0ZXJhbFBhdGggJGYuRnVsbE5hbWUgLVJlbGF0aXZlCiAgJGJ5dGVzID0gW1N5c3RlbS5JTy5GaWxlXTo6UmVhZEFsbEJ5dGVzKCRmLkZ1bGxOYW1lKQogICRoYXNoQnl0ZXMgPSAkc2hhLkNvbXB1dGVIYXNoKCRieXRlcykKICAkaGV4ID0gLWpvaW4gKCRoYXNoQnl0ZXMgfCBGb3JFYWNoLU9iamVjdCB7ICRfLlRvU3RyaW5nKCd4MicpIH0pCiAgIiRyZWxQYXRofCRoZXgiCn0KJGNvbWJpbmVkID0gW3N0cmluZ106OkpvaW4oImBuIiwgJGxpbmVzKQokY29tYmluZWRCeXRlcyA9IFtTeXN0ZW0uVGV4dC5FbmNvZGluZ106OlVURjguR2V0Qnl0ZXMoJGNvbWJpbmVkKQokZGlnZXN0Qnl0ZXMgPSAkc2hhLkNvbXB1dGVIYXNoKCRjb21iaW5lZEJ5dGVzKQokZGlnZXN0ID0gLWpvaW4gKCRkaWdlc3RCeXRlcyB8IEZvckVhY2gtT2JqZWN0IHsgJF8uVG9TdHJpbmcoJ3gyJykgfSkKCmlmICgkbW9kZSAtZXEgJ3dyaXRlJykgewogIFtTeXN0ZW0uSU8uRmlsZV06OldyaXRlQWxsVGV4dCgkaGFzaEZpbGUsICRkaWdlc3QpCiAgZXhpdCAwCn0KCmlmICgtbm90IChUZXN0LVBhdGggLUxpdGVyYWxQYXRoICRoYXNoRmlsZSkpIHsgZXhpdCAxIH0KaWYgKC1ub3QgJGV4ZSkgeyBleGl0IDEgfQppZiAoLW5vdCAoVGVzdC1QYXRoIC1MaXRlcmFsUGF0aCAkZXhlKSkgeyBleGl0IDEgfQokc3RvcmVkID0gW1N5c3RlbS5JTy5GaWxlXTo6UmVhZEFsbFRleHQoJGhhc2hGaWxlKS5UcmltKCkKaWYgKCRzdG9yZWQgLWVxICRkaWdlc3QpIHsgJ2ZyZXNoJyB9Cg==" set "HP_EMBED_EXTRACT=IyBSRVEtMDA5IFRpZXIgNTogdmVyaWZpZXMgY2hlY2tzdW0sIGV4dHJhY3RzLCBhbmQgcGF0Y2hlcyB0aGUgZGlzYWJsZWQtc2l0ZS1pbXBvcnRzIC5fcHRoIGZpbGUKIyBmb3IgYW4gYWxyZWFkeS1kb3dubG9hZGVkIGVtYmVkZGFibGUgUHl0aG9uIHppcC4gQmF0Y2ggaGFzIGFscmVhZHkgZG93bmxvYWRlZCB0aGUgemlwICh2aWEgdGhlCiMgc2FtZSBjdXJsLXRoZW4tSW52b2tlLVdlYlJlcXVlc3QgcGF0dGVybiBhcyA6ZG93bmxvYWRfbWluaWNvbmRhX2V4ZS86ZG93bmxvYWRfZ2V0X3BpcCkgYW5kCiMgcGFzc2VzIGl0cyBwYXRoIHBsdXMgdGhlIGV4cGVjdGVkIFNIQTI1NiBhbmQgZGVzdGluYXRpb24gZGlyZWN0b3J5IGFzIGFyZ3MuIFRoaXMgc2NyaXB0IGRvZXMKIyBOT1QgZG93bmxvYWQgYW55dGhpbmcgaXRzZWxmIGFuZCBkb2VzIE5PVCBicmFuY2ggb24gcmVxdWVzdGVkIHZlcnNpb24gLS0gc2VlIHRoZSBQeXRob24tc2lkZQojIHN0YWdlIGZvciBwZXItcmVxdWVzdCB2ZXJzaW9uIHNlbGVjdGlvbi4KIyBBcmdzOiAkMSA9IHppcCBwYXRoLCAkMiA9IGV4cGVjdGVkIHNoYTI1NiAobG93ZXJjYXNlIGhleCksICQzID0gZGVzdGluYXRpb24gZGlyZWN0b3J5LgojIE91dHB1dDogb24gc3VjY2VzcywgcHJpbnRzIHRoZSBleHRyYWN0ZWQgcHl0aG9uLmV4ZSBwYXRoIG9uIHN0ZG91dCwgZXhpdCAwLiBPbiBmYWlsdXJlCiMgKGNoZWNrc3VtIG1pc21hdGNoLCBleHRyYWN0aW9uIGZhaWx1cmUsIG1pc3NpbmcgLl9wdGgsIG1pc3NpbmcgcHl0aG9uLmV4ZSksIHByaW50cyBub3RoaW5nIGFuZAojIGV4aXRzIDEgLS0gY2FsbGVyIGNoZWNrcyBib3RoIHN0ZG91dCBhbmQgZXhpdCBjb2RlLgojCiMgZGVyaXZlZCByZXF1aXJlbWVudDogaGFzaGluZy9leHRyYWN0aW9uL3RleHQtSU8gZGVsaWJlcmF0ZWx5IHVzZSByYXcgLk5FVCBBUElzCiMgKFtTeXN0ZW0uU2VjdXJpdHkuQ3J5cHRvZ3JhcGh5LlNIQTI1Nl0sIFtTeXN0ZW0uSU8uQ29tcHJlc3Npb24uWmlwRmlsZV0sIFtTeXN0ZW0uSU8uRmlsZV0pCiMgaW5zdGVhZCBvZiB0aGUgR2V0LUZpbGVIYXNoIC8gRXhwYW5kLUFyY2hpdmUgLyBHZXQtQ29udGVudCAvIFNldC1Db250ZW50IGNtZGxldHMuIENvbmZpcm1lZCB2aWEKIyByZWFsIENJIGZhaWx1cmUgKFdpbmRvd3MgUG93ZXJTaGVsbCA1LjEsIGludm9rZWQgYXMgYSBmb3IgL2YgYmFja3RpY2sgc3Vic2hlbGwgZnJvbSBydW5fc2V0dXAuYmF0KQojIHRoYXQgR2V0LUZpbGVIYXNoIHRocm93cyAibm90IHJlY29nbml6ZWQgYXMgdGhlIG5hbWUgb2YgYSBjbWRsZXQiIGluIHRoYXQgZXhhY3QgaW52b2NhdGlvbgojIGNvbnRleHQgLS0gaXRzIG1vZHVsZSAoTWljcm9zb2Z0LlBvd2VyU2hlbGwuVXRpbGl0eSkgd2FzIG5vdCBhdXRvLWxvYWRpbmcsIGV2ZW4gdGhvdWdoIHRoaXMKIyBzY3JpcHQgdGVzdGVkIGZpbmUgbG9jYWxseSB1bmRlciBwd3NoIChQb3dlclNoZWxsIDcgb24gTGludXgpIGJlZm9yZWhhbmQsIHdoaWNoIGRvZXMgbm90IHNoYXJlCiMgdGhlIHNhbWUgbW9kdWxlLWxvYWRpbmcgYmVoYXZpb3IuIFRlc3QtUGF0aC9HZXQtSXRlbSAoTWljcm9zb2Z0LlBvd2VyU2hlbGwuTWFuYWdlbWVudCkgd29ya2VkCiMgZmluZSBpbiB0aGUgc2FtZSBydW4sIHNvIHRoaXMgaXMgc2NvcGVkIHRvIFV0aWxpdHktbW9kdWxlIGNtZGxldHMgc3BlY2lmaWNhbGx5OyAuTkVUIHR5cGVzIGhhdmUKIyBubyBtb2R1bGUtbG9hZGluZyBkZXBlbmRlbmN5IGF0IGFsbCBhbmQgc2lkZXN0ZXAgdGhlIHdob2xlIGNsYXNzIG9mIGZhaWx1cmUuIFNlZQojIGRvY3MvYWdlbnQtbGVzc29ucy1sZWFybmVkLm1kLgokWmlwUGF0aCA9ICRhcmdzWzBdCiRFeHBlY3RlZFNoYTI1NiA9ICRhcmdzWzFdCiREZXN0RGlyID0gJGFyZ3NbMl0KCnRyeSB7CiAgICBpZiAoLW5vdCAoVGVzdC1QYXRoIC1MaXRlcmFsUGF0aCAkWmlwUGF0aCkpIHsKICAgICAgICBbQ29uc29sZV06OkVycm9yLldyaXRlTGluZSgiW2VtYmVkX2V4dHJhY3RdIHppcCBub3QgZm91bmQ6ICRaaXBQYXRoIikKICAgICAgICBleGl0IDEKICAgIH0KICAgICRaaXBTaXplID0gKEdldC1JdGVtIC1MaXRlcmFsUGF0aCAkWmlwUGF0aCkuTGVuZ3RoCiAgICAkU2hhMjU2UHJvdmlkZXIgPSBbU3lzdGVtLlNlY3VyaXR5LkNyeXB0b2dyYXBoeS5TSEEyNTZdOjpDcmVhdGUoKQogICAgJEZpbGVTdHJlYW0gPSBbU3lzdGVtLklPLkZpbGVdOjpPcGVuUmVhZCgkWmlwUGF0aCkKICAgIHRyeSB7CiAgICAgICAgJEhhc2hCeXRlcyA9ICRTaGEyNTZQcm92aWRlci5Db21wdXRlSGFzaCgkRmlsZVN0cmVhbSkKICAgIH0gZmluYWxseSB7CiAgICAgICAgJEZpbGVTdHJlYW0uRGlzcG9zZSgpCiAgICAgICAgJFNoYTI1NlByb3ZpZGVyLkRpc3Bvc2UoKQogICAgfQogICAgJEFjdHVhbEhhc2ggPSAoW0JpdENvbnZlcnRlcl06OlRvU3RyaW5nKCRIYXNoQnl0ZXMpIC1yZXBsYWNlICctJywgJycpLlRvTG93ZXIoKQogICAgaWYgKCRBY3R1YWxIYXNoIC1uZSAkRXhwZWN0ZWRTaGEyNTYuVG9Mb3dlcigpKSB7CiAgICAgICAgW0NvbnNvbGVdOjpFcnJvci5Xcml0ZUxpbmUoIltlbWJlZF9leHRyYWN0XSBjaGVja3N1bSBtaXNtYXRjaDogc2l6ZT0kWmlwU2l6ZSBleHBlY3RlZD0kKCRFeHBlY3RlZFNoYTI1Ni5Ub0xvd2VyKCkpIGFjdHVhbD0kQWN0dWFsSGFzaCIpCiAgICAgICAgZXhpdCAxCiAgICB9CgogICAgaWYgKFRlc3QtUGF0aCAtTGl0ZXJhbFBhdGggJERlc3REaXIpIHsgUmVtb3ZlLUl0ZW0gLVJlY3Vyc2UgLUZvcmNlIC1MaXRlcmFsUGF0aCAkRGVzdERpciB9CiAgICBBZGQtVHlwZSAtQXNzZW1ibHlOYW1lIFN5c3RlbS5JTy5Db21wcmVzc2lvbi5GaWxlU3lzdGVtCiAgICBbU3lzdGVtLklPLkNvbXByZXNzaW9uLlppcEZpbGVdOjpFeHRyYWN0VG9EaXJlY3RvcnkoJFppcFBhdGgsICREZXN0RGlyKQoKICAgICRQdGhGaWxlID0gR2V0LUNoaWxkSXRlbSAtTGl0ZXJhbFBhdGggJERlc3REaXIgLUZpbHRlciAicHl0aG9uKi5fcHRoIiAtRmlsZSB8IFNlbGVjdC1PYmplY3QgLUZpcnN0IDEKICAgIGlmICgtbm90ICRQdGhGaWxlKSB7CiAgICAgICAgW0NvbnNvbGVdOjpFcnJvci5Xcml0ZUxpbmUoIltlbWJlZF9leHRyYWN0XSBubyBweXRob24qLl9wdGggZmlsZSBmb3VuZCB1bmRlciAkRGVzdERpciIpCiAgICAgICAgZXhpdCAxCiAgICB9CiAgICAjIGRlcml2ZWQgcmVxdWlyZW1lbnQ6IHRoZSBlbWJlZGRhYmxlIHppcCdzIC5fcHRoIGZpbGUgc2hpcHMgd2l0aCBDUkxGIGxpbmUgZW5kaW5ncywgYW5kCiAgICAjIC5ORVQgcmVnZXggJCBpbiBtdWx0aWxpbmUgbW9kZSBtYXRjaGVzIGltbWVkaWF0ZWx5IGJlZm9yZSBcbiAtLSBpdCBkb2VzIG5vdCBza2lwIGEKICAgICMgcHJlY2VkaW5nIFxyLCBzbyBhbiBhbmNob3Igb2YgIl4jaW1wb3J0IHNpdGUkIiBhZ2FpbnN0IGEgQ1JMRiBsaW5lIHNpbGVudGx5IG5ldmVyIG1hdGNoZXMKICAgICMgKHRoZSBcciBzaXRzIGJldHdlZW4gInNpdGUiIGFuZCB0aGUgbWF0Y2ggcG9zaXRpb24pLiBccj8gaGFuZGxlcyBib3RoIGxpbmUtZW5kaW5nIHN0eWxlcy4KICAgICRQdGhDb250ZW50ID0gW1N5c3RlbS5JTy5GaWxlXTo6UmVhZEFsbFRleHQoJFB0aEZpbGUuRnVsbE5hbWUpCiAgICAkUHRoQ29udGVudCA9ICRQdGhDb250ZW50IC1yZXBsYWNlICcoP20pXiNpbXBvcnQgc2l0ZVxyPyQnLCAnaW1wb3J0IHNpdGUnCiAgICBbU3lzdGVtLklPLkZpbGVdOjpXcml0ZUFsbFRleHQoJFB0aEZpbGUuRnVsbE5hbWUsICRQdGhDb250ZW50LCBbU3lzdGVtLlRleHQuRW5jb2RpbmddOjpBU0NJSSkKCiAgICAkUHlFeGUgPSBKb2luLVBhdGggJERlc3REaXIgInB5dGhvbi5leGUiCiAgICBpZiAoLW5vdCAoVGVzdC1QYXRoIC1MaXRlcmFsUGF0aCAkUHlFeGUpKSB7CiAgICAgICAgW0NvbnNvbGVdOjpFcnJvci5Xcml0ZUxpbmUoIltlbWJlZF9leHRyYWN0XSBweXRob24uZXhlIG1pc3NpbmcgYWZ0ZXIgZXh0cmFjdGlvbjogJFB5RXhlIikKICAgICAgICBleGl0IDEKICAgIH0KICAgIFtDb25zb2xlXTo6V3JpdGUoJFB5RXhlKQp9IGNhdGNoIHsKICAgIFtDb25zb2xlXTo6RXJyb3IuV3JpdGVMaW5lKCJbZW1iZWRfZXh0cmFjdF0gZXhjZXB0aW9uOiAkKCRfLkV4Y2VwdGlvbi5NZXNzYWdlKSIpCiAgICBleGl0IDEKfQo=" set "HP_EMBED_PYVER_CHECK=IyBSRVEtMDA5IFRpZXIgNSwgUHl0aG9uIHN0YWdlOiBydW5zIHVuZGVyIHRoZSAiYWx3YXlzIGxhdGVzdCIgaW50ZXJwcmV0ZXIgfmVtYmVkX2V4dHJhY3QucHMxCiMgKFBvd2VyU2hlbGwgc3RhZ2UpIGFscmVhZHkgZG93bmxvYWRlZC92ZXJpZmllZC9leHRyYWN0ZWQuIFRoaXMgaXMgdGhlIE9OTFkgcGxhY2UgcGVyLXJlcXVlc3QKIyB2ZXJzaW9uIGxvZ2ljIGxpdmVzIC0tIGRlbGliZXJhdGVseSBQeXRob24sIG5vdCBQb3dlclNoZWxsLCByZXVzaW5nIHRoaXMgY29kZWJhc2UncyBwcm92ZW4KIyB2ZXJzaW9uLWRldGVjdGlvbiBwYXR0ZXJuIGluc3RlYWQgb2YgcmUtZGVyaXZpbmcgaXQgaW4gUG93ZXJTaGVsbC4gRnVsbCByYXRpb25hbGU6CiMgZG9jcy9hZ2VudC1pbnRlcmNvbm5lY3QubWQgIlN0YW5kYWxvbmUgUHl0aG9uLWRvd25sb2FkIHRpZXIiLiAiMy4xNCIgZW50cnkgYmVsb3cgTVVTVCBtYXRjaAojIEhQX0VNQkVEX0xBVEVTVF9QQVRDSC9IUF9FTUJFRF9MQVRFU1RfU0hBMjU2IGluIHJ1bl9zZXR1cC5iYXQgLS0gYSBQYXlsb2FkU3luYy1zdHlsZSB1bml0IHRlc3QKIyBhc3NlcnRzIHRoaXMuIExhc3QgcmVmcmVzaGVkOiAyMDI2LTA3LTA5LgppbXBvcnQgaGFzaGxpYgppbXBvcnQgb3MKaW1wb3J0IHJlCmltcG9ydCBzaHV0aWwKaW1wb3J0IHNvY2tldAppbXBvcnQgc3lzCmltcG9ydCB1cmxsaWIucmVxdWVzdAppbXBvcnQgemlwZmlsZQoKIyBkZXJpdmVkIHJlcXVpcmVtZW50OiB1cmxsaWIucmVxdWVzdC51cmxyZXRyaWV2ZSBoYXMgbm8gdGltZW91dD0gcGFyYW1ldGVyICh2ZXJpZmllZCB2aWEKIyBpbnNwZWN0LnNpZ25hdHVyZSAtLSBwYXNzaW5nIG9uZSByYWlzZXMgVHlwZUVycm9yKSwgc28gYSBzdGFsbGVkIChub3QgcmVmdXNlZCkgY29ubmVjdGlvbgojIGR1cmluZyBkb3dubG9hZF9hbmRfdmVyaWZ5KCkgd291bGQgb3RoZXJ3aXNlIGhhbmcgdGhpcyBvbmUtc2hvdCBzY3JpcHQgZm9yZXZlci4gQSBnbG9iYWwKIyBkZWZhdWx0IHRpbWVvdXQgaXMgc2FmZSBoZXJlIHNpbmNlIHRoZSB3aG9sZSBzY3JpcHQgZXhpdHMgaW1tZWRpYXRlbHkgYWZ0ZXIgdXNlIC0tIG5vdGhpbmcKIyBlbHNlIGluIHRoaXMgc2hvcnQtbGl2ZWQgcHJvY2VzcyBpcyBhZmZlY3RlZC4gTWlycm9ycyB0aGUgY3VybCAtLW1heC10aW1lIDEyMCBhbHJlYWR5IHVzZWQKIyBmb3IgdGhlIFBvd2VyU2hlbGwtc3RhZ2UgZG93bmxvYWQgb2YgdGhlIHNhbWUgemlwIGZhbWlseS4Kc29ja2V0LnNldGRlZmF1bHR0aW1lb3V0KDEyMCkKCiMgbWlub3IgLT4gKHBhdGNoLCBzaGEyNTYpCkVNQkVEX1BZVEhPTl9UQUJMRSA9IHsKICAgICIzLjEwIjogKCIzLjEwLjExIiwgIjYwODYxOWY4NjE5MDc1NjI5YzljNjlmMzYxMzUyYTBkYTZlZDdlNjJmODNhMGUxOWM2M2UwZWEzMmViNzYyOWQiKSwKICAgICIzLjExIjogKCIzLjExLjkiLCAiMDA5ZDZiZjdlM2IyZGRjYTNkNzg0ZmEwOWY5MGZlNTQzMzZkNWI2MGYwZTBmMzA1YzM3ZjQwMGJmODNjZmQzYiIpLAogICAgIjMuMTIiOiAoIjMuMTIuMTAiLCAiNGFjYmVkNmRkMWM3NDRiMDM3NmUzYjFjZjU3Y2U5MDZmOWRjOWU5NWU2ODgyNDU4NGM4MDk5YTYzMDI1YTNjMyIpLAogICAgIjMuMTMiOiAoIjMuMTMuMTQiLCAiOTBiNGU1Yjk4OThiNzJkNzQ0NjUwNTI0YmZmOTIzNzdjMzY3ZjQ0YmQ1ZmJkMDllMzE0ODY1NmMwODBhZDkwNyIpLAogICAgIjMuMTQiOiAoIjMuMTQuNiIsICJkZjkwMWU4NGE4OTZmZjFlZTcyMGFkMDMzNzdlMGM4ZDhjMjI0NGZkYTc5ODA4YWVlYWZmNjMxNmRmMWNiNzVjIiksCn0KTEFURVNUX01JTk9SID0gIjMuMTQiCkZMT09SX01JTk9SID0gIjMuMTAiCgpTUEVDX01JTk9SX1JFID0gcmUuY29tcGlsZShyIihbMC05XStcLlswLTldKykiKQoKCmRlZiBfbWlub3Jfa2V5KG1pbm9yKToKICAgIHRyeToKICAgICAgICBtYWpvciwgc3ViID0gbWlub3Iuc3BsaXQoIi4iKQogICAgICAgIHJldHVybiAoaW50KG1ham9yKSwgaW50KHN1YikpCiAgICBleGNlcHQgKFZhbHVlRXJyb3IsIEF0dHJpYnV0ZUVycm9yKToKICAgICAgICByZXR1cm4gKDAsIDApCgoKZGVmIHJlc29sdmVfcmVxdWVzdGVkX21pbm9yKHB5c3BlYyk6CiAgICAjIEV4dHJhY3RzICJYLlkiIGZyb20gYSBQWVNQRUMgc3RyaW5nIChlLmcuICJweXRob24+PTMuMTAsPDQuMCIpOyBOb25lIGlmIGVtcHR5L3VucGFyc2VhYmxlLgogICAgaWYgbm90IHB5c3BlYzoKICAgICAgICByZXR1cm4gTm9uZQogICAgbWF0Y2ggPSBTUEVDX01JTk9SX1JFLnNlYXJjaChweXNwZWMpCiAgICByZXR1cm4gbWF0Y2guZ3JvdXAoMSkgaWYgbWF0Y2ggZWxzZSBOb25lCgoKZGVmIHJlc29sdmVfdGFibGVfZW50cnkocmVxdWVzdGVkX21pbm9yKToKICAgICMgUmV0dXJucyAobWlub3IsIHBhdGNoLCBzaGEyNTYsIGZlbGxfYmFjayk7IG1pcnJvcnMgdGhlIFBvd2VyU2hlbGwgc3RhZ2UncyBvd24gcnVsZXMuCiAgICBpZiByZXF1ZXN0ZWRfbWlub3IgaW4gRU1CRURfUFlUSE9OX1RBQkxFOgogICAgICAgIHBhdGNoLCBzaGEyNTYgPSBFTUJFRF9QWVRIT05fVEFCTEVbcmVxdWVzdGVkX21pbm9yXQogICAgICAgIHJldHVybiByZXF1ZXN0ZWRfbWlub3IsIHBhdGNoLCBzaGEyNTYsIEZhbHNlCiAgICBtaW5vciA9IEZMT09SX01JTk9SIGlmIF9taW5vcl9rZXkocmVxdWVzdGVkX21pbm9yKSA8IF9taW5vcl9rZXkoRkxPT1JfTUlOT1IpIGVsc2UgTEFURVNUX01JTk9SCiAgICBwYXRjaCwgc2hhMjU2ID0gRU1CRURfUFlUSE9OX1RBQkxFW21pbm9yXQogICAgcmV0dXJuIG1pbm9yLCBwYXRjaCwgc2hhMjU2LCBUcnVlCgoKZGVmIGRvd25sb2FkX2FuZF92ZXJpZnkodXJsLCBleHBlY3RlZF9zaGEyNTYsIGRlc3RfemlwKToKICAgIHVybGxpYi5yZXF1ZXN0LnVybHJldHJpZXZlKHVybCwgZGVzdF96aXApCiAgICBkaWdlc3QgPSBoYXNobGliLnNoYTI1NigpCiAgICB3aXRoIG9wZW4oZGVzdF96aXAsICJyYiIpIGFzIGZoOgogICAgICAgIGZvciBjaHVuayBpbiBpdGVyKGxhbWJkYTogZmgucmVhZCgxIDw8IDIwKSwgYiIiKToKICAgICAgICAgICAgZGlnZXN0LnVwZGF0ZShjaHVuaykKICAgIGFjdHVhbCA9IGRpZ2VzdC5oZXhkaWdlc3QoKS5sb3dlcigpCiAgICBpZiBhY3R1YWwgIT0gZXhwZWN0ZWRfc2hhMjU2Lmxvd2VyKCk6CiAgICAgICAgb3MucmVtb3ZlKGRlc3RfemlwKQogICAgICAgIHJhaXNlIFZhbHVlRXJyb3IoImNoZWNrc3VtIG1pc21hdGNoOiBleHBlY3RlZCB7fSwgZ290IHt9Ii5mb3JtYXQoZXhwZWN0ZWRfc2hhMjU2LCBhY3R1YWwpKQoKCmRlZiBleHRyYWN0X2FuZF9wYXRjaCh6aXBfcGF0aCwgZGVzdF9kaXIpOgogICAgaWYgb3MucGF0aC5pc2RpcihkZXN0X2Rpcik6CiAgICAgICAgc2h1dGlsLnJtdHJlZShkZXN0X2RpcikKICAgIHdpdGggemlwZmlsZS5aaXBGaWxlKHppcF9wYXRoKSBhcyB6ZjoKICAgICAgICB6Zi5leHRyYWN0YWxsKGRlc3RfZGlyKQogICAgcHRoX2ZpbGVzID0gW2YgZm9yIGYgaW4gb3MubGlzdGRpcihkZXN0X2RpcikgaWYgcmUubWF0Y2gociJecHl0aG9uXGQrXC5fcHRoJCIsIGYpXQogICAgaWYgbm90IHB0aF9maWxlczoKICAgICAgICByYWlzZSBGaWxlTm90Rm91bmRFcnJvcigibm8gcHl0aG9uKi5fcHRoIGZpbGUgZm91bmQgYWZ0ZXIgZXh0cmFjdGlvbiIpCiAgICBwdGhfcGF0aCA9IG9zLnBhdGguam9pbihkZXN0X2RpciwgcHRoX2ZpbGVzWzBdKQogICAgd2l0aCBvcGVuKHB0aF9wYXRoLCAiciIsIGVuY29kaW5nPSJhc2NpaSIpIGFzIGZoOgogICAgICAgIGNvbnRlbnQgPSBmaC5yZWFkKCkKICAgIGNvbnRlbnQgPSByZS5zdWIociIoP20pXiNpbXBvcnQgc2l0ZSQiLCAiaW1wb3J0IHNpdGUiLCBjb250ZW50KQogICAgd2l0aCBvcGVuKHB0aF9wYXRoLCAidyIsIGVuY29kaW5nPSJhc2NpaSIsIG5ld2xpbmU9IiIpIGFzIGZoOgogICAgICAgIGZoLndyaXRlKGNvbnRlbnQpCiAgICBweV9leGUgPSBvcy5wYXRoLmpvaW4oZGVzdF9kaXIsICJweXRob24uZXhlIikKICAgIGlmIG5vdCBvcy5wYXRoLmlzZmlsZShweV9leGUpOgogICAgICAgIHJhaXNlIEZpbGVOb3RGb3VuZEVycm9yKCJweXRob24uZXhlIG1pc3NpbmcgYWZ0ZXIgZXh0cmFjdGlvbiIpCiAgICByZXR1cm4gcHlfZXhlCgoKZGVmIG1haW4oKToKICAgICMgZGVzdF9kaXIgaXMgd2hlcmUgVEhJUyBydW5uaW5nIGludGVycHJldGVyIGxpdmVzOyBXaW5kb3dzIHdvbid0IGxldCBhIHByb2Nlc3MgcmVwbGFjZSBpdHMKICAgICMgb3duIGZpbGVzLCBzbyBhIHN3YXAgZXh0cmFjdHMgaW50byBhIHNpYmxpbmcgX3N3YXAgZGlyIGFuZCBiYXRjaCBtb3ZlcyBpdCBpbnRvIHBsYWNlIG9ubHkKICAgICMgYWZ0ZXIgdGhpcyBwcm9jZXNzIGV4aXRzIChsb2NrcyByZWxlYXNlZCkuIFNlZSBkb2NzL2FnZW50LWludGVyY29ubmVjdC5tZC4KICAgIGRlc3RfZGlyID0gc3lzLmFyZ3ZbMV0gaWYgbGVuKHN5cy5hcmd2KSA+IDEgZWxzZSAiIgogICAgc3dhcF9kaXIgPSBkZXN0X2Rpci5yc3RyaXAoIlxcLyIpICsgIl9zd2FwIgogICAgcHlzcGVjID0gb3MuZW52aXJvbi5nZXQoIlBZU1BFQyIsICIiKQogICAgcmVxdWVzdGVkX21pbm9yID0gcmVzb2x2ZV9yZXF1ZXN0ZWRfbWlub3IocHlzcGVjKQoKICAgIGlmIHJlcXVlc3RlZF9taW5vciBpcyBOb25lIG9yIHJlcXVlc3RlZF9taW5vciA9PSBMQVRFU1RfTUlOT1I6CiAgICAgICAgc3lzLnN0ZG91dC53cml0ZSgidW5jaGFuZ2VkfHt9XG4iLmZvcm1hdChMQVRFU1RfTUlOT1IpKQogICAgICAgIHJldHVybiAwCgogICAgbWlub3IsIHBhdGNoLCBzaGEyNTYsIGZlbGxfYmFjayA9IHJlc29sdmVfdGFibGVfZW50cnkocmVxdWVzdGVkX21pbm9yKQogICAgaWYgbWlub3IgPT0gTEFURVNUX01JTk9SOgogICAgICAgICMgQWJvdmUtY2VpbGluZyByZXF1ZXN0IChvbmx5IHBhdGggaGVyZSwgc2luY2UgZXhhY3QtbWF0Y2gtbGF0ZXN0IGlzIGhhbmRsZWQgYWJvdmUpOgogICAgICAgICMgbm8gc3dhcCBuZWVkZWQsIGJ1dCB0YWcgImZlbGxiYWNrIiBub3QgInVuY2hhbmdlZCIgc28gdGhlIGNhbGxlcidzIFdBUk4gc3RpbGwgZmlyZXMuCiAgICAgICAgc3lzLnN0ZG91dC53cml0ZSgiZmVsbGJhY2t8e31cbiIuZm9ybWF0KG1pbm9yKSkKICAgICAgICByZXR1cm4gMAoKICAgIHVybCA9ICJodHRwczovL3d3dy5weXRob24ub3JnL2Z0cC9weXRob24ve3B9L3B5dGhvbi17cH0tZW1iZWQtYW1kNjQuemlwIi5mb3JtYXQocD1wYXRjaCkKICAgIHppcF9wYXRoID0gb3MucGF0aC5qb2luKG9zLmVudmlyb24uZ2V0KCJURU1QIiwgIi4iKSwgInB5dGhvbi17fS1lbWJlZC1hbWQ2NC56aXAiLmZvcm1hdChwYXRjaCkpCiAgICB0cnk6CiAgICAgICAgZG93bmxvYWRfYW5kX3ZlcmlmeSh1cmwsIHNoYTI1NiwgemlwX3BhdGgpCiAgICAgICAgZXh0cmFjdF9hbmRfcGF0Y2goemlwX3BhdGgsIHN3YXBfZGlyKQogICAgZXhjZXB0IEV4Y2VwdGlvbiBhcyBleGM6CiAgICAgICAgc3lzLnN0ZGVyci53cml0ZSgiZW1iZWQgdmVyc2lvbiBzd2FwIGZhaWxlZDoge31cbiIuZm9ybWF0KGV4YykpCiAgICAgICAgaWYgb3MucGF0aC5pc2Rpcihzd2FwX2Rpcik6CiAgICAgICAgICAgIHNodXRpbC5ybXRyZWUoc3dhcF9kaXIsIGlnbm9yZV9lcnJvcnM9VHJ1ZSkKICAgICAgICByZXR1cm4gMQoKICAgIHRhZyA9ICJmZWxsYmFjayIgaWYgZmVsbF9iYWNrIGVsc2UgInN3YXBwZWQiCiAgICBzeXMuc3Rkb3V0LndyaXRlKCJ7fXx7fXx7fVxuIi5mb3JtYXQodGFnLCBtaW5vciwgc3dhcF9kaXIpKQogICAgcmV0dXJuIDAKCgppZiBfX25hbWVfXyA9PSAiX19tYWluX18iOgogICAgc3lzLmV4aXQobWFpbigpKQo=" set "HP_FAILFAST_PROBE=IyBTbGljZSAyYi1DIGZhaWwtZmFzdCBwcm9iZTogbGF1bmNoZXMgdGhlIGNhbGxlcidzIHByb2dyYW0sIHdhaXRzIHVwIHRvIEhQX0ZBSUxGQVNUX1BST0JFX01TIHRvCiMgY2xhc3NpZnkgaXQgYXMgImV4aXRlZCBmYXN0IiAoc3RhbGUvYnJva2VuIGNhY2hlZCBhcnRpZmFjdCAtLSBkaXNjYXJkK3JlYnVpbGQgY2FuZGlkYXRlKSB2cy4KIyAic3RpbGwgcnVubmluZyIgKHRoZSB1c2VyJ3MgcmVhbCwgcG9zc2libHkgbG9uZy1ydW5uaW5nIHByb2dyYW0gLS0gbmV2ZXIgdG91Y2hlZCBhZ2FpbikuIE5ldmVyCiMgY2FsbHMgJHAuS2lsbCgpIC0tIHBhc3QgdGhlIHByb2JlIHdpbmRvdyB0aGUgd2FpdCBpcyB1bmJvdW5kZWQgc28gYSBoZWFsdGh5IGFwcCBpcyBuZXZlcgojIGZvcmNlLXN0b3BwZWQuCiMKIyBJbnB1dHMgdmlhIGVudiB2YXJzIChhdm9pZHMgY21kLmV4ZSBxdW90aW5nIGhhemFyZHMpOiBIUF9QUk9CRV9FWEUsIEhQX1BST0JFX0FSR1MsIEhQX1BST0JFX0NXRCwKIyBIUF9GQUlMRkFTVF9QUk9CRV9NUywgSFBfUFJPQkVfT1VUL0hQX1BST0JFX0VSUiAoZGVmYXVsdCB+cnVuLm91dC50eHQvfnJ1bi5lcnIudHh0KSwKIyBIUF9QUk9CRV9SRVNVTFQgKGRlZmF1bHQgfnByb2JlX3Jlc3VsdC50eHQgLS0gIiRleGNlZWRlZHwkZXhpdGNvZGUiLCBOT1Qgc3Rkb3V0KS4gQ2FsbGVyIG11c3QKIyBwcmUtdHJ1bmNhdGUgb3V0cHV0L3Jlc3VsdCBmaWxlcyBiZWZvcmUgaW52b2tpbmcuCiMKIyBkZXJpdmVkIHJlcXVpcmVtZW50IChbUkVRLTAyNl0gYXJndiBwYXNzdGhyb3VnaCk6IEhQX1BST0JFX0FSR1MgaXMgbm93IGEgZnVsbCwgYWxyZWFkeS1xdW90ZWQKIyBBcmd1bWVudHMgc3RyaW5nIChlLmcuIGAiZW50cnkucHkiICItLWZvbyIgImJhciJgKSwgdXNlZCB2ZXJiYXRpbSAtLSBub3QgYSBzaW5nbGUgYmFyZSBwYXRoCiMgcmUtcXVvdGVkIGhlcmUuIENhbGxlciBxdW90ZXMgZWFjaCB0b2tlbjsgc2VlIHJ1bl9zZXR1cC5iYXQncyBIUF9BUFBfQVJHUy4KIwojIExpdmUtdGVlcyB0aGUgY2hpbGQncyBzdGRvdXQvc3RkZXJyIHNvIGEgc3RkaW4taW50ZXJhY3RpdmUgcHJvZ3JhbSdzIHByb21wdHMgcmVhY2ggYSByZWFsCiMgZG91YmxlLWNsaWNrZWQgdXNlciwgaW5zdGVhZCBvZiBvbmx5IHdyaXRpbmcgY2FwdHVyZWQgb3V0cHV0IHRvIGRpc2sgYXQgZXhpdC4KIwojIGRlcml2ZWQgcmVxdWlyZW1lbnQ6IGRvZXMgTk9UIHVzZSBSZWdpc3Rlci1PYmplY3RFdmVudCBvbiBPdXRwdXREYXRhUmVjZWl2ZWQvRXJyb3JEYXRhUmVjZWl2ZWQuCiMgVGhhdCB3YXMgdGhlIG9yaWdpbmFsIGRlc2lnbiBhbmQgd2FzIGZvdW5kLCB2aWEgdGhpcyByZXBvJ3Mgb3duIGxvY2FsIHB3c2ggdGVzdGluZywgdG8KIyByZW9yZGVyIGxpbmVzIFdJVEhJTiBhIHNpbmdsZSBzdHJlYW0gKGUuZy4gcm91bmQgMidzIG91dHB1dCBsYW5kaW5nIGJlZm9yZSByb3VuZCAxJ3MgaW4gdGhlCiMgY2FwdHVyZWQvdGVlZCB0ZXh0LCBub24tZGV0ZXJtaW5pc3RpY2FsbHkpIC0tIHJvb3QtY2F1c2VkIHRvIGEgY29uZmlybWVkLCBmaWxlZCBQb3dlclNoZWxsIGJ1ZwojIChQb3dlclNoZWxsL1Bvd2VyU2hlbGwjMTE5MzcpOiB0aG9zZSBldmVudHMgZGlzcGF0Y2ggdmlhIFRocmVhZFBvb2wuUXVldWVVc2VyV29ya0l0ZW0sIHdoaWNoCiMgZG9lcyBub3QgZ3VhcmFudGVlIGRlbGl2ZXJ5IG9yZGVyIHdoZW4gc2V2ZXJhbCBsaW5lcyBhcnJpdmUgY2xvc2UgdG9nZXRoZXIuIEZpeGVkIGJ5IHBvbGxpbmcKIyByZWFkcyBkaXJlY3RseSBpbnN0ZWFkOiBvbmx5IE9ORSByZWFkIGlzIGV2ZXIgaW4gZmxpZ2h0IHBlciBzdHJlYW0gYXQgYSB0aW1lICh0aGUgbmV4dCByZWFkIGlzCiMgbm90IGlzc3VlZCB1bnRpbCB0aGUgY3VycmVudCBvbmUgaXMgY29uc3VtZWQpLCBzbyB0aGVyZSBpcyBubyBwb3NzaWJsZSBvdXQtb2Ytb3JkZXIgZGVsaXZlcnkKIyBmb3IgYSBzaW5nbGUgc3RyZWFtIC0tIG9yZGVyaW5nIGlzIHNlbGYtc2VxdWVuY2VkLCBub3QgZGVwZW5kZW50IG9uIGFueSBydW50aW1lJ3MKIyBjYWxsYmFjay1zY2hlZHVsaW5nIGd1YXJhbnRlZS4gQ3Jvc3Mtc3RyZWFtIChzdGRvdXQgdnMgc3RkZXJyKSBpbnRlcmxlYXZpbmcgd2FzIG5ldmVyCiMgZ3VhcmFudGVlZCBhbmQgc3RpbGwgaXNuJ3QgLS0gdGhhdCByZWZsZWN0cyB0aGUgY2hpbGQncyBvd24gdHdvIGluZGVwZW5kZW50IHBpcGVzLCBub3QgYSBidWcuCiMKIyBkZXJpdmVkIHJlcXVpcmVtZW50IChGaW5kaW5nIDksIDIwMjYtMDctMjQpOiByZWFkcyB2aWEgU3RyZWFtUmVhZGVyLlJlYWRBc3luYyhjaGFyW10sIGludCwgaW50KQojIChyYXcgY2h1bmtzKSwgTk9UIFJlYWRMaW5lQXN5bmMoKS4gUmVhZExpbmVBc3luYygpIG9ubHkgcmV0dXJucyBvbmNlIGl0IHNlZXMgYSBmdWxsCiMgbmV3bGluZS10ZXJtaW5hdGVkIGxpbmUgLS0gY29uZmlybWVkIGVtcGlyaWNhbGx5IHRoYXQgUHl0aG9uJ3Mgb3duIGBpbnB1dCgicHJvbXB0IilgIChubwojIHRyYWlsaW5nIG5ld2xpbmUgYnkgZGVzaWduLCBzbyB0aGUgY3Vyc29yIHN0YXlzIG9uIHRoZSBzYW1lIGxpbmUpIGlzIGdlbnVpbmVseSBmbHVzaGVkIHRvIHRoZQojIE9TIHBpcGUgaW1tZWRpYXRlbHkgYnV0IHN0YXlzIGludmlzaWJsZSB0byBhIFJlYWRMaW5lQXN5bmMtYmFzZWQgcmVhZGVyIHVudGlsIHNvbWV0aGluZyBlbHNlCiMgbGF0ZXIgZmx1c2hlcyBhIG5ld2xpbmUsIG9yIHRoZSBwcm9jZXNzIGV4aXRzLiBBIGNodW5rLWJhc2VkIHJlYWQgc3VyZmFjZXMgd2hhdGV2ZXIgYnl0ZXMgYXJlCiMgYWN0dWFsbHkgYXZhaWxhYmxlIHRoZSBtb21lbnQgdGhleSBhcnJpdmUsIG1hdGNoaW5nIGhvdyBhIHJlYWwgdGVybWluYWwgYmVoYXZlcy4gRU9GIGlzIGEKIyAwLWxlbmd0aCByZWFkIChub3QgYSBudWxsIHJlc3VsdCB0aGUgd2F5IFJlYWRMaW5lQXN5bmMgc2lnbmFscyBpdCkuIFNlZQojIGRvY3MvcGxhbi1jbGktaW50ZXJhY3RpdmUtdmVyaWZpY2F0aW9uLm1kIEZpbmRpbmcgOSBmb3IgdGhlIGZ1bGwgZW1waXJpY2FsIHRyYWNlLgojCiMgRnVsbCByYXRpb25hbGUgKyBjaXRhdGlvbnM6IGRvY3MvcGxhbi1jbGktaW50ZXJhY3RpdmUtdmVyaWZpY2F0aW9uLm1kIEZpbmRpbmdzIDViLzYvNy84LzkuCiMKIyBUaGlzIGlzIHRoZSBjYW5vbmljYWwgc291cmNlIGZvciB0aGUgSFBfRkFJTEZBU1RfUFJPQkUgYmFzZTY0IHBheWxvYWQgZW1iZWRkZWQgaW4KIyBydW5fc2V0dXAuYmF0LiBBZnRlciBlZGl0aW5nLCByZS1lbmNvZGUgYW5kIHBhc3RlIGl0IGludG8gdGhlIGBzZXQgIkhQX0ZBSUxGQVNUX1BST0JFPS4uLiJgCiMgbGluZTsgdGVzdHMvdGVzdF9mYWlsZmFzdF9wcm9iZS5weSBhc3NlcnRzIHRoZSBlbWJlZGRlZCBwYXlsb2FkIG1hdGNoZXMgdGhpcyBmaWxlICh3aXRoCiMgQ1JMRi9MRiBub3JtYWxpemVkLCBwZXIgdGhlIC5wczEgUGF5bG9hZFN5bmMgY29udmVudGlvbiAtLSBzZWUKIyBkb2NzL2FnZW50LWxlc3NvbnMtbGVhcm5lZC5tZCAiRW1iZWRkZWQgSGVscGVyIFVwZGF0ZSBXb3JrZmxvdyIpLgokZXhlID0gJGVudjpIUF9QUk9CRV9FWEUKJHJhd0FyZ3MgPSAkZW52OkhQX1BST0JFX0FSR1MKJHdvcmtEaXIgPSAkZW52OkhQX1BST0JFX0NXRAokcHJvYmVNcyA9IFtpbnRdJGVudjpIUF9GQUlMRkFTVF9QUk9CRV9NUwokb3V0UGF0aCA9ICRlbnY6SFBfUFJPQkVfT1VUCmlmICgtbm90ICRvdXRQYXRoKSB7ICRvdXRQYXRoID0gJ35ydW4ub3V0LnR4dCcgfQokZXJyUGF0aCA9ICRlbnY6SFBfUFJPQkVfRVJSCmlmICgtbm90ICRlcnJQYXRoKSB7ICRlcnJQYXRoID0gJ35ydW4uZXJyLnR4dCcgfQokcmVzdWx0UGF0aCA9ICRlbnY6SFBfUFJPQkVfUkVTVUxUCmlmICgtbm90ICRyZXN1bHRQYXRoKSB7ICRyZXN1bHRQYXRoID0gJ35wcm9iZV9yZXN1bHQudHh0JyB9Cgokc2kgPSBOZXctT2JqZWN0IFN5c3RlbS5EaWFnbm9zdGljcy5Qcm9jZXNzU3RhcnRJbmZvCiRzaS5GaWxlTmFtZSA9ICRleGUKaWYgKCRyYXdBcmdzKSB7ICRzaS5Bcmd1bWVudHMgPSAkcmF3QXJncyB9CiRzaS5Xb3JraW5nRGlyZWN0b3J5ID0gJHdvcmtEaXIKJHNpLlVzZVNoZWxsRXhlY3V0ZSA9ICRmYWxzZQokc2kuUmVkaXJlY3RTdGFuZGFyZE91dHB1dCA9ICR0cnVlCiRzaS5SZWRpcmVjdFN0YW5kYXJkRXJyb3IgPSAkdHJ1ZQokcCA9IE5ldy1PYmplY3QgU3lzdGVtLkRpYWdub3N0aWNzLlByb2Nlc3MKJHAuU3RhcnRJbmZvID0gJHNpCiRwLlN0YXJ0KCkgfCBPdXQtTnVsbApXcml0ZS1Ib3N0ICJbSU5GT10gUHJvY2VzcyBJRCAkKCRwLklkKS4gSWYgaXQgc2VlbXMgc3R1Y2s6IFRhc2sgTWFuYWdlciA+IERldGFpbHMgdGFiID4gZmluZCB0aGlzIFBJRCA+IEVuZCBUYXNrICh0aGlzIHdpbmRvdyBzdGF5cyBvcGVuKS4iCgokb3V0QnVmID0gTmV3LU9iamVjdCBTeXN0ZW0uVGV4dC5TdHJpbmdCdWlsZGVyCiRlcnJCdWYgPSBOZXctT2JqZWN0IFN5c3RlbS5UZXh0LlN0cmluZ0J1aWxkZXIKJG91dENodW5rQnVmID0gTmV3LU9iamVjdCBjaGFyW10gNDA5NgokZXJyQ2h1bmtCdWYgPSBOZXctT2JqZWN0IGNoYXJbXSA0MDk2CiRvdXRUYXNrID0gJHAuU3RhbmRhcmRPdXRwdXQuUmVhZEFzeW5jKCRvdXRDaHVua0J1ZiwgMCwgJG91dENodW5rQnVmLkxlbmd0aCkKJGVyclRhc2sgPSAkcC5TdGFuZGFyZEVycm9yLlJlYWRBc3luYygkZXJyQ2h1bmtCdWYsIDAsICRlcnJDaHVua0J1Zi5MZW5ndGgpCiRvdXREb25lID0gJGZhbHNlCiRlcnJEb25lID0gJGZhbHNlCgokc3cgPSBbU3lzdGVtLkRpYWdub3N0aWNzLlN0b3B3YXRjaF06OlN0YXJ0TmV3KCkKJGV4Y2VlZGVkID0gMAp3aGlsZSAoKC1ub3QgJHAuSGFzRXhpdGVkKSAtb3IgKC1ub3QgJG91dERvbmUpIC1vciAoLW5vdCAkZXJyRG9uZSkpIHsKICAgIGlmICgoLW5vdCAkb3V0RG9uZSkgLWFuZCAkb3V0VGFzay5Jc0NvbXBsZXRlZCkgewogICAgICAgICRuID0gJG91dFRhc2suUmVzdWx0CiAgICAgICAgaWYgKCRuIC1lcSAwKSB7CiAgICAgICAgICAgICRvdXREb25lID0gJHRydWUKICAgICAgICB9IGVsc2UgewogICAgICAgICAgICAkY2h1bmsgPSBbc3RyaW5nXTo6bmV3KCRvdXRDaHVua0J1ZiwgMCwgJG4pCiAgICAgICAgICAgIFtDb25zb2xlXTo6T3V0LldyaXRlKCRjaHVuaykKICAgICAgICAgICAgJG51bGwgPSAkb3V0QnVmLkFwcGVuZCgkY2h1bmspCiAgICAgICAgICAgICRvdXRUYXNrID0gJHAuU3RhbmRhcmRPdXRwdXQuUmVhZEFzeW5jKCRvdXRDaHVua0J1ZiwgMCwgJG91dENodW5rQnVmLkxlbmd0aCkKICAgICAgICB9CiAgICB9CiAgICBpZiAoKC1ub3QgJGVyckRvbmUpIC1hbmQgJGVyclRhc2suSXNDb21wbGV0ZWQpIHsKICAgICAgICAkbiA9ICRlcnJUYXNrLlJlc3VsdAogICAgICAgIGlmICgkbiAtZXEgMCkgewogICAgICAgICAgICAkZXJyRG9uZSA9ICR0cnVlCiAgICAgICAgfSBlbHNlIHsKICAgICAgICAgICAgJGNodW5rID0gW3N0cmluZ106Om5ldygkZXJyQ2h1bmtCdWYsIDAsICRuKQogICAgICAgICAgICBbQ29uc29sZV06OkVycm9yLldyaXRlKCRjaHVuaykKICAgICAgICAgICAgJG51bGwgPSAkZXJyQnVmLkFwcGVuZCgkY2h1bmspCiAgICAgICAgICAgICRlcnJUYXNrID0gJHAuU3RhbmRhcmRFcnJvci5SZWFkQXN5bmMoJGVyckNodW5rQnVmLCAwLCAkZXJyQ2h1bmtCdWYuTGVuZ3RoKQogICAgICAgIH0KICAgIH0KICAgIGlmICgoLW5vdCAkZXhjZWVkZWQpIC1hbmQgKCRzdy5FbGFwc2VkTWlsbGlzZWNvbmRzIC1nZSAkcHJvYmVNcykpIHsKICAgICAgICAkZXhjZWVkZWQgPSAxCiAgICB9CiAgICBTdGFydC1TbGVlcCAtTWlsbGlzZWNvbmRzIDIwCn0KJHAuV2FpdEZvckV4aXQoKQoKJG91dEJ1Zi5Ub1N0cmluZygpIHwgU2V0LUNvbnRlbnQgLVBhdGggJG91dFBhdGggLUVuY29kaW5nIEFTQ0lJCiRlcnJCdWYuVG9TdHJpbmcoKSB8IFNldC1Db250ZW50IC1QYXRoICRlcnJQYXRoIC1FbmNvZGluZyBBU0NJSQoiJGV4Y2VlZGVkfCQoJHAuRXhpdENvZGUpIiB8IFNldC1Db250ZW50IC1QYXRoICRyZXN1bHRQYXRoIC1FbmNvZGluZyBBU0NJSQo=" diff --git a/tests/harness.ps1 b/tests/harness.ps1 index 54344c90..0d9e149d 100644 --- a/tests/harness.ps1 +++ b/tests/harness.ps1 @@ -405,6 +405,29 @@ $depMsgPos = $AllText.IndexOf('[INFO] Installing dependencies') $depCallPos = $AllText.IndexOf('[TRACE] heuristic augmentation: ~prep_requirements.py') $depInstallProgress = ($depMsgPos -ge 0) -and ($depCallPos -ge 0) -and ($depMsgPos -lt $depCallPos) Write-Result 'batch.progress.dep_install' "Progress message before dependency install phase: user sees 'Installing dependencies' before the slow pip/conda/uv install step" $depInstallProgress @{ msgIdx = $depMsgPos; opIdx = $depCallPos; ordered = $depInstallProgress } +# derived requirement (CLAUDE.md Active Backlog Item 39): the EXE fast path's freshness +# check switched from mtime-only to a content-hash comparison; the hash must be (re)written +# after a genuine fresh build (:success, gated on HP_FRESH_BUILD_OK -- set ONLY in +# :run_entry_smoke's own genuine build-success branches, never on a skipped/failed rebuild +# or a fast-path reuse -- CodeRabbit review, PR #460, caught the original HP_FASTPATH_USED +# -only gate wrongly allowing a stale leftover EXE's hash to be overwritten) via a dedicated +# subroutine (not inlined, to avoid the parenthesized-block set-then-read hazard). Bodies are +# extracted (bounded by the next label) rather than matched against the whole file, so this +# check cannot pass on unrelated text elsewhere -- mirrors the :hidden_import_recover / +# :dll_bundle_recover scoped-extraction precedent above (see $hiLogSizeAdvance). Static +# wiring guard only -- runtime proof is tests/test_fast_check.py (the embedded script +# itself, via real pwsh) plus tests/selfapps_fastpath_hash.ps1's end-to-end coverage. +$successMatch = [regex]::Match($AllText, '(?ms)^:success\r?\n(?.*?)(?=^:provider_cascade)') +$hasWriteFastHashCall = $successMatch.Success -and ( + $successMatch.Groups['body'].Value -match [regex]::Escape('if defined HP_FRESH_BUILD_OK call :write_fast_hash') +) +$writeFastHashMatch = [regex]::Match($AllText, '(?ms)^:write_fast_hash\r?\n(?.*?)(?=^:run_entry_smoke)') +$hasWriteFastHashLabel = $writeFastHashMatch.Success +$hasWriteModeArg = $writeFastHashMatch.Success -and ( + $writeFastHashMatch.Groups['body'].Value -match '"%HP_FAST_CHECK_PS%"\s+"dist\\%ENVNAME%\.exe"\s+write' +) +$hasFastHashWiring = $hasWriteFastHashLabel -and $hasWriteFastHashCall -and $hasWriteModeArg +Write-Result 'batch.fastpath.hash_write' 'EXE fast path freshness check: :write_fast_hash subroutine exists, is called from :success gated on HP_FRESH_BUILD_OK, and invokes the payload in write mode' $hasFastHashWiring @{ hasLabel = $hasWriteFastHashLabel; hasCall = $hasWriteFastHashCall; hasWriteModeArg = $hasWriteModeArg } # derived requirement: pre-build --collect-submodules double-gate (REQ-005.x) must stay wired. # Static guard against silent deletion; runtime proof is self.collect.submodules (selfapps_collect.ps1). $collectCall = $AllText -match 'call :compute_collect_flags' diff --git a/tests/selfapps_fastpath_hash.ps1 b/tests/selfapps_fastpath_hash.ps1 new file mode 100644 index 00000000..70997858 --- /dev/null +++ b/tests/selfapps_fastpath_hash.ps1 @@ -0,0 +1,182 @@ +# ASCII only +# selfapps_fastpath_hash.ps1 - regression guard for CLAUDE.md Active Backlog Item 39: the +# EXE fast path's freshness check must detect a genuine content change even when the +# changed file's mtime is backdated below the built EXE's own mtime -- the exact failure +# mode a timestamp-preserving delivery method (a ZIP, xcopy, robocopy) produces. Before this +# fix, freshness was mtime-only over *.py files, so this scenario silently reused the stale +# EXE with no signal to the user (see CLAUDE.md's Item 39 "Realistic scenario"). +# +# Run 1: builds the EXE from entry.py printing FASTPATH_TOKEN_V1. +# Run 2: entry.py is rewritten to print FASTPATH_TOKEN_V2 instead, but its mtime is set to a +# date well BEFORE dist\.exe's own mtime (simulating a delivery method that +# preserves the file's original authored timestamp). The OLD mtime-only check would +# have wrongly reused the stale V1 EXE here; the new content-hash check +# (tools/fast_check.ps1) must detect the change regardless of mtime and rebuild. +# Assert: run 2's captured EXE stdout (~run.out.txt, written by both the fast-path-reuse +# launch and the fresh-build smokerun -- see docs/agent-interconnect.md's "EXE fast path" +# section) shows FASTPATH_TOKEN_V2, not V1 -- direct evidence the NEW content actually ran, +# not just that some log line happened to say "rebuilding". Also asserts the "Fast path: +# reusing" log line is ABSENT for run 2, confirming a genuine rebuild occurred rather than +# the new token coincidentally reaching the old EXE some other way. +# +# Lane: uv, non-gating for its first landing -- the isolated tools/fast_check.ps1 script is +# already verified via real pwsh (tests/test_fast_check.py), but this is the first time the +# full :try_fast_exe -> :run_entry_smoke -> :success -> :write_fast_hash cycle is exercised +# end-to-end on real Windows CI. Promote once proven stable across several real runs, +# matching this repo's established graduation pattern (CLAUDE.md's "CI lane gating +# maturity" periodic check). +param() +$ErrorActionPreference = 'Continue' +$here = $PSScriptRoot +$repo = Split-Path -Path $here -Parent +$nd = Join-Path $here '~test-results.ndjson' +$ciNd = Join-Path $repo 'ci_test_results.ndjson' +if (-not (Test-Path $nd)) { New-Item -ItemType File -Path $nd -Force | Out-Null } +if (-not (Test-Path $ciNd)) { New-Item -ItemType File -Path $ciNd -Force | Out-Null } + +function Write-NdjsonRow { + param([hashtable]$Row) + $lane = [Environment]::GetEnvironmentVariable('HP_CI_LANE') + if ($lane -and -not $Row.ContainsKey('lane')) { $Row['lane'] = $lane } + $json = $Row | ConvertTo-Json -Compress -Depth 8 + Add-Content -LiteralPath $nd -Value $json -Encoding Ascii + Add-Content -LiteralPath $ciNd -Value $json -Encoding Ascii +} + +# Non-Windows skip +if (-not $IsWindows) { + $platform = [System.Environment]::OSVersion.Platform.ToString() + Write-NdjsonRow ([ordered]@{ + id = 'self.fastpath.hash.backdated_mtime' + req = 'REQ-018' + pass = $true + desc = 'EXE fast path content-hash freshness (skipped on non-Windows)' + details = [ordered]@{ skip = $true; platform = $platform; reason = 'non-windows-host' } + }) + exit 0 +} + +$batchPath = Join-Path $repo 'run_setup.bat' +if (-not (Test-Path $batchPath)) { + Write-NdjsonRow ([ordered]@{ + id = 'self.fastpath.hash.backdated_mtime' + req = 'REQ-018' + pass = $false + desc = 'EXE fast path content-hash freshness: run_setup.bat not found' + details = [ordered]@{ error = 'run_setup.bat not found at ' + $batchPath } + }) + exit 1 +} + +$workDir = Join-Path $here '~selftest_fastpath_hash' +if (Test-Path $workDir) { Remove-Item -Recurse -Force $workDir } +New-Item -ItemType Directory -Force -Path $workDir | Out-Null +Copy-Item -Path $batchPath -Destination $workDir -Force + +Set-Content -Path (Join-Path $workDir 'entry.py') -Value @' +print("FASTPATH_TOKEN_V1") +'@ -Encoding ASCII + +$prev = if (Test-Path Env:HP_SKIP_PIPREQS) { $env:HP_SKIP_PIPREQS } else { $null } +$env:HP_SKIP_PIPREQS = '1' + +function Invoke-Bootstrap { + param([string]$LogName) + Push-Location $workDir + try { + cmd /c "call run_setup.bat > $LogName 2>&1" + return $LASTEXITCODE + } finally { + Pop-Location + } +} + +try { + # Run 1: fresh build, entry.py prints V1. + $run1Exit = Invoke-Bootstrap '~fastpath_hash_run1.log' + + # derived requirement (CodeRabbit review, PR #460): without this, a completely broken + # :write_fast_hash (e.g. HP_FRESH_BUILD_OK never getting set) would be indistinguishable + # from a working one -- a MISSING hash file also forces run 2 to rebuild (the safe + # default), so every assertion below would still pass even if the write side never fired + # at all. Assert the write side was genuinely exercised, not just coincidentally masked. + # derived requirement (CodeRabbit review round 2): Test-Path alone only proves the file + # exists -- an empty or malformed write (e.g. a truncated/failed PowerShell -File call) + # would still make run 2 rebuild for an unrelated reason (missing-hash safe default), so + # a broken write side could hide behind that same safe default. Read the content and + # validate it matches tools/fast_check.ps1's own canonical format (a 64-char lowercase + # hex SHA256 digest, no trailing newline -- see its WriteAllText call). + $hashFileAfterRun1 = Join-Path $workDir '~fast_check.hash.txt' + $hashWrittenAfterRun1 = Test-Path -LiteralPath $hashFileAfterRun1 + $hashContentAfterRun1 = if ($hashWrittenAfterRun1) { Get-Content -LiteralPath $hashFileAfterRun1 -Raw -Encoding ASCII } else { '' } + $hashContentValid = $hashWrittenAfterRun1 -and ($hashContentAfterRun1 -match '^[0-9a-f]{64}$') + + # Rewrite entry.py to print V2, then backdate its mtime well before dist\.exe's own + # mtime -- simulating a ZIP/xcopy/robocopy delivery that preserves the original + # authored timestamp instead of stamping "now" on extraction. + $entryPath = Join-Path $workDir 'entry.py' + Set-Content -Path $entryPath -Value @' +print("FASTPATH_TOKEN_V2") +'@ -Encoding ASCII + $backdated = Get-Date -Year 2001 -Month 9 -Day 9 + (Get-Item -LiteralPath $entryPath).LastWriteTime = $backdated + (Get-Item -LiteralPath $entryPath).LastWriteTimeUtc = $backdated.ToUniversalTime() + + # derived requirement (CodeRabbit review round 2): if Windows silently rejected or + # normalized the backdated timestamp assignment above, the OLD mtime-only check would + # still (coincidentally) trigger a rebuild for the wrong reason, and this scenario would + # pass without ever having genuinely exercised the new content-hash path. Confirm the + # precondition actually holds -- entry.py's mtime is genuinely older than the EXE dist + # produced in run 1 -- before trusting run 2's outcome as evidence of hash-based detection. + $exeCandidates = @(Get-ChildItem -Path (Join-Path $workDir 'dist') -Filter '*.exe' -ErrorAction SilentlyContinue) + $mtimePreconditionHolds = ($exeCandidates.Count -gt 0) -and ((Get-Item -LiteralPath $entryPath).LastWriteTimeUtc -lt $exeCandidates[0].LastWriteTimeUtc) + + # Run 2: sources genuinely changed (V2), but entry.py's mtime lies about it. + $run2Exit = Invoke-Bootstrap '~fastpath_hash_run2.log' +} finally { + if ($null -eq $prev) { + Remove-Item Env:HP_SKIP_PIPREQS -ErrorAction SilentlyContinue + } else { + $env:HP_SKIP_PIPREQS = $prev + } +} + +$run2LogPath = Join-Path $workDir '~fastpath_hash_run2.log' +$run2SetupLog = Join-Path $workDir '~setup.log' +$run2Lines = if (Test-Path $run2LogPath) { Get-Content -LiteralPath $run2LogPath -Encoding ASCII } else { @() } +$run2Setup = if (Test-Path $run2SetupLog) { Get-Content -LiteralPath $run2SetupLog -Raw -Encoding ASCII } else { '' } +$run2Combined = ($run2Lines -join "`n") + "`n" + $run2Setup + +$runOutPath = Join-Path $workDir '~run.out.txt' +$runOutText = if (Test-Path $runOutPath) { Get-Content -LiteralPath $runOutPath -Raw -Encoding ASCII } else { '' } + +$tokenV2Seen = $runOutText -match 'FASTPATH_TOKEN_V2' +$tokenV1Seen = $runOutText -match 'FASTPATH_TOKEN_V1' +$fastPathReusedRun2 = $run2Combined -match [regex]::Escape('Fast path: reusing') + +# Genuine rebuild detected: run 2 shows the NEW token, not the old one, did not take the +# "reusing" fast path, run 1 genuinely wrote a well-formed hash (not just coincidentally +# masked by the missing-hash-file safe default), and the backdated-mtime precondition this +# whole scenario depends on actually held (entry.py genuinely predates the EXE). +$pass = ($run1Exit -eq 0) -and ($run2Exit -eq 0) -and $hashContentValid -and $mtimePreconditionHolds -and $tokenV2Seen -and (-not $tokenV1Seen) -and (-not $fastPathReusedRun2) + +Write-NdjsonRow ([ordered]@{ + id = 'self.fastpath.hash.backdated_mtime' + req = 'REQ-018' + pass = $pass + desc = 'EXE fast path content-hash freshness detects a genuine change even when the changed file mtime is backdated below the EXE' + details = [ordered]@{ + run1Exit = $run1Exit + run2Exit = $run2Exit + hashWrittenAfterRun1 = $hashWrittenAfterRun1 + hashContentValid = $hashContentValid + mtimePreconditionHolds = $mtimePreconditionHolds + tokenV2Seen = $tokenV2Seen + tokenV1Seen = $tokenV1Seen + fastPathReusedRun2 = $fastPathReusedRun2 + run2Log = '~fastpath_hash_run2.log' + } +}) + +if (-not $pass) { exit 1 } +exit 0 diff --git a/tests/test_fast_check.py b/tests/test_fast_check.py new file mode 100644 index 00000000..13f9b5a8 --- /dev/null +++ b/tests/test_fast_check.py @@ -0,0 +1,111 @@ +"""Tests for tools/fast_check.ps1 -- CLAUDE.md Active Backlog Item 39's fix for the EXE fast +path's freshness check. Previously mtime-only over *.py files: (a) a timestamp-preserving +delivery method (a ZIP, xcopy, robocopy) could carry a genuinely changed file whose mtime +still predates the built EXE, silently reusing stale logic with no signal to the user, and +(b) a requirements.txt/pyproject.toml/runtime.txt change was invisible to the scan entirely. +Fixed by switching to a content-hash comparison (never fooled by a preserved/backdated +mtime) over the same *.py file set, extended to include those three dependency files. + +No importable functions exist -- exercised end-to-end via a real `pwsh` subprocess, mirroring +tests/test_migrate_gitattributes.py's own harness. +""" +import base64 +import re +import shutil +import subprocess +import tempfile +import unittest +from pathlib import Path + +REPO = Path(__file__).resolve().parent.parent +SOURCE = REPO / "tools" / "fast_check.ps1" +PWSH = shutil.which("pwsh") + + +def _run_check(cwd, exe="dist/myapp.exe", mode="check"): + result = subprocess.run( + [PWSH, "-NoProfile", "-NonInteractive", "-File", str(SOURCE), exe, mode], + cwd=str(cwd), + capture_output=True, + text=True, + timeout=30, + ) + return result.returncode, result.stdout.strip(), result.stderr + + +@unittest.skipUnless(PWSH, "pwsh not available") +class FreshnessBehavior(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.mkdtemp() + self.root = Path(self.tmp) + (self.root / "dist").mkdir() + (self.root / "dist" / "myapp.exe").write_text("fake-exe") + (self.root / "entry.py").write_text("print('hello')\n") + (self.root / "requirements.txt").write_text("requests==2.0\n") + + def tearDown(self): + shutil.rmtree(self.tmp, ignore_errors=True) + + def test_check_with_no_stored_hash_is_not_fresh(self): + rc, out, err = _run_check(self.root) + self.assertNotEqual(out, "fresh", err) + + def test_write_then_check_is_fresh(self): + _run_check(self.root, mode="write") + self.assertTrue((self.root / "~fast_check.hash.txt").exists()) + rc, out, err = _run_check(self.root) + self.assertEqual(out, "fresh", err) + + def test_content_change_with_backdated_mtime_is_not_fresh(self): + # The core Item 39 regression case: a ZIP/xcopy/robocopy delivery can carry a + # genuinely changed file whose mtime is preserved from BEFORE the build -- the old + # mtime-only check would have wrongly reported "fresh" here. + _run_check(self.root, mode="write") + entry = self.root / "entry.py" + entry.write_text("print('changed')\n") + old = 1000000000 # 2001-09-09, far before dist/myapp.exe's own mtime + import os + os.utime(entry, (old, old)) + rc, out, err = _run_check(self.root) + self.assertNotEqual(out, "fresh", err) + + def test_dependency_file_only_change_is_not_fresh(self): + # requirements.txt is not a *.py file -- the old mtime scan never looked at it at + # all, so this change (Item 39's exposure (b)) was previously invisible regardless + # of mtime. + _run_check(self.root, mode="write") + (self.root / "requirements.txt").write_text("requests==3.0\n") + rc, out, err = _run_check(self.root) + self.assertNotEqual(out, "fresh", err) + + def test_rewrite_after_change_is_fresh_again(self): + _run_check(self.root, mode="write") + (self.root / "entry.py").write_text("print('changed')\n") + _run_check(self.root, mode="write") + rc, out, err = _run_check(self.root) + self.assertEqual(out, "fresh", err) + + def test_missing_exe_is_not_fresh(self): + _run_check(self.root, mode="write") + (self.root / "dist" / "myapp.exe").unlink() + rc, out, err = _run_check(self.root) + self.assertNotEqual(out, "fresh", err) + + +class PayloadSync(unittest.TestCase): + def test_embedded_base64_matches_source(self): + # See tests/test_migrate_gitattributes.py's own PayloadSync test for why CRLF/LF + # normalization is required here (`*.ps1 text eol=crlf` in .gitattributes). + bat = (REPO / "run_setup.bat").read_text(encoding="ascii", errors="replace") + m = re.search(r'set "HP_FAST_CHECK=([A-Za-z0-9+/=]+)"', bat) + self.assertIsNotNone(m, "HP_FAST_CHECK payload not found in run_setup.bat") + decoded = base64.b64decode(m.group(1)).replace(b"\r\n", b"\n") + source = SOURCE.read_bytes().replace(b"\r\n", b"\n") + self.assertEqual( + decoded, source, + "HP_FAST_CHECK base64 is out of sync with tools/fast_check.ps1; re-sync it.", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/fast_check.ps1 b/tools/fast_check.ps1 new file mode 100644 index 00000000..38c0ed36 --- /dev/null +++ b/tools/fast_check.ps1 @@ -0,0 +1,73 @@ +# ASCII only. CLAUDE.md Active Backlog Item 39: the EXE fast path's freshness check was +# mtime-only over *.py files, so (a) a timestamp-preserving delivery method (a ZIP, xcopy, +# robocopy) could carry a genuinely changed file whose mtime still predates the built EXE, +# silently reusing stale logic with no signal to the user, and (b) a requirements.txt/ +# pyproject.toml/runtime.txt change was invisible to the scan entirely, mtime or not. Fixes +# both: freshness is now a content-hash comparison (never fooled by a preserved/backdated +# mtime) over the SAME *.py file set already scanned, extended to include those three +# dependency files. +# +# Two modes, selected by the second positional argument (default 'check'): +# check (run from :try_fast_exe): print 'fresh' iff a stored hash exists AND matches the +# hash of the CURRENT source set. Prints nothing (falls through to a rebuild) on any +# mismatch, or when no stored hash exists yet (e.g. the first run under this fix, +# or dist\.exe was built by an older run_setup.bat with no hash file at all) -- +# a safe, one-time-cost default that never mis-reports staleness as freshness. +# write (run from :success, only when a fresh build attempt just happened -- see the +# call site's own HP_FASTPATH_USED gate for why the fast-reuse case skips this and +# avoids a redundant second hash pass): unconditionally (re)write the hash of the +# CURRENT source set. Always run from the app root as CWD (both call sites already +# guarantee this), so relative paths captured via Resolve-Path -Relative match +# between the write and a later check. +# +# Uses [System.Security.Cryptography.SHA256] directly, not Get-FileHash -- see +# docs/agent-lessons-learned.md's "Prefer raw .NET types over Utility-module cmdlets" entry: +# Microsoft.PowerShell.Utility is not guaranteed to auto-load in the for/f-backtick and +# -File invocation shapes this repo's embedded helpers run under on real Windows PowerShell +# 5.1, and this class of gap is invisible to local pwsh (Linux, PowerShell 7) testing. +# +# This is the canonical source for the HP_FAST_CHECK base64 payload embedded in +# run_setup.bat. After editing, re-sync via: +# python tools/sync_payload.py HP_FAST_CHECK tools/fast_check.ps1 +# tests/test_fast_check.py asserts the embedded payload matches this file byte-for-byte +# (after CRLF/LF normalization, since this file is eol=crlf but was authored on LF). + +$exe = $args[0] +if (-not $exe) { $exe = $env:HP_FAST_EXE } +$mode = $args[1] +if (-not $mode) { $mode = 'check' } + +$hashFile = '~fast_check.hash.txt' +$infraPattern = '(?i)(^|[/\\])(\.git|\.github|dist|\.venv|\.uv_env|__pycache__|\.conda)([/\\]|$)' +$py = @(Get-ChildItem -Recurse -File -Filter '*.py' | Where-Object { $_.FullName -notmatch $infraPattern -and $_.Name -notlike '~*.py' }) +$depNames = @('requirements.txt', 'pyproject.toml', 'runtime.txt') +$deps = @($depNames | Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } | ForEach-Object { Get-Item -LiteralPath $_ }) +$sources = @($py) + @($deps) + +if (-not $sources) { + if ($mode -eq 'check') { exit 1 } else { exit 0 } +} + +$sha = [System.Security.Cryptography.SHA256]::Create() +$lines = foreach ($f in ($sources | Sort-Object -Property FullName)) { + $relPath = Resolve-Path -LiteralPath $f.FullName -Relative + $bytes = [System.IO.File]::ReadAllBytes($f.FullName) + $hashBytes = $sha.ComputeHash($bytes) + $hex = -join ($hashBytes | ForEach-Object { $_.ToString('x2') }) + "$relPath|$hex" +} +$combined = [string]::Join("`n", $lines) +$combinedBytes = [System.Text.Encoding]::UTF8.GetBytes($combined) +$digestBytes = $sha.ComputeHash($combinedBytes) +$digest = -join ($digestBytes | ForEach-Object { $_.ToString('x2') }) + +if ($mode -eq 'write') { + [System.IO.File]::WriteAllText($hashFile, $digest) + exit 0 +} + +if (-not (Test-Path -LiteralPath $hashFile)) { exit 1 } +if (-not $exe) { exit 1 } +if (-not (Test-Path -LiteralPath $exe)) { exit 1 } +$stored = [System.IO.File]::ReadAllText($hashFile).Trim() +if ($stored -eq $digest) { 'fresh' }