diff --git a/src/mono/wasm/Wasm.Build.Tests/NativeBuildTests.cs b/src/mono/wasm/Wasm.Build.Tests/NativeBuildTests.cs index 6716d4d350a298..5e50fa3d7fc4cb 100644 --- a/src/mono/wasm/Wasm.Build.Tests/NativeBuildTests.cs +++ b/src/mono/wasm/Wasm.Build.Tests/NativeBuildTests.cs @@ -38,6 +38,42 @@ public async Task SimpleNativeBuild(Configuration config, bool aot) await RunForPublishWithWebServer(new BrowserRunOptions(config, ExpectedExitCode: 42)); } + [Theory] + [BuildAndRun(aot: false)] + [TestCategory("native-mono")] + [SkipOnPlatform(TestPlatforms.AnyUnix, "The cmd.exe quoting behavior this covers is Windows-specific.")] + public async Task NativeBuildWithSpecialCharsInTempPath(Configuration config, bool aot) + { + // Regression test for https://github.com/dotnet/runtime/issues/120327. + // Native compilation runs the compiler through a temporary batch file created under the + // temp directory. Windows user profile names can contain parentheses (e.g. "John(US)"), + // which puts parentheses in %TEMP%. `cmd /c ""` then stripped the quotes around + // that path and mis-parsed it at the first '(', failing the native build with + // "'C:\Users\John' is not recognized as an internal or external command". + // The unicode chars additionally cover the UTF-8 (chcp 65001) handling in the same + // RunShellCommand path, which exists so non-ASCII (e.g. GB18030) temp/user paths work. + ProjectInfo info = CreateWasmTemplateProject( + Template.WasmBrowser, + config, + aot, + "parens_temp", + extraProperties: "true"); + + UpdateBrowserProgramFile(); + ReplaceMainJsWithMinimalRunMain(); + + string tempWithParens = Path.Combine(BuildEnvironment.TmpPath, $"tmp ({GetRandomId()}) {s_unicodeChars}"); + Directory.CreateDirectory(tempWithParens); + var envVars = new Dictionary + { + ["TMP"] = tempWithParens, + ["TEMP"] = tempWithParens, + }; + + PublishProject(info, config, new PublishOptions(ExtraBuildEnvironmentVariables: envVars), isNativeBuild: true); + await RunForPublishWithWebServer(new BrowserRunOptions(config, ExpectedExitCode: 42)); + } + [Theory] [BuildAndRun(aot: true)] public void AOTNotSupportedWithNoTrimming(Configuration config, bool aot) diff --git a/src/tasks/Common/Utils.cs b/src/tasks/Common/Utils.cs index 97be25c8559d38..edbaa0fbc3c792 100644 --- a/src/tasks/Common/Utils.cs +++ b/src/tasks/Common/Utils.cs @@ -59,8 +59,14 @@ public static (int exitCode, string output) RunShellCommand( string? label=null) { string scriptFileName = CreateTemporaryBatchFile(command); + // The script path lives under the temp directory, which is typically inside the user + // profile (e.g. C:\Users\John(US)\AppData\Local\Temp\...). If that path contains cmd + // special characters such as '(' or ')', `cmd /c ""` strips the surrounding quotes + // (because it sees special chars between the two quotes) and then mis-parses the now + // unquoted path at the first parenthesis. Use `/S` together with an extra pair of quotes + // so cmd strips only the outermost quotes and runs the still-quoted path verbatim. (string shell, string args) = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) - ? ("cmd", $"/c \"{scriptFileName}\"") + ? ("cmd", $"/S /c \"\"{scriptFileName}\"\"") : ("/bin/sh", $"\"{scriptFileName}\""); string msgPrefix = label == null ? string.Empty : $"[{label}] ";