Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
19 changes: 19 additions & 0 deletions src/winapp-CLI/WinApp.Cli.Tests/ProjectRunServiceCppTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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!;
Expand Down Expand Up @@ -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()
{
Expand Down
36 changes: 36 additions & 0 deletions src/winapp-CLI/WinApp.Cli.Tests/RunCommandProjectModeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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, "<Project />");
_fakeProjectRunService.InputResolutionOverride = new RunInputResolution(WinAppRunMode.Project, vcxproj, _tempDirectory);
SetUnpackagedOutcome(vcxproj, CreateTargetDir(withManifest: false), selfContained: false, arch: "arm64");
GetRequiredService<RunCommand.Handler>().OsArchitecture = () => System.Runtime.InteropServices.Architecture.Arm64;
var command = GetRequiredService<RunCommand>();

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, "<Project />");
_fakeProjectRunService.InputResolutionOverride = new RunInputResolution(WinAppRunMode.Project, vcxproj, _tempDirectory);
var command = GetRequiredService<RunCommand>();

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()
{
Expand Down
13 changes: 13 additions & 0 deletions src/winapp-CLI/WinApp.Cli/Commands/RunCommand.ProjectMode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,19 @@ private async Task<int> 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
Expand Down
10 changes: 9 additions & 1 deletion src/winapp-CLI/WinApp.Cli/Services/ProjectRunService.Cpp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,13 @@ internal static bool IsCppWindowsStoreProject(FileInfo project)
internal static string? CppArchitectureFromProperties(IReadOnlyList<string> properties) =>
TryGetUserProperty(properties, "Platform", out var platform) ? ArchitectureOfCppPlatform(platform) : null;

/// <summary>
/// The configuration a user <c>-p Configuration</c> names (last non-empty value wins), or null when none
/// is set. Lets <c>-p Configuration=Release</c> select the configuration like <c>-c Release</c>.
/// </summary>
internal static string? CppConfigurationFromProperties(IReadOnlyList<string> properties) =>
TryGetUserProperty(properties, "Configuration", out var configuration) ? configuration : null;

private async Task<ProjectBuildOutcome> BuildAndResolveCppAsync(
FileInfo project,
ProjectRunOptions options,
Expand Down Expand Up @@ -166,7 +173,8 @@ private async Task<ProjectBuildOutcome> BuildAndResolveCppAsync(
/// <summary>
/// The global properties shared by the build and evaluate passes: the user's <c>-p</c> first, then the
/// solution context and Configuration/Platform. MSBuild is last-wins, so a user <c>-p:Platform</c> or
/// <c>Solution*</c> is honored by not emitting winapp's own; Configuration always follows <c>-c</c>.
/// <c>Solution*</c> is honored by not emitting winapp's own; Configuration always follows the resolved
/// configuration (<c>-c</c>, or <c>-p Configuration</c> when <c>-c</c> isn't given).
/// </summary>
internal static List<string> BuildCppPropertyTokens(ProjectRunOptions options)
{
Expand Down
Loading