Use powershell -File in the Windows .cmd wrappers - #17364
Open
nohwnd wants to merge 3 commits into
Open
Conversation
The wrappers spliced %* into a -command string that PowerShell then parsed as source code. -projects "a.csproj;b.csproj" arrived as projects=[a.csproj] and PowerShell tried to execute b.csproj as a second statement, swallowing the rest of the line. Paths with spaces split on the space. Exit codes collapsed to 1. With -File everything after the script path reaches the target script as a literal argument. Under -File no string binds to a [bool] parameter, not even 0 or 1, so -warnAsError $false through a wrapper would fail. Relax warnAsError, nodeReuse and msbuildMultiThreaded in build.ps1 and msbuild.ps1, and normalize the value in tools.ps1 instead, so existing callers keep working unchanged in both the .cmd path and the direct powershell path. An unrecognized value is an error, not a silently flipped flag. Switch parameters still cannot be negated through a wrapper, -ci:$false does not work under -File. Omit the switch or call build.ps1 directly. 🤖
A repo's own eng/build.ps1 is not owned by Arcade, so darc will not fix it. Repos copying this pattern into their own wrappers hit the same binding failure, and the ordering makes it non-obvious: the param block binds before tools.ps1 is dot-sourced, so the constraint has to come off at the param. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Updates Windows .cmd wrapper scripts to invoke PowerShell with -File instead of embedding arguments into -command, preventing argument re-parsing (e.g., ; statement separators, spaces splitting) and preserving downstream exit codes. To maintain compatibility for consuming repos that pass boolean values through wrappers, the PR relaxes selected boolean parameters in PowerShell entrypoints and normalizes them centrally.
Changes:
- Switch Windows
.cmdwrappers frompowershell ... -command "& ... %*"topowershell ... -File "script.ps1" %*for safer argument forwarding and correct exit-code propagation. - Relax
warnAsError,nodeReuse, andmsbuildMultiThreadedparameter typing and add centralized boolean-string normalization ineng/common/tools.ps1. - Document Windows wrapper argument behavior and limitations in
Documentation/ArcadeSdk.md.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| Test.cmd | Uses -File when invoking eng\common\Build.ps1 for test runs. |
| Restore.cmd | Uses -File when invoking eng\common\Build.ps1 for restore runs. |
| Build.cmd | Uses -File when invoking eng\common\Build.ps1 for restore/build runs. |
| eng/common/build.cmd | Uses -File when invoking build.ps1 and preserves %ErrorLevel%. |
| eng/common/CIBuild.cmd | Uses -File when invoking Build.ps1 with CI build flags. |
| eng/common/dotnet.cmd | Uses -File for dotnet.ps1 to preserve argument/exit-code behavior. |
| eng/common/dotnet-install.cmd | Uses -File for dotnet-install.ps1. |
| eng/common/init-tools-native.cmd | Uses -File for init-tools-native.ps1 while preserving %ErrorLevel%. |
| eng/common/build.ps1 | Loosens specific boolean parameter types so wrapper-passed strings can be normalized later. |
| eng/common/msbuild.ps1 | Loosens specific boolean parameter types so wrapper-passed strings can be normalized later. |
| eng/common/tools.ps1 | Adds ParseBooleanArgument and normalizes select boolean-like arguments from wrappers. |
| Documentation/ArcadeSdk.md | Documents Windows wrapper argument passing, boolean spellings, and switch-negation limitation. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
The file is eng/common/build.ps1, but four wrappers spelled it Build.ps1. Same normalization microsoft/vstest#16363 did. Makes the wrappers match the repo and makes a grep for build.ps1 find these lines.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Every
.cmdwrapper splices%*into a-commandstring that PowerShell then parses as source code:So
;in an argument becomes a statement separator, and a space splits the argument in two. Semi-colon delimited-projectsis our own documented usage (build.ps1 -help: "Semi-colon delimited list of sln/proj's to build"), and on Windows.csprojis file-associated with Visual Studio, so every entry after the first can open an IDE.Measured on Windows PowerShell 5.1 with the exact wrapper form:
-command-File-projects "a.csproj;b.csproj" /p:Foo=Barprojects=[a.csproj], then PowerShell runsb.csprojas a command and swallows/p:Foo=Barinto that statementprojects=[a.csproj;b.csproj],properties=[/p:Foo=Bar]-projects "C:\my dir\a.csproj"projects=[C:\my],properties=[dir\a.csproj]projects=[C:\my dir\a.csproj]1The exit code one matters for
eng/common/dotnet.cmd, which forwardsdotnet.exeresults throughExitWithExitCode $LASTEXITCODE, and forCIBuild.cmd. Real measurement against the currentdotnet.ps1,dotnet execon a missing dll:With
-Fileeverything after the script path reaches the target script as a literal argument, so all three go away.The semicolon form may well have no users today, and that is part of why it is worth fixing now. What surfaced it was an agent reading the documented syntax, using it as documented, and opening a large number of Visual Studio instances, one per project after the first. Documented syntax that quietly does something destructive instead of failing is a trap rather than a feature. Low usage cuts in favour of the change, not against it: the blast radius of fixing it is small.
Why not keep
-commandand validate insteadA reasonable alternative is a guard in the
.cmditself, before PowerShell is invoked, rejecting any command line containing;. That does work, I measured it. I did not take it, for four reasons.It bans legitimate usage.
/p:NoWarn="NU1605;CS0168"and/p:DefineConstants="A;B"are everyday MSBuild, and-warnNotAsErroris documented intools.ps1as a semi-colon delimited list of warning codes. A batch-level guard cannot tell a semicolon I meant from a semicolon that will detonate, so it has to reject all of them.It leaves spaces broken.
-projects "C:\my dir\a.csproj"still splits into two arguments.It leaves exit codes collapsed to
1, which is thedotnet.cmdproblem above.It covers one character out of a set.
;is not special here, the whole command line is being parsed as source. Measured with/p:Msg="a&b":|and parentheses behave the same way. And every consuming repo would need the same guard pasted into its own wrappers, forever, with any repo that forgets falling back to the original silent behaviour.-Filefixes the class at the source, the guard patches the one instance we tripped over.Boolean parameters
This is the part that needed care. Under
-Fileevery argument arrives as a string, and no string binds to a[bool]parameter. Not$false, notfalse, not0or1. They all fail withRepos call
eng\common\cibuild.cmd -warnAsError $falsetoday, so a wrappers-only change would break them, and there is no replacement value to migrate to. Instead of breaking them I relaxedwarnAsError,nodeReuseandmsbuildMultiThreadedinbuild.ps1andmsbuild.ps1, and normalize the value intools.ps1where all three are already coerced to[bool].$true,true,1,$false,falseand0are accepted, anything else throws. I deliberately did not make an unrecognized value fall back to a default, a typo like-warnAsError tureshould fail rather than silently flip the flag.So consuming repos need no changes. Both call paths keep working:
cibuild.cmd -warnAsError $falsearrives as the literal string$falseand normalizes toFalsebuild.ps1 -warnAsError $falsefrom a- powershell:step is still a real boolean and passes through untouchedNothing in this repo needed updating either. The
-warnAsError $falselines inazure-pipelines-pr.ymlare on- powershell:steps that callbuild.ps1directly, and no.cmdcall site here passes a boolean.Known limitation
Switch parameters cannot be negated through a wrapper.
-ci:$falseworks under-commandand fails under-File, and there is no workaround other than omitting the switch or callingeng\common\build.ps1directly. Nothing in Arcade does this, but a consuming repo might, so it is documented in ArcadeSdk.md.Verified locally
Restore.cmd -warnAsError $false -nodeReuse falsesucceeds, exit 0Build.cmd -projects "C:\my dir\a.csproj;..."receives the path with the space intacteng\common\build.cmd,-warnAsError tureand an empty value fail with exit 1init-tools-native.cmd -DownloadRetries 3 -RetryWaitTimeInSeconds 2 -PathPromotionstill binds[int]and[switch]parametersdotnet.cmdexit code check above/p:Msg="a&b"passthrough, and the;guard alternative aboveFour wrappers spelled the script
Build.ps1while the file on disk isbuild.ps1. Corrected on the lines I was already touching, same normalization vstest did.Same fix in vstest: microsoft/vstest#16363. That one changed only vstest's own wrappers and left
eng/commonto be fixed here.🤖