Skip to content

Pack project-mode support - #829

Open
Zach Teutsch (zateutsch) wants to merge 104 commits into
mainfrom
zt/776-pack-project-mode
Open

Zach Teutsch (zateutsch) wants to merge 104 commits into
mainfrom
zt/776-pack-project-mode

Conversation

@zateutsch

Copy link
Copy Markdown
Contributor

Description

Adds project-mode support to winapp package (issue #776). You can now pass an explicit .csproj directly:
winapp package MyApp.csproj builds the project and packages its output in one step — no need to build separately or hunt for the correct output folder. This is the packaging counterpart to #633, which added project mode to winapp run.

In project mode the command reuses winapp run's build/evaluate infrastructure (IProjectRunService): it builds the project, resolves the build output (TargetDir), and feeds it into the existing MSIX pipeline. Existing folder, multi-folder bundle, and sparse-manifest inputs are unchanged — only a single explicit .csproj triggers project mode.

The reuse seam required extending MsixService.CreateMsixPackageAsync so it is project- and architecture-aware:

  • Project-aware: resolves the Windows App SDK package graph from the built project (via ResolveDotNetPackageListAsync) instead of probing the current directory, so packaging a nested project from a solution root resolves the right dependencies.
  • Architecture-aware: threads the resolved targetArch into runtime staging and activation-manifest embedding, so --arch arm64 stages arm64 runtime bits rather than the host's.
  • Split self-contained: an already self-contained build is packaged as-is instead of double-bundling the runtime.

All new parameters are additive; folder/bundle callers pass null and behave exactly as before. Owning-solution context is threaded into the build so projects that reference $(SolutionDir) build the same as under winapp run.

Usage Example

# Build and package a project in one step
winapp package ./MyApp.csproj

# Release build for arm64, signed with a dev cert
winapp package ./MyApp.csproj -c Release --arch arm64 --cert ./devcert.pfx

# Package an existing build output without rebuilding
winapp package ./MyApp.csproj --no-build

Project mode accepts the same build options as winapp run: -c/--configuration, --arch, -r/--runtime, -f/--framework, --no-build, --no-restore, and repeatable -p/--property. A project that builds as an unpackaged app (WindowsPackageType=None) has no MSIX manifest to package and produces an actionable error.

Related Issue

Closes #776

Type of Change

  • ✨ New feature

Checklist

  • New tests added for new functionality (14 handler tests in PackageCommandProjectModeTests)
  • Tested locally on Windows
  • docs/usage.md updated (if CLI commands changed)
  • Language-specific guides updated (if applicable)
  • Shipped skills updated in plugins/winapp/skills/ (if CLI commands/workflows changed)

Additional Notes

  • Scope: Only an explicit .csproj is project mode; solutions and directory-to-project resolution are intentionally out of scope, so existing directory inputs stay unambiguous.
  • Shared code: TryResolveArchitecture was promoted to RunArchHelper and is now shared by run and package; the non-executable-project error message was made command-neutral.
  • Ship surfaces: regenerated docs/cli-schema.json and the npm typed SDK (winapp-commands.ts); updated docs/usage.md, docs/guides/dotnet.md, the package skill, and the Copilot agent reference.
  • Validation gap: end-to-end MSIX creation from a WinUI .csproj (and cross-arch self-contained staging) was not exercised in CI-less environments that lack the internal NuGet feed; unit + mock coverage is comprehensive, but a reviewer with the feed should confirm the real build->package path on at least one packaged WinUI app.
  • Open decision: --runtime/-r on package only restates --arch (the RID reduces to the same architecture). It's kept for symmetry with winapp run / dotnet build -r; flagging in case reviewers prefer to drop it before it ships.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a9a4773d-85be-4cc0-9cba-b5cbd7c7fa3b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a9a4773d-85be-4cc0-9cba-b5cbd7c7fa3b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a9a4773d-85be-4cc0-9cba-b5cbd7c7fa3b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a9a4773d-85be-4cc0-9cba-b5cbd7c7fa3b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a9a4773d-85be-4cc0-9cba-b5cbd7c7fa3b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a9a4773d-85be-4cc0-9cba-b5cbd7c7fa3b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a9a4773d-85be-4cc0-9cba-b5cbd7c7fa3b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a9a4773d-85be-4cc0-9cba-b5cbd7c7fa3b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a9a4773d-85be-4cc0-9cba-b5cbd7c7fa3b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: a9a4773d-85be-4cc0-9cba-b5cbd7c7fa3b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1b7aaf4b-03f2-4a50-898f-74ff7160b51b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1b7aaf4b-03f2-4a50-898f-74ff7160b51b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1b7aaf4b-03f2-4a50-898f-74ff7160b51b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1b7aaf4b-03f2-4a50-898f-74ff7160b51b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1b7aaf4b-03f2-4a50-898f-74ff7160b51b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1b7aaf4b-03f2-4a50-898f-74ff7160b51b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1b7aaf4b-03f2-4a50-898f-74ff7160b51b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1b7aaf4b-03f2-4a50-898f-74ff7160b51b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1b7aaf4b-03f2-4a50-898f-74ff7160b51b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1b7aaf4b-03f2-4a50-898f-74ff7160b51b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1b7aaf4b-03f2-4a50-898f-74ff7160b51b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1b7aaf4b-03f2-4a50-898f-74ff7160b51b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1b7aaf4b-03f2-4a50-898f-74ff7160b51b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1b7aaf4b-03f2-4a50-898f-74ff7160b51b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1b7aaf4b-03f2-4a50-898f-74ff7160b51b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1b7aaf4b-03f2-4a50-898f-74ff7160b51b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1b7aaf4b-03f2-4a50-898f-74ff7160b51b
Merge parent df86779 while preserving remote recipe-threading changes and stack metadata. Includes current main's image dependency and UI text-attribute updates.

Validation: 499 CLI tests, 15 text-attribute tests and 308 npm tests passed. Canonical x64/ARM64 build and npm/NuGet packaging passed; both NativeAOT binaries rendered SVG assets with verified dimensions and pixels. Generated schema/docs, plugin conformance, npm lint and format checks passed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.ProjectBundle.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.ProjectBundle.cs Fixed

@nmetulev Nikola Metulev (nmetulev) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI-generated review (winappcli pr-review skill) — verify before acting.

Decision: changes required

Reviewed and stress-tested head 69b2c249c846c56bbc1144e5b3747c76bc21b0de, including inherited Native AOT support. Three issues reproduced; no fixes applied during testing.

Generic bundles ignore the evaluated packaging recipe

What is wrong: Single-project packaging forwards resolution.AppxRecipePath, but generic bundle slices do not. The fallback copies the build directory recursively rather than assembling the recipe's intended payload.

Show me: A copied dotnet sample emits a valid external recipe after publish that excludes a harmless plain-text test-only-not-a-key.pfx. Single x64 MSIX: sentinel absent, 249 entries. x64+ARM64 bundle: sentinel present at root and under publish/, 496 entries per slice. Both reproduced twice from clean outputs. An AOT control with explicit per-slice manifest architecture also includes managed leftovers and has no root executable at the manifest's dotnet-app.exe path; native executables instead sit under publish/ and native/.

Why it matters: Bundles can include unintended files and ship the wrong executable layout. This is synthetic sentinel evidence, not exposure of an actual private key.

Smallest fix: Forward the evaluated recipe in ProduceProjectSliceAsync, matching the single-package call, and cover both managed and AOT recipe-based bundles.

Location: PackageCommand.ProjectMode.cs:779-786 versus :506; recursive fallback in MsixService.cs:504-528.

Generic AOT packaging misses manifest architecture

What is wrong: When an external recipe is present, resolution retains build TargetDir, but architecture inference looks for the executable there before staging. The AOT executable is in publish output, so a manifest that omits ProcessorArchitecture stays without it.

Show me: Clean AOT sample single-package runs twice produce MSIX without Identity/@ProcessorArchitecture. Both bundle runs fail with Bundle slice 'dotnet-app_1.0.2.0.msix' has no Identity/@ProcessorArchitecture. No final bundle is produced. MakeAppx accepted single-package creation; installation of that package was not tested.

Why it matters: Project-mode AOT bundles fail despite a known target architecture and successful native publish.

Smallest fix: Use the resolved project architecture or the correctly staged executable for inference when the manifest omits architecture; preserve validation of explicit manifest values.

Location: ProjectRunService.cs:452-460, MsixService.cs:403-418,1126-1134, MsixService.ProjectBundle.cs:55-56.

AOT package misses the toolchain PATH handling already used by run

Show me: Real AI Dev Gallery, same environment with the Visual Studio Installer directory absent from PATH: AOT package fails with MSB3073/exit 123 because vswhere.exe cannot be resolved; run --aot publishes and launches successfully. Prepending the Installer directory makes package succeed.

Why it matters: A machine that can run the AOT workflow fails to package the same app without an extra environment workaround.

Smallest fix: Reuse the existing AOT publish environment setup for the packaging publish path rather than introducing another toolchain detector.

Location: existing BuildAotPublishEnvironment in ProjectRunService.Aot.cs:198-219; native project packaging publish invocation.

Real application coverage

AI Dev Gallery at 2406535b produced managed ARM64/x64 packages, a fully .NET-self-contained ARM64 package, an ARM64 AOT package, and an ARM64+x64 AOT bundle. Native-MSIX bundle slices had correct manifest/PE architectures and clean payloads. Managed -> AOT -> managed reuse showed no stale payload contamination.

The extracted ARM64 AOT package was registered under a unique current-user identity and exercised for 205 seconds: Samples, Filters, applied NPU filtering, and Settings. Loaded modules contained no CoreCLR/JIT or managed app DLL. This is packaged-payload loose-registration coverage, NOT signed-MSIX installation. x64 was cross-compiled, not executed.

Gallery includes the prior app-side FilterRecord AOT binding fix and required TreatWarningsAsErrors=false for dependency trimming/AOT warnings. NuGet mirror configuration was also needed. These are not vanilla-app success claims or comparable performance measurements. Gallery uses native MSIX tooling and did not hit the generic-path defects above.

Temporary registrations/output were removed, no certificates were created/trusted, and both worktrees remained clean. --name identity behavior and --self-contained naming concerns were checked and withdrawn as defects.

Stream redacted publish output directly instead of buffering JSON candidates. Add a real WinUI Native AOT sample regression covering recipe staging and the native entrypoint.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1b7aaf4b-03f2-4a50-898f-74ff7160b51b
Verify the returned process runs the staged native executable, creates a responding window, and survives startup. Clean up the exact test process and unique registration.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1b7aaf4b-03f2-4a50-898f-74ff7160b51b
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1b7aaf4b-03f2-4a50-898f-74ff7160b51b
Merge parent 0040047 including main be26cf7, direct redacted AOT streaming, updated sample regression, and the shipping XML boundary. Preserve all packaging work and stack metadata; no reviewed-defect fixes.

Validation: 742 focused C# tests passed, one expected long-path skip; 308 npm tests passed. Canonical x64/ARM64 native build, npm/four NuGet packaging, generated surfaces, plugin checks, npm lint/format, sample-script parsing, and CLI/library XML packaging boundary passed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.NativePackaging.cs Fixed
Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.NativePackaging.cs Fixed
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1b7aaf4b-03f2-4a50-898f-74ff7160b51b
Merge validated parent c89706b including main 2e307c7 CsWin32 and analyzer dependency updates. Preserve remote packaging work and native stack metadata; sync only, no reviewed-defect fixes or history rewrite.

Validation: 752 focused CLI tests passed, one expected long-path skip; analyzer suite 65/65; npm 308/308. Canonical x64/ARM64 native publish, Debug solution build, npm/four NuGet packages, generated schema/docs, plugin conformance and npm compile/lint/format passed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Narrow the best-effort cleanup catch blocks introduced by project-mode
packaging to the file-system exceptions those Delete/Refresh operations
actually throw (IOException, UnauthorizedAccessException,
SecurityException), keeping the no-op best-effort semantics:
- MsixService.NativePackaging.cs (staged artifact + temp manifest cleanup)
- MsixService.ProjectBundle.cs (staged bundle + temp manifest cleanup)
- PackageCommand.ProjectMode.cs (native + bundle scratch-dir cleanup)

Narrow PathSafety.IsNetworkDriveRoot's two bias-to-unsafe catches to the
documented exceptions Path.GetFullPath and DriveInfo throw, still
returning true (refuse) for those cases.

Use Path.Join instead of Path.Combine in CollectNativeFragmentDllNames so
a non-rooted segment can't be misread as dropping earlier arguments.

The command-boundary `catch (Exception ex)` handlers are intentionally
left broad: they rethrow OperationCanceledException and convert every
other failure into a friendly error and non-zero exit, matching the
established command pattern.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: dcabfb3c-9ea1-44c5-b91b-3b04f86a4289
Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.cs Dismissed
Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.NativePackaging.cs Dismissed
Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.NativePackaging.cs Dismissed
Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.ProjectBundle.cs Dismissed
Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.ProjectBundle.cs Dismissed
Comment thread src/winapp-CLI/WinApp.Cli/Services/MsixService.cs Fixed
Base automatically changed from nmetulev-native-aot-publish-run to main September 18, 2026 00:33
Addresses three reproduced findings from PR review (generic bundle + native
AOT packaging paths):

1. Generic bundle slices ignored the evaluated AppxPackageRecipe: only the
   single-package path forwarded resolution.AppxRecipePath, so bundle slices
   fell back to a recursive whole-folder copy and shipped unintended files /
   the wrong layout. Forward the recipe in ProduceProjectSliceAsync too.

2. Generic AOT packaging left the manifest without Identity/@ProcessorArchitecture
   when PE detection could not find the executable (the AOT exe is in
   publish/native output, not the recipe's build TargetDir), so architecture-only
   bundles failed validation. Add EnsureProcessorArchitecture to stamp the
   resolved target architecture as a fallback, preserving any explicit manifest
   value; call it from UpdateAppxManifestContentAsync.

3. Native AOT packaging did not reuse the AOT publish environment (which
   prepends the VS Installer directory so vswhere.exe resolves), so packaging an
   AOT app failed with MSB3073 on a machine whose PATH omits it, even though
   run --aot succeeds. Thread BuildAotPublishEnvironment() through the native
   publish via the argument-list overload of RunDotnetCommandAsync.

Add tests: generic bundle slice recipe forwarding, EnsureProcessorArchitecture
(stamp/preserve/no-op), and native publish using the env-capable overload.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: dcabfb3c-9ea1-44c5-b91b-3b04f86a4289
#817 (winapp run --aot) squash-merged into main, so main's single squashed
commit conflicts with the individual #817 commits this branch was stacked
on. This branch already contains all of #817's work plus the pack
project-mode feature, so every conflict resolves to this branch's version:
the shared AOT files here are the project-mode superset (PublishAndResolve
split, native-MSIX/signing evaluation, exact-RID handling, WinAppManifestPath
and signing RequestedProperties, and the command-neutral non-executable
error message that pack and run now share). Main carried only the older
run-only versions of those same lines. The @types/node bump and other
main-only changes auto-merged cleanly.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: dcabfb3c-9ea1-44c5-b91b-3b04f86a4289

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Critical path-safety issues and additional documented-behavior and native MSIX validation gaps remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (4)

src/winapp-CLI/WinApp.Cli/Commands/PackageCommand.ProjectMode.cs:569

  • When project signing evaluation returns null, this branch treats the failure as an unsigned policy and proceeds. A project with signing enabled can therefore produce an unsigned MSIX if the preflight evaluation fails transiently or differs from the later publish evaluation; the method's contract says signing should not be silently downgraded. Return an actionable error for props is null (for example, ask for --cert or explicit --no-sign) and keep only SigningEnabled == false on the unsigned path.
            var props = await projectRunService.EvaluateProjectSigningAsync(csproj, buildOptions, cancellationToken);
            if (props is null || props.SigningEnabled == false)
            {
                // Could not evaluate, or signing explicitly disabled → deliver unsigned.
                return (unsigned, null);

src/winapp-CLI/WinApp.Cli/Commands/PackageCommand.ProjectMode.cs:332

  • The new native MSIX branch is only exercised with fakes: the handler tests inject a prebuilt NativeMsixPublishOutcome, while the sample test covers the generic recipe path. No test runs a real WinUI/MSIX-tooling .csproj through dotnet publish, AppxPackageDir, AppxPackageOutput parsing, and final delivery/signing, so the primary documented workflow can regress while the suite remains green. Add one real Windows integration/sample check for this branch (or mark the feature blocked on that validation).
            // MSIX-tooling projects (WinUI / EnableMsixTooling): let the Windows App SDK's own MSIX targets
            // produce the package during publish, then sign and deliver it. The SDK owns file selection and
            // Native AOT native/managed filtering, so winapp never repackages the output. A native project
            // that fails to package is reported as-is — never a silent fall back to generic packaging.
            if (await projectRunService.IsNativeMsixProjectAsync(csproj, buildOptions, cancellationToken))

src/winapp-CLI/WinApp.Cli/Commands/PackageCommand.cs:38

  • The project-mode option set does not define or register --runtime/-r, so winapp package App.csproj --runtime win-arm64 is rejected as an unknown option even though the PR description says project mode accepts the same build options as winapp run, including --runtime/-r. Either implement the documented forwarding/architecture resolution or remove that promise from the user-facing description and generated API surfaces before shipping.
    // Project-mode options (mirrors winapp run; inert unless the input is a .csproj).
    public static Option<string> ConfigurationOption { get; }
    public static Option<string[]> ArchOption { get; }
    public static Option<string?> FrameworkOption { get; }
    public static Option<bool> NoBuildOption { get; }

src/winapp-CLI/WinApp.Cli/Services/ProjectRunService.cs:614

  • With --no-build, RunProjectModeAsync skips IsDefinitivelyUnpackagedAsync, but this predicate still selects the native path solely from MsixPackageSupport/EnableMsixTooling. Thus winapp package App.csproj --no-build -p WindowsPackageType=None can invoke forced PublishAppxPackage=true instead of returning the documented unpackaged-project error. Treat an explicit WindowsPackageType=None as non-native here so the generic path performs the authoritative packaging check.
  • Files reviewed: 37/37 changed files
  • Comments generated: 3
  • Review effort level: Lite (auto)

Note

Copilot is running an experiment and ran this review at Lite.

Comment thread src/winapp-CLI/WinApp.Cli/Services/ProjectRunService.NativeMsix.cs Outdated
Comment thread src/winapp-CLI/WinApp.Cli/Services/ProjectRunService.cs
Comment thread src/winapp-CLI/WinApp.Cli/Commands/PackageCommand.ProjectMode.cs
Resolve the generated npm command conflict from the merged CLI schema, preserving project packaging options and main's explicit empty-string forwarding and UI actions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…MB probes

Three Copilot review findings:

- TryGetLoneRuntimeIdentifier returned the FIRST -p RuntimeIdentifier, but
  MSBuild uses the last assignment. `-p RuntimeIdentifier=win-x64 -p
  RuntimeIdentifier=win-arm64` silently derived x64 and built the wrong
  architecture. Return the last value to match MSBuild.

- ResolveEvaluatedFileIfPresent (WinAppManifestPath / FinalAppxManifestName /
  AppxPackageRecipe) probed a project-evaluated path with File.Exists without
  the network/reparse guard used for PackageCertificateKeyFile, so a project
  with <WinAppManifestPath>\\host\share\...</WinAppManifestPath> could trigger
  an outbound SMB probe on `winapp package`. Treat a network / mapped-drive /
  reparse-redirected path as not-present without probing.

- The native AppxPackageOutput guard's stated "not remote" invariant was
  enforced only lexically (IsNetworkPath + IsUnder), so a reparse point planted
  under the staging directory pointing at a share slipped through. Add
  CrossesReparsePoint so the guarantee actually holds.

Add tests: last-RID-wins, and the network-path guard on the evaluated-file
resolver.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: dcabfb3c-9ea1-44c5-b91b-3b04f86a4289
main added deterministic explicit UI invoke actions (#846), which also
changed the npm command generator (generate-commands.mjs now emits
`!== undefined` guards). That regenerated winapp-commands.ts conflicted with
this branch's project-mode packageApp options. Regenerated winapp-commands.ts,
cli-schema.json, and npm-usage.md from the merged CLI so the wrapper combines
main's generator style and new UI actions with the pack project-mode options.
All other main changes auto-merged. Verified with tsc --noEmit.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: dcabfb3c-9ea1-44c5-b91b-3b04f86a4289
Integrate the author's last RuntimeIdentifier and evaluated-path checks without rewriting either validated main merge. Preserve regenerated npm bindings from the combined schema.

Validation: 821 focused CLI tests passed with one expected skip; canonical x64/ARM64 CLI, npm and four NuGet packaging passed.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

@nmetulev Nikola Metulev (nmetulev) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 AI-generated review (winappcli pr-review skill) — verify before acting.

Decision: changes required

Reviewed commit 394e2fa7ee00b97ab1b4e814daf84afbb14afe9e against main 09a401bc9877625a1e7b2b915fea09833bafbb05. The main merge conflict is resolved, but the five issues below were reproduced using the freshly published CLI. No review fixes were applied.

Must fix

Bundling can silently discard a requested architecture

  • What is wrong: Packages with identical filenames overwrite each other in the bundle staging directory.
  • Show me: On a copy of the WinUI sample, run:
    winapp pack .\winui-app.csproj --arch x64 --arch arm64 -p AppxPackageName=Fixed --no-sign
    The command succeeds, but AppxMetadata/AppxBundleManifest.xml contains only an ARM64 application package named Fixed.msix. Without the filename override, the control bundle contains both x64 and ARM64.
  • Why it matters: A successfully generated distribution artifact silently excludes a requested architecture.
  • Smallest fix: Give each staged package an architecture-qualified filename, as the existing folder-bundle path does, or reject duplicate filenames before copying.
  • Location: MsixService.ProjectBundle.cs:116; BundleService.cs:30-33.

Packaging can ship old files instead of the application just published

  • What is wrong: Generic packaging obtains its output directory through a separate, non-publish evaluation. It can disagree with the actual publish operation.
  • Show me: In an executable project with an authored manifest and no active native MSIX tooling, configure:
    <PublishDir Condition="'$(_IsPublishing)' == 'true'">$(MSBuildProjectDirectory)\shipping\</PublishDir>
    Then run winapp pack .\App.csproj --arch arm64 --no-sign. On a clean project, publishing succeeds but packaging fails looking for the default bin\Release\...\publish\ directory. After priming that default directory with an earlier publication, the command succeeds while packaging the old marker, even though the just-published shipping\ directory contains the updated marker. Expected: package the files from this publication.
  • Why it matters: A successful command can deliver an older application.
  • Smallest fix: Capture output properties from the actual publish invocation, following the existing AOT resolver's approach.
  • Location: ProjectRunService.cs:350-375, 452-460.

A clean checkout can silently bypass project signing

  • What is wrong: Signing policy is selected before restore and never reconsidered. Settings imported from a restored NuGet package are missing during that initial selection.
  • Show me: A local test NuGet package imports the following build properties:
    <PropertyGroup>
      <AppxPackageSigningEnabled>true</AppxPackageSigningEnabled>
      <PackageCertificateKeyFile>$(MSBuildProjectDirectory)\signing-sentinel.pfx</PackageCertificateKeyFile>
    </PropertyGroup>
    The sentinel contains deliberately invalid, harmless text, not a real certificate or key. On a generic packaged project with no restored obj directory, winapp pack .\App.csproj --arch arm64 --output .\result.msix exits successfully and produces an unsigned MSIX with no AppxSignature.p7x. The identical second run correctly attempts signing and fails on the invalid certificate. Expected: the clean run must honor the restored signing requirement and fail too.
  • Why it matters: Clean CI builds can report success without enforcing the project's signing requirement.
  • Smallest fix: Resolve authoritative signing settings after restore, before final delivery. Failed evaluation must not imply "unsigned" unless explicitly requested.
  • Location: PackageCommand.ProjectMode.cs:305-314, 568-602.

Generic publishing can skip a restore it still needs

  • What is wrong: A preliminary build-context restore causes winapp to inject publish --no-restore, although publish-specific dependencies were not restored.
  • Show me: In a two-project solution without an explicit platform, give the generic packaged executable a publish-only dependency:
    <PackageReference Include="Newtonsoft.Json" Version="13.0.3"
                      Condition="'$(_IsPublishing)' == 'true'" />
    Use JsonConvert in the application source. winapp pack .\App.csproj --arch arm64 --no-sign fails compilation because Newtonsoft is missing. The captured invocation shows winapp adding --no-restore. Ordinary dotnet publish .\App.csproj -c Release -r win-arm64 restores the dependency and succeeds.
  • Why it matters: Valid publishing configurations fail despite the user never requesting --no-restore.
  • Smallest fix: Preserve the user's restore preference for publication rather than reusing the build-only "already restored" optimization.
  • Location: ProjectRunService.cs:304-306, 338-341.

Generic NativeAOT packaging still misses toolchain PATH preparation

  • What is wrong: The generic publisher does not supply the environment preparation already used by run --aot and native-MSIX packaging.
  • Show me: With NativeAOT prerequisites installed but the Visual Studio Installer directory absent from PATH, package a generic project configured with <PublishAot>true</PublishAot>. winapp pack .\App.csproj --arch arm64 --no-sign fails with vswhere.exe not recognized / MSB3073, exit 123. The identical command succeeds when only C:\Program Files (x86)\Microsoft Visual Studio\Installer is prepended to the child process's PATH.
  • Why it matters: AOT packaging unnecessarily depends on the caller using a specially configured shell. The already-corrected native-MSIX route does not fix this separate generic route.
  • Smallest fix: Use the existing environment-capable dotnet invocation with BuildAotPublishEnvironment, retaining streamed diagnostics.
  • Location: ProjectRunService.cs:974-1015.

What was exercised

  • Final merged code: 821 focused C# tests passed, one expected long-path skip; 311 npm tests passed.
  • Canonical x64/ARM64 NativeAOT build, npm/four NuGet packages, generated schema/docs, plugin validation, and npm checks passed.
  • Reproduced all five findings with the published ARM64 CLI, version 0.6.3-zt-776-pack-project-mode.156, SHA256 A8F0CAF48459557B16614CB4448D1F3B5988A38E9C348B961E616A6AA854F903, built from the reviewed source tree.
  • Used a real WinUI sample copy for the bundle case and isolated generic managed/AOT projects for the others, with control runs and archive inspection.
  • No application installation, certificate trust changes, or UI launches. The signing test used harmless text, not a real key. Temporary projects were cleaned up.

…ct mode

Addresses review findings on winapp pack project mode:

- Publish evaluation now runs in the publish context (-p:_IsPublishing=true),
  so a project that conditions PublishDir on _IsPublishing is packaged from the
  directory publish actually wrote, not a stale/default one.
- The publish pass no longer reuses the build-context 'already restored'
  optimization, so publish-only (_IsPublishing) dependencies are restored.
- Signing is no longer silently downgraded to unsigned when the project's
  signing configuration cannot be evaluated: the signing probe restores once and
  retries (so package-imported signing props are visible on a clean checkout),
  and an undeterminable configuration is now an actionable error unless the user
  passed --no-sign/--cert/--generate-cert.
- The generic publish pass applies the Native AOT publish environment (VS
  Installer on PATH) so a generic <PublishAot> project packages without a
  specially configured shell, matching native-MSIX packaging and run --aot.
- Bundle staging disambiguates duplicate slice file names so no architecture is
  silently dropped from a bundle.
- Project build-output directories (TargetDir/PublishDir) resolving to a network
  location are rejected before any filesystem probe, matching the keyfile guard.
- Project bundles run the shared BundleValidationService (identity, capabilities,
  dependencies, target device families, applications, architectures) instead of a
  narrower subset, and slice manifest lookup is case-insensitive.
- A known exact-RID ProjectRunException from pre-build probes is surfaced as an
  actionable error instead of the generic 'unexpected error' handler.
- A -p Platform that conflicts with --arch (or any architecture-specific Platform
  in a multi-arch bundle) is rejected rather than mis-packaged.
- Docs note that --manifest/--executable/--skip-pri are rejected for MSIX-tooling
  project mode.

Adds project-mode regression tests for the signing, network-path, and
-p Platform behaviors.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment thread src/winapp-CLI/WinApp.Cli/Services/BundleService.cs Fixed
…aluate

The first attempt restored and re-evaluated signing only when the initial
--getProperty evaluate returned null. But on a clean checkout that evaluate
SUCCEEDS (exit 0) and returns NuGet-imported signing properties empty rather than
failing, so the retry never fired and a project that requires signing could still
be silently delivered unsigned.

Key the restore-and-retry on whether the project has actually been restored (its
evaluated ProjectAssetsFile exists) instead of on a null result, so the clean-
checkout case is covered. Honors --no-restore.

Adds real-service regression tests for the unrestored, restored, and --no-restore
signing paths.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
…ne' may silently drop its earlier arguments'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
var directory = finalPath.Directory!.FullName;
var stem = Path.GetFileNameWithoutExtension(finalPath.Name);
var extension = finalPath.Extension; // includes the leading dot, or empty when there is none
return new FileInfo(Path.Combine(directory, $"{stem}.winapp-{Guid.NewGuid():N}{extension}"));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Project-mode support for winapp package

3 participants