diff --git a/docs/usage.md b/docs/usage.md index 549aeef14..03c57e62a 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -963,7 +963,7 @@ winapp run . --no-build --detach --json ``` - A WinUI 3 C++/WinRT app from the Visual Studio **WinUI Blank App (Packaged)** template runs packaged. Other application projects, such as a console app or a WinUI app built with `-p WindowsPackageType=None`, run unpackaged from their built `.exe`. -- Builds `Debug` for the current architecture by default. `--arch x64|arm64|x86` or `-p Platform=x64|ARM64|Win32` selects the architecture (passing both with different architectures is an error); a custom `-p Platform` name is passed through. +- Builds `Debug` for the current architecture by default. `-c Release` or `-p Configuration=Release` selects the configuration, and `--arch x64|arm64|x86` or `-p Platform=x64|ARM64|Win32` selects the architecture. Passing both forms with different values is an error. A custom `-p Platform` name is passed through. - Restores the `packages.config` NuGet packages first (skip with `--no-restore`) and installs the Windows App Runtime version the build uses, including when the Windows App SDK props are imported from a shared `.props` file rather than listed in the project's own `packages.config`. Packaged apps also get the framework packages the build references, such as the Debug VC++ runtime, installed when missing. - `--framework` and `--aot` apply only to .NET projects. - Build output shows MSBuild's warnings and errors only, with a spinner while it builds; `--verbose` shows MSBuild's full output and exact command. diff --git a/src/winapp-CLI/WinApp.Cli.Tests/ProjectRunServiceCppTests.cs b/src/winapp-CLI/WinApp.Cli.Tests/ProjectRunServiceCppTests.cs index 3c7874e1e..d05cca761 100644 --- a/src/winapp-CLI/WinApp.Cli.Tests/ProjectRunServiceCppTests.cs +++ b/src/winapp-CLI/WinApp.Cli.Tests/ProjectRunServiceCppTests.cs @@ -58,6 +58,7 @@ public sealed class ProjectRunServiceCppTests : IDisposable private static readonly string[] ExpectedBuildTokens = ["-restore", "-p:RestorePackagesConfig=true", "-t:Build", "-p:Configuration=Debug", "-p:Platform=x64"]; private static readonly string[] ExpectedArm64Tokens = ["-p:Foo=Bar", "-p:Configuration=Release", "-p:Platform=ARM64"]; + private static readonly string[] ExpectedReleaseArm64Tokens = ["-p:Platform=ARM64", "-p:Configuration=Release"]; private static readonly string[] ExpectedPackageIds = ["Microsoft.WindowsAppSDK", "Microsoft.WindowsAppSDK.Runtime"]; private DirectoryInfo _tempDir = null!; @@ -537,6 +538,24 @@ public void CppArchitectureFromProperties_MapsStandardPlatforms() Assert.IsNull(ProjectRunService.CppArchitectureFromProperties([])); } + [TestMethod] + public void CppConfigurationFromProperties_ReadsLastNonEmptyValue() + { + Assert.AreEqual("Release", ProjectRunService.CppConfigurationFromProperties(["Configuration=Release"])); + Assert.AreEqual("Release", ProjectRunService.CppConfigurationFromProperties(["Configuration=Debug", "configuration=Release", "Configuration="])); + Assert.IsNull(ProjectRunService.CppConfigurationFromProperties(["Platform=ARM64"])); + } + + [TestMethod] + public void BuildCppPropertyTokens_UserConfigurationProperty_EmitsOneConfiguration() + { + var options = new ProjectRunOptions("Release", "arm64", null, false, false, ["Configuration=Release", "Platform=ARM64"]); + + var tokens = ProjectRunService.BuildCppPropertyTokens(options); + + CollectionAssert.AreEqual(ExpectedReleaseArm64Tokens, tokens); + } + [TestMethod] public void BuildCppCommandDisplay_ShowsOnlyWhatChangesTheBuild() { diff --git a/src/winapp-CLI/WinApp.Cli.Tests/RunCommandProjectModeTests.cs b/src/winapp-CLI/WinApp.Cli.Tests/RunCommandProjectModeTests.cs index d5fd748d4..ac4cfa23c 100644 --- a/src/winapp-CLI/WinApp.Cli.Tests/RunCommandProjectModeTests.cs +++ b/src/winapp-CLI/WinApp.Cli.Tests/RunCommandProjectModeTests.cs @@ -874,6 +874,42 @@ public async Task ProjectMode_Cpp_PlatformThisMachineCannotRun_StillBuildsWithNo Assert.AreEqual("arm64", _fakeProjectRunService.BuildOptions[0].Architecture); } + [TestMethod] + [DataRow(new[] { "-p", "Configuration=Release" }, DisplayName = "-p Configuration alone")] + [DataRow(new[] { "-c", "Release", "-p", "Configuration=Release" }, DisplayName = "-c with matching -p Configuration")] + public async Task ProjectMode_Cpp_ConfigurationProperty_SelectsConfiguration(string[] configArgs) + { + var vcxproj = new FileInfo(Path.Join(_tempDirectory.FullName, "App.vcxproj")); + File.WriteAllText(vcxproj.FullName, ""); + _fakeProjectRunService.InputResolutionOverride = new RunInputResolution(WinAppRunMode.Project, vcxproj, _tempDirectory); + SetUnpackagedOutcome(vcxproj, CreateTargetDir(withManifest: false), selfContained: false, arch: "arm64"); + GetRequiredService().OsArchitecture = () => System.Runtime.InteropServices.Architecture.Arm64; + var command = GetRequiredService(); + + var exitCode = await ParseAndInvokeWithCaptureAsync(command, [vcxproj.FullName, .. configArgs, "-p", "Platform=ARM64", "--detach"]); + + Assert.AreEqual(0, exitCode); + Assert.AreEqual("Release", _fakeProjectRunService.BuildOptions[0].Configuration, + "-p Configuration=Release must select Release instead of the Debug default"); + StringAssert.Contains(TestAnsiConsole.Output, "Release | arm64"); + } + + [TestMethod] + public async Task ProjectMode_Cpp_ConfigurationPropertyConflictingWithConfigurationOption_FailsBeforeBuilding() + { + var vcxproj = new FileInfo(Path.Join(_tempDirectory.FullName, "App.vcxproj")); + File.WriteAllText(vcxproj.FullName, ""); + _fakeProjectRunService.InputResolutionOverride = new RunInputResolution(WinAppRunMode.Project, vcxproj, _tempDirectory); + var command = GetRequiredService(); + + var exitCode = await ParseAndInvokeWithCaptureAsync(command, [vcxproj.FullName, "-c", "Debug", "-p", "Configuration=Release", "--detach"]); + + Assert.AreEqual(1, exitCode); + Assert.AreEqual(0, _fakeProjectRunService.BuildAndResolveCalls.Count, "a conflicting configuration must fail before building"); + StringAssert.Contains($"{ConsoleStdOut}{ConsoleStdErr}{TestAnsiConsole.Output}", + "-p Configuration=Release conflicts with -c Debug. Pass only one of them."); + } + [TestMethod] public async Task ProjectMode_Unpackaged_ExeRequiringAdministrator_ExplainsElevation() { diff --git a/src/winapp-CLI/WinApp.Cli/Commands/RunCommand.ProjectMode.cs b/src/winapp-CLI/WinApp.Cli/Commands/RunCommand.ProjectMode.cs index 499b0b79b..2a7356afc 100644 --- a/src/winapp-CLI/WinApp.Cli/Commands/RunCommand.ProjectMode.cs +++ b/src/winapp-CLI/WinApp.Cli/Commands/RunCommand.ProjectMode.cs @@ -196,6 +196,19 @@ private async Task RunProjectModeAsync( return Fail($"{(aot ? "--aot" : "--framework")} applies to .NET projects and can't be used with a C++ project ({csproj.Name}).", isJson); } + // For C++, -p Configuration=Release is how Visual Studio users pick the configuration, so treat it + // like -c. Both are allowed only when they name the same configuration. + if (isCpp && ProjectRunService.CppConfigurationFromProperties(properties) is { } propertyConfiguration) + { + if (parseResult.GetResult(ConfigurationOption) is { Implicit: false } + && !string.Equals(propertyConfiguration, configuration, StringComparison.OrdinalIgnoreCase)) + { + return Fail($"-p Configuration={propertyConfiguration} conflicts with -c {configuration}. Pass only one of them.", isJson); + } + + configuration = propertyConfiguration; + } + // For C++, -p Platform=ARM64 is how Visual Studio users pick the architecture, so honor it when // --arch/--runtime weren't given instead of defaulting to the machine's architecture. if (isCpp && archOption is null && runtimeOption is null diff --git a/src/winapp-CLI/WinApp.Cli/Services/ProjectRunService.Cpp.cs b/src/winapp-CLI/WinApp.Cli/Services/ProjectRunService.Cpp.cs index 71d84a102..e98bf55a2 100644 --- a/src/winapp-CLI/WinApp.Cli/Services/ProjectRunService.Cpp.cs +++ b/src/winapp-CLI/WinApp.Cli/Services/ProjectRunService.Cpp.cs @@ -108,6 +108,13 @@ internal static bool IsCppWindowsStoreProject(FileInfo project) internal static string? CppArchitectureFromProperties(IReadOnlyList properties) => TryGetUserProperty(properties, "Platform", out var platform) ? ArchitectureOfCppPlatform(platform) : null; + /// + /// The configuration a user -p Configuration names (last non-empty value wins), or null when none + /// is set. Lets -p Configuration=Release select the configuration like -c Release. + /// + internal static string? CppConfigurationFromProperties(IReadOnlyList properties) => + TryGetUserProperty(properties, "Configuration", out var configuration) ? configuration : null; + private async Task BuildAndResolveCppAsync( FileInfo project, ProjectRunOptions options, @@ -166,7 +173,8 @@ private async Task BuildAndResolveCppAsync( /// /// The global properties shared by the build and evaluate passes: the user's -p first, then the /// solution context and Configuration/Platform. MSBuild is last-wins, so a user -p:Platform or - /// Solution* is honored by not emitting winapp's own; Configuration always follows -c. + /// Solution* is honored by not emitting winapp's own; Configuration always follows the resolved + /// configuration (-c, or -p Configuration when -c isn't given). /// internal static List BuildCppPropertyTokens(ProjectRunOptions options) {