fix: clean-machine bootstrap (analyzer pack path) + winget auto-install + CI workflow#430
Conversation
`mur pack-local` was failing on a freshly-cloned machine with:
NuGet.Build.Tasks.Pack.targets(222,5): error
Could not find a part of the path
'...\src\Reactor.Analyzers\bin\Debug\netstandard2.0\'
Root cause: Reactor.csproj packs the analyzer + localization-generator
DLLs into the framework nupkg via `<None Include>` paths that hardcode
`bin\$(Configuration)\netstandard2.0\` — no `$(Platform)` segment. The
two projects are also `<ProjectReference>`s, so when `dotnet pack` is
invoked with `-p:Platform=x64` (or ARM64) the inherited Platform
cascades to the transitive analyzer build and the output actually
lands at `bin\x64\Debug\netstandard2.0\`. Pack then looks at the
platform-less path, doesn't find the file, and fails.
The bug was masked on dev machines by any prior AnyCPU build (VS
solution, bare `dotnet build src/Reactor.Analyzers`, etc.) that left
a stale DLL at the platform-less path.
Fix: set `AppendPlatformToOutputPath=false` on both analyzer csprojs.
These are netstandard2.0 IL with no platform-specific code, so output
should always land at `bin\$(Configuration)\netstandard2.0\` regardless
of what Platform the parent pack invocation inherited.
Reproduced + verified locally with a wiped bin/obj tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… prompt Reduces clean-machine friction. Instead of failing with a doc link when a prerequisite is missing, bootstrap.ps1 now drives a `winget install` itself (hard-failing if winget itself isn't on PATH). - New `Install-WithWinget` helper: single point for winget calls. Runs with `--silent --disable-interactivity --accept-source-agreements --accept-package-agreements`, treats "already installed" exit -1978335189 as success, and refreshes `$env:Path` from Machine + User registry strings so freshly-installed binaries resolve in this same shell (Path edits made by winget don't otherwise propagate to a running process). - .NET 10 SDK: new `Test-DotnetSdk10` probe. When the SDK is missing or pre-10, dump the installed-SDK list (for debugging) and call `Install-WithWinget Microsoft.DotNet.SDK.10`. Re-probe after install and fail with a "open a new shell" hint if it still isn't visible. - Windows App SDK: tri-state `-InstallWinAppSdk` parameter. Unspecified → prompt the dev (default no) since the framework defaults to WindowsAppSDKSelfContained=true and the runtime is only needed for framework-dependent deployment. `-InstallWinAppSdk` → force install non-interactively (use this from CI / fresh-dev-box automation). `-InstallWinAppSdk:$false` → skip the prompt silently. The prompt-by-default for WinAppSDK preserves the existing self-contained happy path (no surprise 50MB install) while making framework-dependent deployment one keystroke away. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a Windows-only workflow that would have caught the clean-machine pack-local regression fixed in a863f87. Runs on PRs that touch bootstrap.ps1, the CLI source, the templates project, or Directory.Build.props. Pipeline (single job, ~25 min): 1. Pre-flight diagnostic dump of dotnet/winget/WindowsAppRuntime state so failures are debuggable from the logs alone. 2. `./bootstrap.ps1 -InstallWinAppSdk -SkipPlugin` non-interactively. Exercises both winget install paths when the runner image is bare. 3. From a fresh-shell step, `mur --version` resolves on PATH (proves the global-tool install propagated to User PATH). 4. `mur doctor` exits 0 with all checks PASS (plugin shows `info` when skipped, not FAIL). 5. Verify `local-nupkgs/Microsoft.UI.Reactor.0.0.0-local.nupkg`, `Microsoft.UI.Reactor.ProjectTemplates.0.0.0-local.nupkg`, and any `Microsoft.UI.Reactor.Cli.*.nupkg` are produced. 6. `dotnet new list reactorapp` finds the template. 7. `dotnet new reactorapp -n TestApp` + restore + build -c Release. 8. `mur upgrade --skip-plugin` succeeds against the bootstrapped tree. 9. Re-run bootstrap.ps1 to validate idempotence (winget already- installed exit code handled, dotnet tool update is a no-op, dotnet new uninstall+install handles the template-engine cache). Deliberately omits actions/setup-dotnet so bootstrap.ps1 itself is responsible for the SDK install. If windows-latest already ships .NET 10, the install branch silently no-ops and the rest of the pipeline still runs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
d09b6a1 to
831d65c
Compare
|
Rebased onto current Conflict resolution:
The new |
The first bootstrap.yml run failed in 23s, at the pre-flight diagnostic:
The `msstore` source requires that you view the following agreements
Do you agree to all the source agreements terms? [Y] Yes [N] No:
An unexpected error occurred while executing the command:
0x8a150042 : Error reading input in prompt
exit=-1978335166
`winget list` and `winget install` both gate on msstore source
acceptance the first time winget runs against an account/runner. On
non-interactive shells (GH Actions, CI in general) the prompt fails
with exit -1978335166, which pwsh's "propagate $LASTEXITCODE on the
final command" behavior then surfaces as the step exit.
Two places fixed:
- bootstrap.ps1 `Test-WindowsAppRuntime20`: add
`--accept-source-agreements` to the existence probe, and reset
$LASTEXITCODE on the way out so a "not installed" status doesn't
leak past the probe into surrounding script logic.
- .github/workflows/bootstrap.yml pre-flight diagnostic step: same
--accept-source-agreements flag on the `winget list`, plus a
`$global:LASTEXITCODE = 0` at the end of the step so the diagnostic
dump can't fail the workflow before bootstrap.ps1 even starts.
`Install-WithWinget` already passes --accept-source-agreements on the
install command, so that path was correct.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Fixes a clean-machine mur pack-local failure by stabilizing analyzer/source-generator output paths, improves bootstrap.ps1 to auto-install prerequisites via winget (with an optional Windows App Runtime install), and adds a GitHub Actions workflow to validate the end-to-end bootstrap flow on Windows runners.
Changes:
- Pin analyzer/source-generator build output paths to exclude inherited
Platform(prevents pack from looking for non-existent DLL paths on clean trees). - Enhance
bootstrap.ps1withInstall-WithWinget, .NET 10 SDK detection/auto-install, and a tri-state-InstallWinAppSdkexperience. - Add
.github/workflows/bootstrap.ymlto run bootstrap + template scaffolding + idempotence checks onwindows-latest.
Show a summary per file
| File | Description |
|---|---|
| src/Reactor.Analyzers/Reactor.Analyzers.csproj | Disables platform-suffixed output paths so packing can find analyzer DLLs reliably. |
| src/Reactor.Localization.Generator/Reactor.Localization.Generator.csproj | Same output-path stabilization for the source generator DLL. |
| bootstrap.ps1 | Adds winget-based installs, .NET 10 probing, and optional Windows App Runtime install prompting/forcing. |
| .github/workflows/bootstrap.yml | New workflow that runs bootstrap end-to-end and validates template/tooling outputs. |
Copilot's findings
Tip
Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Files reviewed: 4/4 changed files
- Comments generated: 2
`[Nullable[bool]]$Foo = $null` doesn't get PowerShell's switch-style parameter binding — passing a bare `-InstallWinAppSdk` errors with "Missing an argument for parameter 'InstallWinAppSdk'." which is what the bootstrap.yml run hit. Tri-state via nullable bool would have required every caller (including the .EXAMPLE block in the doc header) to write `-InstallWinAppSdk:$true`, which is a footgun. Refactor to two mutually-exclusive [switch] parameters: -InstallWinAppSdk force install non-interactively (CI / automation) -NoWinAppSdk skip the prompt silently (neither) prompt the user (default no) Mutual exclusion checked at the top of the script (early exit with a clear error before any side effects). The "default no, prompt" behavior is preserved for interactive dev runs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`dotnet new list reactorapp` writes a multi-line table; pwsh stores the output as a [string[]]. PowerShell's -match/-notmatch on an array returns the matching/non-matching elements rather than a single bool — so `$listing -notmatch 'reactorapp'` returned the header + separator lines (which don't contain "reactorapp"), evaluating truthy and throwing the "template not found" error even though the template WAS registered. Join the array first so the comparison is a single substring check. (Caught by run 3 of bootstrap.ps1 (windows-latest): bootstrap.ps1 itself + mur doctor + nupkg verification all PASS, just the workflow's own validation logic was bugged.) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Scaffolded apps inherit Directory.Build.props:
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
The SelfContained target rejects AnyCPU (it needs a concrete arch to
embed the right runtime), so `dotnet build TestApp` with no `-p:Platform`
fails on a fresh clone:
Microsoft.WindowsAppSDK.SelfContained.targets(74,9): error :
WindowsAppSDKSelfContained requires a supported Windows architecture.
Detect $env:PROCESSOR_ARCHITECTURE and pass it through. This is the
same wart the user flagged earlier ("we don't want to REQUIRE devs to
do self contained") — addressing that properly means changing the
default in Directory.Build.props or in the template, which is bigger
than this PR's scope. For now, unblock CI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…d path
Backs out the AppendPlatformToOutputPath=false approach (which broke
unit tests with CS2012 file-lock contention on
Reactor.Localization.Generator/obj/Debug/netstandard2.0/...) in favor
of teaching Reactor.csproj's <None Include> globs about the inherited-
Platform layout.
The cause of the original clean-machine pack failure is unchanged:
when mur pack-local runs `dotnet pack -p:Platform=x64` (or ARM64), the
inherited Platform cascades to the transitive Reactor.Analyzers and
Reactor.Localization.Generator builds, which then land at
bin\<Platform>\<Configuration>\netstandard2.0\. But Reactor.csproj's
pack glob hardcoded bin\<Configuration>\netstandard2.0\ (no Platform
segment), so pack couldn't find the file on a freshly-cloned machine.
Pinning AppendPlatformToOutputPath=false on the analyzers fixed
pack-local but removed bin/obj segregation across Platform values,
which let two concurrent transitive compiles (e.g. dotnet test +
ProjectReference deduplication race) collide on the same
obj\Debug\netstandard2.0\ output path. CI Unit Tests caught this with:
CSC : error CS2012: Cannot open '...obj\Debug\netstandard2.0\
Reactor.Localization.Generator.dll' for writing -- The process
cannot access the file ... because it is being used by another
process.
New approach: Reactor.csproj computes
$(_ReactorAnalyzerBinDir) = bin\<Configuration>\netstandard2.0
when Platform is empty or AnyCPU, OR
= bin\<Platform>\<Configuration>\netstandard2.0
when Platform is x64 / ARM64.
Pack now finds the analyzers in either layout. The analyzer csprojs
go back to their stock SDK behavior, preserving per-Platform obj/bin
segregation that prevents the build-graph race.
Verified locally:
- mur pack-local from a wiped bin/obj tree succeeds on ARM64 host
(analyzers land in bin\ARM64\Debug\, Reactor.csproj finds them).
- Plain dotnet build src/Reactor (no -p:Platform) would land
analyzers at bin\Debug\, also found by the conditional binding.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
User-reported failure on a freshly-cloned machine:
mur pack-localblew up withNuGet.Build.Tasks.Pack.targets(222,5): error Could not find a part of the path '...\src\Reactor.Analyzers\bin\Debug\netstandard2.0\'. This PR fixes the underlying packaging bug, hardensbootstrap.ps1so missing prerequisites get winget-installed instead of just being reported, and adds a CI workflow that would have caught the regression.Three commits, three concerns
fix(pack): analyzer DLL path on clean machines—src/Reactor/Reactor.csprojpacks the analyzer + localization-generator DLLs via<None Include="...\bin\$(Configuration)\netstandard2.0\..." />(no$(Platform)segment). Both projects are<ProjectReference>s, so whendotnet pack -p:Platform=x64runs, the inherited Platform cascades to the transitive analyzer build and the output lands inbin\x64\Debug\netstandard2.0\instead. Pack reads the platform-less path, doesn't find it, dies. Masked on dev boxes by any prior AnyCPU build leaving a stale DLL behind. Fix: set<AppendPlatformToOutputPath>false</AppendPlatformToOutputPath>on both analyzer csprojs — they're netstandard2.0 IL with no platform-specific code, so output should always land atbin\$(Configuration)\netstandard2.0\regardless of inherited Platform. Reproduced + verified locally on a wiped bin/obj tree.feat(bootstrap): winget auto-install for .NET 10 + optional WinAppSDK promptInstall-WithWingethelper: runs winget with--silent --disable-interactivity --accept-source-agreements --accept-package-agreements, treats already-installed exit-1978335189as success, and rebuilds$env:Pathfrom Machine + User registry strings so freshly-installed binaries resolve in the same shell.winget install Microsoft.DotNet.SDK.10, re-probe. Hard-fail if winget itself isn't on PATH (App Installer is an OS-level prereq this script doesn't try to repair).-InstallWinAppSdk. Unspecified → prompt the dev (default no) since the framework defaults toWindowsAppSDKSelfContained=trueand the machine runtime is only needed for framework-dependent deployment.-InstallWinAppSdk→ force-install non-interactively (for CI / fresh-dev-box automation).-InstallWinAppSdk:$false→ skip silently. Preserves the existing self-contained happy path (no surprise 50MB install on every clean run) while making framework-dependent deployment one keystroke away.ci: bootstrap.yml — exercise bootstrap.ps1 end-to-end on a clean runner— new Windows-only workflow scoped to PRs touchingbootstrap.ps1, the CLI source, the templates project,Directory.Build.props, or the workflow itself. Pipeline: pre-flight diagnostic dump → bootstrap.ps1 non-interactively →mur --versionfrom a fresh shell →mur doctor→ verifylocal-nupkgs/content →dotnet new list reactorapp→ scaffold + restore + build a TestApp →mur upgrade --skip-plugin→ re-run bootstrap.ps1 for idempotence. Deliberately omitsactions/setup-dotnetso bootstrap.ps1 itself is responsible for the SDK install path.Test plan
src/Reactor.Analyzers/{bin,obj}andsrc/Reactor.Localization.Generator/{bin,obj}andsrc/Reactor/{bin,obj}—mur pack-localfailed the same way as in the user's screenshot before the csproj fix, succeeded after.bin/Debug/netstandard2.0/Reactor.Analyzers.dll(nox64/ARM64segment).bootstrap.ymlworkflow fire on this PR and confirm green end-to-end (will show in CI; please block merge until that goes green).-c Releasebuild only). Worth addingdotnet publish -p:WindowsAppSDKSelfContained=falseif we want the runtime-install path also gated.🤖 Generated with Claude Code