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
7 changes: 7 additions & 0 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -636,6 +636,13 @@ Create a loose layout package from a build output folder, register it with Windo
- **Folder mode** β€” the input is a build-output folder (contains a `Package.appxmanifest`/`AppxManifest.xml`).
- **Project mode** β€” the input is a `.csproj`, a `.sln`/`.slnx` solution, or a directory containing one. `winapp run` builds the project and launches it, supporting both **packaged** and **unpackaged** WinUI apps. See [Project mode](#project-mode-net-sdk-projects) below.

> [!TIP]
> Mode selection is silent by default. If a directory was treated as a build-output folder when you
> expected it to be built as a project, re-run with `--verbose` β€” folder mode reports why it was
> chosen (`No .csproj/.sln/.slnx with a runnable app found in '<path>' β€” running it as a
> build-output folder.`). A directory is only built as a project when a `.csproj`/`.sln`/`.slnx`
> with a runnable app sits at its **top level**; it is not searched recursively.

> **This is the preferred command for debugging with package identity** for most frameworks (.NET, C++, Rust, Flutter, Tauri). Unlike [`create-debug-identity`](#create-debug-identity) which registers a sparse package for a single exe, `winapp run` registers the entire folder as a loose layout package, just like a real MSIX install. See the [Debugging Guide](debugging.md) for common debugging workflows.

```bash
Expand Down
60 changes: 59 additions & 1 deletion src/winapp-CLI/WinApp.Cli.Tests/RunCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public class RunCommandTests : BaseCommandTests
private static readonly string[] SupportedArchitectures = ["x64", "arm64", "x86"];
private static readonly string[] ForcedUnpackagedProperties = ["WindowsPackageType=None", "Foo=Bar"];

private const string TestManifestContent = """
internal const string TestManifestContent = """
<?xml version="1.0" encoding="utf-8"?>
<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
Expand Down Expand Up @@ -2249,6 +2249,25 @@ public void CanCurrentOsRunArchitecture_UnknownMoniker_TreatedAsRunnable()
Assert.IsTrue(RunCommand.Handler.CanCurrentOsRunArchitecture("sparc"));
}

[TestMethod]
public async Task RunCommand_FolderMode_AtDebugVerbosity_PrintsDiscoveryBreadcrumb()
{
// This class runs at LogLevel.Debug β€” the level `--verbose` selects β€” so the breadcrumb
// must still be emitted here. It is how a user who pointed at a source directory expecting
// a build finds out why nothing was built. The companion class
// RunCommandFolderModeBreadcrumbTests asserts it is hidden at default verbosity.
await CreateTestManifestAsync();
var command = GetRequiredService<RunCommand>();

var exitCode = await ParseAndInvokeWithCaptureAsync(command, [_tempDirectory.FullName]);

Assert.AreEqual(0, exitCode);
StringAssert.Contains(
TestAnsiConsole.Output,
"No .csproj/.sln/.slnx with a runnable app found",
"Debug/verbose output should explain why a directory fell back to build-output folder mode.");
}

[TestMethod]
public void CanCurrentOsRunArchitecture_IsCaseInsensitive()
{
Expand All @@ -2259,3 +2278,42 @@ public void CanCurrentOsRunArchitecture_IsCaseInsensitive()

#endregion
}

/// <summary>
/// Covers the folder-mode discovery breadcrumb at the CLI's DEFAULT verbosity.
/// <see cref="RunCommandTests"/> runs at <see cref="LogLevel.Debug"/> (the harness default), which
/// is the level <c>--verbose</c> selects, so it cannot observe what a normal run prints. This class
/// pins the level to <see cref="LogLevel.Information"/> β€” what <c>winapp run</c> uses with no
/// verbosity flags β€” to assert the breadcrumb stays hidden.
/// </summary>
[TestClass]
public class RunCommandFolderModeBreadcrumbTests() : BaseCommandTests(logLevel: LogLevel.Information)
{
protected override IServiceCollection ConfigureServices(IServiceCollection services)
=> services
.AddSingleton<IMsixService>(new FakeMsixService())
.AddSingleton<IAppLauncherService>(new FakeAppLauncherService())
.AddSingleton<IDebugOutputService>(new FakeDebugOutputService())
.AddSingleton<IPackageRegistrationService>(new FakePackageRegistrationService())
.AddSingleton<INugetService, FakeNugetService>();

[TestMethod]
public async Task RunCommand_FolderMode_AtDefaultVerbosity_OmitsDiscoveryBreadcrumb()
{
// Running a build-output folder is the normal path β€” it is what every `dotnet run` through
// the NuGet package does, since the targets point winapp at $(OutputPath). Announcing it at
// Information made a routine, successful run look like something had gone wrong.
await File.WriteAllTextAsync(
Path.Join(_tempDirectory.FullName, "appxmanifest.xml"),
RunCommandTests.TestManifestContent,
TestContext.CancellationToken);
var command = GetRequiredService<RunCommand>();

var exitCode = await ParseAndInvokeWithCaptureAsync(command, [_tempDirectory.FullName]);

Assert.AreEqual(0, exitCode, "Folder mode should succeed");
Assert.IsFalse(
TestAnsiConsole.Output.Contains("No .csproj/.sln/.slnx with a runnable app found", StringComparison.Ordinal),
"The folder-mode breadcrumb is a troubleshooting aid and must not appear at default verbosity");
}
}
10 changes: 6 additions & 4 deletions src/winapp-CLI/WinApp.Cli/Commands/RunCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -442,10 +442,12 @@ public override async Task<int> InvokeAsync(ParseResult parseResult, Cancellatio

// Breadcrumb: we reached folder mode because no top-level .csproj/.sln/.slnx with a runnable
// app was found, so the path is treated as a pre-built layout (nothing is built). Without
// this, a user who pointed at a source directory expecting a build only sees a later
// "manifest not found" and can't tell why nothing built. Only meaningful when the input was a
// directory; suppressed for --json (pure stdout) and --quiet (Info off).
if (!isJson && inputFsi is DirectoryInfo && logger.IsEnabled(LogLevel.Information))
// this, a user troubleshooting why a source directory was not built only sees a later
// "manifest not found" and can't tell why nothing built. Keep this at debug level: folder
// mode is the normal, expected path for a build-output directory β€” including every
// `dotnet run` through the NuGet package, which always points winapp at the output
// folder β€” so at Info it reads as a warning about a situation that is entirely routine.
if (!isJson && inputFsi is DirectoryInfo && logger.IsEnabled(LogLevel.Debug))
{
ansiConsole.MarkupLineInterpolated(
$"{UiSymbols.Search} No .csproj/.sln/.slnx with a runnable app found in '{inputFolder.FullName}' β€” running it as a build-output folder.");
Expand Down
Loading