Skip to content
Open
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/guides/cpp.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Package identity is a core concept in the Windows app model. It allows your appl
A standard executable (like one created with `cmake --build`) does not have package identity. This guide shows how to add it for debugging and then package it for distribution.

> [!TIP]
> **Using a Visual Studio C++ project (`.vcxproj`) instead of CMake?** Run `winapp run .` in the project folder: winapp builds it with Visual Studio's MSBuild and launches it with package identity. See [C++ projects](../usage.md#c-projects-vcxproj) and the [cpp-winui-app sample](../../samples/cpp-winui-app/).
> **Using a Visual Studio C++ project (`.vcxproj`) instead of CMake?** Run `winapp run .` in the project folder: winapp builds it with Visual Studio's MSBuild and launches it with package identity. Run `winapp package .\MyApp.vcxproj` to build it and create an MSIX. See [C++ projects](../usage.md#c-projects-vcxproj) and the [cpp-winui-app sample](../../samples/cpp-winui-app/).

## Prerequisites

Expand Down
23 changes: 17 additions & 6 deletions docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -319,15 +319,15 @@ winapp update --setup-sdks experimental

Create MSIX packages from a project or prepared application directories. Requires a manifest file (`Package.appxmanifest` preferred, `appxmanifest.xml` also supported) to be present in the target directory, in the current directory, or passed with the `--manifest` option. (run `init` or `manifest generate` to create a manifest)

Pass a single `.csproj` to build the project and package its output in one step (**project mode**, see [Packaging a project directly](#packaging-a-project-directly) below). Pass multiple input folders to create an `.msixbundle` for multi-architecture distribution (see [Multi-architecture bundles](#multi-architecture-bundles) below).
Pass a single `.csproj` or C++ `.vcxproj` to build the project and package its output in one step (**project mode**, see [Packaging a project directly](#packaging-a-project-directly) below). Pass multiple input folders to create an `.msixbundle` for multi-architecture distribution (see [Multi-architecture bundles](#multi-architecture-bundles) below).

```bash
winapp pack <input-folder> [input-folder...] [options]
```

**Arguments:**

- `input-folder` - A single `.csproj` to build and package (project mode), or one or more directories containing the application files to package. Pass multiple folders (e.g., `./publish/x64 ./publish/arm64`) to create an MSIX bundle. For **sparse identity packages**, pass a sparse `appxmanifest.xml` file directly instead of a folder (see [Sparse identity packages](#sparse-identity-packages) below).
- `input-folder` - A single `.csproj` or C++ `.vcxproj` to build and package (project mode), or one or more directories containing the application files to package. Pass multiple folders (e.g., `./publish/x64 ./publish/arm64`) to create an MSIX bundle. For **sparse identity packages**, pass a sparse `appxmanifest.xml` file directly instead of a folder (see [Sparse identity packages](#sparse-identity-packages) below).

**Options:**

Expand All @@ -344,11 +344,11 @@ winapp pack <input-folder> [input-folder...] [options]
- `--skip-pri` - Skip PRI file generation
- `--executable <path>` - Path to the executable relative to the input folder (also `--exe`). Used to resolve `$targetnametoken$` placeholders in the manifest.

**Project-mode options** (require a `.csproj` input; rejected for folder/bundle/manifest inputs):
**Project-mode options** (require a `.csproj` or `.vcxproj` input; rejected for folder/bundle/manifest inputs):

- `--configuration <name>` (`-c`) - Build configuration (default: `Release`)
- `--arch <arch>` - Target architecture: `x64`, `arm64`, or `x86` (default: the current process architecture)
- `--framework <tfm>` (`-f`) - Target framework moniker for multi-targeted projects
- `--arch <arch>` - Target architecture: `x64`, `arm64`, or `x86` (default: the current process architecture). Repeat it to build a multi-architecture bundle from a `.csproj`.
- `--framework <tfm>` (`-f`) - Target framework moniker for multi-targeted .NET projects
- `--no-build` - Package the existing build output without rebuilding
- `--no-restore` - Skip restoring the project before building
- `--property <name=value>` (`-p`) - MSBuild property, forwarded to build and evaluation (repeatable)
Expand Down Expand Up @@ -386,8 +386,18 @@ The target architecture comes from `--arch`, or from a lone `-p RuntimeIdentifie

The project must build as a packaged app (`EnableMsixTooling=true` with a `Package.appxmanifest`); a project that builds as an unpackaged app (`WindowsPackageType=None`) has no MSIX manifest to package and `winapp pack` reports an actionable error. Folder, bundle, and sparse-manifest inputs are unchanged.

Project mode produces a single `.msix` or an architecture-only `.msixbundle` (see [Multi-architecture bundles](#multi-architecture-bundles)). It does not produce Store-upload archives or resource-split (language/scale) bundles: an explicit `-p UapAppxPackageBuildMode=StoreUpload` or `-p AppxBundleAutoResourcePackageQualifiers=...` is rejected with a note to run the native SDK packaging command directly for those flows.
.NET project mode produces a single `.msix` or an architecture-only `.msixbundle` (see [Multi-architecture bundles](#multi-architecture-bundles)). It does not produce Store-upload archives or resource-split (language/scale) bundles: an explicit `-p UapAppxPackageBuildMode=StoreUpload` or `-p AppxBundleAutoResourcePackageQualifiers=...` is rejected with a note to run the native SDK packaging command directly for those flows.

**C++ projects (`.vcxproj`).** winapp builds the project with Visual Studio's MSBuild, the same build `winapp run` uses (see [C++ projects](#c-projects-vcxproj) for prerequisites), then packages the build output folder:

```powershell
winapp pack .\MyApp.vcxproj --cert .\devcert.pfx
winapp pack .\MyApp.vcxproj -p Platform=ARM64 --generate-cert
```

- Builds `Release` for the current architecture by default. `-c` or `-p Configuration=<name>` selects the configuration, and `--arch` or `-p Platform=x64|ARM64|Win32` selects the architecture.
- A packaged project, such as the **WinUI Blank App (Packaged)** template, is packaged with the `AppxManifest.xml` its build generates. Any other application project needs an `appxmanifest.xml`: run `winapp init` in the project folder once, or pass `--manifest`. The built `.exe` fills a `$targetnametoken$` placeholder in that manifest.
- `--framework` and multiple `--arch` values aren't supported. To build a multi-architecture bundle, build each architecture and pass the output folders: `winapp pack <x64-folder> <arm64-folder>`.
#### Sparse identity packages

When the input is a **sparse `appxmanifest.xml` file** (one declaring `<uap10:AllowExternalContent>true</uap10:AllowExternalContent>` under `<Properties>`) rather than a folder, `winapp pack` builds an **identity-only** `.msix` — it packages just the manifest, with no application binaries or assets. This is step 2 of the [sparse packaging workflow](guides/sparse.md).
Expand Down Expand Up @@ -980,6 +990,7 @@ winapp run . --no-build --detach --json
- Build output shows MSBuild's warnings and errors only, with a spinner while it builds; `--verbose` shows MSBuild's full output and exact command.
- In a folder or solution that also has a runnable C# app, the C# app is selected; use `--project <name>` to run the C++ app. C# libraries and test projects next to a C++ app don't get in the way.
- A C# app that references a C++ project (for example a native DLL) can't be built with `dotnet`. Build it with Visual Studio or `MSBuild.exe`, then run `winapp run <app>.csproj --no-build`, or package its output folder with `winapp package <folder>`.
- To create an MSIX from a C++ project, run `winapp package .\MyApp.vcxproj`. See [Packaging a project directly](#packaging-a-project-directly).

**Prerequisites:** Visual Studio or Build Tools for Visual Studio 2022 version 17.8 or later, with the MSVC C++ build tools for the target architecture (the **Desktop development with C++** workload) and the Windows SDK the project targets. WinUI 3 apps also need **C++ WinUI app development tools**; with several installs, winapp uses the newest one that can build the project. The .NET SDK is not required. When the build tools, platform toolset (`MSB8020`), or Windows SDK (`MSB8036`) are missing, `winapp run` says what to install, including a `winget` command. A project with a `vcpkg.json` needs vcpkg integrated with MSBuild: run `vcpkg integrate install` once.

Expand Down
16 changes: 15 additions & 1 deletion plugins/winapp/skills/winapp-package/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: winapp-package
description: Build an MSIX installer for a desktop, console, or CLI app (Electron, .NET, C++, Rust, Flutter, Tauri) from build output or a .csproj with winapp package, optionally signed. Use for an installer testers can sideload, app distribution, or MSIX in scripts or CI. Not for signing an existing MSIX.
description: Build an MSIX installer for a desktop, console, or CLI app (Electron, .NET, C++, Rust, Flutter, Tauri) from build output or a project file with winapp package, optionally signed. Use for an installer testers can sideload, app distribution, or MSIX in scripts or CI. Not for signing an existing MSIX.
---

**If you can't run `winapp` yourself** (no shell, or the command is denied), give the user the exact `winapp` command(s) for their project instead of only describing the steps.
Expand All @@ -17,6 +17,7 @@ Use this skill when:

What you need depends on the input:
- **Project mode** (`winapp package MyApp.csproj`): an explicit `.csproj` for a packaged app (`EnableMsixTooling=true` with a `Package.appxmanifest`). winapp publishes it and packages the output.
- **C++ project mode** (`winapp package MyApp.vcxproj`): winapp builds the project with Visual Studio's MSBuild and packages the build output. A packaged WinUI project uses the manifest its build generates; other projects need an `appxmanifest.xml` from `winapp init` or `--manifest`.
- **Folder mode** (`winapp package ./bin/Release`): **built app output** in a folder (e.g., `bin/Release/`, `dist/`, `build/`) plus a **`Package.appxmanifest`** in the current directory, passed via `--manifest`, or in the folder.
- **Certificate** (optional, both modes) — `devcert.pfx` from `winapp cert generate` for signing.

Expand Down Expand Up @@ -45,6 +46,19 @@ repeatable `-p`), defaults to the **Release** configuration, resolves the publis
packages it. If the project publishes as an unpackaged app (`WindowsPackageType=None`) there is no
manifest to package and the command errors. Folder, bundle, and sparse-manifest inputs are unchanged.

### Package a C++ project (.vcxproj)

```powershell
# Build with Visual Studio's MSBuild (Release, current architecture) and create an MSIX
winapp package ./MyApp.vcxproj --cert ./devcert.pfx

# Pick the configuration and architecture the Visual Studio way
winapp package ./MyApp.vcxproj -p Configuration=Debug -p Platform=ARM64 --generate-cert
```

One architecture per run: `--framework` and multiple `--arch` values are rejected. For a bundle, build
each architecture and pass the output folders (see Multi-architecture bundles below).

### Basic packaging (unsigned)

```powershell
Expand Down
8 changes: 8 additions & 0 deletions samples/cpp-winui-app/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ winapp run . --no-build

Click **Show package identity** to see the package family name the app is running under.

## Packaging

Build the project and create a signed MSIX in one step:

```powershell
winapp package .\CppWinUIApp.vcxproj --generate-cert
```

## Testing with winapp ui

```powershell
Expand Down
7 changes: 5 additions & 2 deletions src/winapp-CLI/WinApp.Cli.Tests/FakeMsixService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,9 @@ public sealed record CreatePackageArgs(
PackageGraphSource? PackageGraph = null,
bool AutoSign = false,
string? TimestampUrl = null,
FileInfo? AppxRecipe = null);
FileInfo? AppxRecipe = null,
string? Executable = null,
DirectoryInfo? WindowsAppSdkPackageDirectory = null);

/// <summary>The most recent <see cref="CreateMsixPackageAsync"/> call's captured arguments, or null.</summary>
public CreatePackageArgs? LastCreatePackageArgs { get; private set; }
Expand Down Expand Up @@ -277,11 +279,12 @@ public Task<CreateMsixPackageResult> CreateMsixPackageAsync(
bool runtimeAlreadyBundled = false,
string? timestampUrl = null,
FileInfo? appxRecipe = null,
DirectoryInfo? windowsAppSdkPackageDirectory = null,
CancellationToken cancellationToken = default)
{
CreatePackageCalls.Add(inputFolder);
LastCreatePackageArgs = new CreatePackageArgs(
inputFolder, projectFile, framework, noRestore, targetArch, selfContained, runtimeAlreadyBundled, manifestPath, packageGraph, autoSign, timestampUrl, appxRecipe);
inputFolder, projectFile, framework, noRestore, targetArch, selfContained, runtimeAlreadyBundled, manifestPath, packageGraph, autoSign, timestampUrl, appxRecipe, executable, windowsAppSdkPackageDirectory);
if (PackageExceptionToThrow != null)
{
throw PackageExceptionToThrow;
Expand Down
26 changes: 24 additions & 2 deletions src/winapp-CLI/WinApp.Cli.Tests/MsixServiceRuntimeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -346,10 +346,10 @@ private void ArrangeRuntimeMsixCache(string sdkVersion, string runtimeIdentity)
[$"Microsoft.WindowsAppRuntime.msix={runtimeIdentity}"]);
}

private async Task<string> InvokeUpdateWinAppSdkDependencyAsync(string manifest, DotNetPackageListJson? packageList)
private async Task<string> InvokeUpdateWinAppSdkDependencyAsync(string manifest, DotNetPackageListJson? packageList, DirectoryInfo? windowsAppSdkPackageDirectory = null)
{
return await (Task<string>)UpdateWinAppSdkDependencyMethod.Invoke(
_msixService, [manifest, packageList, TestTaskContext, CancellationToken.None])!;
_msixService, [manifest, packageList, TestTaskContext, CancellationToken.None, windowsAppSdkPackageDirectory])!;
}

private static string ManifestWithDependencies(string dependencyElements) =>
Expand Down Expand Up @@ -393,6 +393,28 @@ public async Task UpdateWindowsAppSdkDependency_ExistingRuntimeDependency_Update
Assert.DoesNotContain("5000.0.0.0", result);
}

[TestMethod]
public async Task UpdateWindowsAppSdkDependency_BuildPackageDirectory_WinsOverConfiguredVersion()
{
// The current folder resolves 1.6, but the C++ build imported a 1.8 Windows App SDK.
// The build's runtime must win so the packaged app depends on the runtime it was built against.
ArrangeRuntimeMsixCache("1.6.0", RuntimeIdentity);
var buildSdkDir = _tempDirectory.CreateSubdirectory("packages").CreateSubdirectory("Microsoft.WindowsAppSDK.1.8.0");
var msixArchDir = Directory.CreateDirectory(
Path.Join(buildSdkDir.FullName, "tools", "MSIX", $"win10-{WorkspaceSetupService.GetSystemArchitecture()}"));
File.WriteAllLines(
Path.Join(msixArchDir.FullName, "msix.inventory"),
["Microsoft.WindowsAppRuntime.msix=Microsoft.WindowsAppRuntime.1.8_8000.616.304.0_x64__8wekyb3d8bbwe"]);
var manifest = ManifestWithDependencies(
" <PackageDependency Name=\"Microsoft.WindowsAppRuntime.1.8\" MinVersion=\"8000.616.304.0\" Publisher=\"CN=Microsoft Corporation\" />");

var result = await InvokeUpdateWinAppSdkDependencyAsync(manifest, PackageListWith(SdkPackageId, "1.6.0"), buildSdkDir);

Assert.Contains("Microsoft.WindowsAppRuntime.1.8", result);
Assert.Contains("8000.616.304.0", result);
Assert.DoesNotContain("Microsoft.WindowsAppRuntime.1.6", result);
}

[TestMethod]
public async Task UpdateWindowsAppSdkDependency_DependenciesWithoutRuntime_AddsRuntimeDependency()
{
Expand Down
1 change: 1 addition & 0 deletions src/winapp-CLI/WinApp.Cli.Tests/MsixServiceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1351,6 +1351,7 @@ private static async Task<string> InvokeUpdateAppxManifestContentAsync(MsixServi
null,
CreateTestTaskContext(),
CancellationToken.None,
null,
null
]) as dynamic;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public async Task PackageCommand_Folder_ProjectOnlyOption_Rejected()

Assert.AreEqual(1, exitCode, "a project-only option must be rejected for a folder input");
Assert.AreEqual(0, _fakeMsixService.CreatePackageCalls.Count, "no packaging must run when a project-only option is used with a folder");
StringAssert.Contains(ConsoleStdErr.ToString(), "require a .csproj input");
StringAssert.Contains(ConsoleStdErr.ToString(), "require a .csproj or .vcxproj input");
}

[TestMethod]
Expand Down
Loading
Loading