Skip to content

ci(win): prove the Store package loads before shipping it - #323

Merged
EtienneLescot merged 1 commit into
fix/win-static-crtfrom
ci/verify-appx-package
Aug 9, 2026
Merged

ci(win): prove the Store package loads before shipping it#323
EtienneLescot merged 1 commit into
fix/win-static-crtfrom
ci/verify-appx-package

Conversation

@EtienneLescot

@EtienneLescot EtienneLescot commented Aug 9, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #321, which is the fix. This one is the gate that stops the next one.

Why

Two Windows releases in a row shipped a dependency that could not be resolved on the target machine, and both times someone outside the project found it:

What Found by
1.9.0 the compositor addon was reached through PATH, which MSIX ignores a user, after installing from the Store
1.9.1 the capture helper needed the Visual C++ Redistributable, which is not part of Windows Store certification, as a rejection

Each fix arrived with a guard aimed at the failure already understood. The colocation check would never have caught the redistributable. The import-table check would never have caught the PATH bug. Both are worth keeping, and neither generalises — a static check only knows about the mistakes already made.

What

Ask the Windows loader instead of asserting a pattern.

scripts/verify-appx-native.ps1 registers a built .appx and, from inside the package, calls LoadLibraryEx with LOAD_WITH_ALTERED_SEARCH_PATH on every .dll/.node — the same call Node makes for an addon — and starts each helper executable with no arguments. Whatever the next unresolvable dependency turns out to be, this fails on it. The Windows Store package job runs it on every build.

It deliberately records nothing. A real capture needs a GPU and a desktop session a runner does not usefully have, and a flaky gate gets switched off. The loader is what broke both times, and it can be tested with neither: a helper the loader rejects produces no output and exits 0xC0000135, while one that reaches main() prints its usage. That distinction needs no hardware.

Verified both ways on the 1.9.1 package

Positive — all 17 binaries, all three helpers reaching main() under package identity:

ok   load	compositor_view.node	loaded
ok   start	wgc-capture.exe 	exit=1, said: ERROR: Missing JSON config argument
ok   start	cursor-sampler.exe	exit=1, said: Usage: cursor-sampler <intervalMs> [windowHandle]
ok   start	whisper-stt-server.exe	exit=2, said: FATAL: --model <path-to-ggml.bin> ... is required
All 17 native binaries load under package identity.

Negative — the same package with avutil-60.dll removed:

FAILED count: 7
  FAIL avcodec-62.dll        -> LoadLibraryEx failed, GetLastError=126
  FAIL compositor_view.node  -> LoadLibraryEx failed, GetLastError=126
  ...

The negative case matters more, and it earned its keep immediately: it caught a real defect in the first version of this script. @(... | ConvertFrom-Json) looks like it produces an array and does not — Windows PowerShell writes the whole deserialized array to the pipeline as a single object, so Where-Object { -not $_.ok } was evaluating -not against an array of booleans, which is always false. It printed All 1 native binaries load for seventeen of them and would have reported success no matter what the probe found. Fixed, and the count is now asserted as well.

Documentation

The new section says the part none of this covers. Every failure here shares one shape — the machines we test on have more installed than the machines we ship to — so "it works here" is not evidence about anything, however many times it is repeated. Three layers, in order of preference:

  1. Remove the dependency at the source. A statically linked CRT cannot be missing. Needs no testing at all.
  2. Prove what remains automatically. Static analysis for absences that are known and enumerable; a real load for everything else.
  3. A pristine VM before a Store submission. The only layer that catches a failure nobody has thought of yet.

This PR is layer 2. It does not replace layer 3, and the documentation says so, along with how to keep such a VM (snapshot it before anything is installed; never put a toolchain in it).

Two Windows releases in a row shipped a dependency that could not be resolved on
the target machine, and both times someone outside the project found it. 1.9.0's
compositor addon was reached through PATH, which MSIX ignores, and the editor
opened with a blank preview while audio played. 1.9.1's capture helper needed the
Visual C++ Redistributable, which is not part of Windows, and Store certification
rejected it with recording unusable.

Each fix arrived with a guard aimed at the failure already understood. The
colocation check would never have caught the redistributable; the import-table
check would never have caught the PATH bug. Both are worth keeping and neither
generalises, which is the actual problem: a static check only ever knows about
the mistakes already made.

So ask the Windows loader instead. verify-appx-native.ps1 registers a built appx
and, from inside the package, calls LoadLibraryEx with LOAD_WITH_ALTERED_SEARCH_PATH
on every .dll/.node — the same call Node makes for an addon — and starts each
helper executable with no arguments. Whatever the next unresolvable dependency
turns out to be, this fails on it. The Windows Store job runs it on every build,
enabling Developer Mode on the runner it is about to discard.

It deliberately records nothing. A real capture needs a GPU and a desktop session
that a runner does not usefully have, and a flaky gate gets switched off. The
loader is what broke both times and it can be tested with neither: a helper the
loader rejects produces NO output and exits 0xC0000135, while one that reaches
main() prints its usage. That distinction needs no hardware.

Verified both ways on the 1.9.1 package. All 17 binaries load and all three
helpers reach main() under package identity. With avutil-60.dll removed from the
package, 7 of them fail with ERROR_MOD_NOT_FOUND, compositor_view.node among
them — the negative case matters more than the positive one here, and it caught a
real defect in the first version of this script: `@(... | ConvertFrom-Json)` looks
like it produces an array and does not, because Windows PowerShell writes the
whole deserialized array to the pipeline as one object. `Where-Object { -not
$_.ok }` was evaluating `-not` against an array of booleans, which is always
false. It reported "All 1 native binaries load" for seventeen of them and would
have reported success no matter what the probe found.

The documentation gains the part none of this covers. Every failure here shares
one shape — the machines we test on have more installed than the machines we ship
to — so "it works here" is not evidence about anything. Three layers, in order of
preference: remove the dependency at the source, prove what remains automatically,
and keep a virtual machine whose whole value is what is not installed in it. That
last one is the only thing that catches a failure nobody has thought of yet, and
this change does not replace it.
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1100bf52-f1ba-4e90-9177-7e3f8b4b4e26

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@EtienneLescot
EtienneLescot merged commit 092e59e into fix/win-static-crt Aug 9, 2026
2 checks passed
@EtienneLescot
EtienneLescot deleted the ci/verify-appx-package branch August 9, 2026 20:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant