diff --git a/.gitattributes b/.gitattributes index 05a66486..6403f04d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,12 +1,10 @@ # Auto detect text files and perform LF normalization * text=auto -# Generated files - always use LF to match what generate-llm-docs.ps1 writes +# Generated LLM documentation - always use LF to match what generate-llm-docs.ps1 writes docs/cli-schema.json text eol=lf .github/plugin/plugin.json text eol=lf .github/plugin/skills/** text eol=lf -docs/npm-usage.md text eol=lf -src/winapp-npm/src/winapp-commands.ts text eol=lf # Shell scripts should use LF *.sh text eol=lf diff --git a/.github/actions/collect-metrics/action.yml b/.github/actions/collect-metrics/action.yml index 27be437b..f4f4a044 100644 --- a/.github/actions/collect-metrics/action.yml +++ b/.github/actions/collect-metrics/action.yml @@ -18,20 +18,17 @@ runs: $artifacts = "${{ inputs.artifacts-path }}" $sizes = @{} - # Sum all files excluding PDBs to get total CLI distribution size - $x64Files = Get-ChildItem "$artifacts/cli/win-x64" -File -ErrorAction SilentlyContinue | Where-Object { $_.Extension -ne '.pdb' } - $arm64Files = Get-ChildItem "$artifacts/cli/win-arm64" -File -ErrorAction SilentlyContinue | Where-Object { $_.Extension -ne '.pdb' } + $x64Exe = Get-Item "$artifacts/cli/win-x64/winapp.exe" -ErrorAction SilentlyContinue + $arm64Exe = Get-Item "$artifacts/cli/win-arm64/winapp.exe" -ErrorAction SilentlyContinue $npmPkg = Get-ChildItem "$artifacts/*.tgz" -ErrorAction SilentlyContinue | Select-Object -First 1 $msixX64 = Get-ChildItem "$artifacts/msix-packages/*x64*.msix" -ErrorAction SilentlyContinue | Select-Object -First 1 $msixArm64 = Get-ChildItem "$artifacts/msix-packages/*arm64*.msix" -ErrorAction SilentlyContinue | Select-Object -First 1 - $nugetPkg = Get-ChildItem "$artifacts/nuget/*.nupkg" -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($x64Files) { $sizes["cli-win-x64"] = ($x64Files | Measure-Object -Property Length -Sum).Sum } - if ($arm64Files) { $sizes["cli-win-arm64"] = ($arm64Files | Measure-Object -Property Length -Sum).Sum } + if ($x64Exe) { $sizes["cli-win-x64"] = $x64Exe.Length } + if ($arm64Exe) { $sizes["cli-win-arm64"] = $arm64Exe.Length } if ($npmPkg) { $sizes["npm-package"] = $npmPkg.Length } if ($msixX64) { $sizes["msix-x64"] = $msixX64.Length } if ($msixArm64) { $sizes["msix-arm64"] = $msixArm64.Length } - if ($nugetPkg) { $sizes["nuget-package"] = $nugetPkg.Length } $sizes | ConvertTo-Json | Set-Content "$artifacts/binary-sizes.json" diff --git a/.github/actions/report-metrics/action.yml b/.github/actions/report-metrics/action.yml index 89cfb86b..64ed8c5a 100644 --- a/.github/actions/report-metrics/action.yml +++ b/.github/actions/report-metrics/action.yml @@ -19,9 +19,8 @@ runs: uses: actions/cache/restore@v4 with: path: metrics-baseline - key: build-metrics-baseline - restore-keys: | - build-metrics-main- + key: build-metrics-baseline-no-exact-match + restore-keys: build-metrics-main- # On main, save current metrics as the baseline for future PRs - name: Save metrics baseline @@ -34,27 +33,12 @@ runs: Copy-Item "${{ inputs.artifacts-path }}/startup-time.json" "metrics-baseline/startup.json" -ErrorAction SilentlyContinue Copy-Item "${{ inputs.artifacts-path }}/coverage-summary.json" "metrics-baseline/coverage.json" -ErrorAction SilentlyContinue - # GitHub Actions caches are immutable — delete the old entry so we can save an updated one - - name: Delete old metrics baseline cache - if: github.ref == 'refs/heads/main' - shell: pwsh - env: - GH_TOKEN: ${{ inputs.github-token }} - run: | - $result = gh cache delete "build-metrics-baseline" --repo $env:GITHUB_REPOSITORY 2>&1 - if ($LASTEXITCODE -eq 0) { - Write-Host "Old baseline cache deleted" - } else { - Write-Host "No existing baseline cache to delete (expected on first run)" - } - exit 0 - - name: Cache metrics baseline if: github.ref == 'refs/heads/main' uses: actions/cache/save@v4 with: path: metrics-baseline - key: build-metrics-baseline + key: build-metrics-main-${{ github.sha }} # On PRs, compare against baseline and post a comment - name: Post PR metrics comment @@ -108,7 +92,6 @@ runs: 'npm-package': 'npm-package', 'msix-x64': 'msix-packages', 'msix-arm64': 'msix-packages', - 'nuget-package': 'nuget-packages', }; const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; for (const [sizeKey, artifactName] of Object.entries(artifactNameMap)) { @@ -170,7 +153,6 @@ runs: 'npm-package': 'NPM Package', 'msix-x64': 'MSIX (x64)', 'msix-arm64': 'MSIX (ARM64)', - 'nuget-package': 'NuGet Package', }; let body = '## Build Metrics Report\n\n'; diff --git a/.github/plugin/plugin.json b/.github/plugin/plugin.json index 72f7f7e8..9682e329 100644 --- a/.github/plugin/plugin.json +++ b/.github/plugin/plugin.json @@ -1,7 +1,7 @@ { - "name": "winappcli", + "name": "winapp-cli", "description": "Windows app development, packaging, and distribution. Helps with creating Windows installers (MSIX), code signing, certificates, Windows SDK and Windows App SDK setup, package identity for Windows APIs (push notifications, background tasks, share target), appxmanifest authoring, and Microsoft Store distribution. Supports Electron, .NET, C++, Rust, Flutter, and Tauri apps.", - "version": "0.2.2", + "version": "0.2.1", "author": { "name": "Microsoft", "url": "https://github.com/microsoft/WinAppCli" diff --git a/.github/plugin/skills/winapp-cli/frameworks/SKILL.md b/.github/plugin/skills/winapp-cli/frameworks/SKILL.md index 064a3f34..4672f2be 100644 --- a/.github/plugin/skills/winapp-cli/frameworks/SKILL.md +++ b/.github/plugin/skills/winapp-cli/frameworks/SKILL.md @@ -1,7 +1,7 @@ --- name: winapp-frameworks description: Framework-specific Windows development guidance for Electron, .NET (WPF, WinForms), C++, Rust, Flutter, and Tauri. Use when packaging or adding Windows features to an Electron app, .NET desktop app, Flutter app, Tauri app, Rust app, or C++ app. -version: 0.2.2 +version: 0.2.1 --- ## When to use diff --git a/.github/plugin/skills/winapp-cli/identity/SKILL.md b/.github/plugin/skills/winapp-cli/identity/SKILL.md index b94456d5..af0b0b63 100644 --- a/.github/plugin/skills/winapp-cli/identity/SKILL.md +++ b/.github/plugin/skills/winapp-cli/identity/SKILL.md @@ -1,7 +1,7 @@ --- name: winapp-identity description: Enable Windows package identity for desktop apps to access Windows APIs like push notifications, background tasks, share target, and startup tasks. Use when adding Windows notifications, background tasks, or other identity-requiring Windows features to a desktop app. -version: 0.2.2 +version: 0.2.1 --- ## When to use @@ -83,53 +83,6 @@ After running, launch your exe normally — Windows will recognize it as having - If you have both a debug identity and an installed MSIX, they may conflict — use `--keep-identity` carefully - For Electron apps, use `npx winapp node add-electron-debug-identity` instead (handles Electron-specific paths) -## Debugging: `winapp run` vs `create-debug-identity` - -| | `winapp run` | `create-debug-identity` | -|---|---|---| -| **What it registers** | Full loose layout package (entire folder) | Sparse package (single exe) | -| **How the app launches** | Launched by winapp (AUMID activation or execution alias) | You launch the exe yourself (command line, IDE, etc.) | -| **Simulates MSIX install** | Yes — closest to production behavior | No — sparse identity only | -| **Files stay in place** | Copied to an AppX layout directory | Yes — exe stays at its original path | -| **Debugger-friendly** | Attach to PID after launch, or use `--no-launch` then launch via alias | Launch directly from your IDE's debugger — the exe has identity regardless | -| **Console app support** | `--with-alias` keeps stdin/stdout in terminal | Run exe directly in terminal | -| **Best for** | Most frameworks (.NET, C++, Rust, Flutter, Tauri) | Electron, or when you need full IDE debugger control (F5 startup debugging) | - -### When to use which - -**Default to `winapp run`** for most development — it simulates a real MSIX install with full identity, capabilities, and file associations: - -```powershell -winapp run .\build\output # GUI apps -winapp run .\build\output --with-alias # console apps (preserves stdin/stdout) -``` - -**Use `create-debug-identity` when:** -- **Debugging startup code** — your IDE launches + debugs the exe directly; identity is attached from the first instruction -- **Exe is separate from build output** — e.g., Electron where `electron.exe` is in `node_modules/` -- **Testing sparse package behavior** — `AllowExternalContent`, `TrustedLaunch` - -```powershell -winapp create-debug-identity .\bin\Debug\myapp.exe -# Now launch any way you like — F5, terminal, script — the exe has identity -``` - -### Common debugging scenarios - -| Scenario | Command | Notes | -|----------|---------|-------| -| **Just run with identity** | `winapp run .\build\Debug` | Simplest workflow; add `--with-alias` for console apps | -| **Attach debugger to running app** | `winapp run .\build\Debug`, then attach to PID | Misses startup code | -| **Register identity, launch via AUMID** | `winapp run .\build\Debug --no-launch` | Launch with `start shell:AppsFolder\` or the execution alias (not the exe directly) | -| **F5 startup debugging** | `winapp create-debug-identity .\bin\myapp.exe` | IDE controls process from first instruction; best for debugging activation/startup code | -| **Capture debug output** | `winapp run .\build\Debug --debug-output` | Captures `OutputDebugString`; **blocks other debuggers** (one debugger per process) | -| **Run and auto-clean** | `winapp run .\build\Debug --unregister-on-exit` | Unregisters the dev package after the app exits | -| **Clean up stale registration** | `winapp unregister` | Removes dev packages for the current project (auto-detects from manifest) | - -> **Using Visual Studio with a packaging project?** VS already handles identity, AUMID activation, and debugger attachment from F5. These workflows are most useful for VS Code, terminal-based development, and frameworks VS doesn't natively package (Rust, Flutter, Tauri, Electron, C++). - -For full details including IDE setup examples, see the [Debugging Guide](https://github.com/microsoft/WinAppCli/blob/main/docs/debugging.md). - ## Related skills - Need a manifest? See `winapp-manifest` to generate `appxmanifest.xml` - Need a certificate? See `winapp-signing` — a trusted cert is required for identity registration @@ -140,7 +93,7 @@ For full details including IDE setup examples, see the [Debugging Guide](https:/ | Error | Cause | Solution | |-------|-------|----------| | "appxmanifest.xml not found" | No manifest in current directory | Run `winapp init` or `winapp manifest generate`, or pass `--manifest` | -| "Failed to add package identity" | Previous registration stale or cert untrusted | Run `winapp unregister` to remove stale packages, then `winapp cert install ./devcert.pfx` (admin) | +| "Failed to add package identity" | Previous registration stale or cert untrusted | `Get-AppxPackage *yourapp* \| Remove-AppxPackage`, then `winapp cert install ./devcert.pfx` (admin) | | "Access denied" | Cert not trusted or permission issue | Run `winapp cert install ./devcert.pfx` as admin | | APIs still fail after registration | App launched before registration completed | Close app, re-run `create-debug-identity`, then relaunch | diff --git a/.github/plugin/skills/winapp-cli/manifest/SKILL.md b/.github/plugin/skills/winapp-cli/manifest/SKILL.md index c8b70009..ed90aaea 100644 --- a/.github/plugin/skills/winapp-cli/manifest/SKILL.md +++ b/.github/plugin/skills/winapp-cli/manifest/SKILL.md @@ -1,7 +1,7 @@ --- name: winapp-manifest -description: Create and edit Windows app manifest files (appxmanifest.xml) that define app identity, capabilities, and visual assets, or generate new assets from existing images. Use when creating a Windows app manifest for any app type (GUI, console, CLI tool, service), adding Windows capabilities, generating new app icons and assets, or adding execution aliases, file associations, protocol handlers, or other app extensions. -version: 0.2.2 +description: Create and edit Windows app manifest files (appxmanifest.xml) that define app identity, capabilities, and visual assets. Use when creating a Windows app manifest for any app type (GUI, console, CLI tool, service), adding Windows capabilities, updating app icons and assets, or adding execution aliases, file associations, protocol handlers, or other app extensions. +version: 0.2.1 --- ## When to use @@ -13,7 +13,7 @@ Use this skill when: ## Prerequisites - winapp CLI installed -- Optional: a source image (PNG or SVG, at least 400x400 pixels) for custom app icons +- Optional: a source image (PNG, at least 400x400 pixels) for custom app icons ## Key concepts @@ -64,24 +64,11 @@ Output: # Generate all required icon sizes from one source image winapp manifest update-assets ./my-logo.png -# SVG source images produce the best quality at all sizes -winapp manifest update-assets ./my-logo.svg - # Specify manifest location (if not in current directory) winapp manifest update-assets ./my-logo.png --manifest ./path/to/appxmanifest.xml - -# Generate light theme variants from a separate image -winapp manifest update-assets ./my-logo.png --light-image ./my-logo-light.png - -# Use the same image for both (generates all MRT light theme qualifiers) -winapp manifest update-assets ./my-logo.png --light-image ./my-logo.png ``` -The source image should be at least 400x400 pixels (PNG or SVG recommended). The command reads the manifest to determine which asset sizes are needed and generates: -- **5 scale variants** per asset (100%, 125%, 150%, 200%, 400%) -- **14 plated + 14 unplated targetsize variants** for the app icon (44x44) -- **app.ico** — multi-resolution ICO file for shell integration. If an existing `.ico` file is present in the assets directory, it is replaced in-place (preserving the original filename) -- With `--light-image`: light theme variants using the correct MRT qualifiers per asset type +The source image should be at least 400x400 pixels (PNG recommended). The command reads the manifest to determine which asset sizes are needed and generates them all. ### Add an execution alias @@ -164,7 +151,7 @@ Key fields to edit: - The `sparse` template adds `uap10:AllowExternalContent="true"` for apps that need identity but run outside the MSIX container - You can manually edit `appxmanifest.xml` after generation — it's a standard XML file - Image assets must match the paths referenced in the manifest — `update-assets` handles this automatically -- For logos, transparent PNGs or SVGs work best. SVG source images are rendered as vectors directly at each target size, producing pixel-perfect results. Use a square image for best results across all sizes. +- For logos, transparent PNGs work best. Use a square image for best results across all sizes. - **`$targetnametoken$` placeholder:** When `winapp manifest generate` creates `appxmanifest.xml`, it sets `Application.Executable` to `$targetnametoken$.exe` by default. This is a valid placeholder that gets automatically resolved by `winapp package --executable ` at packaging time — you rarely need to override it during manifest generation. If `--executable` is provided to `winapp manifest generate`, winapp reads `FileVersionInfo` from the actual exe to auto-fill package name, description, publisher, and extract an icon, so the exe must already exist on disk. ## Related skills @@ -176,7 +163,7 @@ Key fields to edit: | Error | Cause | Solution | |-------|-------|----------| | "Manifest already exists" | `appxmanifest.xml` present | Use `--if-exists overwrite` to replace, or edit existing file directly | -| "Invalid source image" | Image too small or wrong format | Use PNG or SVG, at least 400x400 pixels | +| "Invalid source image" | Image too small or wrong format | Use PNG, at least 400x400 pixels | | "Publisher mismatch" during packaging | Manifest publisher ≠ cert publisher | Edit `Identity.Publisher` in manifest, or regenerate cert with `--manifest` | @@ -213,13 +200,12 @@ Generate new assets for images referenced in an appxmanifest.xml from a single s | Argument | Required | Description | |----------|----------|-------------| -| `` | Yes | Path to source image file (SVG, PNG, ICO, JPG, BMP, GIF) | +| `` | Yes | Path to source image file | #### Options | Option | Description | Default | |--------|-------------|---------| -| `--light-image` | Path to source image for light theme variants (SVG, PNG, ICO, JPG, BMP, GIF) | (none) | | `--manifest` | Path to AppxManifest.xml or Package.appxmanifest file (default: search current directory) | (none) | ### `winapp manifest add-alias` diff --git a/.github/plugin/skills/winapp-cli/package/SKILL.md b/.github/plugin/skills/winapp-cli/package/SKILL.md index 89080b8b..a8f70dca 100644 --- a/.github/plugin/skills/winapp-cli/package/SKILL.md +++ b/.github/plugin/skills/winapp-cli/package/SKILL.md @@ -1,7 +1,7 @@ --- name: winapp-package description: Package a Windows app as an MSIX installer for distribution or testing. Use when creating a Windows installer, packaging an Electron/Flutter/.NET/Rust/C++/Tauri app for Windows, building an MSIX, distributing a desktop app, packaging a console app or CLI tool, or adding MSIX packaging to a build script or CI/CD pipeline. -version: 0.2.2 +version: 0.2.1 --- ## When to use diff --git a/.github/plugin/skills/winapp-cli/setup/SKILL.md b/.github/plugin/skills/winapp-cli/setup/SKILL.md index 54c476ab..0a3e305b 100644 --- a/.github/plugin/skills/winapp-cli/setup/SKILL.md +++ b/.github/plugin/skills/winapp-cli/setup/SKILL.md @@ -1,7 +1,7 @@ --- name: winapp-setup description: Set up a Windows app project for MSIX packaging, Windows SDK access, or Windows API usage. Use when adding Windows support to an Electron, .NET, C++, Rust, Flutter, or Tauri project, or restoring SDK packages after cloning. -version: 0.2.2 +version: 0.2.1 --- ## When to use @@ -24,8 +24,6 @@ npm install --save-dev @microsoft/winappcli You need an **existing app project** — `winapp init` does **not** create new projects, it adds Windows platform files to your existing codebase. -> **Already have a `Package.appxmanifest`?** .NET projects that already have a packaging manifest (e.g., WinUI 3 apps or projects with an existing MSIX packaging setup) likely **don't need `winapp init`**. Ensure your `.csproj` references the `Microsoft.WindowsAppSDK` NuGet package and has the right properties for packaged builds (e.g., `MSIX`). WinUI 3 apps created from Visual Studio templates are typically already fully configured — you can go straight to building and using `winapp run` or `winapp package`. - ## Key concepts **`appxmanifest.xml`** is the most important file winapp creates — it declares your app's identity, capabilities, and visual assets. Most winapp commands require it (`package`, `run`, `cert generate --manifest`). @@ -94,10 +92,6 @@ winapp run ./dist --manifest ./out/AppxManifest.xml --args "--my-flag value" # Register identity without launching (useful for attaching a debugger manually) winapp run ./bin/Debug --no-launch - -# Launch and capture OutputDebugString messages and first-chance exceptions -# Note: prevents other debuggers (VS, VS Code) from attaching — use --no-launch if you need those instead -winapp run ./bin/Debug --debug-output ``` Use `winapp run` during iterative development — it creates a loose layout package, registers a debug identity, and launches the app in one step. For identity-only registration without loose layout, use `winapp create-debug-identity` instead. @@ -121,7 +115,6 @@ For console apps, add `--with-alias` to preserve stdin/stdout in the current ter For full debugging scenarios and IDE setup, see the [Debugging Guide](https://github.com/microsoft/WinAppCli/blob/main/docs/debugging.md). - ## Recommended workflow 1. **Initialize** — `winapp init --use-defaults` in your existing project @@ -216,6 +209,7 @@ Creates packaged layout, registers the Application, and launches the packaged ap | Option | Description | Default | |--------|-------------|---------| | `--args` | Command-line arguments to pass to the application | (none) | +| `--clean` | Remove the existing package's application data (LocalState, settings, etc.) before re-deploying. By default, application data is preserved across re-deployments. | (none) | | `--debug-output` | Capture OutputDebugString messages and first-chance exceptions from the launched application. Only one debugger can attach to a process at a time, so other debuggers (Visual Studio, VS Code) cannot be used simultaneously. Use --no-launch instead if you need to attach a different debugger. Cannot be combined with --no-launch or --json. | (none) | | `--json` | Format output as JSON | (none) | | `--manifest` | Path to the appxmanifest.xml (default: auto-detect from input folder or current directory) | (none) | diff --git a/.github/plugin/skills/winapp-cli/signing/SKILL.md b/.github/plugin/skills/winapp-cli/signing/SKILL.md index e51330e3..c3e7f03a 100644 --- a/.github/plugin/skills/winapp-cli/signing/SKILL.md +++ b/.github/plugin/skills/winapp-cli/signing/SKILL.md @@ -1,7 +1,7 @@ --- name: winapp-signing description: Create and manage code signing certificates for Windows apps and MSIX packages. Use when generating a certificate, signing a Windows app or installer, or fixing certificate trust issues. -version: 0.2.2 +version: 0.2.1 --- ## When to use diff --git a/.github/plugin/skills/winapp-cli/troubleshoot/SKILL.md b/.github/plugin/skills/winapp-cli/troubleshoot/SKILL.md index e379da42..74ca21a7 100644 --- a/.github/plugin/skills/winapp-cli/troubleshoot/SKILL.md +++ b/.github/plugin/skills/winapp-cli/troubleshoot/SKILL.md @@ -1,7 +1,7 @@ --- name: winapp-troubleshoot description: Diagnose and fix common Windows app packaging, signing, identity, and SDK errors. Use when encountering errors with MSIX packaging, certificate signing, Windows SDK setup, or app installation. -version: 0.2.2 +version: 0.2.1 --- ## When to use @@ -39,7 +39,7 @@ Does the project have an appxmanifest.xml? │ └─ winapp update ├─ Need a dev certificate? │ └─ winapp cert generate (then winapp cert install for trust) - ├─ Need package identity for debugging? (see [Debugging Guide](https://github.com/microsoft/WinAppCli/blob/main/docs/debugging.md)) + ├─ Need package identity for debugging? │ ├─ Exe is in your build output folder? (most frameworks) │ │ └─ winapp run │ └─ Exe is separate from app code? (Electron, sparse testing) @@ -65,22 +65,6 @@ Does the project have an appxmanifest.xml? - Projects with NuGet package references (e.g., `.csproj` referencing `Microsoft.Windows.SDK.BuildTools`) can use winapp commands without `winapp.yaml` - For Electron projects, use the npm package (`npm install --save-dev @microsoft/winappcli`) which includes Node.js-specific commands under `npx winapp node` -## Debugging approach quick reference - -| Goal | Command | Key detail | -|------|---------|------------| -| Run with identity (most common) | `winapp run .\build\Debug` | Registers loose layout + launches; add `--with-alias` for console apps | -| Attach debugger to running app | `winapp run .\build\Debug` → attach to PID | Misses startup code | -| Register identity, launch manually | `winapp run .\build\Debug --no-launch` | Launch via `start shell:AppsFolder\` or execution alias — **not** the exe directly | -| F5 startup debugging (IDE launches exe) | `winapp create-debug-identity .\bin\myapp.exe` | Exe has identity regardless of how it's launched; best for debugging activation/startup code | -| Capture OutputDebugString | `winapp run .\build\Debug --debug-output` | **Blocks other debuggers** — use `--no-launch` if you need VS Code/WinDbg | -| Run and auto-clean | `winapp run .\build\Debug --unregister-on-exit` | Unregisters the dev package after the app exits | -| Clean up stale registration | `winapp unregister` | Removes dev-mode packages for the current project | - -> **Visual Studio users:** If you have a packaging project, VS already handles identity and debugging from F5 — you likely don't need winapp for debugging. These workflows are for VS Code, terminal, and frameworks VS doesn't natively package. - -For full details, see the [Debugging Guide](https://github.com/microsoft/WinAppCli/blob/main/docs/debugging.md). - ## Prerequisites & state matrix | Command | Requires | Creates/Modifies | @@ -94,7 +78,6 @@ For full details, see the [Debugging Guide](https://github.com/microsoft/WinAppC | `cert install` | Certificate file + admin | Machine certificate store | | `create-debug-identity` | `appxmanifest.xml` + exe + trusted cert | Registers sparse package with Windows | | `run` | Build output folder + `appxmanifest.xml` | Registers loose layout package, launches app | -| `unregister` | `appxmanifest.xml` (auto-detect or `--manifest`) | Removes dev-mode package registrations | | `package` | Build output + `appxmanifest.xml` | `.msix` file | | `sign` | File + certificate | Signed file (in-place) | | `create-external-catalog` | Directory with executables | `CodeIntegrityExternal.cat` | diff --git a/.github/workflows/build-package.yml b/.github/workflows/build-package.yml index e369d1e4..84a1ad6a 100644 --- a/.github/workflows/build-package.yml +++ b/.github/workflows/build-package.yml @@ -10,7 +10,6 @@ on: permissions: contents: write # Required for creating releases pull-requests: write # Required for binary size report comments - actions: write # Required for deleting old metrics baseline cache jobs: # Fast docs validation - runs in parallel with the main build to fail PRs early on doc drift @@ -180,11 +179,6 @@ jobs: steps: - name: Checkout uses: actions/checkout@v5 - - - name: Enable Windows Developer Mode - run: | - # Registry key to enable Developer Mode - reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /v "AllowDevelopmentWithoutDevLicense" /d 1 /f - name: Setup Node.js uses: actions/setup-node@v5 diff --git a/.pipelines/templates/build.yaml b/.pipelines/templates/build.yaml index d0c2ba09..906462bd 100644 --- a/.pipelines/templates/build.yaml +++ b/.pipelines/templates/build.yaml @@ -15,7 +15,7 @@ steps: inputs: pwsh: true filePath: $(System.DefaultWorkingDirectory)\scripts\build-cli.ps1 - arguments: "-Stable:$${{ parameters.stable }} -SkipMsix" + arguments: "-Stable:$${{ parameters.stable }} -SkipNpm -SkipMsix" - ${{ if eq(parameters['DoEsrp'], 'true') }}: - task: EsrpCodeSigning@5 displayName: Code Sign ESRP - CLI diff --git a/AGENTS.md b/AGENTS.md index 8f2b0a89..269b536a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -89,31 +89,3 @@ The following files are auto-generated from `cli-schema.json` via `scripts/gener **To edit skill content**, modify the hand-written templates in `docs/fragments/skills/winapp-cli/`. Each template file (e.g., `package.md`, `manifest.md`) contains the workflow docs, examples, and troubleshooting content. The auto-generation script appends command reference tables from the CLI schema. Running `scripts/build-cli.ps1` triggers regeneration automatically. Skill descriptions (used for Copilot skill matching) are defined in the `$SkillDescriptions` hashtable in `scripts/generate-llm-docs.ps1`. - -## C# service architecture guidelines - -### File size limits -- **Target**: ≤500 lines per file -- **Soft limit**: ~800 lines — if approaching this, look for extraction opportunities -- **Hard limit**: Do not let any single file exceed ~1,000 lines. Split into partial classes or extract services. - -### Service patterns -Use the appropriate pattern for new code: - -| Pattern | When to use | Example | -|---------|------------|---------| -| **Interface + DI service** | Stateful logic, needs dependencies | `IPriService` / `PriService` | -| **Static helper** | Pure functions, no DI needed | `PeHelper`, `MrtAssetHelper` | -| **Data document** | Wraps a file/data format with typed access | `AppxManifestDocument` | -| **Partial class** | Splitting a large service with tight internal coupling | `MsixService.Runtime.cs` | - -### Separation of concerns -- One responsibility per service/helper file -- Extract shared logic into helpers rather than duplicating across services -- If a method group only uses 1-2 of a service's 10+ dependencies, it's a candidate for extraction - -### XML handling -- **Use `XDocument` / `XElement`** (System.Xml.Linq) for structured XML manipulation — never regex -- Regex is acceptable ONLY for: pre-parse placeholder replacement (`$targetnametoken$`), raw text scanning before XML is valid -- Use `AppxManifestDocument` for AppxManifest.xml operations — it provides typed access and namespace-aware queries -- When adding new manifest manipulation, add methods to `AppxManifestDocument` rather than writing regex in consuming code diff --git a/README.md b/README.md index f4675a80..97b7d4e6 100644 --- a/README.md +++ b/README.md @@ -168,14 +168,11 @@ npx winapp --help **App Identity & Debugging:** -- [`pack`](./docs/usage.md#pack) - Create MSIX packages from directories +- [`package`](./docs/usage.md#package) - Create MSIX packages from directories - [`run`](./docs/usage.md#run) - Run app as a packaged application for debugging (loose layout registration) - [`create-debug-identity`](./docs/usage.md#create-debug-identity) - Add sparse package identity to an existing exe -- [`unregister`](./docs/usage.md#unregister) - Remove sideloaded dev packages registered by `run` or `create-debug-identity` - [`manifest`](./docs/usage.md#manifest) - Generate and manage AppxManifest.xml files -See also: [Debugging Guide](./docs/debugging.md) — choosing between `winapp run` and `create-debug-identity`, IDE setup, and debugging scenarios. - **Certificates & Signing:** - [`cert`](./docs/usage.md#cert) - Generate and install development certificates diff --git a/docs/cli-schema.json b/docs/cli-schema.json index f19730b4..a3ce54b7 100644 --- a/docs/cli-schema.json +++ b/docs/cli-schema.json @@ -1,6 +1,6 @@ { "name": "winapp", - "version": "0.2.2", + "version": "0.2.1", "schemaVersion": "1.0", "description": "CLI for Windows app development, including package identity, packaging, managing appxmanifest.xml, test certificates, Windows (App) SDK projections, and more. For use with any app framework targeting Windows", "hidden": false, @@ -998,7 +998,7 @@ "hidden": false, "arguments": { "image-path": { - "description": "Path to source image file (SVG, PNG, ICO, JPG, BMP, GIF)", + "description": "Path to source image file", "order": 0, "hidden": false, "valueType": "System.IO.FileInfo", @@ -1010,18 +1010,6 @@ } }, "options": { - "--light-image": { - "description": "Path to source image for light theme variants (SVG, PNG, ICO, JPG, BMP, GIF)", - "hidden": false, - "valueType": "System.IO.FileInfo", - "hasDefaultValue": false, - "arity": { - "minimum": 1, - "maximum": 1 - }, - "required": false, - "recursive": false - }, "--manifest": { "description": "Path to AppxManifest.xml or Package.appxmanifest file (default: search current directory)", "hidden": false, @@ -1356,6 +1344,19 @@ "required": false, "recursive": false }, + "--clean": { + "description": "Remove the existing package's application data (LocalState, settings, etc.) before re-deploying. By default, application data is preserved across re-deployments.", + "hidden": false, + "valueType": "System.Boolean", + "hasDefaultValue": true, + "defaultValue": false, + "arity": { + "minimum": 0, + "maximum": 1 + }, + "required": false, + "recursive": false + }, "--debug-output": { "description": "Capture OutputDebugString messages and first-chance exceptions from the launched application. Only one debugger can attach to a process at a time, so other debuggers (Visual Studio, VS Code) cannot be used simultaneously. Use --no-launch instead if you need to attach a different debugger. Cannot be combined with --no-launch or --json.", "hidden": false, diff --git a/docs/debugging.md b/docs/debugging.md deleted file mode 100644 index 1c0cffbc..00000000 --- a/docs/debugging.md +++ /dev/null @@ -1,141 +0,0 @@ -# Debugging with Package Identity - -Many Windows APIs (push notifications, background tasks, share target, startup tasks, Windows AI APIs) require your app to have **package identity**. During development, you don't want to build a full MSIX installer every time you test — winapp provides two commands to give your app identity on the fly. - -> **Using Visual Studio with a packaging project?** If you are already using Visual Studio for your packaged project, you likely don't need winapp for debugging. Visual Studio already handles package registration, identity, AUMID activation, debugger attachment, and activation-code debugging — all from F5. It also offers **Debug → Other Debug Targets → Debug Installed App Package** for advanced scenarios. The workflows below are most useful for **VS Code users, terminal-based workflows, and frameworks that VS doesn't natively package** (Rust, Flutter, Tauri, Electron, plain C++). - -## Two approaches: `winapp run` vs `create-debug-identity` - -| | `winapp run` | `create-debug-identity` | -|---|---|---| -| **What it registers** | Full loose layout package (entire folder) | Sparse package (single exe) | -| **How the app launches** | Launched by winapp (AUMID activation or execution alias) | You launch the exe yourself (command line, IDE, etc.) | -| **Simulates MSIX install** | Yes — closest to production behavior | No — sparse identity only | -| **Files stay in place** | Copied to an AppX layout directory | Yes — exe stays at its original path | -| **Identity scope** | Entire folder contents (exe, DLLs, assets) | Single executable | -| **Debugger-friendly** | Attach to PID after launch, or use `--no-launch` then launch via alias | Launch directly from your IDE's debugger — the exe has identity regardless | -| **Console app support** | `--with-alias` keeps stdin/stdout in terminal | Run exe directly in terminal | -| **Best for** | Most frameworks (.NET, C++, Rust, Flutter, Tauri) | Electron, or when you need full IDE debugger control (F5) | - -## When to use which - -### Default: `winapp run` - -Use `winapp run` for most development workflows. It simulates a real MSIX install — your app gets the same identity, capabilities, and file associations it would have in production. - -```powershell -# Build your app, then: -winapp run .\build\output -``` - -### Use `create-debug-identity` when: - -- **Your exe is separate from your build output** — e.g., Electron apps where `electron.exe` lives in `node_modules/` -- **You need to debug startup code** and can't attach a debugger fast enough after AUMID launch -- **With some debuggers where you can't launch with AUMID** and need identity on the launched process — `create-debug-identity` registers the exe so it has identity no matter how it's started -- **You're testing sparse package behavior** specifically (AllowExternalContent, TrustedLaunch) - -```powershell -# Register identity for an exe, then launch it however you want: -winapp create-debug-identity .\bin\Debug\myapp.exe -.\bin\Debug\myapp.exe # or F5 in your IDE -``` - -## Debugging scenarios - -### Scenario A: Just run with identity - -The simplest workflow — build, run with identity, done. - -```powershell -winapp run .\build\Debug -``` - -Winapp registers the folder as a loose layout package and launches the app. Identity-requiring APIs work immediately. This covers the majority of development and testing scenarios. - -For **console apps** that need stdin/stdout in the current terminal, add `--with-alias`: - -```powershell -winapp run .\build\Debug --with-alias -``` - -### Scenario B: Attach a debugger to a running app - -Launch with `winapp run`, note the PID, then attach your IDE's debugger. - -```powershell -winapp run .\build\Debug -# Output: Process ID: 12345 -``` - -Then in your IDE: -- **VS Code**: Run and Debug → select "Attach" configuration (see [IDE setup](#ide-setup) below) -- **WinDbg**: `windbg -p 12345` - -> **Limitation:** You'll miss any code that runs before you attach. For startup debugging, use Scenario D (`create-debug-identity`). - -### Scenario C: Register identity, then launch via AUMID or alias from your IDE - -Use `--no-launch` to register the package, then launch the app through its AUMID (reported by `run`) or **execution alias** from your IDE. - -**Step 1:** Register the package without launching: - -```powershell -winapp run .\build\Debug --no-launch -``` - -**Step 2:** Configure your IDE to launch via the AUMID or the **execution alias** (not the exe directly). -* Launching with AUMID: Use the command `start shell:AppsFolder\`. `winapp run` outputs the AUMID when the app is registered. -* Launching with the alias: The alias must be defined in the appxmanifest.xml (or Package.appxmanifest). - -> **Important:** Simply launching the exe in the build folder will **not** give it identity. The app must be started via AUMID activation or its execution alias. This is how loose layout packages work - identity is tied to the activation path, not the exe file. - -### Scenario D: Launch from your IDE with identity (startup debugging) - -This is the best approach for **debugging startup code with full IDE control** - your IDE's debugger controls the process from the very first instruction, and the exe has identity no matter how it's launched. - -```powershell -winapp create-debug-identity .\build\Debug\myapp.exe -``` - -Now launch the exe any way you like — from the terminal, from VS Code's F5, from a script. The exe has identity because Windows registered a **sparse package** pointing directly at it. - -> **How it differs from `winapp run`:** With `create-debug-identity`, identity is tied to the exe itself (via `Add-AppxPackage -ExternalLocation`). With `winapp run`, identity is tied to the loose layout package — the app must be launched through AUMID or an alias. This makes `create-debug-identity` the better choice when you need your IDE to launch and debug the exe directly. - -> This is also the best approach for **Electron apps** where the exe path differs from your source directory. - -### Scenario E: Capture debug output - -Capture `OutputDebugString` messages and first-chance exceptions inline: - -```powershell -winapp run .\build\Debug --debug-output -``` - -> **Important:** This attaches winapp as the debugger. Windows only allows one debugger per process, so you **cannot** also attach Visual Studio, VS Code, or WinDbg. - -## IDE setup - -### VS Code - -**Attach to running process** — add to `.vscode/launch.json`: - -```json -{ - "name": "Attach to Process", - "type": "coreclr", - "request": "attach", - "processId": "${command:pickProcess}" -} -``` - -For C++/Rust, use `"type": "cppvsdbg"` (MSVC) or `"type": "lldb"` (LLDB): - -```json -{ - "name": "Attach (C++)", - "type": "cppvsdbg", - "request": "attach", - "processId": "${command:pickProcess}" -} -``` diff --git a/docs/dotnet-run-support.md b/docs/dotnet-run-support.md index 2188c4af..18e5ff4b 100644 --- a/docs/dotnet-run-support.md +++ b/docs/dotnet-run-support.md @@ -96,7 +96,6 @@ samples/ | `WinAppCliPath` | (in package) | Path to the winapp.exe CLI | | `WinAppRunUseExecutionAlias` | `false` | Launch via execution alias instead of AUMID. Keeps console I/O in the current terminal. Requires `uap5:ExecutionAlias` in the manifest. Cannot be combined with `WinAppRunNoLaunch`. | | `WinAppRunNoLaunch` | `false` | Only register package identity without launching the app. Cannot be combined with `WinAppRunUseExecutionAlias`. | -| `WinAppRunDebugOutput` | `false` | Attach as a debugger to capture `OutputDebugString` messages and first-chance exceptions. Only one debugger can attach at a time, so Visual Studio or VS Code cannot debug simultaneously. Use `WinAppRunNoLaunch` instead to attach a different debugger. Cannot be combined with `WinAppRunNoLaunch`. | ### Targets (Microsoft.Windows.SDK.BuildTools.WinApp.targets) @@ -201,7 +200,7 @@ Capture OutputDebugString messages and first-chance exceptions: ``` -Production Blockers +## Outstanding Production Blockers ### 1. CLI AOT Build Issues (BLOCKING) diff --git a/docs/fragments/skills/winapp-cli/identity.md b/docs/fragments/skills/winapp-cli/identity.md index 1add2bd1..f8b1baad 100644 --- a/docs/fragments/skills/winapp-cli/identity.md +++ b/docs/fragments/skills/winapp-cli/identity.md @@ -78,53 +78,6 @@ After running, launch your exe normally — Windows will recognize it as having - If you have both a debug identity and an installed MSIX, they may conflict — use `--keep-identity` carefully - For Electron apps, use `npx winapp node add-electron-debug-identity` instead (handles Electron-specific paths) -## Debugging: `winapp run` vs `create-debug-identity` - -| | `winapp run` | `create-debug-identity` | -|---|---|---| -| **What it registers** | Full loose layout package (entire folder) | Sparse package (single exe) | -| **How the app launches** | Launched by winapp (AUMID activation or execution alias) | You launch the exe yourself (command line, IDE, etc.) | -| **Simulates MSIX install** | Yes — closest to production behavior | No — sparse identity only | -| **Files stay in place** | Copied to an AppX layout directory | Yes — exe stays at its original path | -| **Debugger-friendly** | Attach to PID after launch, or use `--no-launch` then launch via alias | Launch directly from your IDE's debugger — the exe has identity regardless | -| **Console app support** | `--with-alias` keeps stdin/stdout in terminal | Run exe directly in terminal | -| **Best for** | Most frameworks (.NET, C++, Rust, Flutter, Tauri) | Electron, or when you need full IDE debugger control (F5 startup debugging) | - -### When to use which - -**Default to `winapp run`** for most development — it simulates a real MSIX install with full identity, capabilities, and file associations: - -```powershell -winapp run .\build\output # GUI apps -winapp run .\build\output --with-alias # console apps (preserves stdin/stdout) -``` - -**Use `create-debug-identity` when:** -- **Debugging startup code** — your IDE launches + debugs the exe directly; identity is attached from the first instruction -- **Exe is separate from build output** — e.g., Electron where `electron.exe` is in `node_modules/` -- **Testing sparse package behavior** — `AllowExternalContent`, `TrustedLaunch` - -```powershell -winapp create-debug-identity .\bin\Debug\myapp.exe -# Now launch any way you like — F5, terminal, script — the exe has identity -``` - -### Common debugging scenarios - -| Scenario | Command | Notes | -|----------|---------|-------| -| **Just run with identity** | `winapp run .\build\Debug` | Simplest workflow; add `--with-alias` for console apps | -| **Attach debugger to running app** | `winapp run .\build\Debug`, then attach to PID | Misses startup code | -| **Register identity, launch via AUMID** | `winapp run .\build\Debug --no-launch` | Launch with `start shell:AppsFolder\` or the execution alias (not the exe directly) | -| **F5 startup debugging** | `winapp create-debug-identity .\bin\myapp.exe` | IDE controls process from first instruction; best for debugging activation/startup code | -| **Capture debug output** | `winapp run .\build\Debug --debug-output` | Captures `OutputDebugString`; **blocks other debuggers** (one debugger per process) | -| **Run and auto-clean** | `winapp run .\build\Debug --unregister-on-exit` | Unregisters the dev package after the app exits | -| **Clean up stale registration** | `winapp unregister` | Removes dev packages for the current project (auto-detects from manifest) | - -> **Using Visual Studio with a packaging project?** VS already handles identity, AUMID activation, and debugger attachment from F5. These workflows are most useful for VS Code, terminal-based development, and frameworks VS doesn't natively package (Rust, Flutter, Tauri, Electron, C++). - -For full details including IDE setup examples, see the [Debugging Guide](https://github.com/microsoft/WinAppCli/blob/main/docs/debugging.md). - ## Related skills - Need a manifest? See `winapp-manifest` to generate `appxmanifest.xml` - Need a certificate? See `winapp-signing` — a trusted cert is required for identity registration @@ -135,6 +88,6 @@ For full details including IDE setup examples, see the [Debugging Guide](https:/ | Error | Cause | Solution | |-------|-------|----------| | "appxmanifest.xml not found" | No manifest in current directory | Run `winapp init` or `winapp manifest generate`, or pass `--manifest` | -| "Failed to add package identity" | Previous registration stale or cert untrusted | Run `winapp unregister` to remove stale packages, then `winapp cert install ./devcert.pfx` (admin) | +| "Failed to add package identity" | Previous registration stale or cert untrusted | `Get-AppxPackage *yourapp* \| Remove-AppxPackage`, then `winapp cert install ./devcert.pfx` (admin) | | "Access denied" | Cert not trusted or permission issue | Run `winapp cert install ./devcert.pfx` as admin | | APIs still fail after registration | App launched before registration completed | Close app, re-run `create-debug-identity`, then relaunch | diff --git a/docs/fragments/skills/winapp-cli/manifest.md b/docs/fragments/skills/winapp-cli/manifest.md index dd74abb7..6f6a858a 100644 --- a/docs/fragments/skills/winapp-cli/manifest.md +++ b/docs/fragments/skills/winapp-cli/manifest.md @@ -8,7 +8,7 @@ Use this skill when: ## Prerequisites - winapp CLI installed -- Optional: a source image (PNG or SVG, at least 400x400 pixels) for custom app icons +- Optional: a source image (PNG, at least 400x400 pixels) for custom app icons ## Key concepts @@ -59,24 +59,11 @@ Output: # Generate all required icon sizes from one source image winapp manifest update-assets ./my-logo.png -# SVG source images produce the best quality at all sizes -winapp manifest update-assets ./my-logo.svg - # Specify manifest location (if not in current directory) winapp manifest update-assets ./my-logo.png --manifest ./path/to/appxmanifest.xml - -# Generate light theme variants from a separate image -winapp manifest update-assets ./my-logo.png --light-image ./my-logo-light.png - -# Use the same image for both (generates all MRT light theme qualifiers) -winapp manifest update-assets ./my-logo.png --light-image ./my-logo.png ``` -The source image should be at least 400x400 pixels (PNG or SVG recommended). The command reads the manifest to determine which asset sizes are needed and generates: -- **5 scale variants** per asset (100%, 125%, 150%, 200%, 400%) -- **14 plated + 14 unplated targetsize variants** for the app icon (44x44) -- **app.ico** — multi-resolution ICO file for shell integration. If an existing `.ico` file is present in the assets directory, it is replaced in-place (preserving the original filename) -- With `--light-image`: light theme variants using the correct MRT qualifiers per asset type +The source image should be at least 400x400 pixels (PNG recommended). The command reads the manifest to determine which asset sizes are needed and generates them all. ### Add an execution alias @@ -159,7 +146,7 @@ Key fields to edit: - The `sparse` template adds `uap10:AllowExternalContent="true"` for apps that need identity but run outside the MSIX container - You can manually edit `appxmanifest.xml` after generation — it's a standard XML file - Image assets must match the paths referenced in the manifest — `update-assets` handles this automatically -- For logos, transparent PNGs or SVGs work best. SVG source images are rendered as vectors directly at each target size, producing pixel-perfect results. Use a square image for best results across all sizes. +- For logos, transparent PNGs work best. Use a square image for best results across all sizes. - **`$targetnametoken$` placeholder:** When `winapp manifest generate` creates `appxmanifest.xml`, it sets `Application.Executable` to `$targetnametoken$.exe` by default. This is a valid placeholder that gets automatically resolved by `winapp package --executable ` at packaging time — you rarely need to override it during manifest generation. If `--executable` is provided to `winapp manifest generate`, winapp reads `FileVersionInfo` from the actual exe to auto-fill package name, description, publisher, and extract an icon, so the exe must already exist on disk. ## Related skills @@ -171,5 +158,5 @@ Key fields to edit: | Error | Cause | Solution | |-------|-------|----------| | "Manifest already exists" | `appxmanifest.xml` present | Use `--if-exists overwrite` to replace, or edit existing file directly | -| "Invalid source image" | Image too small or wrong format | Use PNG or SVG, at least 400x400 pixels | +| "Invalid source image" | Image too small or wrong format | Use PNG, at least 400x400 pixels | | "Publisher mismatch" during packaging | Manifest publisher ≠ cert publisher | Edit `Identity.Publisher` in manifest, or regenerate cert with `--manifest` | diff --git a/docs/fragments/skills/winapp-cli/setup.md b/docs/fragments/skills/winapp-cli/setup.md index 1ffa9ca1..ea7b566c 100644 --- a/docs/fragments/skills/winapp-cli/setup.md +++ b/docs/fragments/skills/winapp-cli/setup.md @@ -19,8 +19,6 @@ npm install --save-dev @microsoft/winappcli You need an **existing app project** — `winapp init` does **not** create new projects, it adds Windows platform files to your existing codebase. -> **Already have a `Package.appxmanifest`?** .NET projects that already have a packaging manifest (e.g., WinUI 3 apps or projects with an existing MSIX packaging setup) likely **don't need `winapp init`**. Ensure your `.csproj` references the `Microsoft.WindowsAppSDK` NuGet package and has the right properties for packaged builds (e.g., `MSIX`). WinUI 3 apps created from Visual Studio templates are typically already fully configured — you can go straight to building and using `winapp run` or `winapp package`. - ## Key concepts **`appxmanifest.xml`** is the most important file winapp creates — it declares your app's identity, capabilities, and visual assets. Most winapp commands require it (`package`, `run`, `cert generate --manifest`). @@ -89,10 +87,6 @@ winapp run ./dist --manifest ./out/AppxManifest.xml --args "--my-flag value" # Register identity without launching (useful for attaching a debugger manually) winapp run ./bin/Debug --no-launch - -# Launch and capture OutputDebugString messages and first-chance exceptions -# Note: prevents other debuggers (VS, VS Code) from attaching — use --no-launch if you need those instead -winapp run ./bin/Debug --debug-output ``` Use `winapp run` during iterative development — it creates a loose layout package, registers a debug identity, and launches the app in one step. For identity-only registration without loose layout, use `winapp create-debug-identity` instead. @@ -116,7 +110,7 @@ For console apps, add `--with-alias` to preserve stdin/stdout in the current ter For full debugging scenarios and IDE setup, see the [Debugging Guide](https://github.com/microsoft/WinAppCli/blob/main/docs/debugging.md). -## Recommendedworkflow +## Recommended workflow 1. **Initialize** — `winapp init --use-defaults` in your existing project 2. **Configure** — edit `appxmanifest.xml` to add capabilities your app needs (e.g., `runFullTrust`, `internetClient`) diff --git a/docs/fragments/skills/winapp-cli/troubleshoot.md b/docs/fragments/skills/winapp-cli/troubleshoot.md index 3fa36d1f..1892cfa2 100644 --- a/docs/fragments/skills/winapp-cli/troubleshoot.md +++ b/docs/fragments/skills/winapp-cli/troubleshoot.md @@ -34,7 +34,7 @@ Does the project have an appxmanifest.xml? │ └─ winapp update ├─ Need a dev certificate? │ └─ winapp cert generate (then winapp cert install for trust) - ├─ Need package identity for debugging? (see [Debugging Guide](https://github.com/microsoft/WinAppCli/blob/main/docs/debugging.md)) + ├─ Need package identity for debugging? │ ├─ Exe is in your build output folder? (most frameworks) │ │ └─ winapp run │ └─ Exe is separate from app code? (Electron, sparse testing) @@ -60,22 +60,6 @@ Does the project have an appxmanifest.xml? - Projects with NuGet package references (e.g., `.csproj` referencing `Microsoft.Windows.SDK.BuildTools`) can use winapp commands without `winapp.yaml` - For Electron projects, use the npm package (`npm install --save-dev @microsoft/winappcli`) which includes Node.js-specific commands under `npx winapp node` -## Debugging approach quick reference - -| Goal | Command | Key detail | -|------|---------|------------| -| Run with identity (most common) | `winapp run .\build\Debug` | Registers loose layout + launches; add `--with-alias` for console apps | -| Attach debugger to running app | `winapp run .\build\Debug` → attach to PID | Misses startup code | -| Register identity, launch manually | `winapp run .\build\Debug --no-launch` | Launch via `start shell:AppsFolder\` or execution alias — **not** the exe directly | -| F5 startup debugging (IDE launches exe) | `winapp create-debug-identity .\bin\myapp.exe` | Exe has identity regardless of how it's launched; best for debugging activation/startup code | -| Capture OutputDebugString | `winapp run .\build\Debug --debug-output` | **Blocks other debuggers** — use `--no-launch` if you need VS Code/WinDbg | -| Run and auto-clean | `winapp run .\build\Debug --unregister-on-exit` | Unregisters the dev package after the app exits | -| Clean up stale registration | `winapp unregister` | Removes dev-mode packages for the current project | - -> **Visual Studio users:** If you have a packaging project, VS already handles identity and debugging from F5 — you likely don't need winapp for debugging. These workflows are for VS Code, terminal, and frameworks VS doesn't natively package. - -For full details, see the [Debugging Guide](https://github.com/microsoft/WinAppCli/blob/main/docs/debugging.md). - ## Prerequisites & state matrix | Command | Requires | Creates/Modifies | @@ -89,7 +73,6 @@ For full details, see the [Debugging Guide](https://github.com/microsoft/WinAppC | `cert install` | Certificate file + admin | Machine certificate store | | `create-debug-identity` | `appxmanifest.xml` + exe + trusted cert | Registers sparse package with Windows | | `run` | Build output folder + `appxmanifest.xml` | Registers loose layout package, launches app | -| `unregister` | `appxmanifest.xml` (auto-detect or `--manifest`) | Removes dev-mode package registrations | | `package` | Build output + `appxmanifest.xml` | `.msix` file | | `sign` | File + certificate | Signed file (in-place) | | `create-external-catalog` | Directory with executables | `CodeIntegrityExternal.cat` | diff --git a/docs/gui-usage.md b/docs/gui-usage.md new file mode 100644 index 00000000..86e8e82c --- /dev/null +++ b/docs/gui-usage.md @@ -0,0 +1,64 @@ +# 🧪 Windows Identity App Usage + +This is an **experimental** app/sample (GUI) that wraps the CLI and provides an intuitive, drag-and-drop experience with the following features: + +- Supports .NET (Winforms, WPF..etc) apps, Python scripts/folders, MSIX +- Drop in a WinForms, WPF executable (.exe) to add development/debug app identity (via external location/sparse packaging) in a single click! +- Drop in a WinForms, WPF folder to package your app (MSIX) in a single click +- Drop in an MSIX to sign and register it locally in a single click +- Drop in a Python (.py) file to add debug identity in a single click + +We would love your feedback on this UI-based approach and whether it adds value or doesn't fit with your development workflows. + +
+ + + + + +
+ Windows Identity Tool Interface + + Windows Identity Tool Options +
+
+ +## Install the GUI Tool + +There are 2 ways to run the GUI (experimental). + +### Download the MSIX + +1. **[👉 Download Latest Experimental Build (unsigned .msix)](https://github.com/microsoft/WinAppCli/releases/tag/v0.1.1-gui)** +2. Run Powershell as **Administrator** and `Add-AppPackage -Path -AllowUnsigned` + +`` should be replaced with the full path of the downloaded build (msix file). +This experimental app requires the `winapp` CLI to be added to the user's PATH to function. Ensure you have the [latest CLI version](https://github.com/microsoft/WinAppCli/releases/latest) added to your path (installing the CLI MSIX is the easiest way to do this). + +### Build the repository + +Clone and build this repository. Run winapp.cli in Visual Studio to build and run the app. + +## Usage + +### .NET apps (WPF, WinForms) + +- Drop in an .exe from your binaries folder to add debug identity to it. The app will find the .csproj for the .exe (ie. if your .exe is in the /bin folder, the app will find the parent .csproj and create Assets and appxmanifest in that location). The .exe will be granted app identity via external location (sparse) packaging. +- Drop in a folder, and that app will be packaged into an MSIX + +### Python + +- This is currently a feature we are experimenting with. Currently, python files/scripts (.py) are supported or entire folders for packaging + +### MSIX + +- Drop in an MSIX and a cert will be created, installed and registered locally + +## Feedback for this Experimental App + +Please note that this app is experimental and may have issues as we gather feedback on the functionality, usefulness and value of the UI-based solution. If you see value or issues in this app, please let us know: + +- [File an issue](https://github.com/microsoft/WinAppCli/issues): please ensure that you are not filing a duplicate issue or bug +- Send any feedback to : Do you love this tool? Are there features or fixes you want to see? Let us know! + +The app will add functionality for Electron and mirror the CLI going forward depending on user feedback. diff --git a/docs/guides/cpp.md b/docs/guides/cpp.md index e71ed31f..6b1cab31 100644 --- a/docs/guides/cpp.md +++ b/docs/guides/cpp.md @@ -153,19 +153,24 @@ You can open `appxmanifest.xml` to further customize properties like the display ## 5. Debug with Identity -To test features that require identity (like Notifications) without fully packaging the app, you can use `winapp run`. This registers a loose layout package (just like a real MSIX install) and launches the app in one step. +To test features that require identity (like Notifications) without fully packaging the app, you can use `winapp create-debug-identity`. This applies a temporary identity to your executable using the manifest we just generated. 1. **Build the executable**: ```powershell cmake --build build --config Debug ``` -2. **Run with identity**: +2. **Apply Debug Identity**: + Run the following command on your built executable: ```powershell - winapp run .\build\Debug --with-alias + winapp create-debug-identity .\build\Debug\cpp-app.exe ``` -The `--with-alias` flag launches the app via its execution alias so console output stays in the current terminal. This requires a `uap5:ExecutionAlias` in the manifest — you can add one with `winapp manifest add-alias`. +3. **Run the Executable**: + Run the executable directly: + ```powershell + .\build\Debug\cpp-app.exe + ``` You should now see output similar to: ``` @@ -173,16 +178,23 @@ Package Family Name: cpp-app_12345abcde ``` This confirms your app is running with a valid package identity! -### Alternative: Sparse package identity +### Automating Debug Identity (Optional) -If you need sparse package behavior specifically (identity without copying files), you can use `create-debug-identity` instead: +To streamline your development workflow, you can configure CMake to automatically apply debug identity after building in Debug configuration. Add this to your `CMakeLists.txt`: -```powershell -winapp create-debug-identity .\build\Debug\cpp-app.exe -.\build\Debug\cpp-app.exe +```cmake +# Add a post-build command to apply debug identity in Debug builds +add_custom_command(TARGET cpp-app POST_BUILD + COMMAND $<$:winapp> + $<$:create-debug-identity> + $<$:$> + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMAND_EXPAND_LISTS + COMMENT "Applying debug identity to executable..." +) ``` -> **Tip:** For advanced debugging workflows (attaching debuggers, IDE setup, startup debugging), see the [Debugging Guide](../debugging.md). +With this configuration, simply running `cmake --build build --config Debug` will automatically apply the debug identity, and you can immediately run the executable with identity without the manual step. ## 6. Using Windows App SDK (Optional) @@ -209,6 +221,15 @@ target_link_libraries(cpp-app PRIVATE WindowsApp.lib OneCoreUap.lib) # Add Windows App SDK include directory target_include_directories(cpp-app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/.winapp/include) +# Add post-build command to apply debug identity in Debug builds +add_custom_command(TARGET cpp-app POST_BUILD + COMMAND $<$:winapp> + $<$:create-debug-identity> + $<$:$> + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMAND_EXPAND_LISTS + COMMENT "Applying debug identity to executable..." +) ``` ### Update main.cpp @@ -258,7 +279,7 @@ Rebuild the application with the Windows App SDK headers: ```powershell cmake --build build --config Debug -winapp run .\build\Debug --with-alias +.\build\Debug\cpp-app.exe ``` You should now see output like: @@ -369,6 +390,15 @@ target_link_libraries(cpp-app PRIVATE WindowsApp.lib OneCoreUap.lib) # Add Windows App SDK include directory target_include_directories(cpp-app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/.winapp/include) +# Add a post-build command to apply debug identity in Debug builds +add_custom_command(TARGET cpp-app POST_BUILD + COMMAND $<$:winapp> + $<$:create-debug-identity> + $<$:$> + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMAND_EXPAND_LISTS + COMMENT "Applying debug identity to executable..." +) ``` With this setup: diff --git a/docs/guides/flutter.md b/docs/guides/flutter.md index 37248239..934d81a9 100644 --- a/docs/guides/flutter.md +++ b/docs/guides/flutter.md @@ -207,16 +207,16 @@ You can open `appxmanifest.xml` to further customize properties like the display ## 5. Debug with Identity -To test features that require identity (like Notifications) without fully packaging the app, you can use `winapp run`. This registers a loose layout package (just like a real MSIX install) and launches the app in one step. +To test features that require identity (like Notifications) without fully packaging the app, you can use `winapp create-debug-identity`. This applies a temporary identity to your executable using the manifest we just generated. -1. **Build the app**: +1. **Apply Debug Identity**: ```powershell - flutter build windows + winapp create-debug-identity .\build\windows\x64\runner\Release\flutter_app.exe ``` -2. **Run with identity**: +2. **Run the executable**: ```powershell - winapp run .\build\windows\x64\runner\Release + .\build\windows\x64\runner\Release\flutter_app.exe ``` You should now see the app with a green indicator showing: @@ -225,7 +225,7 @@ Package Family Name: flutterapp.debug_xxxxxxxx ``` This confirms your app is running with a valid package identity! -> **Tip:** For advanced debugging workflows (attaching debuggers, IDE setup, startup debugging), see the [Debugging Guide](../debugging.md). +> **Note**: After running `flutter clean` or rebuilding, you'll need to re-run `create-debug-identity` since the executable is replaced. ## 6. Using Windows App SDK (Optional) @@ -359,7 +359,8 @@ Rebuild the application: ```powershell flutter build windows -winapp run .\build\windows\x64\runner\Release +winapp create-debug-identity .\build\windows\x64\runner\Release\flutter_app.exe +.\build\windows\x64\runner\Release\flutter_app.exe ``` You should now see output like: diff --git a/docs/guides/rust.md b/docs/guides/rust.md index d2292747..5bdbd711 100644 --- a/docs/guides/rust.md +++ b/docs/guides/rust.md @@ -93,7 +93,6 @@ When prompted: - **Package name**: Press Enter to accept the default (rust-app) - **Publisher name**: Press Enter to accept the default or enter your name - **Version**: Press Enter to accept 1.0.0.0 -- **Description**: Press Enter to accept the default or enter a description - **Setup SDKs**: Select "Do not setup SDKs" This command will: @@ -157,8 +156,6 @@ Package Family Name: rust-app_12345abcde ``` This confirms your app is running with a valid package identity! -> **Tip:** For advanced debugging workflows (attaching debuggers, IDE setup, startup debugging), see the [Debugging Guide](../debugging.md). - ## 6. Package with MSIX Once you're ready to distribute your app, you can package it as an MSIX using the same manifest. diff --git a/docs/guides/tauri.md b/docs/guides/tauri.md index e337df86..c9235bd0 100644 --- a/docs/guides/tauri.md +++ b/docs/guides/tauri.md @@ -152,21 +152,21 @@ You can open `appxmanifest.xml` to further customize properties like the display ## 4. Debug with Identity -To debug with identity, we need to build the Rust backend and run it with `winapp run`. Since `npm run tauri dev` manages the process lifecycle, it's harder to inject the identity there. Instead, we'll create a custom script. +To debug with identity, we need to build the Rust backend, apply the debug identity to the executable, and then run it directly. Since `npm run tauri dev` manages the process lifecycle, it's harder to inject the identity there. Instead, we'll create a custom script. 1. **Add Script**: Open `package.json` and add a new script `tauri:dev:withidentity`: ```json "scripts": { "tauri": "tauri", - "tauri:dev:withidentity": "cargo build --manifest-path src-tauri/Cargo.toml && (if not exist dist mkdir dist) && copy /Y src-tauri\\target\\debug\\tauri-app.exe dist\\ >nul && winapp run .\\dist" + "tauri:dev:withidentity": "cargo build --manifest-path src-tauri/Cargo.toml && winapp create-debug-identity src-tauri/target/debug/tauri-app.exe && .\\src-tauri\\target\\debug\\tauri-app.exe" } ``` **What this script does:** * `cargo build ...`: Recompiles the Rust backend. - * `copy ... dist\\`: Stages just the exe into a `dist` folder (the `target\debug` folder is very large and contains intermediate build artifacts that aren't part of your app). - * `winapp run .\\dist`: Registers a loose layout package (just like a real MSIX install) and launches the app. + * `winapp create-debug-identity ...`: Applies the temporary identity from your `appxmanifest.xml` to the built executable. + * `...tauri-app.exe`: Runs the executable directly. 2. **Run the Script**: @@ -176,26 +176,24 @@ To debug with identity, we need to build the Rust backend and run it with `winap You should now see the app open and display a "Package family name", confirming it is running with identity! You can now start using and debugging APIs that require package identity, such as Notifications or the new AI APIs like Phi Silica. -> **Tip:** For advanced debugging workflows (attaching debuggers, IDE setup, startup debugging), see the [Debugging Guide](../debugging.md). - ## 5. Package with MSIX Once you're ready to distribute your app, you can package it as an MSIX which will provide the package identity to your application. -First, add a `pack:msix` script to your `package.json`: +### Build Release +Build your application in release mode: -```json -"scripts": { - "tauri": "tauri", - "tauri:dev:withidentity": "...", - "pack:msix": "npm run tauri -- build && (if not exist dist mkdir dist) && copy /Y src-tauri\\target\\release\\tauri-app.exe dist\\ >nul && winapp pack .\\dist --cert .\\devcert.pfx" -} +```powershell +npm run tauri build ``` -**What this script does:** -* `npm run tauri -- build`: Builds the Rust backend in release mode. -* `copy ... dist\\`: Stages just the exe into a `dist` folder (the `target\release` folder is very large and contains intermediate build artifacts that aren't part of your app). -* `winapp pack .\\dist --cert .\\devcert.pfx`: Packages and signs the app as MSIX. +### Prepare Package Directory +Create a directory to hold your package files and copy your release executable. + +```powershell +mkdir dist +copy .\src-tauri\target\release\tauri-app.exe .\dist\ +``` ### Generate a Development Certificate @@ -205,10 +203,13 @@ Before packaging, you need a development certificate for signing. Generate one i winapp cert generate --if-exists skip ``` -### Build, Stage, and Pack +### Sign and Pack + +Now you can package and sign: ```powershell -npm run pack:msix +# package and sign the app with the generated certificate +winapp pack .\dist --cert .\devcert.pfx ``` > Note: The `pack` command automatically uses the appxmanifest.xml from your current directory and copies it to the target folder before packaging. The generated .msix file will be in the current directory. diff --git a/docs/npm-usage.md b/docs/npm-usage.md index dc5319c8..0de47d62 100644 --- a/docs/npm-usage.md +++ b/docs/npm-usage.md @@ -266,8 +266,7 @@ function manifestUpdateAssets(options: ManifestUpdateAssetsOptions): Promise --- -### `run()` +### `runApp()` Creates packaged layout, registers the Application, and launches the packaged application. ```typescript -function run(options: RunOptions): Promise +function runApp(options: RunOptions): Promise ``` **Options:** @@ -336,6 +335,7 @@ function run(options: RunOptions): Promise |----------|------|----------|-------------| | `inputFolder` | `string` | Yes | Input folder containing the app to run | | `args` | `string \| undefined` | No | Command-line arguments to pass to the application | +| `clean` | `boolean \| undefined` | No | Remove the existing package's application data (LocalState, settings, etc.) before re-deploying. By default, application data is preserved across re-deployments. | | `debugOutput` | `boolean \| undefined` | No | Capture OutputDebugString messages and first-chance exceptions from the launched application. Only one debugger can attach to a process at a time, so other debuggers (Visual Studio, VS Code) cannot be used simultaneously. Use --no-launch instead if you need to attach a different debugger. Cannot be combined with --no-launch or --json. | | `json` | `boolean \| undefined` | No | Format output as JSON | | `manifest` | `string \| undefined` | No | Path to the appxmanifest.xml (default: auto-detect from input folder or current directory) | @@ -868,8 +868,7 @@ type ManifestTemplates = "packaged" | "sparse" | Property | Type | Required | Description | |----------|------|----------|-------------| -| `imagePath` | `string` | Yes | Path to source image file (SVG, PNG, ICO, JPG, BMP, GIF) | -| `lightImage` | `string \| undefined` | No | Path to source image for light theme variants (SVG, PNG, ICO, JPG, BMP, GIF) | +| `imagePath` | `string` | Yes | Path to source image file | | `manifest` | `string \| undefined` | No | Path to AppxManifest.xml or Package.appxmanifest file (default: search current directory) | | `quiet` | `boolean \| undefined` | No | Suppress progress messages. | | `verbose` | `boolean \| undefined` | No | Enable verbose output. | @@ -911,6 +910,7 @@ type ManifestTemplates = "packaged" | "sparse" |----------|------|----------|-------------| | `inputFolder` | `string` | Yes | Input folder containing the app to run | | `args` | `string \| undefined` | No | Command-line arguments to pass to the application | +| `clean` | `boolean \| undefined` | No | Remove the existing package's application data (LocalState, settings, etc.) before re-deploying. By default, application data is preserved across re-deployments. | | `debugOutput` | `boolean \| undefined` | No | Capture OutputDebugString messages and first-chance exceptions from the launched application. Only one debugger can attach to a process at a time, so other debuggers (Visual Studio, VS Code) cannot be used simultaneously. Use --no-launch instead if you need to attach a different debugger. Cannot be combined with --no-launch or --json. | | `json` | `boolean \| undefined` | No | Format output as JSON | | `manifest` | `string \| undefined` | No | Path to the appxmanifest.xml (default: auto-detect from input folder or current directory) | diff --git a/docs/usage.md b/docs/usage.md index 8a5374d8..8745a5c9 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -204,7 +204,7 @@ winapp pack ./dist --executable MyApp.exe Create app identity for debugging using [sparse packaging](https://learn.microsoft.com/en-us/windows/apps/desktop/modernize/grant-identity-to-nonpackaged-apps). The exe stays in its original location — Windows associates identity with it via `Add-AppxPackage -ExternalLocation`. -> **When to use this vs `winapp run`:** Use `create-debug-identity` when the exe is **separate from your app code** (e.g., Electron apps where `electron.exe` is in `node_modules`), or when specifically testing sparse package behavior. For most frameworks where the exe is in your build output folder, use [`winapp run`](#run) instead — it registers a full loose layout package and launches the app. See the [Debugging Guide](debugging.md) for a full comparison. +> **When to use this vs `winapp run`:** Use `create-debug-identity` when the exe is **separate from your app code** (e.g., Electron apps where `electron.exe` is in `node_modules`), or when specifically testing sparse package behavior. For most frameworks where the exe is in your build output folder, use [`winapp run`](#run) instead — it registers a full loose layout package and launches the app. ```bash winapp create-debug-identity [entrypoint] [options] @@ -309,7 +309,7 @@ winapp manifest generate ./src --package-name MyApp --publisher-name "CN=My Comp Create a loose layout package from a build output folder, register it with Windows via `Add-AppxPackage`, and launch the application — simulating a full MSIX install for debugging. Returns the process ID for debugger attachment. -> **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. +> **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. ```bash winapp run [options] @@ -328,6 +328,13 @@ winapp run [options] - `--with-alias` - Launch the app using its execution alias instead of AUMID activation. The app runs in the current terminal with inherited stdin/stdout/stderr. Requires a `uap5:ExecutionAlias` in the manifest (use `winapp manifest add-alias` to add one). Cannot be combined with `--no-launch`. Cannot be combined with `--json`. - `--debug-output` - Capture `OutputDebugString` messages and first-chance exceptions from the launched application. Only one debugger can attach to a process at a time, so other debuggers (Visual Studio, VS Code) cannot be used simultaneously. Use `--no-launch` instead if you need to attach a different debugger. Cannot be combined with `--no-launch`. Cannot be combined with `--json`. - `--unregister-on-exit` - Unregister the development package after the application exits. Only removes packages registered in development mode. Cannot be combined with `--no-launch`. +- `--clean` - Remove the existing package's application data (LocalState, settings, etc.) before re-deploying. By default, application data is preserved across re-deployments. + +**Application data persistence:** + +By default, `winapp run` preserves your application's data (`LocalState`, `RoamingState`, `Settings`, etc.) when re-deploying. If your app writes data to `ApplicationData.Current.LocalFolder` or `Environment.GetFolderPath(SpecialFolder.LocalApplicationData)` within the package context, that data will survive across `winapp run` invocations. + +Use `--clean` when you need a fresh start (e.g., to reset corrupted state or test first-run behavior). **What it does:** @@ -363,6 +370,9 @@ winapp run ./bin/Debug --with-alias --debug-output # Run and automatically clean up registration on exit winapp run ./bin/Debug --with-alias --unregister-on-exit + +# Wipe application data (LocalState, settings) and start fresh +winapp run ./bin/Debug --clean ``` **MSBuild properties (NuGet package):** @@ -468,32 +478,28 @@ winapp manifest update-assets [options] **Arguments:** -- `image-path` - Path to source image file (PNG, JPG, SVG, ICO, GIF, BMP, etc.) +- `image-path` - Path to source image file (PNG, JPG, GIF, etc.) **Options:** - `--manifest ` - Path to AppxManifest.xml file (default: search current directory) -- `--light-image ` - Path to a separate source image for light theme variants **Description:** -Takes a single source image and generates a comprehensive set of MSIX image assets based on the manifest's asset references: - -For each asset referenced in the manifest: -- **5 scale variants** — base (no suffix), `.scale-125`, `.scale-150`, `.scale-200`, `.scale-400` - -For the app icon (Square44x44Logo / AppList, 44×44 base): -- **14 plated targetsize variants** — `.targetsize-{16,20,24,30,32,36,40,48,60,64,72,80,96,256}` -- **14 unplated targetsize variants** — `.targetsize-{size}_altform-unplated` - -Additionally: -- **app.ico** — Multi-resolution ICO file (16, 24, 32, 48, 256) for shell integration. If an existing `.ico` file is found in the assets directory (e.g. `AppIcon.ico` from a project template), it is replaced in-place rather than creating a duplicate - -With `--light-image`: -- **Light theme targetsize variants** — `.targetsize-{size}_altform-lightunplated` (app icon) -- **Light theme scale variants** — `.scale-{factor}_altform-colorful_theme-light` (tiles, store logo) - -**SVG support:** SVG files are fully supported as source images. They are rendered as vectors directly at each target size, producing pixel-perfect results at all resolutions. +Takes a single source image and automatically generates all 12 required MSIX image assets at the correct dimensions: + +- Square44x44Logo.png (44×44) +- Square44x44Logo.scale-200.png (88×88) +- Square44x44Logo.targetsize-24_altform-unplated.png (24×24) +- Square150x150Logo.png (150×150) +- Square150x150Logo.scale-200.png (300×300) +- Wide310x150Logo.png (310×150) +- Wide310x150Logo.scale-200.png (620×300) +- SplashScreen.png (620×300) +- SplashScreen.scale-200.png (1240×600) +- StoreLogo.png (50×50) +- LockScreenLogo.png (24×24) +- LockScreenLogo.scale-200.png (48×48) The command scales images proportionally while maintaining aspect ratio, centering them with transparent backgrounds when needed. Assets are saved to the `Assets` directory relative to the manifest location. @@ -503,18 +509,9 @@ The command scales images proportionally while maintaining aspect ratio, centeri # Generate assets with auto-detected manifest winapp manifest update-assets mylogo.png -# Use an SVG source for best quality at all sizes -winapp manifest update-assets mylogo.svg - # Specify manifest location explicitly winapp manifest update-assets mylogo.png --manifest ./dist/appxmanifest.xml -# Generate light theme variants from a separate image -winapp manifest update-assets mylogo.png --light-image mylogo-light.png - -# Use the same image for both (generates all MRT light theme qualifiers) -winapp manifest update-assets mylogo.png --light-image mylogo.png - # With verbose output winapp manifest update-assets mylogo.png --verbose ``` diff --git a/llms.txt b/llms.txt index 995916b0..b08fa03f 100644 --- a/llms.txt +++ b/llms.txt @@ -26,7 +26,6 @@ The plugin is at `.github/plugin/` and includes: - [CLI Schema](https://raw.githubusercontent.com/microsoft/WinAppCli/main/docs/cli-schema.json): Machine-readable JSON schema of all commands, options, and types - [Usage Guide](https://github.com/microsoft/WinAppCli/blob/main/docs/usage.md): Full documentation for humans -- [Debugging Guide](https://github.com/microsoft/WinAppCli/blob/main/docs/debugging.md): `winapp run` vs `create-debug-identity`, IDE setup, and debugging workflows ## Guides diff --git a/plugin.json b/plugin.json deleted file mode 100644 index 2e09a949..00000000 --- a/plugin.json +++ /dev/null @@ -1,67 +0,0 @@ -{ - "name": "winappcli", - "description": "Windows app development, packaging, and distribution. Helps with creating Windows installers (MSIX), code signing, certificates, Windows SDK and Windows App SDK setup, package identity for Windows APIs (push notifications, background tasks, share target), appxmanifest authoring, and Microsoft Store distribution. Supports Electron, .NET, C++, Rust, Flutter, and Tauri apps.", - "version": "0.2.2", - "author": { - "name": "Microsoft", - "url": "https://github.com/microsoft/WinAppCli" - }, - "license": "MIT", - "keywords": [ - "windows", - "msix", - "packaging", - "installer", - "identity", - "electron", - "winapp", - "windows app sdk", - "windows sdk", - "appxmanifest.xml", - "appxmanifest", - "code signing", - "certificate", - "pfx", - "signtool", - "makeappx", - "windows store", - "microsoft store", - "windows distribution", - "windows packaging", - "windows installer", - "package identity", - "sparse package", - "push notifications", - "background tasks", - "share target", - "startup task", - "tauri", - "flutter", - "rust", - "dotnet", - "wpf", - "winforms", - "cpp", - "cppwinrt", - "windows api", - "winrt", - "uwp", - "desktop app", - "win32" - ], - "category": "windows-development", - "tags": [ - "windows", - "msix", - "packaging", - "installer", - "desktop", - "notifications", - "sdk", - "signing", - "certificate", - "distribution" - ], - "agents": ".github/plugin/agents/", - "skills": ".github/plugin/skills/winapp-cli/" -} diff --git a/samples/cpp-app-winui/.gitignore b/samples/cpp-app-winui/.gitignore deleted file mode 100644 index 6b6ab294..00000000 --- a/samples/cpp-app-winui/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ - -# Development certificate -devcert.pfx - -# Windows SDK packages and generated files -.winapp - -.winapp-tools \ No newline at end of file diff --git a/samples/cpp-app-winui/Assets/LockScreenLogo.png b/samples/cpp-app-winui/Assets/LockScreenLogo.png deleted file mode 100644 index 180ad62f..00000000 Binary files a/samples/cpp-app-winui/Assets/LockScreenLogo.png and /dev/null differ diff --git a/samples/cpp-app-winui/Assets/LockScreenLogo.scale-200.png b/samples/cpp-app-winui/Assets/LockScreenLogo.scale-200.png deleted file mode 100644 index 7440f0d4..00000000 Binary files a/samples/cpp-app-winui/Assets/LockScreenLogo.scale-200.png and /dev/null differ diff --git a/samples/cpp-app-winui/Assets/SplashScreen.png b/samples/cpp-app-winui/Assets/SplashScreen.png deleted file mode 100644 index a634fef0..00000000 Binary files a/samples/cpp-app-winui/Assets/SplashScreen.png and /dev/null differ diff --git a/samples/cpp-app-winui/Assets/SplashScreen.scale-200.png b/samples/cpp-app-winui/Assets/SplashScreen.scale-200.png deleted file mode 100644 index 32f486a8..00000000 Binary files a/samples/cpp-app-winui/Assets/SplashScreen.scale-200.png and /dev/null differ diff --git a/samples/cpp-app-winui/CMakeLists.txt b/samples/cpp-app-winui/CMakeLists.txt deleted file mode 100644 index 3d680319..00000000 --- a/samples/cpp-app-winui/CMakeLists.txt +++ /dev/null @@ -1,100 +0,0 @@ -cmake_minimum_required(VERSION 3.20) -project(cpp-app-winui) - -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -# Download winapp CLI if not available in PATH -find_program(WINAPP_CLI winapp) -if(NOT WINAPP_CLI) - set(WINAPP_DIR "${CMAKE_CURRENT_SOURCE_DIR}/.winapp-tools") - set(WINAPP_CLI "${WINAPP_DIR}/winapp.exe") - - if(NOT EXISTS "${WINAPP_CLI}") - message(STATUS "Downloading winapp CLI...") - - # Determine architecture - if(CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|aarch64") - set(WINAPP_ARCH "arm64") - else() - set(WINAPP_ARCH "x64") - endif() - - # Download and extract - set(WINAPP_ZIP "${CMAKE_CURRENT_BINARY_DIR}/winappcli.zip") - file(DOWNLOAD - "https://github.com/microsoft/WinAppCli/releases/latest/download/winappcli-${WINAPP_ARCH}.zip" - "${WINAPP_ZIP}" - SHOW_PROGRESS - ) - - file(ARCHIVE_EXTRACT INPUT "${WINAPP_ZIP}" DESTINATION "${WINAPP_DIR}") - file(REMOVE "${WINAPP_ZIP}") - message(STATUS "winapp CLI downloaded to ${WINAPP_DIR}") - endif() -endif() - -# Automatically restore Windows App SDK headers and generate certificate if needed -# This runs once during CMake configuration, not on every build -if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/.winapp/include") - message(STATUS "Restoring Windows App SDK headers...") - execute_process( - COMMAND "${WINAPP_CLI}" restore - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - RESULT_VARIABLE RESTORE_RESULT - ) - if(NOT RESTORE_RESULT EQUAL 0) - message(WARNING "Failed to restore Windows App SDK. Run 'winapp restore' manually.") - endif() -endif() - -if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/devcert.pfx") - message(STATUS "Generating development certificate...") - execute_process( - COMMAND "${WINAPP_CLI}" cert generate --if-exists skip - WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} - RESULT_VARIABLE CERT_RESULT - ) - if(NOT CERT_RESULT EQUAL 0) - message(WARNING "Failed to generate certificate. Run 'winapp cert generate' manually.") - endif() -endif() - -# WinUI 3 windowed application (WIN32 = Windows subsystem, no console) -add_executable(cpp-app-winui WIN32 main.cpp) - -# C++/WinRT + WinUI headers are large; need /bigobj for MSVC -if(MSVC) - target_compile_options(cpp-app-winui PRIVATE /bigobj) -endif() - -# Link Windows Runtime and bootstrapper libraries -target_link_libraries(cpp-app-winui PRIVATE WindowsApp.lib OneCoreUap.lib Microsoft.WindowsAppRuntime.Bootstrap.lib) - -# Add Windows App SDK include and lib directories -target_include_directories(cpp-app-winui PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/.winapp/include) - -# Determine build architecture for lib/bin paths -if(CMAKE_SYSTEM_PROCESSOR MATCHES "ARM64|aarch64") - set(WINAPP_LIB_ARCH "arm64") -else() - set(WINAPP_LIB_ARCH "x64") -endif() - -target_link_directories(cpp-app-winui PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/.winapp/lib/${WINAPP_LIB_ARCH}) - -# Embed the application manifest for DPI awareness -if(MSVC) - target_sources(cpp-app-winui PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/app.manifest") -endif() - -# Copy Windows App SDK runtime DLLs to the build output directory -set(WINAPP_BIN_DIR "${CMAKE_CURRENT_SOURCE_DIR}/.winapp/bin/${WINAPP_LIB_ARCH}") -if(EXISTS "${WINAPP_BIN_DIR}") - add_custom_command(TARGET cpp-app-winui POST_BUILD - COMMAND ${CMAKE_COMMAND} -E copy_directory - "${WINAPP_BIN_DIR}" - "$" - COMMENT "Copying Windows App SDK runtime DLLs..." - ) -endif() diff --git a/samples/cpp-app-winui/README.md b/samples/cpp-app-winui/README.md deleted file mode 100644 index f51003f8..00000000 --- a/samples/cpp-app-winui/README.md +++ /dev/null @@ -1,53 +0,0 @@ -# C++ WinUI 3 Sample Application - -This sample demonstrates a WinUI 3 desktop window built entirely in C++ with CMake - no XAML files, no MIDL, no MSBuild. The UI is constructed programmatically using C++/WinRT projections. - -For the console-only version, see the [C++ Sample](../cpp-app/). - -## What This Sample Shows - -- **Programmatic WinUI 3** — Window, Button, TextBlock, StackPanel created in code -- Using Windows App Model APIs to retrieve package identity -- Using `winapp run` to run the app packaged with MSIX identity -- MSIX packaging with app manifest and assets -- CMake integration with Windows App SDK headers and runtime DLLs - -## Prerequisites - -- Visual Studio with C++ Desktop development workload -- CMake 3.20 or later -- Windows App SDK runtime installed -- WinApp CLI installed - -## Building and Running - -### Build the Application - -```powershell -cmake -B build -cmake --build build --config Debug -``` - -### Run without Identity - -```powershell -.\build\Debug\cpp-app-winui.exe -``` - -Click the **Check Identity** button — it will show "Not packaged". - -### Run with Identity (Debug) - -```powershell -winapp run .\build\Debug -``` - -This registers a loose layout package (just like a real MSIX install), then launches the WinUI 3 window. Click the **Check Identity** button to see the Package Family Name. - -## Technical Notes - -This sample uses a non-standard approach to WinUI 3: all UI is built programmatically in C++ without XAML files. This avoids the need for XAML compilation (which requires MSBuild), but means: - -- Controls use basic styling (no `XamlControlsResources`) -- The `App` class implements a minimal `IXamlMetadataProvider` (returns null for all types) -- `MddBootstrapInitialize2` handles Windows App SDK initialization for both packaged and unpackaged execution diff --git a/samples/cpp-app-winui/appxmanifest.xml b/samples/cpp-app-winui/appxmanifest.xml deleted file mode 100644 index c0b6264b..00000000 --- a/samples/cpp-app-winui/appxmanifest.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - - - - - - cpp-app-winui - nikolame - Assets\StoreLogo.png - - - - - - - - - - - - - - - - - - - - - - diff --git a/samples/cpp-app-winui/main.cpp b/samples/cpp-app-winui/main.cpp deleted file mode 100644 index f86a55c6..00000000 --- a/samples/cpp-app-winui/main.cpp +++ /dev/null @@ -1,134 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -// Forward-declare bootstrap APIs to avoid header conflicts with C++/WinRT -extern "C" { - HRESULT __stdcall MddBootstrapInitialize2( - UINT32 majorMinorVersion, - PCWSTR versionTag, - PACKAGE_VERSION minVersion, - UINT32 options) noexcept; - void __stdcall MddBootstrapShutdown() noexcept; -} - -// Windows App SDK 1.8 version constants -constexpr UINT32 kWinAppSdkMajorMinor = 0x00010008; -// OnPackageIdentity_NOOP: skip bootstrap when running packaged (via winapp run) -constexpr UINT32 kBootstrapOptions_OnPackageIdentity_NOOP = 0x0010; - -using namespace winrt; -using namespace Microsoft::UI::Xaml; -using namespace Microsoft::UI::Xaml::Controls; - -struct App : ApplicationT -{ - // Minimal IXamlMetadataProvider (no custom XAML types) - Microsoft::UI::Xaml::Markup::IXamlType GetXamlType(Windows::UI::Xaml::Interop::TypeName const&) - { - return nullptr; - } - Microsoft::UI::Xaml::Markup::IXamlType GetXamlType(hstring const&) - { - return nullptr; - } - com_array GetXmlnsDefinitions() - { - return {}; - } - - void OnLaunched(LaunchActivatedEventArgs const&) - { - m_window = Window(); - - auto panel = StackPanel(); - panel.HorizontalAlignment(HorizontalAlignment::Center); - panel.VerticalAlignment(VerticalAlignment::Center); - panel.Spacing(16); - - auto title = TextBlock(); - title.Text(L"WinUI 3 CMAKE C++ Sample"); - title.FontSize(24); - title.HorizontalAlignment(HorizontalAlignment::Center); - - auto infoText = TextBlock(); - infoText.Text(L"Click the button to check package identity"); - infoText.HorizontalAlignment(HorizontalAlignment::Center); - infoText.TextWrapping(TextWrapping::Wrap); - - auto button = Button(); - button.Content(box_value(L"Check Identity")); - button.HorizontalAlignment(HorizontalAlignment::Center); - button.Click([infoText](auto&&, auto&&) { - UINT32 length = 0; - LONG result = GetCurrentPackageFamilyName(&length, nullptr); - - if (result == ERROR_INSUFFICIENT_BUFFER) { - std::wstring familyName(length, L'\0'); - result = GetCurrentPackageFamilyName(&length, familyName.data()); - - if (result == ERROR_SUCCESS) { - familyName.resize(wcslen(familyName.c_str())); - infoText.Text(hstring(L"Package Family Name: ") + hstring(familyName)); - } else { - infoText.Text(L"Error retrieving Package Family Name"); - } - } else { - infoText.Text(L"Not packaged \u2014 run with 'winapp run' for identity"); - } - }); - - panel.Children().Append(title); - panel.Children().Append(button); - panel.Children().Append(infoText); - - m_window.Content(panel); - m_window.Title(L"C++ WinUI 3 Sample"); - m_window.Activate(); - } - -private: - Window m_window{ nullptr }; -}; - -int WINAPI wWinMain(HINSTANCE, HINSTANCE, LPWSTR, int) -{ - try - { - // COM must be initialized before the bootstrap - init_apartment(apartment_type::single_threaded); - - // Initialize the Windows App SDK for unpackaged execution. - // When running packaged (winapp run), OnPackageIdentity_NOOP skips this. - PACKAGE_VERSION minVer{}; - minVer.Version = 0x1F4002A304760000u; - HRESULT hr = MddBootstrapInitialize2( - kWinAppSdkMajorMinor, L"", minVer, - kBootstrapOptions_OnPackageIdentity_NOOP); - if (FAILED(hr)) - { - MessageBoxW(nullptr, - L"Failed to initialize Windows App SDK.\n" - L"Make sure the Windows App SDK runtime is installed.", - L"WinUI 3 Error", MB_ICONERROR); - return static_cast(hr); - } - - Application::Start([](auto&&) { make(); }); - - MddBootstrapShutdown(); - } - catch (hresult_error const& ex) - { - MessageBoxW(nullptr, ex.message().c_str(), L"WinUI 3 Error", MB_ICONERROR); - return static_cast(ex.code()); - } - return 0; -} diff --git a/samples/cpp-app-winui/winapp.yaml b/samples/cpp-app-winui/winapp.yaml deleted file mode 100644 index 9f7e3925..00000000 --- a/samples/cpp-app-winui/winapp.yaml +++ /dev/null @@ -1,15 +0,0 @@ -packages: - - name: Microsoft.Windows.CppWinRT - version: 2.0.250303.1 - - name: Microsoft.Windows.SDK.BuildTools - version: 10.0.26100.7705 - - name: Microsoft.WindowsAppSDK - version: 1.8.260317003 - - name: Microsoft.Windows.ImplementationLibrary - version: 1.0.260126.7 - - name: Microsoft.Windows.SDK.CPP - version: 10.0.26100.7705 - - name: Microsoft.Windows.SDK.CPP.x64 - version: 10.0.26100.7705 - - name: Microsoft.Windows.SDK.CPP.arm64 - version: 10.0.26100.7705 diff --git a/samples/cpp-app/CMakeLists.txt b/samples/cpp-app/CMakeLists.txt index e3243138..8ad3ac94 100644 --- a/samples/cpp-app/CMakeLists.txt +++ b/samples/cpp-app/CMakeLists.txt @@ -67,3 +67,13 @@ target_link_libraries(cpp-app PRIVATE WindowsApp.lib OneCoreUap.lib) # Add Windows App SDK include directory target_include_directories(cpp-app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/.winapp/include) + +# Add a post-build command to apply debug identity in Debug builds +add_custom_command(TARGET cpp-app POST_BUILD + COMMAND $<$:winapp> + $<$:create-debug-identity> + $<$:$> + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} + COMMAND_EXPAND_LISTS + COMMENT "Applying debug identity to executable..." +) \ No newline at end of file diff --git a/samples/cpp-app/README.md b/samples/cpp-app/README.md index b0ef2ce3..8ffb2063 100644 --- a/samples/cpp-app/README.md +++ b/samples/cpp-app/README.md @@ -8,23 +8,16 @@ For a complete step-by-step guide, see the [C++ Getting Started Guide](../../doc - Basic C++ console application built with CMake - Using Windows App Model APIs to retrieve package identity -- Using `winapp run` to run the app packaged (registers a loose layout package, just like a real MSIX install) +- Configuring CMake to automatically apply debug identity after building in Debug configuration - MSIX packaging with app manifest and assets ## Prerequisites - Visual Studio Native Desktop workload or Visual Studio with C++ development tools - CMake 3.20 or later -- WinApp CLI installed ## Building and Running -### Restore dependencies - -```powershell -winapp restore -``` - ### Build the Application ```powershell @@ -32,20 +25,13 @@ cmake -B build cmake --build build --config Debug ``` -### Run without Identity +### Run -```powershell -.\build\Debug\cpp-app.exe -``` -*Output should be: "Not packaged"* - -### Run with Identity (Debug) +The CMakeLists.txt is configured to automatically apply debug identity when building in Debug configuration. Simply build and run: ```powershell -winapp run .\build\Debug --with-alias +cmake --build build --config Debug +.\build\Debug\cpp-app.exe ``` -This registers a loose layout package (just like a real MSIX install), then launches the app via its execution alias so console output stays in the current terminal. - -*Output should show the Package Family Name.* -> **Note:** The `--with-alias` flag requires a `uap5:ExecutionAlias` in the manifest. This sample's `appxmanifest.xml` already includes one. You can add one to an appxmanifest.xml with `winapp manifest add-alias`. +Output: `Package Family Name: cpp-app_12345abcde` diff --git a/samples/cpp-app/appxmanifest.xml b/samples/cpp-app/appxmanifest.xml index 368b8fbd..bb0a4b1d 100644 --- a/samples/cpp-app/appxmanifest.xml +++ b/samples/cpp-app/appxmanifest.xml @@ -9,8 +9,7 @@ xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10" xmlns:desktop6="http://schemas.microsoft.com/appx/manifest/desktop/windows10/6" xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10" - xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5" - IgnorableNamespaces="uap uap2 uap3 rescap desktop desktop6 uap10 uap5"> + IgnorableNamespaces="uap uap2 uap3 rescap desktop desktop6 uap10"> - - - - - - - diff --git a/samples/cpp-app/winapp.yaml b/samples/cpp-app/winapp.yaml index 9f7e3925..2d5f7ee6 100644 --- a/samples/cpp-app/winapp.yaml +++ b/samples/cpp-app/winapp.yaml @@ -2,14 +2,14 @@ packages: - name: Microsoft.Windows.CppWinRT version: 2.0.250303.1 - name: Microsoft.Windows.SDK.BuildTools - version: 10.0.26100.7705 + version: 10.0.26100.7175 - name: Microsoft.WindowsAppSDK - version: 1.8.260317003 + version: 1.8.251106002 - name: Microsoft.Windows.ImplementationLibrary - version: 1.0.260126.7 + version: 1.0.250325.1 - name: Microsoft.Windows.SDK.CPP - version: 10.0.26100.7705 + version: 10.0.26100.7175 - name: Microsoft.Windows.SDK.CPP.x64 - version: 10.0.26100.7705 + version: 10.0.26100.7175 - name: Microsoft.Windows.SDK.CPP.arm64 - version: 10.0.26100.7705 + version: 10.0.26100.7175 diff --git a/samples/dotnet-app/dotnet-app.csproj b/samples/dotnet-app/dotnet-app.csproj index 3ce9c211..0da23353 100644 --- a/samples/dotnet-app/dotnet-app.csproj +++ b/samples/dotnet-app/dotnet-app.csproj @@ -8,8 +8,6 @@ enable true - - diff --git a/samples/electron/Assets/AppList.png b/samples/electron/Assets/AppList.png deleted file mode 100644 index 3eb4d21d..00000000 Binary files a/samples/electron/Assets/AppList.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.scale-125.png b/samples/electron/Assets/AppList.scale-125.png deleted file mode 100644 index 974739a3..00000000 Binary files a/samples/electron/Assets/AppList.scale-125.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.scale-150.png b/samples/electron/Assets/AppList.scale-150.png deleted file mode 100644 index 39bf5d07..00000000 Binary files a/samples/electron/Assets/AppList.scale-150.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.scale-200.png b/samples/electron/Assets/AppList.scale-200.png deleted file mode 100644 index a162005d..00000000 Binary files a/samples/electron/Assets/AppList.scale-200.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.scale-400.png b/samples/electron/Assets/AppList.scale-400.png deleted file mode 100644 index d24dcbbe..00000000 Binary files a/samples/electron/Assets/AppList.scale-400.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-16.png b/samples/electron/Assets/AppList.targetsize-16.png deleted file mode 100644 index bdf43892..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-16.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-16_altform-unplated.png b/samples/electron/Assets/AppList.targetsize-16_altform-unplated.png deleted file mode 100644 index bdf43892..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-16_altform-unplated.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-20.png b/samples/electron/Assets/AppList.targetsize-20.png deleted file mode 100644 index 0fabfe2c..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-20.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-20_altform-unplated.png b/samples/electron/Assets/AppList.targetsize-20_altform-unplated.png deleted file mode 100644 index 0fabfe2c..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-20_altform-unplated.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-24.png b/samples/electron/Assets/AppList.targetsize-24.png deleted file mode 100644 index e86ebf14..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-24.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-24_altform-unplated.png b/samples/electron/Assets/AppList.targetsize-24_altform-unplated.png deleted file mode 100644 index e86ebf14..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-24_altform-unplated.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-256.png b/samples/electron/Assets/AppList.targetsize-256.png deleted file mode 100644 index d8f2f2aa..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-256.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-256_altform-unplated.png b/samples/electron/Assets/AppList.targetsize-256_altform-unplated.png deleted file mode 100644 index d8f2f2aa..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-256_altform-unplated.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-30.png b/samples/electron/Assets/AppList.targetsize-30.png deleted file mode 100644 index eee71aaa..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-30.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-30_altform-unplated.png b/samples/electron/Assets/AppList.targetsize-30_altform-unplated.png deleted file mode 100644 index eee71aaa..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-30_altform-unplated.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-32.png b/samples/electron/Assets/AppList.targetsize-32.png deleted file mode 100644 index 8500b46b..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-32.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-32_altform-unplated.png b/samples/electron/Assets/AppList.targetsize-32_altform-unplated.png deleted file mode 100644 index 8500b46b..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-32_altform-unplated.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-36.png b/samples/electron/Assets/AppList.targetsize-36.png deleted file mode 100644 index 9ccdb312..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-36.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-36_altform-unplated.png b/samples/electron/Assets/AppList.targetsize-36_altform-unplated.png deleted file mode 100644 index 9ccdb312..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-36_altform-unplated.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-40.png b/samples/electron/Assets/AppList.targetsize-40.png deleted file mode 100644 index 49f9a6a0..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-40.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-40_altform-unplated.png b/samples/electron/Assets/AppList.targetsize-40_altform-unplated.png deleted file mode 100644 index 49f9a6a0..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-40_altform-unplated.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-48.png b/samples/electron/Assets/AppList.targetsize-48.png deleted file mode 100644 index e08734f8..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-48.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-48_altform-unplated.png b/samples/electron/Assets/AppList.targetsize-48_altform-unplated.png deleted file mode 100644 index e08734f8..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-48_altform-unplated.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-60.png b/samples/electron/Assets/AppList.targetsize-60.png deleted file mode 100644 index bb700593..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-60.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-60_altform-unplated.png b/samples/electron/Assets/AppList.targetsize-60_altform-unplated.png deleted file mode 100644 index bb700593..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-60_altform-unplated.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-64.png b/samples/electron/Assets/AppList.targetsize-64.png deleted file mode 100644 index 2c32c2bc..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-64.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-64_altform-unplated.png b/samples/electron/Assets/AppList.targetsize-64_altform-unplated.png deleted file mode 100644 index 2c32c2bc..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-64_altform-unplated.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-72.png b/samples/electron/Assets/AppList.targetsize-72.png deleted file mode 100644 index 9eb82f00..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-72.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-72_altform-unplated.png b/samples/electron/Assets/AppList.targetsize-72_altform-unplated.png deleted file mode 100644 index 9eb82f00..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-72_altform-unplated.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-80.png b/samples/electron/Assets/AppList.targetsize-80.png deleted file mode 100644 index df4cea4c..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-80.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-80_altform-unplated.png b/samples/electron/Assets/AppList.targetsize-80_altform-unplated.png deleted file mode 100644 index df4cea4c..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-80_altform-unplated.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-96.png b/samples/electron/Assets/AppList.targetsize-96.png deleted file mode 100644 index ad36b4b3..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-96.png and /dev/null differ diff --git a/samples/electron/Assets/AppList.targetsize-96_altform-unplated.png b/samples/electron/Assets/AppList.targetsize-96_altform-unplated.png deleted file mode 100644 index ad36b4b3..00000000 Binary files a/samples/electron/Assets/AppList.targetsize-96_altform-unplated.png and /dev/null differ diff --git a/samples/electron/Assets/LockScreenLogo.png b/samples/electron/Assets/LockScreenLogo.png new file mode 100644 index 00000000..770ffb20 Binary files /dev/null and b/samples/electron/Assets/LockScreenLogo.png differ diff --git a/samples/electron/Assets/LockScreenLogo.scale-200.png b/samples/electron/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 00000000..f2291177 Binary files /dev/null and b/samples/electron/Assets/LockScreenLogo.scale-200.png differ diff --git a/samples/electron/Assets/MedTile.png b/samples/electron/Assets/MedTile.png deleted file mode 100644 index ee285b96..00000000 Binary files a/samples/electron/Assets/MedTile.png and /dev/null differ diff --git a/samples/electron/Assets/MedTile.scale-125.png b/samples/electron/Assets/MedTile.scale-125.png deleted file mode 100644 index 7b3430d0..00000000 Binary files a/samples/electron/Assets/MedTile.scale-125.png and /dev/null differ diff --git a/samples/electron/Assets/MedTile.scale-150.png b/samples/electron/Assets/MedTile.scale-150.png deleted file mode 100644 index 5df9f2e3..00000000 Binary files a/samples/electron/Assets/MedTile.scale-150.png and /dev/null differ diff --git a/samples/electron/Assets/MedTile.scale-400.png b/samples/electron/Assets/MedTile.scale-400.png deleted file mode 100644 index 60b8401c..00000000 Binary files a/samples/electron/Assets/MedTile.scale-400.png and /dev/null differ diff --git a/samples/electron/Assets/SplashScreen.png b/samples/electron/Assets/SplashScreen.png new file mode 100644 index 00000000..106123b0 Binary files /dev/null and b/samples/electron/Assets/SplashScreen.png differ diff --git a/samples/electron/Assets/SplashScreen.scale-200.png b/samples/electron/Assets/SplashScreen.scale-200.png new file mode 100644 index 00000000..5890acb7 Binary files /dev/null and b/samples/electron/Assets/SplashScreen.scale-200.png differ diff --git a/samples/electron/Assets/Square150x150Logo.png b/samples/electron/Assets/Square150x150Logo.png new file mode 100644 index 00000000..b45ced33 Binary files /dev/null and b/samples/electron/Assets/Square150x150Logo.png differ diff --git a/samples/electron/Assets/MedTile.scale-200.png b/samples/electron/Assets/Square150x150Logo.scale-200.png similarity index 100% rename from samples/electron/Assets/MedTile.scale-200.png rename to samples/electron/Assets/Square150x150Logo.scale-200.png diff --git a/samples/electron/Assets/Square44x44Logo.png b/samples/electron/Assets/Square44x44Logo.png new file mode 100644 index 00000000..6a037f3d Binary files /dev/null and b/samples/electron/Assets/Square44x44Logo.png differ diff --git a/samples/electron/Assets/Square44x44Logo.scale-200.png b/samples/electron/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 00000000..d667a00f Binary files /dev/null and b/samples/electron/Assets/Square44x44Logo.scale-200.png differ diff --git a/samples/electron/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/samples/electron/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 00000000..5147944e Binary files /dev/null and b/samples/electron/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/samples/electron/Assets/StoreLogo.png b/samples/electron/Assets/StoreLogo.png index d75339b6..23042e9d 100644 Binary files a/samples/electron/Assets/StoreLogo.png and b/samples/electron/Assets/StoreLogo.png differ diff --git a/samples/electron/Assets/StoreLogo.scale-125.png b/samples/electron/Assets/StoreLogo.scale-125.png deleted file mode 100644 index 6ecb44a4..00000000 Binary files a/samples/electron/Assets/StoreLogo.scale-125.png and /dev/null differ diff --git a/samples/electron/Assets/StoreLogo.scale-150.png b/samples/electron/Assets/StoreLogo.scale-150.png deleted file mode 100644 index 68330b47..00000000 Binary files a/samples/electron/Assets/StoreLogo.scale-150.png and /dev/null differ diff --git a/samples/electron/Assets/StoreLogo.scale-200.png b/samples/electron/Assets/StoreLogo.scale-200.png deleted file mode 100644 index ccb3035e..00000000 Binary files a/samples/electron/Assets/StoreLogo.scale-200.png and /dev/null differ diff --git a/samples/electron/Assets/StoreLogo.scale-400.png b/samples/electron/Assets/StoreLogo.scale-400.png deleted file mode 100644 index 722f763f..00000000 Binary files a/samples/electron/Assets/StoreLogo.scale-400.png and /dev/null differ diff --git a/samples/electron/Assets/Wide310x150Logo.png b/samples/electron/Assets/Wide310x150Logo.png new file mode 100644 index 00000000..6d748b09 Binary files /dev/null and b/samples/electron/Assets/Wide310x150Logo.png differ diff --git a/samples/electron/Assets/Wide310x150Logo.scale-200.png b/samples/electron/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 00000000..106123b0 Binary files /dev/null and b/samples/electron/Assets/Wide310x150Logo.scale-200.png differ diff --git a/samples/electron/Assets/WideTile.png b/samples/electron/Assets/WideTile.png deleted file mode 100644 index fca4b440..00000000 Binary files a/samples/electron/Assets/WideTile.png and /dev/null differ diff --git a/samples/electron/Assets/WideTile.scale-125.png b/samples/electron/Assets/WideTile.scale-125.png deleted file mode 100644 index 8b2f47a6..00000000 Binary files a/samples/electron/Assets/WideTile.scale-125.png and /dev/null differ diff --git a/samples/electron/Assets/WideTile.scale-150.png b/samples/electron/Assets/WideTile.scale-150.png deleted file mode 100644 index 1d8fb4f5..00000000 Binary files a/samples/electron/Assets/WideTile.scale-150.png and /dev/null differ diff --git a/samples/electron/Assets/WideTile.scale-200.png b/samples/electron/Assets/WideTile.scale-200.png deleted file mode 100644 index 91abeb72..00000000 Binary files a/samples/electron/Assets/WideTile.scale-200.png and /dev/null differ diff --git a/samples/electron/Assets/WideTile.scale-400.png b/samples/electron/Assets/WideTile.scale-400.png deleted file mode 100644 index 5798c4d2..00000000 Binary files a/samples/electron/Assets/WideTile.scale-400.png and /dev/null differ diff --git a/samples/electron/Assets/app.ico b/samples/electron/Assets/app.ico deleted file mode 100644 index c72e0751..00000000 Binary files a/samples/electron/Assets/app.ico and /dev/null differ diff --git a/samples/electron/appxmanifest.xml b/samples/electron/appxmanifest.xml index a0c3bdb3..196194ae 100644 --- a/samples/electron/appxmanifest.xml +++ b/samples/electron/appxmanifest.xml @@ -1,15 +1,15 @@ - + - + xmlns:uap10="http://schemas.microsoft.com/appx/manifest/uap/windows10/10" + IgnorableNamespaces="uap uap2 uap3 rescap desktop desktop6 uap10"> - - + + @@ -41,9 +41,9 @@ DisplayName="Electron winapp Sample" Description="Electron Application" BackgroundColor="transparent" - Square150x150Logo="Assets\MedTile.png" - Square44x44Logo="Assets\AppList.png"> - + Square150x150Logo="Assets\Square150x150Logo.png" + Square44x44Logo="Assets\Square44x44Logo.png"> + diff --git a/samples/electron/package-lock.json b/samples/electron/package-lock.json index fb2f81b8..81881aba 100644 --- a/samples/electron/package-lock.json +++ b/samples/electron/package-lock.json @@ -22,8 +22,8 @@ "@electron-forge/plugin-auto-unpack-natives": "^7.10.2", "@electron-forge/plugin-fuses": "^7.10.2", "@electron/fuses": "^1.8.0", - "@microsoft/winappcli": "^0.2.0", - "electron": "39.8.5", + "@microsoft/winappcli": "^0.1.8", + "electron": "39.2.7", "nan": "^2.24.0", "node-addon-api": "^8.5.0", "node-gyp": "^12.1.0" @@ -1178,9 +1178,9 @@ } }, "node_modules/@microsoft/winappcli": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@microsoft/winappcli/-/winappcli-0.2.0.tgz", - "integrity": "sha512-PU6kmCxE1F+fLyzcnkhnGqJFLKWBMGr8FJ1ooCQgzkJOsEj0vbGLG1opie8XeVaYbjQBN77vZ4AYqwB2pPTupQ==", + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@microsoft/winappcli/-/winappcli-0.1.8.tgz", + "integrity": "sha512-n6WGOtqfIF748GlPIPOWI5J2yMNTeyWZ8mFcYOlmv9zAa5ztWPxnNQIZNrw5xSvRLx23x/hTt/3geQn3qYx7Yg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1188,10 +1188,10 @@ "win32" ], "bin": { - "winapp": "dist/cli.js" + "winapp": "cli.js" }, "engines": { - "node": ">=18.0.0" + "node": ">=14.0.0" } }, "node_modules/@nodelib/fs.scandir": { @@ -1671,9 +1671,9 @@ } }, "node_modules/@xmldom/xmldom": { - "version": "0.8.12", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.12.tgz", - "integrity": "sha512-9k/gHF6n/pAi/9tqr3m3aqkuiNosYTurLLUtc7xQ9sxB/wm7WPygCv8GYa6mS0fLJEHhqMC1ATYhz++U/lRHqg==", + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", "dev": true, "license": "MIT", "engines": { @@ -2610,9 +2610,9 @@ "license": "MIT" }, "node_modules/electron": { - "version": "39.8.5", - "resolved": "https://registry.npmjs.org/electron/-/electron-39.8.5.tgz", - "integrity": "sha512-q6+LiQIcTadSyvtPgLDQkCtVA9jQJXQVMrQcctfOJILh6OFMN+UJJLRkuUTy8CZDYeCIBn1ZycqsL1dAXugxZA==", + "version": "39.2.7", + "resolved": "https://registry.npmjs.org/electron/-/electron-39.2.7.tgz", + "integrity": "sha512-KU0uFS6LSTh4aOIC3miolcbizOFP7N1M46VTYVfqIgFiuA2ilfNaOHLDS9tCMvwwHRowAsvqBrh9NgMXcTOHCQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -4540,9 +4540,9 @@ } }, "node_modules/lodash": { - "version": "4.18.1", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", - "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "dev": true, "license": "MIT" }, @@ -5892,9 +5892,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", - "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, "license": "MIT", "engines": { @@ -7008,9 +7008,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { diff --git a/samples/electron/package.json b/samples/electron/package.json index 756005b0..a19cc146 100644 --- a/samples/electron/package.json +++ b/samples/electron/package.json @@ -34,8 +34,8 @@ "@electron-forge/plugin-auto-unpack-natives": "^7.10.2", "@electron-forge/plugin-fuses": "^7.10.2", "@electron/fuses": "^1.8.0", - "@microsoft/winappcli": "^0.2.0", - "electron": "39.8.5", + "@microsoft/winappcli": "^0.1.8", + "electron": "39.2.7", "nan": "^2.24.0", "node-addon-api": "^8.5.0", "node-gyp": "^12.1.0" diff --git a/samples/electron/winapp.yaml b/samples/electron/winapp.yaml index 3ae0ae46..23dbb170 100644 --- a/samples/electron/winapp.yaml +++ b/samples/electron/winapp.yaml @@ -2,14 +2,14 @@ packages: - name: Microsoft.Windows.CppWinRT version: 2.0.250303.1 - name: Microsoft.Windows.SDK.BuildTools - version: 10.0.28000.1-RTM + version: 10.0.26100.6584 - name: Microsoft.WindowsAppSDK - version: 2.0.0-experimental6 + version: 2.0.250930001-experimental1 - name: Microsoft.Windows.ImplementationLibrary - version: 1.0.260126.7 + version: 1.0.250325.1 - name: Microsoft.Windows.SDK.CPP - version: 10.0.28000.1-RTM + version: 10.0.26100.6584 - name: Microsoft.Windows.SDK.CPP.x64 - version: 10.0.28000.1-RTM + version: 10.0.26100.6584 - name: Microsoft.Windows.SDK.CPP.arm64 - version: 10.0.28000.1-RTM + version: 10.0.26100.6584 diff --git a/samples/flutter-app/README.md b/samples/flutter-app/README.md index 961efd05..779ea86d 100644 --- a/samples/flutter-app/README.md +++ b/samples/flutter-app/README.md @@ -38,8 +38,11 @@ winapp cert generate --if-exists skip # Build the app flutter build windows -# Run with identity -winapp run .\build\windows\x64\runner\Release +# Apply debug identity +winapp create-debug-identity .\build\windows\x64\runner\Release\flutter_app.exe + +# Run the app +.\build\windows\x64\runner\Release\flutter_app.exe ``` The Flutter window will display: diff --git a/samples/flutter-app/lib/main.dart b/samples/flutter-app/lib/main.dart index 3803fdd6..0e6101e1 100644 --- a/samples/flutter-app/lib/main.dart +++ b/samples/flutter-app/lib/main.dart @@ -54,17 +54,10 @@ Future getWindowsAppRuntimeVersion() async { } /// Shows a Windows App SDK toast notification via a native method channel. -Future showNotification() async { - if (!Platform.isWindows) return null; - try { - const channel = MethodChannel('com.example/winapp_sdk'); - await channel.invokeMethod('showNotification'); - return null; - } on PlatformException catch (e) { - return '${e.code}: ${e.message}'; - } catch (e) { - return e.toString(); - } +Future showNotification() async { + if (!Platform.isWindows) return; + const channel = MethodChannel('com.example/winapp_sdk'); + await channel.invokeMethod('showNotification'); } void main() { @@ -178,17 +171,7 @@ class _MyHomePageState extends State { const SizedBox(height: 24), if (_packageFamilyName != null) ElevatedButton.icon( - onPressed: () async { - final error = await showNotification(); - if (error != null && mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Notification error: $error'), - backgroundColor: Colors.red, - ), - ); - } - }, + onPressed: showNotification, icon: const Icon(Icons.notifications), label: const Text('Show Notification'), ) diff --git a/samples/flutter-app/pubspec.lock b/samples/flutter-app/pubspec.lock index 77c0e79a..3e1328b2 100644 --- a/samples/flutter-app/pubspec.lock +++ b/samples/flutter-app/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: async - sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.13.1" + version: "2.13.0" boolean_selector: dependency: transitive description: @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: characters - sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.0" clock: dependency: transitive description: @@ -45,10 +45,10 @@ packages: dependency: "direct main" description: name: cupertino_icons - sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.9" + version: "1.0.8" fake_async: dependency: transitive description: @@ -61,10 +61,10 @@ packages: dependency: "direct main" description: name: ffi - sha256: "6d7fd89431262d8f3125e81b50d3847a091d846eafcd4fdb88dd06f36d705a45" + sha256: d07d37192dbf97461359c1518788f203b0c9102cfd2c35a716b823741219542c url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.1.5" flutter: dependency: "direct main" description: flutter @@ -119,18 +119,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.17" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.1" meta: dependency: transitive description: @@ -196,10 +196,10 @@ packages: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.7" vector_math: dependency: transitive description: diff --git a/samples/flutter-app/winapp.yaml b/samples/flutter-app/winapp.yaml index 9f7e3925..804022c5 100644 --- a/samples/flutter-app/winapp.yaml +++ b/samples/flutter-app/winapp.yaml @@ -2,14 +2,14 @@ packages: - name: Microsoft.Windows.CppWinRT version: 2.0.250303.1 - name: Microsoft.Windows.SDK.BuildTools - version: 10.0.26100.7705 + version: 10.0.26100.7463 - name: Microsoft.WindowsAppSDK - version: 1.8.260317003 + version: 1.8.260101001 - name: Microsoft.Windows.ImplementationLibrary version: 1.0.260126.7 - name: Microsoft.Windows.SDK.CPP - version: 10.0.26100.7705 + version: 10.0.26100.7463 - name: Microsoft.Windows.SDK.CPP.x64 - version: 10.0.26100.7705 + version: 10.0.26100.7463 - name: Microsoft.Windows.SDK.CPP.arm64 - version: 10.0.26100.7705 + version: 10.0.26100.7463 diff --git a/samples/flutter-app/windows/runner/winapp_sdk_plugin.cpp b/samples/flutter-app/windows/runner/winapp_sdk_plugin.cpp index bc9c39db..cc9223ba 100644 --- a/samples/flutter-app/windows/runner/winapp_sdk_plugin.cpp +++ b/samples/flutter-app/windows/runner/winapp_sdk_plugin.cpp @@ -10,7 +10,6 @@ #include void RegisterWinAppSdkPlugin(flutter::FlutterEngine* engine) { - auto channel = std::make_unique>( engine->messenger(), "com.example/winapp_sdk", &flutter::StandardMethodCodec::GetInstance()); @@ -34,6 +33,7 @@ void RegisterWinAppSdkPlugin(flutter::FlutterEngine* engine) { } } else if (call.method_name() == "showNotification") { try { + // Build a simple toast notification using Windows App SDK auto builder = winrt::Microsoft::Windows::AppNotifications::Builder:: AppNotificationBuilder(); builder.AddText(L"Hello from Flutter!"); diff --git a/samples/rust-app/Cargo.lock b/samples/rust-app/Cargo.lock index 5af8482b..68a80516 100644 --- a/samples/rust-app/Cargo.lock +++ b/samples/rust-app/Cargo.lock @@ -46,54 +46,32 @@ checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "windows" -version = "0.62.2" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" -dependencies = [ - "windows-collections", - "windows-core", - "windows-future", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" dependencies = [ "windows-core", + "windows-targets", ] [[package]] name = "windows-core" -version = "0.62.2" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ "windows-implement", "windows-interface", - "windows-link", "windows-result", "windows-strings", -] - -[[package]] -name = "windows-future" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" -dependencies = [ - "windows-core", - "windows-link", - "windows-threading", + "windows-targets", ] [[package]] name = "windows-implement" -version = "0.60.2" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" dependencies = [ "proc-macro2", "quote", @@ -102,9 +80,9 @@ dependencies = [ [[package]] name = "windows-interface" -version = "0.59.3" +version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" dependencies = [ "proc-macro2", "quote", @@ -112,44 +90,84 @@ dependencies = [ ] [[package]] -name = "windows-link" -version = "0.2.1" +name = "windows-result" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets", +] [[package]] -name = "windows-numerics" -version = "0.3.1" +name = "windows-strings" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows-core", - "windows-link", + "windows-result", + "windows-targets", ] [[package]] -name = "windows-result" -version = "0.4.1" +name = "windows-targets" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows-link", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] -name = "windows-strings" -version = "0.5.1" +name = "windows_aarch64_gnullvm" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] -name = "windows-threading" -version = "0.2.1" +name = "windows_aarch64_msvc" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" -dependencies = [ - "windows-link", -] +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" diff --git a/samples/rust-app/Cargo.toml b/samples/rust-app/Cargo.toml index 91c74d3a..420e882d 100644 --- a/samples/rust-app/Cargo.toml +++ b/samples/rust-app/Cargo.toml @@ -4,4 +4,4 @@ version = "0.1.0" edition = "2024" [dependencies] -windows = { version = "0.62.2", features = ["ApplicationModel", "UI_Notifications", "Data_Xml_Dom"] } +windows = { version = "0.58", features = ["ApplicationModel", "UI_Notifications", "Data_Xml_Dom"] } diff --git a/samples/tauri-app/README.md b/samples/tauri-app/README.md index 487e6726..441faf7c 100644 --- a/samples/tauri-app/README.md +++ b/samples/tauri-app/README.md @@ -1,68 +1,7 @@ -# Tauri Sample Application +# Tauri + Vanilla -This sample demonstrates how to use winapp CLI with a Tauri application to add package identity, use Windows APIs, and package as MSIX. +This template should help get you started developing with Tauri in vanilla HTML, CSS and Javascript. -For a complete step-by-step guide, see the [Tauri Getting Started Guide](../../docs/guides/tauri.md). +## Recommended IDE Setup -## What This Sample Shows - -- Tauri desktop application (Rust + vanilla HTML/JS) -- Using Windows `ApplicationModel` APIs from Rust to retrieve package identity -- Sending Windows toast notifications via `windows::UI::Notifications` -- Conditionally enabling UI features based on package identity status -- MSIX packaging with app manifest and assets - -## Prerequisites - -- [Tauri prerequisites](https://v2.tauri.app/start/prerequisites/) (Rust, system dependencies) -- Node.js -- winapp CLI installed via winget: `winget install Microsoft.winappcli --source winget` - -## Building and Running - -### First Time Setup - -```powershell -# Install npm dependencies -npm install - -# Generate a dev certificate (first time only) -winapp cert generate --if-exists skip -``` - -### Run - -```powershell -# Build the Rust backend -cargo build --manifest-path src-tauri/Cargo.toml - -# Stage the exe (the target\debug folder contains build artifacts that aren't needed) -mkdir -Force dist | Out-Null -copy .\src-tauri\target\debug\tauri-app.exe .\dist\ - -# Run with identity -winapp run .\dist -``` - -Or use the npm script which does all of the above: - -```powershell -npm run tauri:dev:withidentity -``` - -The Tauri window will display the Package Family Name. The "Send Notification" button is enabled only when the app has package identity — click it to send a Windows toast notification. - -### Package as MSIX - -```powershell -npm run pack:msix -``` - -This builds in release mode, stages the exe to `dist/`, and packages+signs it. Then install: - -```powershell -# Install certificate (first time only, requires admin) -winapp cert install .\devcert.pfx -``` - -Double-click the generated `.msix` file to install. The app will be available in your Start Menu. +- [VS Code](https://code.visualstudio.com/) + [Tauri](https://marketplace.visualstudio.com/items?itemName=tauri-apps.tauri-vscode) + [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) diff --git a/samples/tauri-app/package.json b/samples/tauri-app/package.json index 3592c41f..35cc494d 100644 --- a/samples/tauri-app/package.json +++ b/samples/tauri-app/package.json @@ -5,8 +5,7 @@ "type": "module", "scripts": { "tauri": "tauri", - "tauri:dev:withidentity": "cargo build --manifest-path src-tauri/Cargo.toml && (if not exist dist mkdir dist) && copy /Y src-tauri\\target\\debug\\tauri-app.exe dist\\ >nul && winapp run .\\dist", - "pack:msix": "npm run tauri -- build && (if not exist dist mkdir dist) && copy /Y src-tauri\\target\\release\\tauri-app.exe dist\\ >nul && winapp pack .\\dist --cert .\\devcert.pfx" + "tauri:dev:withidentity": "cargo build --manifest-path src-tauri/Cargo.toml && winapp create-debug-identity src-tauri/target/debug/tauri-app.exe && .\\src-tauri\\target\\debug\\tauri-app.exe" }, "devDependencies": { "@tauri-apps/cli": "^2" diff --git a/samples/tauri-app/src-tauri/Cargo.toml b/samples/tauri-app/src-tauri/Cargo.toml index fb3139ff..230934e7 100644 --- a/samples/tauri-app/src-tauri/Cargo.toml +++ b/samples/tauri-app/src-tauri/Cargo.toml @@ -24,5 +24,5 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" [target.'cfg(windows)'.dependencies] -windows = { version = "0.62.2", features = ["ApplicationModel", "UI_Notifications", "Data_Xml_Dom"] } +windows = { version = "0.58", features = ["ApplicationModel", "UI_Notifications", "Data_Xml_Dom"] } diff --git a/samples/wpf-app/wpf-app.csproj b/samples/wpf-app/wpf-app.csproj index 7f7c88c7..8ff39233 100644 --- a/samples/wpf-app/wpf-app.csproj +++ b/samples/wpf-app/wpf-app.csproj @@ -10,10 +10,10 @@ - + - + diff --git a/scripts/generate-llm-docs.ps1 b/scripts/generate-llm-docs.ps1 index 815371f1..8a9eac46 100644 --- a/scripts/generate-llm-docs.ps1 +++ b/scripts/generate-llm-docs.ps1 @@ -237,7 +237,7 @@ $SkillDescriptions = @{ "package" = "Package a Windows app as an MSIX installer for distribution or testing. Use when creating a Windows installer, packaging an Electron/Flutter/.NET/Rust/C++/Tauri app for Windows, building an MSIX, distributing a desktop app, packaging a console app or CLI tool, or adding MSIX packaging to a build script or CI/CD pipeline." "identity" = "Enable Windows package identity for desktop apps to access Windows APIs like push notifications, background tasks, share target, and startup tasks. Use when adding Windows notifications, background tasks, or other identity-requiring Windows features to a desktop app." "signing" = "Create and manage code signing certificates for Windows apps and MSIX packages. Use when generating a certificate, signing a Windows app or installer, or fixing certificate trust issues." - "manifest" = "Create and edit Windows app manifest files (appxmanifest.xml) that define app identity, capabilities, and visual assets, or generate new assets from existing images. Use when creating a Windows app manifest for any app type (GUI, console, CLI tool, service), adding Windows capabilities, generating new app icons and assets, or adding execution aliases, file associations, protocol handlers, or other app extensions." + "manifest" = "Create and edit Windows app manifest files (appxmanifest.xml) that define app identity, capabilities, and visual assets. Use when creating a Windows app manifest for any app type (GUI, console, CLI tool, service), adding Windows capabilities, updating app icons and assets, or adding execution aliases, file associations, protocol handlers, or other app extensions." "troubleshoot" = "Diagnose and fix common Windows app packaging, signing, identity, and SDK errors. Use when encountering errors with MSIX packaging, certificate signing, Windows SDK setup, or app installation." "frameworks" = "Framework-specific Windows development guidance for Electron, .NET (WPF, WinForms), C++, Rust, Flutter, and Tauri. Use when packaging or adding Windows features to an Electron app, .NET desktop app, Flutter app, Tauri app, Rust app, or C++ app." } diff --git a/scripts/package-msix.ps1 b/scripts/package-msix.ps1 index bf8e35e6..412f30af 100644 --- a/scripts/package-msix.ps1 +++ b/scripts/package-msix.ps1 @@ -223,19 +223,18 @@ try Write-Host "[COPY] Creating $Architecture package layout..." -ForegroundColor Blue - # Copy exe and native runtime dependencies (e.g., libSkiaSharp.dll), exclude PDBs - Write-Host " - Copying binaries from $SourceBinPath..." -ForegroundColor Gray + # Copy only the exe from the source + Write-Host " - Copying winapp.exe from $SourceBinPath..." -ForegroundColor Gray $SourceExe = Join-Path $SourceBinPath "winapp.exe" + $TargetExe = Join-Path $LayoutPath "winapp.exe" if (-not (Test-Path $SourceExe)) { Write-Error "winapp.exe not found at $SourceExe" return } - Get-ChildItem -Path $SourceBinPath -File | Where-Object { $_.Extension -ne '.pdb' } | ForEach-Object { - Copy-Item $_.FullName $LayoutPath -Force - Write-Host " - Copied $($_.Name)" -ForegroundColor Gray - } + Copy-Item $SourceExe $TargetExe -Force + Write-Host " - Copied winapp.exe" -ForegroundColor Gray # Copy Assets folder $TargetAssetsPath = Join-Path $LayoutPath "Assets" diff --git a/scripts/package-nuget.ps1 b/scripts/package-nuget.ps1 index 2c81c923..bd877a21 100644 --- a/scripts/package-nuget.ps1 +++ b/scripts/package-nuget.ps1 @@ -147,13 +147,8 @@ try New-Item -ItemType Directory -Path $ToolsX64Path -Force | Out-Null New-Item -ItemType Directory -Path $ToolsArm64Path -Force | Out-Null - # Copy all files except PDBs (includes native runtime dependencies like libSkiaSharp.dll) - Get-ChildItem -Path $X64Path -File | Where-Object { $_.Extension -ne '.pdb' } | ForEach-Object { - Copy-Item $_.FullName $ToolsX64Path -Force - } - Get-ChildItem -Path $Arm64Path -File | Where-Object { $_.Extension -ne '.pdb' } | ForEach-Object { - Copy-Item $_.FullName $ToolsArm64Path -Force - } + Copy-Item -Path "$X64Path\*.exe" -Destination $ToolsX64Path -Recurse -Force + Copy-Item -Path "$Arm64Path\*.exe" -Destination $ToolsArm64Path -Recurse -Force Write-Host "[COPY] CLI binaries copied successfully" -ForegroundColor Green diff --git a/scripts/test-e2e-electron.ps1 b/scripts/test-e2e-electron.ps1 index c11651e6..e26e5eb2 100644 --- a/scripts/test-e2e-electron.ps1 +++ b/scripts/test-e2e-electron.ps1 @@ -371,7 +371,7 @@ try { Write-TestStep "Adding Electron debug identity..." 11 - $addIdentityCommand = "npx winapp node add-electron-debug-identity" + $addIdentityCommand = "npx winapp node add-electron-debug-identity --no-install" Assert-Command $addIdentityCommand "Failed to add Electron debug identity" # ======================================================================== diff --git a/src/winapp-CLI/Directory.Packages.props b/src/winapp-CLI/Directory.Packages.props index 8985472c..0c457ae5 100644 --- a/src/winapp-CLI/Directory.Packages.props +++ b/src/winapp-CLI/Directory.Packages.props @@ -1,14 +1,13 @@ - - + + - - + + - - + diff --git a/src/winapp-CLI/WinApp.Cli.Tests/AppLauncherServiceTests.cs b/src/winapp-CLI/WinApp.Cli.Tests/AppLauncherServiceTests.cs deleted file mode 100644 index 23aff463..00000000 --- a/src/winapp-CLI/WinApp.Cli.Tests/AppLauncherServiceTests.cs +++ /dev/null @@ -1,76 +0,0 @@ -// Copyright (c) Microsoft Corporation and Contributors. All rights reserved. -// Licensed under the MIT License. - -using WinApp.Cli.Services; - -namespace WinApp.Cli.Tests; - -[TestClass] -public class AppLauncherServiceTests -{ - private readonly AppLauncherService _service = new( - new Microsoft.Extensions.Logging.Abstractions.NullLogger()); - - // Known publisher → publisherId mappings obtained from Get-AppxPackage on Windows. - // These are the ground truth values computed by the Windows platform. - - [TestMethod] - [DataRow( - "CN=Microsoft Windows, O=Microsoft Corporation, L=Redmond, S=Washington, C=US", - "cw5n1h2txyewy", - DisplayName = "Microsoft Windows publisher")] - [DataRow( - "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US", - "8wekyb3d8bbwe", - DisplayName = "Microsoft Corporation publisher")] - [DataRow( - "CN=CA0D5344-F590-41F9-BE2C-16BE6FCEE1DF", - "rn9aeerfb38dg", - DisplayName = "GUID-style publisher")] - [DataRow( - "CN=83564403-0B26-46B8-9D84-040F43691D31", - "dt26b99r8h8gj", - DisplayName = "GUID-style publisher 2")] - [DataRow( - "CN=Metulev", - "j3adjyj8sqwmw", - DisplayName = "Simple CN publisher")] - public void ComputePackageFamilyName_MatchesWindowsValue(string publisher, string expectedPublisherId) - { - var pfn = _service.ComputePackageFamilyName("TestPackage", publisher); - - Assert.AreEqual($"TestPackage_{expectedPublisherId}", pfn); - } - - [TestMethod] - public void ComputePackageFamilyName_PublisherIsCaseSensitive() - { - // Windows treats publisher DN as case-sensitive for hash computation. - // "CN=Test" and "cn=test" produce different publisher IDs. - var pfn1 = _service.ComputePackageFamilyName("Pkg", "CN=Test"); - var pfn2 = _service.ComputePackageFamilyName("Pkg", "cn=test"); - - Assert.AreNotEqual(pfn1, pfn2, "Publisher comparison should be case-sensitive"); - } - - [TestMethod] - public void ComputePackageFamilyName_PublisherIdIs13Chars() - { - var pfn = _service.ComputePackageFamilyName("Pkg", "CN=AnyPublisher"); - - // Format: {name}_{publisherId} where publisherId is exactly 13 chars - var parts = pfn.Split('_'); - Assert.AreEqual(2, parts.Length, "PFN should have exactly one underscore"); - Assert.AreEqual(13, parts[1].Length, "Publisher ID should be exactly 13 characters"); - } - - [TestMethod] - public void ComputePackageFamilyName_PublisherIdIsLowercase() - { - var pfn = _service.ComputePackageFamilyName("Pkg", "CN=SomePublisher"); - var publisherId = pfn.Split('_')[1]; - - Assert.AreEqual(publisherId, publisherId.ToLowerInvariant(), - "Publisher ID should be lowercase"); - } -} diff --git a/src/winapp-CLI/WinApp.Cli.Tests/AppxManifestDocumentTests.cs b/src/winapp-CLI/WinApp.Cli.Tests/AppxManifestDocumentTests.cs deleted file mode 100644 index 07068827..00000000 --- a/src/winapp-CLI/WinApp.Cli.Tests/AppxManifestDocumentTests.cs +++ /dev/null @@ -1,780 +0,0 @@ -// Copyright (c) Microsoft Corporation and Contributors. All rights reserved. -// Licensed under the MIT License. - -using WinApp.Cli.Services; - -namespace WinApp.Cli.Tests; - -[TestClass] -public class AppxManifestDocumentTests -{ - private const string MinimalManifest = """ - - - - - Test App - - - - - - - - - - - - - - """; - - private const string BareMinimalManifest = """ - - - - - """; - - #region Parse / ToXml Round-Trip - - [TestMethod] - public void Parse_AndToXml_RoundTrips_PreservesContent() - { - var doc = AppxManifestDocument.Parse(MinimalManifest); - var xml = doc.ToXml(); - - Assert.Contains("TestApp", xml); - Assert.Contains("CN=Test", xml); - Assert.Contains("1.0.0.0", xml); - } - - [TestMethod] - public void Load_FromFile_AndSave_RoundTrips() - { - var tempDir = Path.Combine(Path.GetTempPath(), $"AppxDocTest_{Guid.NewGuid():N}"); - Directory.CreateDirectory(tempDir); - try - { - var filePath = Path.Combine(tempDir, "appxmanifest.xml"); - File.WriteAllText(filePath, MinimalManifest); - - var doc = AppxManifestDocument.Load(filePath); - Assert.AreEqual("TestApp", doc.IdentityName); - - doc.IdentityName = "ModifiedApp"; - var savePath = Path.Combine(tempDir, "saved.xml"); - doc.Save(savePath); - - var reloaded = AppxManifestDocument.Load(savePath); - Assert.AreEqual("ModifiedApp", reloaded.IdentityName); - - // Verify UTF-8 no BOM - var bytes = File.ReadAllBytes(savePath); - Assert.IsFalse(bytes.Length >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF, - "File should not have a UTF-8 BOM"); - } - finally - { - Directory.Delete(tempDir, true); - } - } - - [TestMethod] - public void Load_FromStream_Works() - { - using var stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(MinimalManifest)); - var doc = AppxManifestDocument.Load(stream); - - Assert.AreEqual("TestApp", doc.IdentityName); - Assert.AreEqual("CN=Test", doc.IdentityPublisher); - } - - #endregion - - #region Identity Properties - - [TestMethod] - public void IdentityName_GetAndSet() - { - var doc = AppxManifestDocument.Parse(MinimalManifest); - - Assert.AreEqual("TestApp", doc.IdentityName); - doc.IdentityName = "NewName"; - Assert.AreEqual("NewName", doc.IdentityName); - } - - [TestMethod] - public void IdentityPublisher_GetAndSet() - { - var doc = AppxManifestDocument.Parse(MinimalManifest); - - Assert.AreEqual("CN=Test", doc.IdentityPublisher); - doc.IdentityPublisher = "CN=NewPublisher"; - Assert.AreEqual("CN=NewPublisher", doc.IdentityPublisher); - } - - [TestMethod] - public void IdentityVersion_GetAndSet() - { - var doc = AppxManifestDocument.Parse(MinimalManifest); - - Assert.AreEqual("1.0.0.0", doc.IdentityVersion); - doc.IdentityVersion = "2.0.0.0"; - Assert.AreEqual("2.0.0.0", doc.IdentityVersion); - } - - [TestMethod] - public void IdentityProcessorArchitecture_GetAndSet() - { - var doc = AppxManifestDocument.Parse(MinimalManifest); - - Assert.AreEqual("x64", doc.IdentityProcessorArchitecture); - doc.IdentityProcessorArchitecture = "arm64"; - Assert.AreEqual("arm64", doc.IdentityProcessorArchitecture); - } - - [TestMethod] - public void IdentityProperties_SetNull_RemovesAttribute() - { - var doc = AppxManifestDocument.Parse(MinimalManifest); - - doc.IdentityProcessorArchitecture = null; - Assert.IsNull(doc.IdentityProcessorArchitecture); - - // Other attributes should be unaffected - Assert.AreEqual("TestApp", doc.IdentityName); - } - - [TestMethod] - public void IdentityProperties_CreatesIdentityElement_WhenMissing() - { - // A manifest with no Identity element - var xml = """ - - - """; - var doc = AppxManifestDocument.Parse(xml); - - Assert.IsNull(doc.IdentityName); - - doc.IdentityName = "CreatedApp"; - Assert.AreEqual("CreatedApp", doc.IdentityName); - Assert.IsNotNull(doc.GetIdentityElement()); - } - - [TestMethod] - public void IdentityProperties_SetNullOnMissingElement_IsNoOp() - { - var xml = """ - - - """; - var doc = AppxManifestDocument.Parse(xml); - - // Should not throw or create an Identity element - doc.IdentityName = null; - Assert.IsNull(doc.GetIdentityElement()); - } - - #endregion - - #region Application Properties - - [TestMethod] - public void ApplicationId_GetAndSet() - { - var doc = AppxManifestDocument.Parse(MinimalManifest); - - Assert.AreEqual("App", doc.ApplicationId); - doc.ApplicationId = "NewApp"; - Assert.AreEqual("NewApp", doc.ApplicationId); - } - - [TestMethod] - public void ApplicationExecutable_GetAndSet() - { - var doc = AppxManifestDocument.Parse(MinimalManifest); - - Assert.AreEqual("TestApp.exe", doc.ApplicationExecutable); - doc.ApplicationExecutable = "Other.exe"; - Assert.AreEqual("Other.exe", doc.ApplicationExecutable); - } - - [TestMethod] - public void ApplicationEntryPoint_GetAndSet() - { - var doc = AppxManifestDocument.Parse(MinimalManifest); - - Assert.AreEqual("Windows.FullTrustApplication", doc.ApplicationEntryPoint); - doc.ApplicationEntryPoint = "App.Main"; - Assert.AreEqual("App.Main", doc.ApplicationEntryPoint); - } - - [TestMethod] - public void ApplicationProperties_ReturnNull_WhenNoApplicationElement() - { - var doc = AppxManifestDocument.Parse(BareMinimalManifest); - - Assert.IsNull(doc.ApplicationId); - Assert.IsNull(doc.ApplicationExecutable); - Assert.IsNull(doc.ApplicationEntryPoint); - } - - [TestMethod] - public void ApplicationProperties_SetOnMissing_IsNoOp() - { - var doc = AppxManifestDocument.Parse(BareMinimalManifest); - - doc.ApplicationId = "ShouldNotThrow"; - Assert.IsNull(doc.ApplicationId); // still null because there's no Application element - } - - #endregion - - #region VisualElements - - [TestMethod] - public void VisualElementsDisplayName_GetAndSet() - { - var doc = AppxManifestDocument.Parse(MinimalManifest); - - Assert.AreEqual("Test App", doc.VisualElementsDisplayName); - doc.VisualElementsDisplayName = "New Name"; - Assert.AreEqual("New Name", doc.VisualElementsDisplayName); - } - - [TestMethod] - public void VisualElementsDisplayName_ReturnsNull_WhenMissing() - { - var doc = AppxManifestDocument.Parse(BareMinimalManifest); - Assert.IsNull(doc.VisualElementsDisplayName); - } - - #endregion - - #region Resource Languages - - [TestMethod] - public void GetResourceLanguages_ReturnsList() - { - var doc = AppxManifestDocument.Parse(MinimalManifest); - - var languages = doc.GetResourceLanguages(); - Assert.AreEqual(1, languages.Count); - Assert.AreEqual("en-US", languages[0]); - } - - [TestMethod] - public void GetResourceLanguages_ReturnsEmpty_WhenNoResources() - { - var doc = AppxManifestDocument.Parse(BareMinimalManifest); - - var languages = doc.GetResourceLanguages(); - Assert.AreEqual(0, languages.Count); - } - - [TestMethod] - public void SetResourceLanguages_SingleLanguage() - { - var doc = AppxManifestDocument.Parse(MinimalManifest); - - doc.SetResourceLanguages(["fr-FR"]); - var languages = doc.GetResourceLanguages(); - - Assert.AreEqual(1, languages.Count); - Assert.AreEqual("fr-FR", languages[0]); - } - - [TestMethod] - public void SetResourceLanguages_MultipleLanguages() - { - var doc = AppxManifestDocument.Parse(MinimalManifest); - - doc.SetResourceLanguages(["en-US", "fr-FR", "de-DE"]); - var languages = doc.GetResourceLanguages(); - - Assert.AreEqual(3, languages.Count); - Assert.AreEqual("en-US", languages[0]); - Assert.AreEqual("fr-FR", languages[1]); - Assert.AreEqual("de-DE", languages[2]); - } - - [TestMethod] - public void SetResourceLanguages_ReplacesExisting() - { - var doc = AppxManifestDocument.Parse(MinimalManifest); - - // Replace en-US with ja-JP - doc.SetResourceLanguages(["ja-JP"]); - var languages = doc.GetResourceLanguages(); - - Assert.AreEqual(1, languages.Count); - Assert.AreEqual("ja-JP", languages[0]); - Assert.DoesNotContain("en-US", doc.ToXml()); - } - - [TestMethod] - public void SetResourceLanguages_CreatesResourcesElement_WhenMissing() - { - var doc = AppxManifestDocument.Parse(BareMinimalManifest); - - doc.SetResourceLanguages(["en-US"]); - var languages = doc.GetResourceLanguages(); - - Assert.AreEqual(1, languages.Count); - Assert.AreEqual("en-US", languages[0]); - } - - #endregion - - #region Namespace Management - - [TestMethod] - public void AddIgnorableNamespace_AddsNew() - { - var xml = """ - - - """; - var doc = AppxManifestDocument.Parse(xml); - - doc.AddIgnorableNamespace("rescap"); - - var result = doc.ToXml(); - Assert.Contains("IgnorableNamespaces=\"uap rescap\"", result); - } - - [TestMethod] - public void AddIgnorableNamespace_SkipsDuplicate() - { - var xml = """ - - - """; - var doc = AppxManifestDocument.Parse(xml); - - doc.AddIgnorableNamespace("rescap"); - - var result = doc.ToXml(); - // Should not duplicate - Assert.AreEqual(1, CountOccurrences(result, "rescap")); - } - - [TestMethod] - public void AddIgnorableNamespace_CreatesAttribute_WhenMissing() - { - var xml = """ - - - """; - var doc = AppxManifestDocument.Parse(xml); - - doc.AddIgnorableNamespace("build"); - - var result = doc.ToXml(); - Assert.Contains("IgnorableNamespaces=\"build\"", result); - } - - [TestMethod] - public void EnsureNamespace_AddsXmlnsDeclaration() - { - var xml = """ - - - """; - var doc = AppxManifestDocument.Parse(xml); - - doc.EnsureNamespace("build", AppxManifestDocument.BuildNs); - - var result = doc.ToXml(); - Assert.Contains("xmlns:build=\"http://schemas.microsoft.com/developer/appx/2015/build\"", result); - } - - [TestMethod] - public void EnsureNamespace_DoesNotDuplicate() - { - var xml = """ - - - """; - var doc = AppxManifestDocument.Parse(xml); - - doc.EnsureNamespace("build", AppxManifestDocument.BuildNs); - - var result = doc.ToXml(); - Assert.AreEqual(1, CountOccurrences(result, "xmlns:build=")); - } - - #endregion - - #region Capabilities - - [TestMethod] - public void EnsureCapability_AddsNew() - { - var doc = AppxManifestDocument.Parse(BareMinimalManifest); - - doc.EnsureCapability("runFullTrust"); - - var xml = doc.ToXml(); - Assert.Contains("runFullTrust", xml); - Assert.Contains(" - - - - - """; - var doc = AppxManifestDocument.Parse(xml); - - // Adding the same capability (case-insensitive match) should not duplicate - doc.EnsureCapability("runFullTrust"); - - var result = doc.ToXml(); - Assert.AreEqual(1, CountOccurrences(result, "runFullTrust")); - } - - [TestMethod] - public void EnsureCapability_WithCustomNamespace() - { - var doc = AppxManifestDocument.Parse(BareMinimalManifest); - - doc.EnsureCapability("runFullTrust", AppxManifestDocument.RescapNs); - - var result = doc.ToXml(); - Assert.Contains("runFullTrust", result); - } - - [TestMethod] - public void EnsureCapability_CreatesCapabilitiesElement_WhenMissing() - { - var doc = AppxManifestDocument.Parse(BareMinimalManifest); - Assert.IsNull(doc.GetCapabilitiesElement()); - - doc.EnsureCapability("internetClient"); - - Assert.IsNotNull(doc.GetCapabilitiesElement()); - Assert.Contains("internetClient", doc.ToXml()); - } - - #endregion - - #region Build Metadata - - [TestMethod] - public void SetBuildMetadata_CreatesSection() - { - var doc = AppxManifestDocument.Parse(BareMinimalManifest); - - doc.SetBuildMetadata("Microsoft.WinAppCli", "1.0.0"); - - var xml = doc.ToXml(); - Assert.Contains("Microsoft.WinAppCli", xml); - Assert.Contains("1.0.0", xml); - } - - [TestMethod] - public void SetBuildMetadata_UpdatesExisting() - { - var xml = """ - - - - - - - """; - var doc = AppxManifestDocument.Parse(xml); - - doc.SetBuildMetadata("Microsoft.WinAppCli", "2.0.0"); - - var result = doc.ToXml(); - Assert.Contains("2.0.0", result); - Assert.DoesNotContain("0.0.1", result); - Assert.AreEqual(1, CountOccurrences(result, "Microsoft.WinAppCli")); - } - - [TestMethod] - public void SetBuildMetadata_AddsAlongsideExisting() - { - var xml = """ - - - - - - - """; - var doc = AppxManifestDocument.Parse(xml); - - doc.SetBuildMetadata("Microsoft.WinAppCli", "3.0.0"); - - var result = doc.ToXml(); - Assert.Contains("OtherTool", result); - Assert.Contains("Microsoft.WinAppCli", result); - } - - #endregion - - #region Package-Level Extensions - - [TestMethod] - public void GetOrCreatePackageLevelExtensionsElement_CreatesAfterApplications() - { - var doc = AppxManifestDocument.Parse(MinimalManifest); - Assert.IsNull(doc.GetExtensionsElement()); // no package-level extensions yet - - var extensions = doc.GetOrCreatePackageLevelExtensionsElement(); - Assert.IsNotNull(extensions); - - var xml = doc.ToXml(); - var applicationsClose = xml.IndexOf("", StringComparison.Ordinal); - var extensionsOpen = xml.IndexOf(" applicationsClose, - "Package-level Extensions should be after "); - } - - [TestMethod] - public void GetOrCreatePackageLevelExtensionsElement_ReturnsExisting() - { - var xml = """ - - - - - - - - - """; - var doc = AppxManifestDocument.Parse(xml); - - var extensions = doc.GetOrCreatePackageLevelExtensionsElement(); - - Assert.IsNotNull(extensions); - Assert.IsTrue(extensions.HasElements, "Should return existing Extensions with children"); - } - - [TestMethod] - public void AddInProcessServerExtension_AddsCorrectStructure() - { - var doc = AppxManifestDocument.Parse(MinimalManifest); - - doc.AddInProcessServerExtension("MyRuntime.dll", ["My.Namespace.ClassA", "My.Namespace.ClassB"]); - - var xml = doc.ToXml(); - Assert.Contains("MyRuntime.dll", xml); - Assert.Contains("My.Namespace.ClassA", xml); - Assert.Contains("My.Namespace.ClassB", xml); - Assert.Contains("windows.activatableClass.inProcessServer", xml); - Assert.Contains("ThreadingModel=\"both\"", xml); - } - - [TestMethod] - public void GetRegisteredExtensionDllPaths_ReturnsAllPaths() - { - var xml = """ - - - - - RuntimeA.dll - - - - - - RuntimeB.dll - - - - - - """; - var doc = AppxManifestDocument.Parse(xml); - - var paths = doc.GetRegisteredExtensionDllPaths(); - - Assert.AreEqual(2, paths.Count); - Assert.IsTrue(paths.Contains("RuntimeA.dll")); - Assert.IsTrue(paths.Contains("RuntimeB.dll")); - } - - [TestMethod] - public void GetRegisteredExtensionDllPaths_IsCaseInsensitive() - { - var xml = """ - - - - - Runtime.dll - - - - - runtime.dll - - - - - """; - var doc = AppxManifestDocument.Parse(xml); - - var paths = doc.GetRegisteredExtensionDllPaths(); - - // Case-insensitive dedup: should be only 1 - Assert.AreEqual(1, paths.Count); - } - - [TestMethod] - public void GetRegisteredExtensionDllPaths_ReturnsEmpty_WhenNoPaths() - { - var doc = AppxManifestDocument.Parse(BareMinimalManifest); - - var paths = doc.GetRegisteredExtensionDllPaths(); - Assert.AreEqual(0, paths.Count); - } - - #endregion - - #region Package Dependencies - - [TestMethod] - public void HasPackageDependency_ReturnsFalse_WhenNone() - { - var doc = AppxManifestDocument.Parse(BareMinimalManifest); - Assert.IsFalse(doc.HasPackageDependency("Microsoft.WindowsAppRuntime")); - } - - [TestMethod] - public void SetPackageDependency_AddsNew() - { - var doc = AppxManifestDocument.Parse(MinimalManifest); - - doc.SetPackageDependency( - "Microsoft.WindowsAppRuntime.1.5", - "5001.178.1908.0", - "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"); - - Assert.IsTrue(doc.HasPackageDependency("Microsoft.WindowsAppRuntime")); - var xml = doc.ToXml(); - Assert.Contains("Microsoft.WindowsAppRuntime.1.5", xml); - Assert.Contains("5001.178.1908.0", xml); - } - - [TestMethod] - public void SetPackageDependency_UpdatesExisting() - { - var doc = AppxManifestDocument.Parse(MinimalManifest); - - doc.SetPackageDependency("MyDep", "1.0.0.0", "CN=Pub"); - doc.SetPackageDependency("MyDep", "2.0.0.0", "CN=Pub"); - - var xml = doc.ToXml(); - Assert.Contains("MinVersion=\"2.0.0.0\"", xml); - // The old MinVersion should be gone (but 1.0.0.0 exists in Identity, so check specifically) - Assert.DoesNotContain("MinVersion=\"1.0.0.0\"", xml); - Assert.AreEqual(1, CountOccurrences(xml, "MyDep")); - } - - [TestMethod] - public void SetPackageDependency_CreatesDependenciesElement_WhenMissing() - { - // BareMinimalManifest has no Dependencies element - var xml = """ - - - - """; - var doc = AppxManifestDocument.Parse(xml); - Assert.IsNull(doc.GetDependenciesElement()); - - doc.SetPackageDependency("TestDep", "1.0.0.0", "CN=Test"); - - Assert.IsNotNull(doc.GetDependenciesElement()); - Assert.IsTrue(doc.HasPackageDependency("TestDep")); - } - - #endregion - - #region Element Accessors - - [TestMethod] - public void GetFirstApplicationElement_ReturnsCorrectElement() - { - var doc = AppxManifestDocument.Parse(MinimalManifest); - var app = doc.GetFirstApplicationElement(); - - Assert.IsNotNull(app); - Assert.AreEqual("App", app.Attribute("Id")?.Value); - } - - [TestMethod] - public void GetVisualElements_ReturnsCorrectElement() - { - var doc = AppxManifestDocument.Parse(MinimalManifest); - var ve = doc.GetVisualElements(); - - Assert.IsNotNull(ve); - Assert.AreEqual("Test App", ve.Attribute("DisplayName")?.Value); - } - - [TestMethod] - public void GetResourcesElement_ReturnsCorrectElement() - { - var doc = AppxManifestDocument.Parse(MinimalManifest); - Assert.IsNotNull(doc.GetResourcesElement()); - } - - [TestMethod] - public void GetDependenciesElement_ReturnsCorrectElement() - { - var doc = AppxManifestDocument.Parse(MinimalManifest); - Assert.IsNotNull(doc.GetDependenciesElement()); - } - - [TestMethod] - public void AllAccessors_ReturnNull_ForBareManifest() - { - var doc = AppxManifestDocument.Parse(BareMinimalManifest); - - Assert.IsNull(doc.GetFirstApplicationElement()); - Assert.IsNull(doc.GetVisualElements()); - Assert.IsNull(doc.GetResourcesElement()); - Assert.IsNull(doc.GetExtensionsElement()); - Assert.IsNull(doc.GetCapabilitiesElement()); - } - - #endregion - - #region Helpers - - private static int CountOccurrences(string text, string pattern) - { - int count = 0; - int index = 0; - while ((index = text.IndexOf(pattern, index, StringComparison.Ordinal)) != -1) - { - count++; - index += pattern.Length; - } - return count; - } - - #endregion -} diff --git a/src/winapp-CLI/WinApp.Cli.Tests/EndToEndTests.cs b/src/winapp-CLI/WinApp.Cli.Tests/EndToEndTests.cs index 63dfaab0..6b88be26 100644 --- a/src/winapp-CLI/WinApp.Cli.Tests/EndToEndTests.cs +++ b/src/winapp-CLI/WinApp.Cli.Tests/EndToEndTests.cs @@ -19,6 +19,7 @@ public class EndToEndTests : BaseCommandTests protected override IServiceCollection ConfigureServices(IServiceCollection services) { return services + .AddSingleton() .AddSingleton(); } diff --git a/src/winapp-CLI/WinApp.Cli.Tests/FakeMsixService.cs b/src/winapp-CLI/WinApp.Cli.Tests/FakeMsixService.cs index 7773eb03..6e7f9a3e 100644 --- a/src/winapp-CLI/WinApp.Cli.Tests/FakeMsixService.cs +++ b/src/winapp-CLI/WinApp.Cli.Tests/FakeMsixService.cs @@ -13,7 +13,7 @@ namespace WinApp.Cli.Tests; internal class FakeMsixService : IMsixService { public MsixIdentityResult FakeIdentityResult { get; set; } = new("TestPackage", "CN=TestPublisher", "TestApp"); - public List AddLooseLayoutCalls { get; } = []; + public List<(string ManifestPath, bool Clean)> AddLooseLayoutCalls { get; } = []; public Exception? ExceptionToThrow { get; set; } public Task AddLooseLayoutIdentityAsync( @@ -21,9 +21,10 @@ public Task AddLooseLayoutIdentityAsync( DirectoryInfo inputDirectory, DirectoryInfo outputAppXDirectory, TaskContext taskContext, + bool clean = false, CancellationToken cancellationToken = default) { - AddLooseLayoutCalls.Add(appxManifestPath.FullName); + AddLooseLayoutCalls.Add((appxManifestPath.FullName, clean)); if (ExceptionToThrow != null) { throw ExceptionToThrow; diff --git a/src/winapp-CLI/WinApp.Cli.Tests/FakePackageRegistrationService.cs b/src/winapp-CLI/WinApp.Cli.Tests/FakePackageRegistrationService.cs index 8e8c1fda..aeebe287 100644 --- a/src/winapp-CLI/WinApp.Cli.Tests/FakePackageRegistrationService.cs +++ b/src/winapp-CLI/WinApp.Cli.Tests/FakePackageRegistrationService.cs @@ -13,7 +13,7 @@ internal class FakePackageRegistrationService : IPackageRegistrationService { public List RegisterLooseLayoutCalls { get; } = []; public List<(string ManifestPath, string ExternalLocation)> RegisterSparseCalls { get; } = []; - public List UnregisterCalls { get; } = []; + public List<(string PackageName, bool PreserveAppData)> UnregisterCalls { get; } = []; public List InstallPackageCalls { get; } = []; public List GetInstalledVersionCalls { get; } = []; public List FindDevPackagesCalls { get; } = []; @@ -48,9 +48,9 @@ public Task RegisterSparseAsync(string manifestPath, string externalLocation, Ca return Task.CompletedTask; } - public Task UnregisterAsync(string packageName, CancellationToken cancellationToken = default) + public Task UnregisterAsync(string packageName, bool preserveAppData = true, CancellationToken cancellationToken = default) { - UnregisterCalls.Add(packageName); + UnregisterCalls.Add((packageName, preserveAppData)); return Task.FromResult(FakeUnregisterResult); } diff --git a/src/winapp-CLI/WinApp.Cli.Tests/FakePowerShellService.cs b/src/winapp-CLI/WinApp.Cli.Tests/FakePowerShellService.cs new file mode 100644 index 00000000..a0bc9ec4 --- /dev/null +++ b/src/winapp-CLI/WinApp.Cli.Tests/FakePowerShellService.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation and Contributors. All rights reserved. +// Licensed under the MIT License. + +using WinApp.Cli.ConsoleTasks; +using WinApp.Cli.Services; + +namespace WinApp.Cli.Tests; + +internal class FakePowerShellService : IPowerShellService +{ + public Task<(int exitCode, string output, string error)> RunCommandAsync(string command, TaskContext taskContext, bool elevated = false, Dictionary? environmentVariables = null, CancellationToken cancellationToken = default) + { + return Task.FromResult((0, "Fake PowerShell command executed successfully.", string.Empty)); + } +} diff --git a/src/winapp-CLI/WinApp.Cli.Tests/IncrementalCopyHelperTests.cs b/src/winapp-CLI/WinApp.Cli.Tests/IncrementalCopyHelperTests.cs deleted file mode 100644 index 19044314..00000000 --- a/src/winapp-CLI/WinApp.Cli.Tests/IncrementalCopyHelperTests.cs +++ /dev/null @@ -1,280 +0,0 @@ -// Copyright (c) Microsoft Corporation and Contributors. All rights reserved. -// Licensed under the MIT License. - -using WinApp.Cli.Services; - -namespace WinApp.Cli.Tests; - -[TestClass] -public class IncrementalCopyHelperTests -{ - private DirectoryInfo _tempDir = null!; - - [TestInitialize] - public void Setup() - { - _tempDir = new DirectoryInfo(Path.Combine(Path.GetTempPath(), $"IncrementalCopyTest_{Guid.NewGuid():N}")); - _tempDir.Create(); - } - - [TestCleanup] - public void Cleanup() - { - if (_tempDir.Exists) - { - _tempDir.Delete(recursive: true); - } - } - - private DirectoryInfo CreateSubDir(string name) - { - var dir = new DirectoryInfo(Path.Combine(_tempDir.FullName, name)); - dir.Create(); - return dir; - } - - private static FileInfo WriteFile(DirectoryInfo dir, string relativePath, string content) - { - var path = Path.Combine(dir.FullName, relativePath); - var file = new FileInfo(path); - file.Directory?.Create(); - File.WriteAllText(path, content); - return file; - } - - #region SyncDirectory Tests - - [TestMethod] - public void SyncDirectory_FirstSync_CopiesAllFiles() - { - var source = CreateSubDir("source"); - var dest = CreateSubDir("dest"); - WriteFile(source, "app.exe", "exe-content"); - WriteFile(source, "app.dll", "dll-content"); - WriteFile(source, "sub\\lib.dll", "lib-content"); - - var result = IncrementalCopyHelper.SyncDirectory(source, dest); - - Assert.AreEqual(3, result.Copied); - Assert.AreEqual(0, result.Skipped); - Assert.AreEqual(0, result.Deleted); - Assert.IsTrue(File.Exists(Path.Combine(dest.FullName, "app.exe"))); - Assert.IsTrue(File.Exists(Path.Combine(dest.FullName, "app.dll"))); - Assert.IsTrue(File.Exists(Path.Combine(dest.FullName, "sub", "lib.dll"))); - } - - [TestMethod] - public void SyncDirectory_UnchangedFiles_AreSkipped() - { - var source = CreateSubDir("source"); - var dest = CreateSubDir("dest"); - WriteFile(source, "app.exe", "exe-content"); - WriteFile(source, "app.dll", "dll-content"); - - // First sync copies everything - IncrementalCopyHelper.SyncDirectory(source, dest); - - // Second sync should skip everything (no changes) - var result = IncrementalCopyHelper.SyncDirectory(source, dest); - - Assert.AreEqual(0, result.Copied); - Assert.AreEqual(2, result.Skipped); - Assert.AreEqual(0, result.Deleted); - } - - [TestMethod] - public void SyncDirectory_ModifiedFile_IsCopied() - { - var source = CreateSubDir("source"); - var dest = CreateSubDir("dest"); - WriteFile(source, "app.exe", "original"); - - // First sync - IncrementalCopyHelper.SyncDirectory(source, dest); - - // Modify the source file (different content = different size) - Thread.Sleep(50); // ensure timestamp differs - WriteFile(source, "app.exe", "modified-content-longer"); - - var result = IncrementalCopyHelper.SyncDirectory(source, dest); - - Assert.AreEqual(1, result.Copied); - Assert.AreEqual(0, result.Skipped); - Assert.AreEqual(0, result.Deleted); - Assert.AreEqual("modified-content-longer", File.ReadAllText(Path.Combine(dest.FullName, "app.exe"))); - } - - [TestMethod] - public void SyncDirectory_StaleFile_IsDeleted() - { - var source = CreateSubDir("source"); - var dest = CreateSubDir("dest"); - WriteFile(source, "app.exe", "exe"); - WriteFile(source, "old.dll", "old"); - - // First sync copies both - IncrementalCopyHelper.SyncDirectory(source, dest); - Assert.IsTrue(File.Exists(Path.Combine(dest.FullName, "old.dll"))); - - // Remove old.dll from source - File.Delete(Path.Combine(source.FullName, "old.dll")); - - // Second sync should delete old.dll from dest - var result = IncrementalCopyHelper.SyncDirectory(source, dest); - - Assert.AreEqual(0, result.Copied); - Assert.AreEqual(1, result.Skipped); - Assert.AreEqual(1, result.Deleted); - Assert.IsFalse(File.Exists(Path.Combine(dest.FullName, "old.dll"))); - } - - [TestMethod] - public void SyncDirectory_ProtectedFiles_AreNotDeleted() - { - var source = CreateSubDir("source"); - var dest = CreateSubDir("dest"); - WriteFile(source, "app.exe", "exe"); - - // First sync - IncrementalCopyHelper.SyncDirectory(source, dest); - - // Manually create protected files in dest that don't exist in source - WriteFile(dest, "appxmanifest.xml", ""); - WriteFile(dest, "resources.pri", "pri-data"); - WriteFile(dest, "stale.dll", "stale"); - - var protectedFiles = new HashSet(StringComparer.OrdinalIgnoreCase) - { - "appxmanifest.xml", - "resources.pri" - }; - - var result = IncrementalCopyHelper.SyncDirectory(source, dest, protectedFiles); - - // stale.dll should be deleted, but protected files should survive - Assert.AreEqual(1, result.Deleted); - Assert.IsTrue(File.Exists(Path.Combine(dest.FullName, "appxmanifest.xml"))); - Assert.IsTrue(File.Exists(Path.Combine(dest.FullName, "resources.pri"))); - Assert.IsFalse(File.Exists(Path.Combine(dest.FullName, "stale.dll"))); - } - - [TestMethod] - public void SyncDirectory_NestedOutputInsideSource_IsExcluded() - { - var source = CreateSubDir("source"); - var dest = new DirectoryInfo(Path.Combine(source.FullName, "AppX")); - dest.Create(); - - WriteFile(source, "app.exe", "exe"); - // This file is inside the dest folder (nested inside source) - WriteFile(dest, "should-not-recurse.txt", "nested"); - - var result = IncrementalCopyHelper.SyncDirectory(source, dest); - - // Should only copy app.exe, not recurse into dest - Assert.AreEqual(1, result.Copied); - Assert.IsTrue(File.Exists(Path.Combine(dest.FullName, "app.exe"))); - } - - [TestMethod] - public void SyncDirectory_CreatesDestDirectory_IfNotExists() - { - var source = CreateSubDir("source"); - var dest = new DirectoryInfo(Path.Combine(_tempDir.FullName, "nonexistent_dest")); - WriteFile(source, "app.exe", "exe"); - - Assert.IsFalse(dest.Exists); - - var result = IncrementalCopyHelper.SyncDirectory(source, dest); - - Assert.AreEqual(1, result.Copied); - Assert.IsTrue(File.Exists(Path.Combine(dest.FullName, "app.exe"))); - } - - [TestMethod] - public void SyncDirectory_SubdirectoriesInSource_AreCopied() - { - var source = CreateSubDir("source"); - var dest = CreateSubDir("dest"); - WriteFile(source, "root.dll", "root"); - WriteFile(source, "runtimes\\win-x64\\native\\lib.dll", "native-lib"); - WriteFile(source, "wwwroot\\index.html", ""); - - var result = IncrementalCopyHelper.SyncDirectory(source, dest); - - Assert.AreEqual(3, result.Copied); - Assert.IsTrue(File.Exists(Path.Combine(dest.FullName, "runtimes", "win-x64", "native", "lib.dll"))); - Assert.IsTrue(File.Exists(Path.Combine(dest.FullName, "wwwroot", "index.html"))); - } - - #endregion - - #region CopyFiles Tests - - [TestMethod] - public void CopyFiles_FirstCopy_CopiesAll() - { - var source = CreateSubDir("source"); - var dest = CreateSubDir("dest"); - var file1 = WriteFile(source, "icon.png", "icon-data"); - var file2 = WriteFile(source, "assets\\logo.png", "logo-data"); - - var files = new List<(FileInfo, string)> - { - (file1, "icon.png"), - (file2, "assets\\logo.png"), - }; - - var (copied, skipped) = IncrementalCopyHelper.CopyFiles(files, dest); - - Assert.AreEqual(2, copied); - Assert.AreEqual(0, skipped); - Assert.IsTrue(File.Exists(Path.Combine(dest.FullName, "icon.png"))); - Assert.IsTrue(File.Exists(Path.Combine(dest.FullName, "assets", "logo.png"))); - } - - [TestMethod] - public void CopyFiles_UnchangedFiles_AreSkipped() - { - var source = CreateSubDir("source"); - var dest = CreateSubDir("dest"); - var file1 = WriteFile(source, "icon.png", "icon-data"); - - var files = new List<(FileInfo, string)> { (file1, "icon.png") }; - - // First copy - IncrementalCopyHelper.CopyFiles(files, dest); - - // Second copy should skip - var (copied, skipped) = IncrementalCopyHelper.CopyFiles(files, dest); - - Assert.AreEqual(0, copied); - Assert.AreEqual(1, skipped); - } - - [TestMethod] - public void CopyFiles_ModifiedFile_IsCopied() - { - var source = CreateSubDir("source"); - var dest = CreateSubDir("dest"); - var file1 = WriteFile(source, "icon.png", "original"); - - var files = new List<(FileInfo, string)> { (file1, "icon.png") }; - - // First copy - IncrementalCopyHelper.CopyFiles(files, dest); - - // Modify the source - Thread.Sleep(50); - file1 = WriteFile(source, "icon.png", "modified-content-longer"); - files = [(file1, "icon.png")]; - - var (copied, skipped) = IncrementalCopyHelper.CopyFiles(files, dest); - - Assert.AreEqual(1, copied); - Assert.AreEqual(0, skipped); - Assert.AreEqual("modified-content-longer", File.ReadAllText(Path.Combine(dest.FullName, "icon.png"))); - } - - #endregion -} diff --git a/src/winapp-CLI/WinApp.Cli.Tests/InitCommandTests.cs b/src/winapp-CLI/WinApp.Cli.Tests/InitCommandTests.cs index 27b7055e..ed252655 100644 --- a/src/winapp-CLI/WinApp.Cli.Tests/InitCommandTests.cs +++ b/src/winapp-CLI/WinApp.Cli.Tests/InitCommandTests.cs @@ -15,7 +15,8 @@ public class InitCommandTests : BaseCommandTests { protected override IServiceCollection ConfigureServices(IServiceCollection services) { - return services; + return services + .AddSingleton(); } [TestMethod] diff --git a/src/winapp-CLI/WinApp.Cli.Tests/ManifestCommandTests.cs b/src/winapp-CLI/WinApp.Cli.Tests/ManifestCommandTests.cs index e750399d..245e9378 100644 --- a/src/winapp-CLI/WinApp.Cli.Tests/ManifestCommandTests.cs +++ b/src/winapp-CLI/WinApp.Cli.Tests/ManifestCommandTests.cs @@ -160,9 +160,9 @@ public async Task ManifestGenerateCommandWithLogoShouldGenerateAssets() // Verify expected MSIX asset files were generated var expectedAssets = new[] { - "AppList.png", - "MedTile.png", - "WideTile.png", + "Square44x44Logo.png", + "Square150x150Logo.png", + "Wide310x150Logo.png", "StoreLogo.png" }; @@ -526,74 +526,4 @@ public async Task ManifestGenerateCommandWithEntrypointShouldUseVersionInfoFromE Assert.Contains("CN=Microsoft Corporation", manifestContent, "Manifest publisher should be set to Microsoft Corporation from executable"); } - - [TestMethod] - public void DetermineIcoOutputPathShouldDefaultToAppIcoWhenNoExistingIco() - { - var assetsDir = _tempDirectory.CreateSubdirectory("AssetsEmpty"); - - var result = ManifestService.DetermineIcoOutputPath(assetsDir, TestTaskContext); - - Assert.AreEqual(Path.Combine(assetsDir.FullName, "app.ico"), result); - } - - [TestMethod] - public void DetermineIcoOutputPathShouldDefaultToAppIcoWhenDirectoryDoesNotExist() - { - var nonExistentDir = new DirectoryInfo(Path.Combine(_tempDirectory.FullName, "NonExistent")); - - var result = ManifestService.DetermineIcoOutputPath(nonExistentDir, TestTaskContext); - - Assert.AreEqual(Path.Combine(nonExistentDir.FullName, "app.ico"), result); - } - - [TestMethod] - public void DetermineIcoOutputPathShouldReuseSingleExistingIco() - { - var assetsDir = _tempDirectory.CreateSubdirectory("AssetsSingle"); - var existingIco = Path.Combine(assetsDir.FullName, "AppIcon.ico"); - File.WriteAllBytes(existingIco, [0]); - - var result = ManifestService.DetermineIcoOutputPath(assetsDir, TestTaskContext); - - Assert.AreEqual(existingIco, result); - } - - [TestMethod] - public void DetermineIcoOutputPathShouldPreferAppIconWhenMultipleExist() - { - var assetsDir = _tempDirectory.CreateSubdirectory("AssetsMulti"); - File.WriteAllBytes(Path.Combine(assetsDir.FullName, "AppIcon.ico"), [0]); - File.WriteAllBytes(Path.Combine(assetsDir.FullName, "favicon.ico"), [0]); - - var result = ManifestService.DetermineIcoOutputPath(assetsDir, TestTaskContext); - - Assert.AreEqual(Path.Combine(assetsDir.FullName, "AppIcon.ico"), result); - } - - [TestMethod] - public void DetermineIcoOutputPathShouldPreferAppOverIconInName() - { - var assetsDir = _tempDirectory.CreateSubdirectory("AssetsHeuristic"); - File.WriteAllBytes(Path.Combine(assetsDir.FullName, "app.ico"), [0]); - File.WriteAllBytes(Path.Combine(assetsDir.FullName, "icon.ico"), [0]); - - // "app" is preferred over "icon" in the heuristic - var result = ManifestService.DetermineIcoOutputPath(assetsDir, TestTaskContext); - - Assert.AreEqual(Path.Combine(assetsDir.FullName, "app.ico"), result); - } - - [TestMethod] - public void DetermineIcoOutputPathShouldCreateAppIcoWhenNoHeuristicMatch() - { - var assetsDir = _tempDirectory.CreateSubdirectory("AssetsUnrelated"); - File.WriteAllBytes(Path.Combine(assetsDir.FullName, "tray.ico"), [0]); - File.WriteAllBytes(Path.Combine(assetsDir.FullName, "cursor.ico"), [0]); - - var result = ManifestService.DetermineIcoOutputPath(assetsDir, TestTaskContext); - - // Unrelated ICO files should not be touched; fall back to app.ico - Assert.AreEqual(Path.Combine(assetsDir.FullName, "app.ico"), result); - } } diff --git a/src/winapp-CLI/WinApp.Cli.Tests/ManifestUpdateAssetsCommandTests.cs b/src/winapp-CLI/WinApp.Cli.Tests/ManifestUpdateAssetsCommandTests.cs index b5a8c56c..52a286e3 100644 --- a/src/winapp-CLI/WinApp.Cli.Tests/ManifestUpdateAssetsCommandTests.cs +++ b/src/winapp-CLI/WinApp.Cli.Tests/ManifestUpdateAssetsCommandTests.cs @@ -10,19 +10,17 @@ public class ManifestUpdateAssetsCommandTests : BaseCommandTests { private string _testManifestPath = null!; private string _testImagePath = null!; - private string _testLightImagePath = null!; [TestInitialize] public void Setup() { + // Create a test manifest file _testManifestPath = Path.Combine(_tempDirectory.FullName, "appxmanifest.xml"); CreateTestManifest(_testManifestPath); + // Create a test image file _testImagePath = Path.Combine(_tempDirectory.FullName, "testlogo.png"); PngHelper.CreateTestImage(_testImagePath); - - _testLightImagePath = Path.Combine(_tempDirectory.FullName, "testlogo-light.png"); - PngHelper.CreateTestImage(_testLightImagePath); } private static void CreateTestManifest(string path) @@ -56,8 +54,10 @@ private static void CreateTestManifest(string path) [TestMethod] public void ManifestUpdateAssetsCommandShouldBeAvailable() { + // Arrange & Act var manifestCommand = GetRequiredService(); + // Assert Assert.IsNotNull(manifestCommand, "ManifestCommand should be created"); Assert.IsTrue(manifestCommand.Subcommands.Any(c => c.Name == "update-assets"), "Should have 'update-assets' subcommand"); @@ -66,28 +66,33 @@ public void ManifestUpdateAssetsCommandShouldBeAvailable() [TestMethod] public async Task ManifestUpdateAssetsCommandShouldGenerateAssets() { + // Arrange var updateAssetsCommand = GetRequiredService(); var args = new[] { _testImagePath, - "--manifest", _testManifestPath, + "--manifest", _testManifestPath }; + // Act var parseResult = updateAssetsCommand.Parse(args); var exitCode = await parseResult.InvokeAsync(); + // Assert Assert.AreEqual(0, exitCode, "Update-assets command should complete successfully"); + // Verify Assets directory was created var assetsDir = Path.Combine(_tempDirectory.FullName, "Assets"); Assert.IsTrue(Directory.Exists(assetsDir), "Assets directory should be created"); + // Verify assets referenced in manifest were generated + // The test manifest references: StoreLogo.png, Square150x150Logo.png, Square44x44Logo.png, Wide310x150Logo.png var expectedAssets = new[] { "Square44x44Logo.png", "Square150x150Logo.png", "Wide310x150Logo.png", - "StoreLogo.png", - "app.ico", + "StoreLogo.png" }; foreach (var asset in expectedAssets) @@ -100,125 +105,77 @@ public async Task ManifestUpdateAssetsCommandShouldGenerateAssets() [TestMethod] public void ManifestUpdateAssetsCommandShouldFailWithNonExistentManifest() { + // Arrange var updateAssetsCommand = GetRequiredService(); var nonExistentManifest = Path.Combine(_tempDirectory.FullName, "nonexistent.xml"); var args = new[] { _testImagePath, - "--manifest", nonExistentManifest, + "--manifest", nonExistentManifest }; + // Act var parseResult = updateAssetsCommand.Parse(args); + // Assert Assert.IsNotEmpty(parseResult.Errors, "Should have parse errors for non-existent manifest"); } [TestMethod] public void ManifestUpdateAssetsCommandShouldFailWithNonExistentImage() { + // Arrange var updateAssetsCommand = GetRequiredService(); var nonExistentImage = Path.Combine(_tempDirectory.FullName, "nonexistent.png"); var args = new[] { nonExistentImage, - "--manifest", _testManifestPath, + "--manifest", _testManifestPath }; + // Act var parseResult = updateAssetsCommand.Parse(args); + // Assert Assert.IsNotEmpty(parseResult.Errors, "Should have parse errors for non-existent image"); } [TestMethod] public async Task ManifestUpdateAssetsCommandShouldGenerateCorrectSizes() { + // Arrange var updateAssetsCommand = GetRequiredService(); var args = new[] { _testImagePath, - "--manifest", _testManifestPath, + "--manifest", _testManifestPath }; + // Act var parseResult = updateAssetsCommand.Parse(args); var exitCode = await parseResult.InvokeAsync(); + // Assert Assert.AreEqual(0, exitCode, "Update-assets command should complete successfully"); + // Verify specific asset sizes var assetsDir = Path.Combine(_tempDirectory.FullName, "Assets"); - AssertImageDimensions(Path.Combine(assetsDir, "Square44x44Logo.scale-125.png"), 55, 55); - AssertImageDimensions(Path.Combine(assetsDir, "Square44x44Logo.scale-150.png"), 66, 66); - AssertImageDimensions(Path.Combine(assetsDir, "Square44x44Logo.scale-200.png"), 88, 88); - AssertImageDimensions(Path.Combine(assetsDir, "Square44x44Logo.scale-400.png"), 176, 176); - AssertImageDimensions(Path.Combine(assetsDir, "Square150x150Logo.scale-200.png"), 300, 300); - AssertImageDimensions(Path.Combine(assetsDir, "Square44x44Logo.targetsize-20.png"), 20, 20); - AssertImageDimensions(Path.Combine(assetsDir, "Square44x44Logo.targetsize-20_altform-unplated.png"), 20, 20); - AssertImageDimensions(Path.Combine(assetsDir, "Square44x44Logo.targetsize-256.png"), 256, 256); - AssertImageDimensions(Path.Combine(assetsDir, "Square44x44Logo.targetsize-256_altform-unplated.png"), 256, 256); - } - - [TestMethod] - public async Task ManifestUpdateAssetsCommandShouldGenerateLightThemeAssets() - { - var updateAssetsCommand = GetRequiredService(); - var args = new[] - { - _testImagePath, - "--manifest", _testManifestPath, - "--light-image", _testLightImagePath, - }; - - var parseResult = updateAssetsCommand.Parse(args); - var exitCode = await parseResult.InvokeAsync(); - - Assert.AreEqual(0, exitCode, "Update-assets command should complete successfully with light image"); - - var assetsDir = Path.Combine(_tempDirectory.FullName, "Assets"); - var expectedAssets = new[] - { - "Square44x44Logo.scale-100_altform-colorful_theme-light.png", - "Square44x44Logo.scale-200_altform-colorful_theme-light.png", - "StoreLogo.scale-400_altform-colorful_theme-light.png", - "Square44x44Logo.targetsize-20_altform-lightunplated.png", - "Square44x44Logo.targetsize-256_altform-lightunplated.png", - }; - - foreach (var asset in expectedAssets) - { - Assert.IsTrue(File.Exists(Path.Combine(assetsDir, asset)), $"Asset {asset} should be generated"); - } - } - - [TestMethod] - public async Task ManifestUpdateAssetsCommandShouldGenerateIcoWithExpectedFrames() - { - var updateAssetsCommand = GetRequiredService(); - var args = new[] - { - _testImagePath, - "--manifest", _testManifestPath, - }; - - var parseResult = updateAssetsCommand.Parse(args); - var exitCode = await parseResult.InvokeAsync(); - Assert.AreEqual(0, exitCode, "Update-assets command should complete successfully"); - - var icoPath = Path.Combine(_tempDirectory.FullName, "Assets", "app.ico"); - Assert.IsTrue(File.Exists(icoPath), "app.ico should be generated"); - - using var stream = File.OpenRead(icoPath); - using var reader = new BinaryReader(stream); - Assert.AreEqual((ushort)0, reader.ReadUInt16(), "ICO reserved field should be 0"); - Assert.AreEqual((ushort)1, reader.ReadUInt16(), "ICO type should be 1"); - Assert.AreEqual((ushort)5, reader.ReadUInt16(), "ICO should contain 5 image frames"); + // Check that scale-200 assets exist (which should be 2x the base size) + Assert.IsTrue(File.Exists(Path.Combine(assetsDir, "Square44x44Logo.scale-200.png")), + "Square44x44Logo.scale-200.png should exist"); + Assert.IsTrue(File.Exists(Path.Combine(assetsDir, "Square150x150Logo.scale-200.png")), + "Square150x150Logo.scale-200.png should exist"); } [TestMethod] public async Task ManifestUpdateAssetsCommandShouldOverwriteExistingAssets() { + // Arrange var assetsDir = Path.Combine(_tempDirectory.FullName, "Assets"); Directory.CreateDirectory(assetsDir); + // Create a dummy existing asset var existingAssetPath = Path.Combine(assetsDir, "Square150x150Logo.png"); File.WriteAllText(existingAssetPath, "old content"); var oldLength = new FileInfo(existingAssetPath).Length; @@ -227,12 +184,14 @@ public async Task ManifestUpdateAssetsCommandShouldOverwriteExistingAssets() var args = new[] { _testImagePath, - "--manifest", _testManifestPath, + "--manifest", _testManifestPath }; + // Act var parseResult = updateAssetsCommand.Parse(args); var exitCode = await parseResult.InvokeAsync(); + // Assert Assert.AreEqual(0, exitCode, "Update-assets command should complete successfully"); Assert.IsTrue(File.Exists(existingAssetPath), "Asset should still exist"); @@ -243,28 +202,36 @@ public async Task ManifestUpdateAssetsCommandShouldOverwriteExistingAssets() [TestMethod] public void ManifestUpdateAssetsCommandHelpShouldDisplayCorrectInformation() { + // Arrange var updateAssetsCommand = GetRequiredService(); var args = new[] { "--help" }; + // Act var parseResult = updateAssetsCommand.Parse(args); + // Assert Assert.IsNotNull(parseResult, "Parse result should not be null"); + // The help option should be recognized and not produce errors } [TestMethod] public async Task ManifestUpdateAssetsCommandShouldLogProgress() { + // Arrange var updateAssetsCommand = GetRequiredService(); var args = new[] { _testImagePath, "--manifest", _testManifestPath, - "--verbose", + "--verbose" }; + // Act var exitCode = await ParseAndInvokeWithCaptureAsync(updateAssetsCommand, args); + // Assert Assert.AreEqual(0, exitCode, "Update-assets command should complete successfully"); + Assert.Contains("Updating assets for manifest", TestAnsiConsole.Output, "Should log update message"); Assert.Contains("generated", TestAnsiConsole.Output.ToLowerInvariant(), "Should log generation progress"); } @@ -272,190 +239,23 @@ public async Task ManifestUpdateAssetsCommandShouldLogProgress() [TestMethod] public async Task ManifestUpdateAssetsCommandShouldInferManifestFromCurrentDirectory() { + // Arrange var updateAssetsCommand = GetRequiredService(); + // Only provide the image path, manifest should be inferred var args = new[] { - _testImagePath, + _testImagePath }; + // Act var parseResult = updateAssetsCommand.Parse(args); var exitCode = await parseResult.InvokeAsync(); + // Assert Assert.AreEqual(0, exitCode, "Update-assets command should complete successfully when manifest is inferred"); + // Verify Assets directory was created var assetsDir = Path.Combine(_tempDirectory.FullName, "Assets"); Assert.IsTrue(Directory.Exists(assetsDir), "Assets directory should be created"); - Assert.IsTrue(File.Exists(Path.Combine(assetsDir, "app.ico")), "app.ico should be generated when manifest is inferred"); - } - - [TestMethod] - public async Task ManifestUpdateAssetsCommandShouldGenerateAssetsFromSvg() - { - var svgImagePath = Path.Combine(_tempDirectory.FullName, "testlogo.svg"); - PngHelper.CreateTestSvgImage(svgImagePath); - - var updateAssetsCommand = GetRequiredService(); - var args = new[] - { - svgImagePath, - "--manifest", _testManifestPath, - }; - - var parseResult = updateAssetsCommand.Parse(args); - var exitCode = await parseResult.InvokeAsync(); - - Assert.AreEqual(0, exitCode, "Update-assets command should complete successfully with SVG source"); - - var assetsDir = Path.Combine(_tempDirectory.FullName, "Assets"); - Assert.IsTrue(Directory.Exists(assetsDir), "Assets directory should be created"); - - var expectedAssets = new[] - { - "Square44x44Logo.png", - "Square150x150Logo.png", - "Wide310x150Logo.png", - "StoreLogo.png", - "app.ico", - }; - - foreach (var asset in expectedAssets) - { - var assetPath = Path.Combine(assetsDir, asset); - Assert.IsTrue(File.Exists(assetPath), $"Asset {asset} should be generated from SVG source"); - } - } - - [TestMethod] - public async Task ManifestUpdateAssetsCommandShouldGenerateCorrectSizesFromSvg() - { - var svgImagePath = Path.Combine(_tempDirectory.FullName, "testlogo.svg"); - PngHelper.CreateTestSvgImage(svgImagePath); - - var updateAssetsCommand = GetRequiredService(); - var args = new[] - { - svgImagePath, - "--manifest", _testManifestPath, - }; - - var parseResult = updateAssetsCommand.Parse(args); - var exitCode = await parseResult.InvokeAsync(); - - Assert.AreEqual(0, exitCode, "Update-assets command should complete successfully with SVG source"); - - var assetsDir = Path.Combine(_tempDirectory.FullName, "Assets"); - AssertImageDimensions(Path.Combine(assetsDir, "Square44x44Logo.scale-125.png"), 55, 55); - AssertImageDimensions(Path.Combine(assetsDir, "Square44x44Logo.scale-200.png"), 88, 88); - AssertImageDimensions(Path.Combine(assetsDir, "Square150x150Logo.scale-200.png"), 300, 300); - AssertImageDimensions(Path.Combine(assetsDir, "Square44x44Logo.targetsize-32.png"), 32, 32); - AssertImageDimensions(Path.Combine(assetsDir, "Square44x44Logo.targetsize-32_altform-unplated.png"), 32, 32); - } - - private static void AssertImageDimensions(string imagePath, int expectedWidth, int expectedHeight) - { - Assert.IsTrue(File.Exists(imagePath), $"Asset {Path.GetFileName(imagePath)} should exist"); - using var bitmap = new System.Drawing.Bitmap(imagePath); - Assert.AreEqual(expectedWidth, bitmap.Width, $"{Path.GetFileName(imagePath)} width mismatch"); - Assert.AreEqual(expectedHeight, bitmap.Height, $"{Path.GetFileName(imagePath)} height mismatch"); - } - - private static void CreateNewNamingManifest(string path) - { - var manifestContent = @" - - - - TestPackage - TestPublisher - Assets\StoreLogo.png - - - - - - - - -"; - File.WriteAllText(path, manifestContent); - } - - [TestMethod] - public async Task ManifestUpdateAssetsCommandShouldGenerateAssetsWithNewNaming() - { - var newNamingManifest = Path.Combine(_tempDirectory.FullName, "appxmanifest-new.xml"); - CreateNewNamingManifest(newNamingManifest); - - var updateAssetsCommand = GetRequiredService(); - var args = new[] - { - _testImagePath, - "--manifest", newNamingManifest, - }; - - var parseResult = updateAssetsCommand.Parse(args); - var exitCode = await parseResult.InvokeAsync(); - - Assert.AreEqual(0, exitCode, "Update-assets command should succeed with new naming manifest"); - - var assetsDir = Path.Combine(_tempDirectory.FullName, "Assets"); - - // Base assets use new names - var expectedBaseAssets = new[] { "AppList.png", "MedTile.png", "WideTile.png", "StoreLogo.png", "app.ico" }; - foreach (var asset in expectedBaseAssets) - { - Assert.IsTrue(File.Exists(Path.Combine(assetsDir, asset)), $"Asset {asset} should be generated"); - } - - // Scale variants use new names - AssertImageDimensions(Path.Combine(assetsDir, "AppList.scale-200.png"), 88, 88); - AssertImageDimensions(Path.Combine(assetsDir, "MedTile.scale-200.png"), 300, 300); - AssertImageDimensions(Path.Combine(assetsDir, "WideTile.scale-200.png"), 620, 300); - - // Targetsize variants generated for AppList (44x44 app icon) - AssertImageDimensions(Path.Combine(assetsDir, "AppList.targetsize-48.png"), 48, 48); - AssertImageDimensions(Path.Combine(assetsDir, "AppList.targetsize-48_altform-unplated.png"), 48, 48); - AssertImageDimensions(Path.Combine(assetsDir, "AppList.targetsize-256.png"), 256, 256); - - // No targetsize variants for non-app-icon assets - Assert.IsFalse(File.Exists(Path.Combine(assetsDir, "MedTile.targetsize-48.png")), - "MedTile should not have targetsize variants"); - } - - [TestMethod] - public async Task ManifestUpdateAssetsCommandShouldReplaceExistingIcoByName() - { - // Pre-create an Assets directory with an existing ICO file (simulating a project template) - var assetsDir = Path.Combine(_tempDirectory.FullName, "Assets"); - Directory.CreateDirectory(assetsDir); - File.WriteAllBytes(Path.Combine(assetsDir, "AppIcon.ico"), [0x00]); - - var updateAssetsCommand = GetRequiredService(); - var args = new[] - { - _testImagePath, - "--manifest", _testManifestPath, - }; - - var parseResult = updateAssetsCommand.Parse(args); - var exitCode = await parseResult.InvokeAsync(); - - Assert.AreEqual(0, exitCode, "Update-assets command should complete successfully"); - - // The existing AppIcon.ico should be replaced (size > 1 byte placeholder) - var replacedIco = Path.Combine(assetsDir, "AppIcon.ico"); - Assert.IsTrue(File.Exists(replacedIco), "AppIcon.ico should still exist"); - Assert.IsTrue(new FileInfo(replacedIco).Length > 1, "AppIcon.ico should be regenerated with real content"); - - // No duplicate app.ico should be created - Assert.IsFalse(File.Exists(Path.Combine(assetsDir, "app.ico")), - "app.ico should NOT be created when an existing ICO file is present"); } } diff --git a/src/winapp-CLI/WinApp.Cli.Tests/MsixServiceTests.cs b/src/winapp-CLI/WinApp.Cli.Tests/MsixServiceTests.cs index 0250a6b8..57662258 100644 --- a/src/winapp-CLI/WinApp.Cli.Tests/MsixServiceTests.cs +++ b/src/winapp-CLI/WinApp.Cli.Tests/MsixServiceTests.cs @@ -3,6 +3,7 @@ using System.Reflection; using System.Text; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using WinApp.Cli.ConsoleTasks; using WinApp.Cli.Services; diff --git a/src/winapp-CLI/WinApp.Cli.Tests/PackageCommandTests.cs b/src/winapp-CLI/WinApp.Cli.Tests/PackageCommandTests.cs index 12dda16b..510edb53 100644 --- a/src/winapp-CLI/WinApp.Cli.Tests/PackageCommandTests.cs +++ b/src/winapp-CLI/WinApp.Cli.Tests/PackageCommandTests.cs @@ -1617,276 +1617,4 @@ private async Task EnsureWinAppSdkRuntimeInTestCacheAsync(string winAppSdkVersio } } } - - #region AppxRecipe helpers - - /// - /// Manifest content that includes build:Metadata with makepri.exe entry, - /// mimicking what MSBuild generates during dotnet build. - /// - private const string MSBuildGeneratedManifestContent = @" - - - - Recipe Test Package - Test Publisher - Test package with MSBuild metadata - Assets\Logo.png - - - - - - - - - - - - - - - -"; - - /// - /// Creates a .build.appxrecipe XML file that maps source files to their package paths. - /// - private static string CreateAppxRecipeContent(string inputDir, (string relativeSource, string packagePath)[] files, string? manifestRelativePath = null) - { - var sb = new System.Text.StringBuilder(); - sb.AppendLine(@""); - sb.AppendLine(@""); - sb.AppendLine(@" "); - - // Add manifest entry - var manifestSource = manifestRelativePath ?? "AppxManifest.xml"; - sb.AppendLine($@" "); - sb.AppendLine(@" AppxManifest.xml"); - sb.AppendLine(@" "); - - // Add file entries - foreach (var (relativeSource, packagePath) in files) - { - sb.AppendLine($@" "); - sb.AppendLine($@" {packagePath}"); - sb.AppendLine(@" "); - } - - sb.AppendLine(@" "); - sb.AppendLine(@""); - return sb.ToString(); - } - - #endregion - - #region AppxRecipe packaging tests - - [TestMethod] - public async Task CreateMsixPackageAsync_WithAppxRecipe_OnlyIncludesRecipeFiles() - { - // Arrange — create an input folder with an MSBuild-generated manifest, - // a .build.appxrecipe, and some files that should NOT end up in the package. - var packageDir = new DirectoryInfo(Path.Combine(_tempDirectory.FullName, "RecipeTestPackage")); - packageDir.Create(); - - await File.WriteAllTextAsync(Path.Combine(packageDir.FullName, "AppxManifest.xml"), MSBuildGeneratedManifestContent, TestContext.CancellationToken); - await File.WriteAllTextAsync(Path.Combine(packageDir.FullName, "TestApp.exe"), "fake exe content", TestContext.CancellationToken); - await File.WriteAllTextAsync(Path.Combine(packageDir.FullName, "TestApp.dll"), "fake dll content", TestContext.CancellationToken); - - var assetsDir = Path.Combine(packageDir.FullName, "Assets"); - Directory.CreateDirectory(assetsDir); - await File.WriteAllTextAsync(Path.Combine(assetsDir, "Logo.png"), "fake logo content", TestContext.CancellationToken); - - // Files that should NOT be in the package (not in recipe) - await File.WriteAllTextAsync(Path.Combine(packageDir.FullName, "TestApp.pdb"), "debug symbols", TestContext.CancellationToken); - await File.WriteAllTextAsync(Path.Combine(packageDir.FullName, "TestApp.deps.json"), "{}", TestContext.CancellationToken); - - // Create the .build.appxrecipe — only lists the files that belong in the package - var recipeContent = CreateAppxRecipeContent(packageDir.FullName, - [ - ("TestApp.exe", "TestApp.exe"), - ("TestApp.dll", "TestApp.dll"), - (@"Assets\Logo.png", @"Assets\Logo.png"), - ]); - await File.WriteAllTextAsync(Path.Combine(packageDir.FullName, "TestApp.build.appxrecipe"), recipeContent, TestContext.CancellationToken); - - await File.WriteAllTextAsync(_configService.ConfigPath.FullName, "packages: []", TestContext.CancellationToken); - - // Act - var result = await _msixService.CreateMsixPackageAsync( - inputFolder: packageDir, - outputPath: _tempDirectory, - TestTaskContext, - packageName: "RecipeTestPackage", - skipPri: true, - autoSign: false, - cancellationToken: CancellationToken.None - ); - - // Assert - Assert.IsTrue(result.MsixPath.Exists, "MSIX package should exist"); - - using var archive = ZipFile.OpenRead(result.MsixPath.FullName); - var entryNames = archive.Entries.Select(e => e.FullName).ToList(); - - // Files listed in the recipe should be present - Assert.IsTrue(entryNames.Any(e => e.Equals("TestApp.exe", StringComparison.OrdinalIgnoreCase)), - "MSIX should include TestApp.exe from recipe"); - Assert.IsTrue(entryNames.Any(e => e.Equals("TestApp.dll", StringComparison.OrdinalIgnoreCase)), - "MSIX should include TestApp.dll from recipe"); - Assert.IsTrue(entryNames.Any(e => e.Equals("Assets/Logo.png", StringComparison.OrdinalIgnoreCase)), - "MSIX should include Assets/Logo.png from recipe"); - - // Files NOT in the recipe should be excluded - Assert.IsFalse(entryNames.Any(e => e.Equals("TestApp.pdb", StringComparison.OrdinalIgnoreCase)), - "MSIX should NOT include TestApp.pdb (not in recipe)"); - Assert.IsFalse(entryNames.Any(e => e.Equals("TestApp.deps.json", StringComparison.OrdinalIgnoreCase)), - "MSIX should NOT include TestApp.deps.json (not in recipe)"); - Assert.IsFalse(entryNames.Any(e => e.Contains(".appxrecipe", StringComparison.OrdinalIgnoreCase)), - "MSIX should NOT include the .appxrecipe file itself"); - } - - [TestMethod] - public async Task CreateMsixPackageAsync_MSBuildManifestWithoutRecipe_FallsBackToFullCopy() - { - // Arrange — MSBuild-generated manifest but no .build.appxrecipe file - var packageDir = new DirectoryInfo(Path.Combine(_tempDirectory.FullName, "NoRecipeTestPackage")); - packageDir.Create(); - - await File.WriteAllTextAsync(Path.Combine(packageDir.FullName, "AppxManifest.xml"), MSBuildGeneratedManifestContent, TestContext.CancellationToken); - await File.WriteAllTextAsync(Path.Combine(packageDir.FullName, "TestApp.exe"), "fake exe content", TestContext.CancellationToken); - - var assetsDir = Path.Combine(packageDir.FullName, "Assets"); - Directory.CreateDirectory(assetsDir); - await File.WriteAllTextAsync(Path.Combine(assetsDir, "Logo.png"), "fake logo content", TestContext.CancellationToken); - - // Extra file — without a recipe, full copy should include it - await File.WriteAllTextAsync(Path.Combine(packageDir.FullName, "TestApp.pdb"), "debug symbols", TestContext.CancellationToken); - - await File.WriteAllTextAsync(_configService.ConfigPath.FullName, "packages: []", TestContext.CancellationToken); - - // Act - var result = await _msixService.CreateMsixPackageAsync( - inputFolder: packageDir, - outputPath: _tempDirectory, - TestTaskContext, - packageName: "NoRecipeTestPackage", - skipPri: true, - autoSign: false, - cancellationToken: CancellationToken.None - ); - - // Assert — all files should be present because we fell back to full copy - Assert.IsTrue(result.MsixPath.Exists, "MSIX package should exist"); - - using var archive = ZipFile.OpenRead(result.MsixPath.FullName); - var entryNames = archive.Entries.Select(e => e.FullName).ToList(); - - Assert.IsTrue(entryNames.Any(e => e.Equals("TestApp.exe", StringComparison.OrdinalIgnoreCase)), - "MSIX should include TestApp.exe"); - Assert.IsTrue(entryNames.Any(e => e.Equals("TestApp.pdb", StringComparison.OrdinalIgnoreCase)), - "MSIX should include TestApp.pdb (full copy fallback)"); - } - - [TestMethod] - public async Task CreateMsixPackageAsync_NonMSBuildManifest_UsesFullCopy() - { - // Arrange — standard manifest without build:Metadata - var packageDir = new DirectoryInfo(Path.Combine(_tempDirectory.FullName, "NonMSBuildTestPackage")); - CreateTestPackageStructure(packageDir); - - // Add extra file — should be included since it's a non-MSBuild manifest - await File.WriteAllTextAsync(Path.Combine(packageDir.FullName, "extra.dll"), "extra dll content", TestContext.CancellationToken); - - // Even though there's a .appxrecipe, it should be ignored for non-MSBuild manifests - var recipeContent = CreateAppxRecipeContent(packageDir.FullName, - [ - ("TestApp.exe", "TestApp.exe"), - ]); - await File.WriteAllTextAsync(Path.Combine(packageDir.FullName, "TestApp.build.appxrecipe"), recipeContent, TestContext.CancellationToken); - - await File.WriteAllTextAsync(_configService.ConfigPath.FullName, "packages: []", TestContext.CancellationToken); - - // Act - var result = await _msixService.CreateMsixPackageAsync( - inputFolder: packageDir, - outputPath: _tempDirectory, - TestTaskContext, - packageName: "NonMSBuildTestPackage", - skipPri: true, - autoSign: false, - cancellationToken: CancellationToken.None - ); - - // Assert — all files should be present (non-MSBuild manifests always use full copy) - Assert.IsTrue(result.MsixPath.Exists, "MSIX package should exist"); - - using var archive = ZipFile.OpenRead(result.MsixPath.FullName); - var entryNames = archive.Entries.Select(e => e.FullName).ToList(); - - Assert.IsTrue(entryNames.Any(e => e.Equals("TestApp.exe", StringComparison.OrdinalIgnoreCase)), - "MSIX should include TestApp.exe"); - Assert.IsTrue(entryNames.Any(e => e.Equals("extra.dll", StringComparison.OrdinalIgnoreCase)), - "MSIX should include extra.dll (full copy for non-MSBuild manifest)"); - } - - [TestMethod] - public async Task CreateMsixPackageAsync_WithAppxRecipe_MapsPackagePathsCorrectly() - { - // Arrange — recipe maps a file from a flat source to a nested package path - var packageDir = new DirectoryInfo(Path.Combine(_tempDirectory.FullName, "RecipePathMappingPackage")); - packageDir.Create(); - - await File.WriteAllTextAsync(Path.Combine(packageDir.FullName, "AppxManifest.xml"), MSBuildGeneratedManifestContent, TestContext.CancellationToken); - await File.WriteAllTextAsync(Path.Combine(packageDir.FullName, "TestApp.exe"), "fake exe content", TestContext.CancellationToken); - - // File at root that the recipe says should go to a subdirectory - await File.WriteAllTextAsync(Path.Combine(packageDir.FullName, "logo.png"), "logo content", TestContext.CancellationToken); - - var assetsDir = Path.Combine(packageDir.FullName, "Assets"); - Directory.CreateDirectory(assetsDir); - await File.WriteAllTextAsync(Path.Combine(assetsDir, "Logo.png"), "asset logo content", TestContext.CancellationToken); - - var recipeContent = CreateAppxRecipeContent(packageDir.FullName, - [ - ("TestApp.exe", "TestApp.exe"), - ("logo.png", @"Assets\Logo.png"), // remap: root → Assets subdirectory - ]); - await File.WriteAllTextAsync(Path.Combine(packageDir.FullName, "TestApp.build.appxrecipe"), recipeContent, TestContext.CancellationToken); - - await File.WriteAllTextAsync(_configService.ConfigPath.FullName, "packages: []", TestContext.CancellationToken); - - // Act - var result = await _msixService.CreateMsixPackageAsync( - inputFolder: packageDir, - outputPath: _tempDirectory, - TestTaskContext, - packageName: "RecipePathMappingPackage", - skipPri: true, - autoSign: false, - cancellationToken: CancellationToken.None - ); - - // Assert - Assert.IsTrue(result.MsixPath.Exists, "MSIX package should exist"); - - using var archive = ZipFile.OpenRead(result.MsixPath.FullName); - var entryNames = archive.Entries.Select(e => e.FullName).ToList(); - - // The logo.png from root should appear at Assets/Logo.png per recipe mapping - Assert.IsTrue(entryNames.Any(e => e.Equals("Assets/Logo.png", StringComparison.OrdinalIgnoreCase)), - "MSIX should include Assets/Logo.png mapped from root logo.png by recipe"); - Assert.IsTrue(entryNames.Any(e => e.Equals("TestApp.exe", StringComparison.OrdinalIgnoreCase)), - "MSIX should include TestApp.exe"); - } - - #endregion } diff --git a/src/winapp-CLI/WinApp.Cli.Tests/PngHelper.cs b/src/winapp-CLI/WinApp.Cli.Tests/PngHelper.cs index 8b2f8742..e2edbd2b 100644 --- a/src/winapp-CLI/WinApp.Cli.Tests/PngHelper.cs +++ b/src/winapp-CLI/WinApp.Cli.Tests/PngHelper.cs @@ -25,16 +25,6 @@ internal static void CreateTestImage(string path) File.WriteAllBytes(path, pngData); } - internal static void CreateTestSvgImage(string path) - { - var svgContent = """ - - - - """; - File.WriteAllText(path, svgContent); - } - /// /// Verifies that all pixels in the image are fully transparent (alpha = 0). /// diff --git a/src/winapp-CLI/WinApp.Cli.Tests/PowerShellServiceTests.cs b/src/winapp-CLI/WinApp.Cli.Tests/PowerShellServiceTests.cs new file mode 100644 index 00000000..56d4d2a3 --- /dev/null +++ b/src/winapp-CLI/WinApp.Cli.Tests/PowerShellServiceTests.cs @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation and Contributors. All rights reserved. +// Licensed under the MIT License. + +using WinApp.Cli.Services; + +namespace WinApp.Cli.Tests; + +[TestClass] +[DoNotParallelize] +public class PowerShellServiceTests() : BaseCommandTests(configPaths: false) +{ + [TestMethod] + public async Task RunCommandAsync_WithRestoreStyleStdOut_ShouldReturnStdOut() + { + var service = GetRequiredService(); + + var (exitCode, output, error) = await service.RunCommandAsync( + "Write-Output 'SKIP|Microsoft.WindowsAppRuntime.1.8.msix|Already installed'; Write-Output 'INSTALLING|2 packages will be installed'", + TestTaskContext, + cancellationToken: TestContext.CancellationToken); + + Assert.AreEqual(0, exitCode); + Assert.IsTrue( + output.Contains("SKIP|Microsoft.WindowsAppRuntime.1.8.msix|Already installed", StringComparison.Ordinal), + $"Expected SKIP marker in output. Captured output:\n{output}"); + Assert.IsTrue( + output.Contains("INSTALLING|2 packages will be installed", StringComparison.Ordinal), + $"Expected INSTALLING marker in output. Captured output:\n{output}"); + Assert.IsTrue(string.IsNullOrWhiteSpace(error)); + } + + [TestMethod] + public async Task RunCommandAsync_WithRestoreStyleStdErr_ShouldReturnStdErr() + { + var service = GetRequiredService(); + + var (exitCode, output, error) = await service.RunCommandAsync( + "[Console]::Error.WriteLine('ERROR|Microsoft.WindowsAppRuntime.1.8.msix|Installation failed'); exit 1", + TestTaskContext, + cancellationToken: TestContext.CancellationToken); + + Assert.AreNotEqual(0, exitCode); + Assert.IsTrue(string.IsNullOrWhiteSpace(output)); + Assert.IsTrue( + error.Contains("ERROR|Microsoft.WindowsAppRuntime.1.8.msix|Installation failed", StringComparison.Ordinal), + $"Expected ERROR marker in stderr. Captured error:\n{error}"); + } +} diff --git a/src/winapp-CLI/WinApp.Cli.Tests/RunCommandTests.cs b/src/winapp-CLI/WinApp.Cli.Tests/RunCommandTests.cs index 86c18b1a..997b6010 100644 --- a/src/winapp-CLI/WinApp.Cli.Tests/RunCommandTests.cs +++ b/src/winapp-CLI/WinApp.Cli.Tests/RunCommandTests.cs @@ -13,7 +13,6 @@ public class RunCommandTests : BaseCommandTests { private FakeMsixService _fakeMsixService = null!; private FakeAppLauncherService _fakeAppLauncherService = null!; - private FakeDebugOutputService _fakeDebugOutputService = null!; private const string TestManifestContent = """ @@ -49,11 +48,9 @@ protected override IServiceCollection ConfigureServices(IServiceCollection servi { _fakeMsixService = new FakeMsixService(); _fakeAppLauncherService = new FakeAppLauncherService(); - _fakeDebugOutputService = new FakeDebugOutputService(); return services .AddSingleton(_fakeMsixService) .AddSingleton(_fakeAppLauncherService) - .AddSingleton(_fakeDebugOutputService) .AddSingleton(); } @@ -152,6 +149,34 @@ public async Task ParseOptions_AllOptions_AreParsedCorrectly() Assert.AreEqual(_tempDirectory.FullName, folder.FullName); } + [TestMethod] + public void ParseOptions_Clean_IsParsedCorrectly() + { + // Arrange + var command = GetRequiredService(); + + // Act + var parseResult = command.Parse([_tempDirectory.FullName, "--clean"]); + + // Assert + Assert.IsEmpty(parseResult.Errors, "There should be no parsing errors"); + Assert.IsTrue(parseResult.GetValue(RunCommand.CleanOption)); + } + + [TestMethod] + public void ParseOptions_CleanNotSpecified_DefaultsToFalse() + { + // Arrange + var command = GetRequiredService(); + + // Act + var parseResult = command.Parse([_tempDirectory.FullName]); + + // Assert + Assert.IsEmpty(parseResult.Errors, "There should be no parsing errors"); + Assert.IsFalse(parseResult.GetValue(RunCommand.CleanOption)); + } + #endregion #region Handler tests @@ -169,9 +194,42 @@ public async Task RunCommand_WithNoLaunch_RegistersIdentityButDoesNotLaunch() // Assert Assert.AreEqual(0, exitCode, "Command should succeed"); Assert.AreEqual(1, _fakeMsixService.AddLooseLayoutCalls.Count, "Debug identity should be created"); + Assert.IsFalse(_fakeMsixService.AddLooseLayoutCalls[0].Clean, "Default run should preserve app data (clean=false)"); Assert.AreEqual(0, _fakeAppLauncherService.LaunchCalls.Count, "Application should NOT be launched with --no-launch"); } + [TestMethod] + public async Task RunCommand_WithClean_PassesCleanThroughToMsixService() + { + // Arrange + await CreateTestManifestAsync(); + var command = GetRequiredService(); + + // Act + var exitCode = await ParseAndInvokeWithCaptureAsync(command, [_tempDirectory.FullName, "--no-launch", "--clean"]); + + // Assert + Assert.AreEqual(0, exitCode, "Command should succeed"); + Assert.AreEqual(1, _fakeMsixService.AddLooseLayoutCalls.Count, "Debug identity should be created"); + Assert.IsTrue(_fakeMsixService.AddLooseLayoutCalls[0].Clean, "--clean should be passed through to MSIX service"); + } + + [TestMethod] + public async Task RunCommand_WithoutClean_DefaultsToPreservingAppData() + { + // Arrange + await CreateTestManifestAsync(); + var command = GetRequiredService(); + + // Act + var exitCode = await ParseAndInvokeWithCaptureAsync(command, [_tempDirectory.FullName, "--no-launch"]); + + // Assert + Assert.AreEqual(0, exitCode, "Command should succeed"); + Assert.AreEqual(1, _fakeMsixService.AddLooseLayoutCalls.Count, "Debug identity should be created"); + Assert.IsFalse(_fakeMsixService.AddLooseLayoutCalls[0].Clean, "Without --clean, app data should be preserved"); + } + [TestMethod] public async Task RunCommand_WithNoLaunchAndManifest_RegistersIdentityButDoesNotLaunch() { @@ -202,7 +260,7 @@ public async Task RunCommand_WithInputFolder_ResolvesManifestFromFolder() // Assert Assert.AreEqual(0, exitCode, "Command should succeed"); Assert.AreEqual(1, _fakeMsixService.AddLooseLayoutCalls.Count, "Debug identity should be created"); - StringAssert.Contains(_fakeMsixService.AddLooseLayoutCalls[0], subFolder.FullName, + StringAssert.Contains(_fakeMsixService.AddLooseLayoutCalls[0].ManifestPath, subFolder.FullName, "Manifest should be resolved from the input folder"); } @@ -220,7 +278,7 @@ public async Task RunCommand_WithInputFolderAndManifest_UsesExplicitManifest() // Assert Assert.AreEqual(0, exitCode, "Command should succeed"); Assert.AreEqual(1, _fakeMsixService.AddLooseLayoutCalls.Count, "Debug identity should be created"); - StringAssert.Contains(_fakeMsixService.AddLooseLayoutCalls[0], manifest.FullName, + StringAssert.Contains(_fakeMsixService.AddLooseLayoutCalls[0].ManifestPath, manifest.FullName, "Explicit --manifest should take priority"); } @@ -412,189 +470,4 @@ public async Task RunCommand_WithAlias_RegistersIdentityButDoesNotLaunchByAumid( } #endregion - - #region --debug-output option tests - - [TestMethod] - public void ParseOptions_DebugOutput_IsParsedCorrectly() - { - // Arrange - var command = GetRequiredService(); - - // Act - var parseResult = command.Parse([_tempDirectory.FullName, "--debug-output"]); - - // Assert - Assert.IsEmpty(parseResult.Errors, "There should be no parsing errors"); - Assert.IsTrue(parseResult.GetValue(RunCommand.DebugOutputOption)); - } - - [TestMethod] - public void ParseOptions_DebugOutputNotSpecified_DefaultsToFalse() - { - // Arrange - var command = GetRequiredService(); - - // Act - var parseResult = command.Parse([_tempDirectory.FullName]); - - // Assert - Assert.IsEmpty(parseResult.Errors, "There should be no parsing errors"); - Assert.IsFalse(parseResult.GetValue(RunCommand.DebugOutputOption)); - } - - [TestMethod] - public async Task RunCommand_DebugOutputAndNoLaunch_ReturnsError() - { - // Arrange - --debug-output and --no-launch are mutually exclusive - await CreateTestManifestAsync(); - var command = GetRequiredService(); - - // Act - var exitCode = await ParseAndInvokeWithCaptureAsync(command, [_tempDirectory.FullName, "--debug-output", "--no-launch"]); - - // Assert - Assert.AreEqual(1, exitCode, "Command should fail when both --debug-output and --no-launch are specified"); - Assert.AreEqual(0, _fakeMsixService.AddLooseLayoutCalls.Count, "No identity should be created"); - Assert.AreEqual(0, _fakeAppLauncherService.LaunchCalls.Count, "No application should be launched"); - Assert.AreEqual(0, _fakeDebugOutputService.AttachCalls.Count, "Debug loop should not run"); - } - - [TestMethod] - public async Task RunCommand_DebugOutput_LaunchesByAumidAndCallsDebugService() - { - // Arrange - await CreateTestManifestAsync(); - var command = GetRequiredService(); - - // Act - var exitCode = await ParseAndInvokeWithCaptureAsync(command, [_tempDirectory.FullName, "--debug-output"]); - - // Assert - Assert.AreEqual(0, exitCode, "Command should succeed"); - Assert.AreEqual(1, _fakeMsixService.AddLooseLayoutCalls.Count, "Debug identity should be created"); - Assert.AreEqual(1, _fakeAppLauncherService.LaunchCalls.Count, "Application should be launched via AUMID"); - Assert.AreEqual(1, _fakeDebugOutputService.AttachCalls.Count, "Debug service should be called"); - Assert.AreEqual(_fakeAppLauncherService.FakeProcessId, _fakeDebugOutputService.AttachCalls[0], - "Debug service should receive the launched process ID"); - } - - [TestMethod] - public async Task RunCommand_DebugOutputWithAlias_SkipsAumidLaunch() - { - // Arrange - with both --debug-output and --with-alias, the execution alias path is used. - // LaunchViaExecutionAliasAsync will fail because there's no processed manifest in AppX output, - // but verify that AUMID launch is not used. - await CreateTestManifestAsync(); - var command = GetRequiredService(); - - // Act - var exitCode = await ParseAndInvokeWithCaptureAsync(command, [_tempDirectory.FullName, "--debug-output", "--with-alias"]); - - // Assert - identity should be created but AUMID launch should NOT be used - Assert.AreEqual(1, _fakeMsixService.AddLooseLayoutCalls.Count, "Debug identity should be created"); - Assert.AreEqual(0, _fakeAppLauncherService.LaunchCalls.Count, - "Application should NOT be launched via AUMID when --with-alias is specified"); - } - - [TestMethod] - public async Task RunCommand_DebugOutput_UsesDebugServiceExitCode() - { - // Arrange - await CreateTestManifestAsync(); - _fakeDebugOutputService.FakeExitCode = 42; - var command = GetRequiredService(); - - // Act - var exitCode = await ParseAndInvokeWithCaptureAsync(command, [_tempDirectory.FullName, "--debug-output"]); - - // Assert - Assert.AreEqual(42, exitCode, "Exit code should come from the debug service"); - } - - [TestMethod] - public async Task RunCommand_JsonAndDebugOutput_ReturnsError() - { - // Arrange - --json and --debug-output are mutually exclusive - await CreateTestManifestAsync(); - var command = GetRequiredService(); - - // Act - var exitCode = await ParseAndInvokeWithCaptureAsync(command, [_tempDirectory.FullName, "--debug-output", "--json"]); - - // Assert - Assert.AreEqual(1, exitCode, "Command should fail when both --json and --debug-output are specified"); - Assert.AreEqual(0, _fakeMsixService.AddLooseLayoutCalls.Count, "No identity should be created"); - Assert.AreEqual(0, _fakeAppLauncherService.LaunchCalls.Count, "No application should be launched"); - Assert.AreEqual(0, _fakeDebugOutputService.AttachCalls.Count, "Debug loop should not run"); - } - - [TestMethod] - public async Task RunCommand_JsonAndWithAlias_ReturnsError() - { - // Arrange - --json and --with-alias are mutually exclusive - await CreateTestManifestAsync(); - var command = GetRequiredService(); - - // Act - var exitCode = await ParseAndInvokeWithCaptureAsync(command, [_tempDirectory.FullName, "--with-alias", "--json"]); - - // Assert - Assert.AreEqual(1, exitCode, "Command should fail when both --json and --with-alias are specified"); - Assert.AreEqual(0, _fakeMsixService.AddLooseLayoutCalls.Count, "No identity should be created"); - Assert.AreEqual(0, _fakeAppLauncherService.LaunchCalls.Count, "No application should be launched"); - } - - [TestMethod] - public async Task RunCommand_DebugOutput_PropagatesFailureExitCode() - { - // Arrange — debug service returns -1 (e.g., DebugActiveProcess failed) - await CreateTestManifestAsync(); - _fakeDebugOutputService.FakeExitCode = -1; - var command = GetRequiredService(); - - // Act - var exitCode = await ParseAndInvokeWithCaptureAsync(command, [_tempDirectory.FullName, "--debug-output"]); - - // Assert - Assert.AreEqual(-1, exitCode, "Failure exit code from the debug service should propagate"); - } - - [TestMethod] - public async Task RunCommand_DebugOutputWithAliasAndNoLaunch_ReturnsError() - { - // Arrange — all three flags conflict; --with-alias + --no-launch is caught first - await CreateTestManifestAsync(); - var command = GetRequiredService(); - - // Act - var exitCode = await ParseAndInvokeWithCaptureAsync(command, - [_tempDirectory.FullName, "--debug-output", "--with-alias", "--no-launch"]); - - // Assert - Assert.AreEqual(1, exitCode, "Command should fail with conflicting flags"); - Assert.AreEqual(0, _fakeMsixService.AddLooseLayoutCalls.Count, "No identity should be created"); - Assert.AreEqual(0, _fakeDebugOutputService.AttachCalls.Count, "Debug loop should not run"); - } - - [TestMethod] - public async Task RunCommand_DebugOutputWithArgs_ForwardsArgsToLauncher() - { - // Arrange - await CreateTestManifestAsync(); - var command = GetRequiredService(); - - // Act - var exitCode = await ParseAndInvokeWithCaptureAsync(command, - [_tempDirectory.FullName, "--debug-output", "--args", "--my-flag value"]); - - // Assert - Assert.AreEqual(0, exitCode, "Command should succeed"); - Assert.AreEqual(1, _fakeAppLauncherService.LaunchCalls.Count, "Application should be launched"); - Assert.AreEqual("--my-flag value", _fakeAppLauncherService.LaunchCalls[0].Arguments, - "Arguments should be forwarded to the launcher"); - Assert.AreEqual(1, _fakeDebugOutputService.AttachCalls.Count, "Debug service should be called"); - } - - #endregion } diff --git a/src/winapp-CLI/WinApp.Cli.Tests/UnregisterCommandTests.cs b/src/winapp-CLI/WinApp.Cli.Tests/UnregisterCommandTests.cs index 21aeca83..ea6e70b4 100644 --- a/src/winapp-CLI/WinApp.Cli.Tests/UnregisterCommandTests.cs +++ b/src/winapp-CLI/WinApp.Cli.Tests/UnregisterCommandTests.cs @@ -77,7 +77,7 @@ public async Task UnregisterCommand_WithManifest_UnregistersDevPackages() // Assert Assert.AreEqual(0, exitCode); Assert.IsTrue(_fakePackageRegistrationService.FindDevPackagesCalls.Contains("TestPackage")); - Assert.IsTrue(_fakePackageRegistrationService.UnregisterCalls.Contains("TestPackage")); + Assert.IsTrue(_fakePackageRegistrationService.UnregisterCalls.Any(c => c.PackageName == "TestPackage")); } [TestMethod] @@ -158,7 +158,7 @@ public async Task UnregisterCommand_WithForce_SkipsLocationCheck() // Assert Assert.AreEqual(0, exitCode); - Assert.IsTrue(_fakePackageRegistrationService.UnregisterCalls.Contains("TestPackage")); + Assert.IsTrue(_fakePackageRegistrationService.UnregisterCalls.Any(c => c.PackageName == "TestPackage")); } [TestMethod] diff --git a/src/winapp-CLI/WinApp.Cli.Tests/WorkspaceSetupServiceTests.cs b/src/winapp-CLI/WinApp.Cli.Tests/WorkspaceSetupServiceTests.cs index cf44bcc6..97093329 100644 --- a/src/winapp-CLI/WinApp.Cli.Tests/WorkspaceSetupServiceTests.cs +++ b/src/winapp-CLI/WinApp.Cli.Tests/WorkspaceSetupServiceTests.cs @@ -15,7 +15,8 @@ public class WorkspaceSetupServiceTests : BaseCommandTests { protected override IServiceCollection ConfigureServices(IServiceCollection services) { - return services; + return services + .AddSingleton(); } #region Helper methods @@ -207,6 +208,7 @@ protected override IServiceCollection ConfigureServices(IServiceCollection servi _fakeDotNetService = new FakeDotNetService(); return services + .AddSingleton() .AddSingleton() .AddSingleton(_fakeNugetService) .AddSingleton(_fakeDotNetService); diff --git a/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/AppList.png b/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/AppList.png deleted file mode 100644 index 6135405f..00000000 Binary files a/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/AppList.png and /dev/null differ diff --git a/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/AppList.scale-200.png b/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/AppList.scale-200.png deleted file mode 100644 index f713bba6..00000000 Binary files a/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/AppList.scale-200.png and /dev/null differ diff --git a/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/AppList.targetsize-24_altform-unplated.png b/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/AppList.targetsize-24_altform-unplated.png deleted file mode 100644 index dc9f5bea..00000000 Binary files a/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/AppList.targetsize-24_altform-unplated.png and /dev/null differ diff --git a/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/MedTile.png b/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/MedTile.png deleted file mode 100644 index 9c81c0fc..00000000 Binary files a/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/MedTile.png and /dev/null differ diff --git a/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/MedTile.scale-200.png b/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/MedTile.scale-200.png deleted file mode 100644 index 53ee3777..00000000 Binary files a/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/MedTile.scale-200.png and /dev/null differ diff --git a/samples/cpp-app-winui/Assets/Square150x150Logo.png b/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/Square150x150Logo.png similarity index 100% rename from samples/cpp-app-winui/Assets/Square150x150Logo.png rename to src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/Square150x150Logo.png diff --git a/samples/cpp-app-winui/Assets/Square150x150Logo.scale-200.png b/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/Square150x150Logo.scale-200.png similarity index 100% rename from samples/cpp-app-winui/Assets/Square150x150Logo.scale-200.png rename to src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/Square150x150Logo.scale-200.png diff --git a/samples/cpp-app-winui/Assets/Square44x44Logo.png b/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/Square44x44Logo.png similarity index 100% rename from samples/cpp-app-winui/Assets/Square44x44Logo.png rename to src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/Square44x44Logo.png diff --git a/samples/cpp-app-winui/Assets/Square44x44Logo.scale-200.png b/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/Square44x44Logo.scale-200.png similarity index 100% rename from samples/cpp-app-winui/Assets/Square44x44Logo.scale-200.png rename to src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/Square44x44Logo.scale-200.png diff --git a/samples/cpp-app-winui/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/Square44x44Logo.targetsize-24_altform-unplated.png similarity index 100% rename from samples/cpp-app-winui/Assets/Square44x44Logo.targetsize-24_altform-unplated.png rename to src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/Square44x44Logo.targetsize-24_altform-unplated.png diff --git a/samples/cpp-app-winui/Assets/Wide310x150Logo.png b/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/Wide310x150Logo.png similarity index 100% rename from samples/cpp-app-winui/Assets/Wide310x150Logo.png rename to src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/Wide310x150Logo.png diff --git a/samples/cpp-app-winui/Assets/Wide310x150Logo.scale-200.png b/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/Wide310x150Logo.scale-200.png similarity index 100% rename from samples/cpp-app-winui/Assets/Wide310x150Logo.scale-200.png rename to src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/Wide310x150Logo.scale-200.png diff --git a/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/WideTile.png b/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/WideTile.png deleted file mode 100644 index 49a431d6..00000000 Binary files a/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/WideTile.png and /dev/null differ diff --git a/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/WideTile.scale-200.png b/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/WideTile.scale-200.png deleted file mode 100644 index 8b4a5d0d..00000000 Binary files a/src/winapp-CLI/WinApp.Cli/Assets/msix_default_assets/WideTile.scale-200.png and /dev/null differ diff --git a/src/winapp-CLI/WinApp.Cli/Commands/CreateDebugIdentityCommand.cs b/src/winapp-CLI/WinApp.Cli/Commands/CreateDebugIdentityCommand.cs index ee8be829..c4e9cb77 100644 --- a/src/winapp-CLI/WinApp.Cli/Commands/CreateDebugIdentityCommand.cs +++ b/src/winapp-CLI/WinApp.Cli/Commands/CreateDebugIdentityCommand.cs @@ -76,13 +76,7 @@ public override async Task InvokeAsync(ParseResult parseResult, Cancellatio } catch (Exception error) { - var baseEx = error.GetBaseException(); - var message = string.IsNullOrWhiteSpace(baseEx.Message) ? error.Message : baseEx.Message; - if (baseEx.HResult != 0) - { - message += $" (0x{baseEx.HResult:X8})"; - } - return (1, $"{UiSymbols.Error} Failed to add package identity: {message}"); + return (1, $"{UiSymbols.Error} Failed to add package identity: {error.GetBaseException().Message}"); } return (0, "Package identity created successfully."); diff --git a/src/winapp-CLI/WinApp.Cli/Commands/ManifestUpdateAssetsCommand.cs b/src/winapp-CLI/WinApp.Cli/Commands/ManifestUpdateAssetsCommand.cs index cbacfb00..d9a8d01c 100644 --- a/src/winapp-CLI/WinApp.Cli/Commands/ManifestUpdateAssetsCommand.cs +++ b/src/winapp-CLI/WinApp.Cli/Commands/ManifestUpdateAssetsCommand.cs @@ -15,13 +15,12 @@ internal class ManifestUpdateAssetsCommand : Command, IShortDescription public static Argument ImageArgument { get; } public static Option ManifestOption { get; } - public static Option LightImageOption { get; } static ManifestUpdateAssetsCommand() { ImageArgument = new Argument("image-path") { - Description = "Path to source image file (SVG, PNG, ICO, JPG, BMP, GIF)" + Description = "Path to source image file" }; ImageArgument.AcceptExistingOnly(); @@ -30,19 +29,12 @@ static ManifestUpdateAssetsCommand() Description = "Path to AppxManifest.xml or Package.appxmanifest file (default: search current directory)" }; ManifestOption.AcceptExistingOnly(); - - LightImageOption = new Option("--light-image") - { - Description = "Path to source image for light theme variants (SVG, PNG, ICO, JPG, BMP, GIF)" - }; - LightImageOption.AcceptExistingOnly(); } public ManifestUpdateAssetsCommand() : base("update-assets", "Generate new assets for images referenced in an appxmanifest.xml from a single source image. Source image should be at least 400x400 pixels.") { Arguments.Add(ImageArgument); Options.Add(ManifestOption); - Options.Add(LightImageOption); } public class Handler(IManifestService manifestService, ICurrentDirectoryProvider currentDirectoryProvider, IStatusService statusService, ILogger logger) : AsynchronousCommandLineAction @@ -51,7 +43,6 @@ public override async Task InvokeAsync(ParseResult parseResult, Cancellatio { var imagePath = parseResult.GetValue(ImageArgument); var manifestPath = parseResult.GetValue(ManifestOption); - var lightImagePath = parseResult.GetValue(LightImageOption); // If manifest path is not provided, try to find it in the current directory if (manifestPath == null) @@ -62,7 +53,6 @@ public override async Task InvokeAsync(ParseResult parseResult, Cancellatio logger.LogError("{UISymbol} Could not find AppxManifest.xml/Package.appxmanifest in current directory or parent directories", UiSymbols.Error); return 1; } - logger.LogDebug("Found manifest at: {ManifestPath}", manifestPath.FullName); } @@ -76,7 +66,7 @@ public override async Task InvokeAsync(ParseResult parseResult, Cancellatio { try { - await manifestService.UpdateManifestAssetsAsync(manifestPath, imagePath, taskContext, lightImagePath, cancellationToken); + await manifestService.UpdateManifestAssetsAsync(manifestPath, imagePath, taskContext, cancellationToken); return (0, "Successfully updated assets for manifest."); } catch (Exception ex) diff --git a/src/winapp-CLI/WinApp.Cli/Commands/RunCommand.cs b/src/winapp-CLI/WinApp.Cli/Commands/RunCommand.cs index bcaa864c..22e06895 100644 --- a/src/winapp-CLI/WinApp.Cli/Commands/RunCommand.cs +++ b/src/winapp-CLI/WinApp.Cli/Commands/RunCommand.cs @@ -26,6 +26,7 @@ internal partial class RunCommand : Command, IShortDescription public static Option WithAliasOption { get; } public static Option DebugOutputOption { get; } public static Option UnregisterOnExitOption { get; } + public static Option CleanOption { get; } static RunCommand() { @@ -71,6 +72,11 @@ static RunCommand() { Description = "Unregister the development package after the application exits. Only removes packages registered in development mode." }; + + CleanOption = new Option("--clean") + { + Description = "Remove the existing package's application data (LocalState, settings, etc.) before re-deploying. By default, application data is preserved across re-deployments." + }; } public RunCommand() : base("run", "Creates packaged layout, registers the Application, and launches the packaged application.") @@ -83,6 +89,7 @@ public RunCommand() : base("run", "Creates packaged layout, registers the Applic Options.Add(WithAliasOption); Options.Add(DebugOutputOption); Options.Add(UnregisterOnExitOption); + Options.Add(CleanOption); Options.Add(WinAppRootCommand.JsonOption); } @@ -106,6 +113,7 @@ public override async Task InvokeAsync(ParseResult parseResult, Cancellatio var withAlias = parseResult.GetValue(WithAliasOption); var debugOutput = parseResult.GetValue(DebugOutputOption); var unregisterOnExit = parseResult.GetValue(UnregisterOnExitOption); + var clean = parseResult.GetValue(CleanOption); var isJson = parseResult.GetValue(WinAppRootCommand.JsonOption); // Validate mutually exclusive options @@ -194,6 +202,7 @@ public override async Task InvokeAsync(ParseResult parseResult, Cancellatio inputFolder, outputAppXDirectory, taskContext, + clean, cancellationToken); packageFamilyName = appLauncherService.ComputePackageFamilyName( @@ -285,7 +294,6 @@ public override async Task InvokeAsync(ParseResult parseResult, Cancellatio return exitCode; } - // Wait for the launched process to exit before returning. // The process may have already exited by the time we get here (common for // fast-starting apps), in which case GetProcessById throws ArgumentException. @@ -355,7 +363,7 @@ private async Task UnregisterDevPackageAsync(string packageName, CancellationTok continue; } - await packageRegistrationService.UnregisterAsync(pkg.Name, cancellationToken); + await packageRegistrationService.UnregisterAsync(pkg.Name, preserveAppData: false, cancellationToken); logger.LogDebug("Unregistered package {FullName} on exit.", pkg.FullName); } } @@ -395,7 +403,6 @@ private async Task LaunchViaExecutionAliasAsync( var alias = aliases[0]; // Use the first alias - // Launch the execution alias process with inherited stdio var psi = new ProcessStartInfo { diff --git a/src/winapp-CLI/WinApp.Cli/Commands/UnregisterCommand.cs b/src/winapp-CLI/WinApp.Cli/Commands/UnregisterCommand.cs index a9817cee..1eeb9b89 100644 --- a/src/winapp-CLI/WinApp.Cli/Commands/UnregisterCommand.cs +++ b/src/winapp-CLI/WinApp.Cli/Commands/UnregisterCommand.cs @@ -121,8 +121,8 @@ public override async Task InvokeAsync(ParseResult parseResult, Cancellatio } } - // Unregister - await packageRegistrationService.UnregisterAsync(name, cancellationToken); + // Explicit unregister command — remove package and its data + await packageRegistrationService.UnregisterAsync(name, preserveAppData: false, cancellationToken); if (!isJson) { diff --git a/src/winapp-CLI/WinApp.Cli/Helpers/HostBuilderExtensions.cs b/src/winapp-CLI/WinApp.Cli/Helpers/HostBuilderExtensions.cs index 6c1ef84b..d7934a31 100644 --- a/src/winapp-CLI/WinApp.Cli/Helpers/HostBuilderExtensions.cs +++ b/src/winapp-CLI/WinApp.Cli/Helpers/HostBuilderExtensions.cs @@ -28,10 +28,10 @@ public static IServiceCollection ConfigureServices(this IServiceCollection servi .AddSingleton() .AddSingleton() .AddSingleton() - .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton() .AddSingleton() .AddSingleton() @@ -41,6 +41,7 @@ public static IServiceCollection ConfigureServices(this IServiceCollection servi .AddSingleton() .AddSingleton() .AddSingleton() + .AddSingleton() .AddSingleton(AnsiConsole.Console) .AddSingleton() .AddSingleton(); diff --git a/src/winapp-CLI/WinApp.Cli/Program.cs b/src/winapp-CLI/WinApp.Cli/Program.cs index d6591e04..800d0054 100644 --- a/src/winapp-CLI/WinApp.Cli/Program.cs +++ b/src/winapp-CLI/WinApp.Cli/Program.cs @@ -106,13 +106,6 @@ static async Task Main(string[] args) var parseResult = rootCommand.Parse(args); - // Set WINAPP_CLI_CALLER env var from --caller option so telemetry picks it up - var caller = parseResult.GetValue(WinAppRootCommand.CallerOption); - if (!string.IsNullOrWhiteSpace(caller)) - { - Environment.SetEnvironmentVariable("WINAPP_CLI_CALLER", caller); - } - try { CommandInvokedEvent.Log(parseResult.CommandResult); diff --git a/src/winapp-CLI/WinApp.Cli/Services/IImageAssetService.cs b/src/winapp-CLI/WinApp.Cli/Services/IImageAssetService.cs index c13fddce..43a8ee84 100644 --- a/src/winapp-CLI/WinApp.Cli/Services/IImageAssetService.cs +++ b/src/winapp-CLI/WinApp.Cli/Services/IImageAssetService.cs @@ -17,30 +17,23 @@ internal interface IImageAssetService { /// /// Generates MSIX image assets from a source image and saves them to the specified directory. - /// Uses the default manifest asset references to generate the standard MSIX asset set. + /// Uses a hardcoded list of standard MSIX asset specifications. /// /// Path to the source image file /// Directory where generated assets will be saved /// Task context for status messages - /// Optional path to the source image for light theme variants /// Cancellation token /// Task that completes when all assets are generated - Task GenerateAssetsAsync( - FileInfo sourceImagePath, - DirectoryInfo outputDirectory, - TaskContext taskContext, - FileInfo? lightImagePath = null, - CancellationToken cancellationToken = default); + Task GenerateAssetsAsync(FileInfo sourceImagePath, DirectoryInfo outputDirectory, TaskContext taskContext, CancellationToken cancellationToken = default); /// /// Generates MSIX image assets from a source image based on asset references from the manifest. - /// Creates the base asset, scale variants, targetsize variants, and optional light-theme variants. + /// Creates the base asset and scaled variants (scale-200, targetsize variants) matching the aspect ratio. /// /// Path to the source image file /// Directory where the manifest is located (assets are relative to this) /// Asset references extracted from the manifest /// Task context for status messages - /// Optional path to the source image for light theme variants /// Cancellation token /// Task that completes when all assets are generated Task GenerateAssetsFromManifestAsync( @@ -48,16 +41,5 @@ Task GenerateAssetsFromManifestAsync( DirectoryInfo manifestDirectory, IReadOnlyList assetReferences, TaskContext taskContext, - FileInfo? lightImagePath = null, CancellationToken cancellationToken = default); - - /// - /// Generates a multi-resolution ICO file from the source image. - /// - /// Path to the source image file - /// Output path for the generated ICO file - /// Task context for status messages - /// Cancellation token - /// Task that completes when the ICO file is generated - Task GenerateIcoAsync(FileInfo sourceImagePath, string outputPath, TaskContext taskContext, CancellationToken cancellationToken = default); } diff --git a/src/winapp-CLI/WinApp.Cli/Services/IManifestService.cs b/src/winapp-CLI/WinApp.Cli/Services/IManifestService.cs index 3f31c29e..0ef6ca4a 100644 --- a/src/winapp-CLI/WinApp.Cli/Services/IManifestService.cs +++ b/src/winapp-CLI/WinApp.Cli/Services/IManifestService.cs @@ -42,7 +42,6 @@ public Task UpdateManifestAssetsAsync( FileInfo manifestPath, FileInfo imagePath, TaskContext taskContext, - FileInfo? lightImagePath = null, CancellationToken cancellationToken = default); public Task AddExecutionAliasAsync( diff --git a/src/winapp-CLI/WinApp.Cli/Services/IMsixService.cs b/src/winapp-CLI/WinApp.Cli/Services/IMsixService.cs index ae129fcb..032b2725 100644 --- a/src/winapp-CLI/WinApp.Cli/Services/IMsixService.cs +++ b/src/winapp-CLI/WinApp.Cli/Services/IMsixService.cs @@ -38,5 +38,6 @@ public Task AddLooseLayoutIdentityAsync( DirectoryInfo inputDirectory, DirectoryInfo outputAppXDirectory, TaskContext taskContext, + bool clean = false, CancellationToken cancellationToken = default); } diff --git a/src/winapp-CLI/WinApp.Cli/Services/IPackageRegistrationService.cs b/src/winapp-CLI/WinApp.Cli/Services/IPackageRegistrationService.cs index 1e94be56..344a2110 100644 --- a/src/winapp-CLI/WinApp.Cli/Services/IPackageRegistrationService.cs +++ b/src/winapp-CLI/WinApp.Cli/Services/IPackageRegistrationService.cs @@ -30,9 +30,13 @@ internal interface IPackageRegistrationService /// Unregisters an installed package by name. Returns true if a package was found and removed. /// /// The package identity name (e.g. MyCompany.MyApp). + /// + /// When true, preserves the package's application data (LocalState, RoamingState, Settings, etc.) + /// during removal. Only supported for packages registered in development mode. + /// /// Cancellation token. /// True if a package was unregistered, false if no matching package was found. - Task UnregisterAsync(string packageName, CancellationToken cancellationToken = default); + Task UnregisterAsync(string packageName, bool preserveAppData = true, CancellationToken cancellationToken = default); /// /// Installs an MSIX/APPX package file, optionally forcing application shutdown. diff --git a/src/winapp-CLI/WinApp.Cli/Services/IPowerShellService.cs b/src/winapp-CLI/WinApp.Cli/Services/IPowerShellService.cs new file mode 100644 index 00000000..0d894679 --- /dev/null +++ b/src/winapp-CLI/WinApp.Cli/Services/IPowerShellService.cs @@ -0,0 +1,16 @@ +// Copyright (c) Microsoft Corporation and Contributors. All rights reserved. +// Licensed under the MIT License. + +using WinApp.Cli.ConsoleTasks; + +namespace WinApp.Cli.Services; + +internal interface IPowerShellService +{ + public Task<(int exitCode, string output, string error)> RunCommandAsync( + string command, + TaskContext taskContext, + bool elevated = false, + Dictionary? environmentVariables = null, + CancellationToken cancellationToken = default); +} diff --git a/src/winapp-CLI/WinApp.Cli/Services/ImageAssetService.cs b/src/winapp-CLI/WinApp.Cli/Services/ImageAssetService.cs index d703fc10..ca23ba26 100644 --- a/src/winapp-CLI/WinApp.Cli/Services/ImageAssetService.cs +++ b/src/winapp-CLI/WinApp.Cli/Services/ImageAssetService.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation and Contributors. All rights reserved. // Licensed under the MIT License. -using SkiaSharp; -using Svg.Skia; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; @@ -11,235 +9,101 @@ namespace WinApp.Cli.Services; -/// -/// Wraps either a raster Bitmap or an SVG SKPicture so that SVG sources can be -/// rendered directly at each target size without an intermediate rasterization step. -/// -internal sealed class ImageSource : IDisposable +internal class ImageAssetService : IImageAssetService { - private readonly Bitmap? bitmap; - private readonly SKSvg? svg; - private readonly SKPicture? svgPicture; - private readonly SKRect svgBounds; - - private ImageSource(Bitmap bitmap) - { - this.bitmap = bitmap; - AspectRatio = (float)bitmap.Width / bitmap.Height; - } - - private ImageSource(SKSvg svg, SKPicture picture, SKRect bounds) - { - this.svg = svg; - svgPicture = picture; - svgBounds = bounds; - - var width = Math.Max(1, (int)Math.Ceiling(bounds.Width)); - var height = Math.Max(1, (int)Math.Ceiling(bounds.Height)); - AspectRatio = (float)width / height; - } - - public float AspectRatio { get; } + // Define the required asset specifications for MSIX packages (used for fallback/default generation) + private static readonly (string FileName, int Width, int Height)[] AssetSpecifications = + [ + ("Square44x44Logo.png", 44, 44), + ("Square44x44Logo.scale-200.png", 88, 88), + ("Square44x44Logo.targetsize-24_altform-unplated.png", 24, 24), + ("Square150x150Logo.png", 150, 150), + ("Square150x150Logo.scale-200.png", 300, 300), + ("Wide310x150Logo.png", 310, 150), + ("Wide310x150Logo.scale-200.png", 620, 300), + ("StoreLogo.png", 50, 50), + ]; - public bool IsSvg => svgPicture != null; + // Scale factors to generate for each asset + private static readonly (string Suffix, float Scale)[] ScaleVariants = + [ + ("", 1.0f), // Base (scale-100) + (".scale-200", 2.0f), // scale-200 + ]; - public string DimensionsLabel => bitmap != null - ? $"{bitmap.Width}x{bitmap.Height}" - : $"{(int)Math.Ceiling(svgBounds.Width)}x{(int)Math.Ceiling(svgBounds.Height)} (SVG)"; + // Target size variants for square assets (for taskbar, Start menu, etc.) + private static readonly int[] TargetSizes = [16, 24, 32, 48, 256]; - /// - /// Renders the source at the exact target size and returns PNG bytes. - /// SVG sources are rasterized via SkiaSharp at the target resolution. - /// Raster sources are scaled via GDI+ high-quality bicubic. - /// - public byte[] RenderToPng(int targetWidth, int targetHeight) + public async Task GenerateAssetsAsync(FileInfo sourceImagePath, DirectoryInfo outputDirectory, TaskContext taskContext, CancellationToken cancellationToken = default) { - if (svgPicture != null) + if (!sourceImagePath.Exists) { - return RenderSvgToPng(targetWidth, targetHeight); + throw new FileNotFoundException($"Source image not found: {sourceImagePath.FullName}"); } - return RenderBitmapToPng(bitmap!, targetWidth, targetHeight); - } + taskContext.AddStatusMessage($"{UiSymbols.Info} Generating MSIX image assets from: {sourceImagePath.FullName}"); - private byte[] RenderSvgToPng(int targetWidth, int targetHeight) - { - using var skBitmap = new SKBitmap(targetWidth, targetHeight); - using (var canvas = new SKCanvas(skBitmap)) + // Load the source image + Bitmap sourceImage; + try { - canvas.Clear(SKColors.Transparent); - - // Fit SVG into target maintaining aspect ratio, centered - var svgWidth = svgBounds.Width; - var svgHeight = svgBounds.Height; - var svgAspect = svgWidth / svgHeight; - var targetAspect = (float)targetWidth / targetHeight; - - float scale; - float offsetX, offsetY; - if (svgAspect > targetAspect) + if (sourceImagePath.Extension.Equals(".ico", StringComparison.OrdinalIgnoreCase)) { - scale = targetWidth / svgWidth; - offsetX = 0; - offsetY = (targetHeight - svgHeight * scale) / 2f; + using var icon = new Icon(sourceImagePath.FullName); + sourceImage = icon.ToBitmap(); } else { - scale = targetHeight / svgHeight; - offsetX = (targetWidth - svgWidth * scale) / 2f; - offsetY = 0; + sourceImage = new Bitmap(sourceImagePath.FullName); } - - canvas.Translate(offsetX - svgBounds.Left * scale, offsetY - svgBounds.Top * scale); - canvas.Scale(scale); - canvas.DrawPicture(svgPicture); - canvas.Flush(); - } - - using var image = SKImage.FromBitmap(skBitmap); - using var data = image.Encode(SKEncodedImageFormat.Png, 100); - return data.ToArray(); - } - - private static byte[] RenderBitmapToPng(Bitmap source, int targetWidth, int targetHeight) - { - var sourceAspect = (float)source.Width / source.Height; - var targetAspect = (float)targetWidth / targetHeight; - - int scaledWidth; - int scaledHeight; - if (sourceAspect > targetAspect) - { - scaledWidth = targetWidth; - scaledHeight = (int)(targetWidth / sourceAspect); - } - else - { - scaledHeight = targetHeight; - scaledWidth = (int)(targetHeight * sourceAspect); - } - - using var targetBitmap = new Bitmap(targetWidth, targetHeight, PixelFormat.Format32bppArgb); - using var graphics = Graphics.FromImage(targetBitmap); - - graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; - graphics.SmoothingMode = SmoothingMode.HighQuality; - graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; - graphics.CompositingQuality = CompositingQuality.HighQuality; - graphics.CompositingMode = CompositingMode.SourceOver; - graphics.Clear(Color.Transparent); - - var x = (targetWidth - scaledWidth) / 2f; - var y = (targetHeight - scaledHeight) / 2f; - graphics.DrawImage(source, new RectangleF(x, y, scaledWidth, scaledHeight)); - - using var ms = new MemoryStream(); - targetBitmap.Save(ms, ImageFormat.Png); - return ms.ToArray(); - } - - public static ImageSource FromFile(FileInfo path) - { - if (path.Extension.Equals(".svg", StringComparison.OrdinalIgnoreCase)) - { - return FromSvgFile(path); } - - if (path.Extension.Equals(".ico", StringComparison.OrdinalIgnoreCase)) + catch (Exception ex) { - using var icon = new Icon(path.FullName); - return new ImageSource(icon.ToBitmap()); + throw new InvalidOperationException($"Failed to decode image: {sourceImagePath.FullName}. Please ensure the file is a valid image format.", ex); } - return new ImageSource(new Bitmap(path.FullName)); - } - - private static ImageSource FromSvgFile(FileInfo path) - { - var svg = new SKSvg(); - try + using (sourceImage) { - using var stream = File.OpenRead(path.FullName); - svg.Load(stream); + taskContext.AddDebugMessage($"Source image size: {sourceImage.Width}x{sourceImage.Height}"); - var picture = svg.Picture; - if (picture == null) + // Ensure output directory exists + if (!outputDirectory.Exists) { - throw new InvalidOperationException( - $"Failed to render SVG image: {path.FullName}. The file may be corrupted or contain unsupported SVG features."); + outputDirectory.Create(); } - var bounds = picture.CullRect; - var width = (int)Math.Ceiling(bounds.Width); - var height = (int)Math.Ceiling(bounds.Height); - - if (width <= 0 || height <= 0) + // Generate each required asset + var successCount = 0; + foreach (var (fileName, width, height) in AssetSpecifications) { - throw new InvalidOperationException( - $"SVG image has invalid dimensions ({width}x{height}): {path.FullName}. Ensure the SVG has a valid viewBox or width/height attributes."); + try + { + var outputPath = Path.Combine(outputDirectory.FullName, fileName); + await GenerateAssetAsync(sourceImage, outputPath, width, height, cancellationToken); + successCount++; + taskContext.AddDebugMessage($" {UiSymbols.Check} Generated: {fileName} ({width}x{height})"); + } + catch (Exception ex) + { + taskContext.AddDebugMessage($" {UiSymbols.Warning} Failed to generate {fileName}: {ex.Message}"); + } + } + if (successCount == AssetSpecifications.Length) + { + taskContext.AddStatusMessage($"{UiSymbols.Info} Successfully generated {AssetSpecifications.Length} image assets in: {outputDirectory.FullName}"); + } + else + { + taskContext.AddStatusMessage($"{UiSymbols.Info} Successfully generated {successCount} of {AssetSpecifications.Length} image assets in: {outputDirectory.FullName}"); } - - return new ImageSource(svg, picture, bounds); - } - catch - { - svg.Dispose(); - throw; } } - public void Dispose() - { - bitmap?.Dispose(); - svg?.Dispose(); // Deterministically frees the SKPicture it owns - } -} - -internal class ImageAssetService : IImageAssetService -{ - private static readonly ManifestAssetReference[] DefaultAssetReferences = - [ - new("AppList.png", 44, 44), - new("MedTile.png", 150, 150), - new("WideTile.png", 310, 150), - new("StoreLogo.png", 50, 50), - ]; - - private static readonly (string Suffix, float Scale)[] ScaleVariants = - [ - ("", 1.0f), - (".scale-125", 1.25f), - (".scale-150", 1.5f), - (".scale-200", 2.0f), - (".scale-400", 4.0f), - ]; - - private static readonly int[] TargetSizes = [16, 20, 24, 30, 32, 36, 40, 48, 60, 64, 72, 80, 96, 256]; - - private static readonly int[] IcoSizes = [16, 24, 32, 48, 256]; - - public Task GenerateAssetsAsync( - FileInfo sourceImagePath, - DirectoryInfo outputDirectory, - TaskContext taskContext, - FileInfo? lightImagePath = null, - CancellationToken cancellationToken = default) - { - return GenerateAssetsFromManifestAsync( - sourceImagePath, - outputDirectory, - DefaultAssetReferences, - taskContext, - lightImagePath, - cancellationToken); - } - public async Task GenerateAssetsFromManifestAsync( FileInfo sourceImagePath, DirectoryInfo manifestDirectory, IReadOnlyList assetReferences, TaskContext taskContext, - FileInfo? lightImagePath = null, CancellationToken cancellationToken = default) { if (!sourceImagePath.Exists) @@ -247,140 +111,98 @@ public async Task GenerateAssetsFromManifestAsync( throw new FileNotFoundException($"Source image not found: {sourceImagePath.FullName}"); } - if (lightImagePath is { Exists: false }) - { - throw new FileNotFoundException($"Light theme source image not found: {lightImagePath.FullName}"); - } - if (assetReferences.Count == 0) { taskContext.AddStatusMessage($"{UiSymbols.Warning} No asset references found in manifest. No assets generated."); return; } - taskContext.AddStatusMessage($"{UiSymbols.Info} Generating MSIX image assets from: {sourceImagePath.FullName}"); + taskContext.AddStatusMessage($"{UiSymbols.Info} Generating MSIX image assets from manifest references: {sourceImagePath.FullName}"); - ImageSource source; + // Load the source image + Bitmap sourceImage; try { - source = ImageSource.FromFile(sourceImagePath); - } - catch (Exception ex) - { - throw new InvalidOperationException($"Failed to decode image: {sourceImagePath.FullName}. Please ensure the file is a valid image format.", ex); - } - - ImageSource? lightSource = null; - try - { - if (lightImagePath != null) + if (sourceImagePath.Extension.Equals(".ico", StringComparison.OrdinalIgnoreCase)) + { + using var icon = new Icon(sourceImagePath.FullName); + sourceImage = icon.ToBitmap(); + } + else { - lightSource = ImageSource.FromFile(lightImagePath); + sourceImage = new Bitmap(sourceImagePath.FullName); } } catch (Exception ex) { - source.Dispose(); - throw new InvalidOperationException($"Failed to decode image: {lightImagePath!.FullName}. Please ensure the file is a valid image format.", ex); + throw new InvalidOperationException($"Failed to decode image: {sourceImagePath.FullName}. Please ensure the file is a valid image format.", ex); } - using (source) - using (lightSource) + using (sourceImage) { - taskContext.AddDebugMessage($"Source image: {source.DimensionsLabel}"); - if (lightSource != null) - { - taskContext.AddDebugMessage($"Light image: {lightSource.DimensionsLabel}"); - } + taskContext.AddDebugMessage($"Source image size: {sourceImage.Width}x{sourceImage.Height}"); - var (successCount, totalCount) = await Task.Run(() => - { - var success = 0; - var total = 0; + var successCount = 0; + var totalCount = 0; - foreach (var assetReference in assetReferences) + foreach (var assetRef in assetReferences) { - var assetFullPath = Path.Combine(manifestDirectory.FullName, assetReference.RelativePath); - var assetDirectory = Path.GetDirectoryName(assetFullPath) ?? manifestDirectory.FullName; - var assetFileName = Path.GetFileNameWithoutExtension(assetReference.RelativePath); - var assetExtension = Path.GetExtension(assetReference.RelativePath); - - if (!Directory.Exists(assetDirectory)) + // Get the directory and base filename + var assetFullPath = Path.Combine(manifestDirectory.FullName, assetRef.RelativePath); + var assetDirectory = Path.GetDirectoryName(assetFullPath); + var assetFileName = Path.GetFileNameWithoutExtension(assetRef.RelativePath); + var assetExtension = Path.GetExtension(assetRef.RelativePath); + + // Ensure asset directory exists + if (!string.IsNullOrEmpty(assetDirectory) && !Directory.Exists(assetDirectory)) { Directory.CreateDirectory(assetDirectory); } + // Generate scale variants foreach (var (suffix, scale) in ScaleVariants) { - var scaledWidth = (int)Math.Round(assetReference.BaseWidth * scale, MidpointRounding.AwayFromZero); - var scaledHeight = (int)Math.Round(assetReference.BaseHeight * scale, MidpointRounding.AwayFromZero); + totalCount++; + var scaledWidth = (int)(assetRef.BaseWidth * scale); + var scaledHeight = (int)(assetRef.BaseHeight * scale); var scaledFileName = $"{assetFileName}{suffix}{assetExtension}"; - var scaledPath = Path.Combine(assetDirectory, scaledFileName); + var scaledPath = Path.Combine(assetDirectory ?? manifestDirectory.FullName, scaledFileName); - total++; - if (TryGenerateAsset(source, scaledPath, scaledFileName, scaledWidth, scaledHeight, taskContext)) + try { - success++; + await GenerateAssetAsync(sourceImage, scaledPath, scaledWidth, scaledHeight, cancellationToken); + successCount++; + taskContext.AddDebugMessage($" {UiSymbols.Check} Generated: {scaledFileName} ({scaledWidth}x{scaledHeight})"); } - - if (lightSource != null) + catch (Exception ex) { - var lightScaleFileName = $"{assetFileName}.scale-{GetScalePercentage(scale)}_altform-colorful_theme-light{assetExtension}"; - var lightScalePath = Path.Combine(assetDirectory, lightScaleFileName); - - total++; - if (TryGenerateAsset(lightSource, lightScalePath, lightScaleFileName, scaledWidth, scaledHeight, taskContext)) - { - success++; - } + taskContext.AddDebugMessage($" {UiSymbols.Warning} Failed to generate {scaledFileName}: {ex.Message}"); } - - cancellationToken.ThrowIfCancellationRequested(); } - if (IsTargetSizeAsset(assetReference)) + // Generate targetsize variants for square assets (used for taskbar icons, etc.) + if (assetRef.BaseWidth == assetRef.BaseHeight && assetFileName.Contains("44x44", StringComparison.OrdinalIgnoreCase)) { foreach (var targetSize in TargetSizes) { - var platedFileName = $"{assetFileName}.targetsize-{targetSize}{assetExtension}"; - var platedPath = Path.Combine(assetDirectory, platedFileName); + totalCount++; + var targetFileName = $"{assetFileName}.targetsize-{targetSize}_altform-unplated{assetExtension}"; + var targetPath = Path.Combine(assetDirectory ?? manifestDirectory.FullName, targetFileName); - total++; - if (TryGenerateAsset(source, platedPath, platedFileName, targetSize, targetSize, taskContext)) + try { - success++; + await GenerateAssetAsync(sourceImage, targetPath, targetSize, targetSize, cancellationToken); + successCount++; + taskContext.AddDebugMessage($" {UiSymbols.Check} Generated: {targetFileName} ({targetSize}x{targetSize})"); } - - var unplatedFileName = $"{assetFileName}.targetsize-{targetSize}_altform-unplated{assetExtension}"; - var unplatedPath = Path.Combine(assetDirectory, unplatedFileName); - - total++; - if (TryGenerateAsset(source, unplatedPath, unplatedFileName, targetSize, targetSize, taskContext)) - { - success++; - } - - if (lightSource != null) + catch (Exception ex) { - var lightTargetFileName = $"{assetFileName}.targetsize-{targetSize}_altform-lightunplated{assetExtension}"; - var lightTargetPath = Path.Combine(assetDirectory, lightTargetFileName); - - total++; - if (TryGenerateAsset(lightSource, lightTargetPath, lightTargetFileName, targetSize, targetSize, taskContext)) - { - success++; - } + taskContext.AddDebugMessage($" {UiSymbols.Warning} Failed to generate {targetFileName}: {ex.Message}"); } - - cancellationToken.ThrowIfCancellationRequested(); } } } - return (success, total); - }, - cancellationToken); - if (successCount == totalCount) { taskContext.AddStatusMessage($"{UiSymbols.Info} Successfully generated {totalCount} image assets"); @@ -392,125 +214,52 @@ public async Task GenerateAssetsFromManifestAsync( } } - public async Task GenerateIcoAsync(FileInfo sourceImagePath, string outputPath, TaskContext taskContext, CancellationToken cancellationToken = default) + private static async Task GenerateAssetAsync(Bitmap sourceImage, string outputPath, int targetWidth, int targetHeight, CancellationToken cancellationToken) { - if (!sourceImagePath.Exists) - { - throw new FileNotFoundException($"Source image not found: {sourceImagePath.FullName}"); - } - - taskContext.AddStatusMessage($"{UiSymbols.Info} Generating ICO file: {outputPath}"); - - ImageSource source; - try + await Task.Run(() => { - source = ImageSource.FromFile(sourceImagePath); - } - catch (Exception ex) - { - throw new InvalidOperationException($"Failed to decode image: {sourceImagePath.FullName}. Please ensure the file is a valid image format.", ex); - } + // Calculate scaling to fit target dimensions while maintaining aspect ratio + var sourceAspect = (float)sourceImage.Width / sourceImage.Height; + var targetAspect = (float)targetWidth / targetHeight; - using (source) - { - var outputDirectory = Path.GetDirectoryName(outputPath); - if (!string.IsNullOrEmpty(outputDirectory) && !Directory.Exists(outputDirectory)) + int scaledWidth, scaledHeight; + if (sourceAspect > targetAspect) { - Directory.CreateDirectory(outputDirectory); + // Source is wider - fit to width + scaledWidth = targetWidth; + scaledHeight = (int)(targetWidth / sourceAspect); } - - await Task.Run(() => + else { - var pngFrames = new List(IcoSizes.Length); - - foreach (var size in IcoSizes) - { - cancellationToken.ThrowIfCancellationRequested(); - pngFrames.Add(source.RenderToPng(size, size)); - } - - WriteIcoFile(outputPath, IcoSizes, pngFrames); - }, cancellationToken); - } - - taskContext.AddStatusMessage($"{UiSymbols.Info} Generated ICO file with {IcoSizes.Length} sizes"); - } - - private static int GetScalePercentage(float scale) - { - return (int)Math.Round(scale * 100, MidpointRounding.AwayFromZero); - } - - private static bool IsTargetSizeAsset(ManifestAssetReference assetReference) - { - // App icon assets (44x44) get targetsize variants regardless of naming convention - // Supports both old naming (Square44x44Logo) and new naming (AppList) - return assetReference.BaseWidth == 44 - && assetReference.BaseHeight == 44; - } - - private static bool TryGenerateAsset( - ImageSource source, - string outputPath, - string fileName, - int targetWidth, - int targetHeight, - TaskContext taskContext) - { - try - { - GenerateAsset(source, outputPath, targetWidth, targetHeight); - taskContext.AddDebugMessage($" {UiSymbols.Check} Generated: {fileName} ({targetWidth}x{targetHeight})"); - return true; - } - catch (Exception ex) - { - taskContext.AddDebugMessage($" {UiSymbols.Warning} Failed to generate {fileName}: {ex.Message}"); - return false; - } - } + // Source is taller - fit to height + scaledHeight = targetHeight; + scaledWidth = (int)(targetHeight * sourceAspect); + } - private static void WriteIcoFile(string outputPath, int[] sizes, List pngFrames) - { - if (sizes.Length != pngFrames.Count) - { - throw new InvalidOperationException("ICO size and frame counts must match."); - } + // Create the target bitmap with the required dimensions + using var targetBitmap = new Bitmap(targetWidth, targetHeight, PixelFormat.Format32bppArgb); + using var graphics = Graphics.FromImage(targetBitmap); - using var fileStream = new FileStream(outputPath, FileMode.Create, FileAccess.Write); - using var writer = new BinaryWriter(fileStream); + // Set high-quality rendering options + graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; + graphics.SmoothingMode = SmoothingMode.HighQuality; + graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; + graphics.CompositingQuality = CompositingQuality.HighQuality; + graphics.CompositingMode = CompositingMode.SourceOver; - writer.Write((ushort)0); - writer.Write((ushort)1); - writer.Write((ushort)sizes.Length); + // Fill with transparent background + graphics.Clear(Color.Transparent); - var dataOffset = 6 + (16 * sizes.Length); - for (var i = 0; i < sizes.Length; i++) - { - var size = sizes[i]; - var pngData = pngFrames[i]; - - writer.Write((byte)(size >= 256 ? 0 : size)); - writer.Write((byte)(size >= 256 ? 0 : size)); - writer.Write((byte)0); - writer.Write((byte)0); - writer.Write((ushort)1); - writer.Write((ushort)32); - writer.Write((uint)pngData.Length); - writer.Write((uint)dataOffset); - - dataOffset += pngData.Length; - } + // Calculate position to center the scaled image + var x = (targetWidth - scaledWidth) / 2f; + var y = (targetHeight - scaledHeight) / 2f; + var destRect = new RectangleF(x, y, scaledWidth, scaledHeight); - foreach (var pngData in pngFrames) - { - writer.Write(pngData); - } - } + // Draw the scaled image + graphics.DrawImage(sourceImage, destRect); - private static void GenerateAsset(ImageSource source, string outputPath, int targetWidth, int targetHeight) - { - var pngData = source.RenderToPng(targetWidth, targetHeight); - File.WriteAllBytes(outputPath, pngData); + // Save as PNG + targetBitmap.Save(outputPath, ImageFormat.Png); + }, cancellationToken); } } diff --git a/src/winapp-CLI/WinApp.Cli/Services/ManifestService.cs b/src/winapp-CLI/WinApp.Cli/Services/ManifestService.cs index b42c148a..3146c754 100644 --- a/src/winapp-CLI/WinApp.Cli/Services/ManifestService.cs +++ b/src/winapp-CLI/WinApp.Cli/Services/ManifestService.cs @@ -160,7 +160,7 @@ await manifestTemplateService.GenerateCompleteManifestAsync( } if (manifestPath.Exists) { - await UpdateManifestAssetsAsync(manifestPath, logoPath, taskContext, cancellationToken: cancellationToken); + await UpdateManifestAssetsAsync(manifestPath, logoPath, taskContext, cancellationToken); } } @@ -244,48 +244,32 @@ public async Task UpdateManifestAssetsAsync( FileInfo manifestPath, FileInfo imagePath, TaskContext taskContext, - FileInfo? lightImagePath = null, CancellationToken cancellationToken = default) { taskContext.AddStatusMessage($"{UiSymbols.Info} Updating assets for manifest: {manifestPath.FullName}"); + // Determine the manifest directory var manifestDir = manifestPath.Directory; if (manifestDir == null) { throw new InvalidOperationException("Could not determine manifest directory"); } + // Extract asset references from the manifest var assetReferences = ExtractAssetReferencesFromManifest(manifestPath, taskContext); - DirectoryInfo assetsDir; if (assetReferences.Count > 0) { - await imageAssetService.GenerateAssetsFromManifestAsync(imagePath, manifestDir, assetReferences, taskContext, lightImagePath, cancellationToken); - - // Place app.ico alongside the app icon asset (44x44), falling back to - // the most common asset directory so we don't depend on parse order. - var appIconRef = assetReferences.FirstOrDefault(r => r.BaseWidth == 44 && r.BaseHeight == 44); - var relativeAssetsDirectory = Path.GetDirectoryName( - appIconRef?.RelativePath ?? GetMostCommonAssetDirectory(assetReferences)); - var assetsDirectoryPath = string.IsNullOrWhiteSpace(relativeAssetsDirectory) - ? manifestDir.FullName - : Path.Combine(manifestDir.FullName, relativeAssetsDirectory); - assetsDir = new DirectoryInfo(assetsDirectoryPath); + // Generate assets based on manifest references + await imageAssetService.GenerateAssetsFromManifestAsync(imagePath, manifestDir, assetReferences, taskContext, cancellationToken); } else { + // Fallback to default behavior if no asset references found taskContext.AddStatusMessage($"{UiSymbols.Warning} No asset references found in manifest, generating default assets"); - assetsDir = manifestDir.CreateSubdirectory("Assets"); - await imageAssetService.GenerateAssetsAsync(imagePath, assetsDir, taskContext, lightImagePath, cancellationToken); - } - - if (!assetsDir.Exists) - { - assetsDir.Create(); + var assetsDir = manifestDir.CreateSubdirectory("Assets"); + await imageAssetService.GenerateAssetsAsync(imagePath, assetsDir, taskContext, cancellationToken); } - - var icoPath = DetermineIcoOutputPath(assetsDir, taskContext); - await imageAssetService.GenerateIcoAsync(imagePath, icoPath, taskContext, cancellationToken); } /// @@ -309,19 +293,13 @@ internal static List ExtractAssetReferencesFromManifest( // Known asset types and their base dimensions var assetTypeDimensions = new Dictionary(StringComparer.OrdinalIgnoreCase) { - // Square logos (old naming) + // Square logos { "Square44x44Logo", (44, 44) }, { "Square71x71Logo", (71, 71) }, { "Square150x150Logo", (150, 150) }, { "Square310x310Logo", (310, 310) }, - // Wide logos (old naming) + // Wide logos { "Wide310x150Logo", (310, 150) }, - // New naming convention - { "AppList", (44, 44) }, - { "SmallTile", (71, 71) }, - { "MedTile", (150, 150) }, - { "WideTile", (310, 150) }, - { "LargeTile", (310, 310) }, // Store logo (typically 50x50) { "Logo", (50, 50) }, { "StoreLogo", (50, 50) }, @@ -491,6 +469,8 @@ private static (int Width, int Height) GetDimensionsFromPath(string path, Dictio [GeneratedRegex(@"(\d+)x(\d+)", RegexOptions.IgnoreCase)] private static partial Regex DimensionRegex(); + private static readonly XNamespace AppxDefaultNs = "http://schemas.microsoft.com/appx/manifest/foundation/windows10"; + private static readonly XNamespace Uap5Ns = "http://schemas.microsoft.com/appx/manifest/uap/windows10/5"; [GeneratedRegex(@"^(\s*)<([\w:.-]+)((?:\s+[\w:.-]+\s*=\s*""[^""]*"")+)\s*(\/?>)\s*$")] private static partial Regex TagPattern(); @@ -519,7 +499,7 @@ public async Task AddExecutionAliasAsync( } // Find the target Application element - var applications = root.Descendants(AppxManifestDocument.DefaultNs + "Application").ToList(); + var applications = root.Descendants(AppxDefaultNs + "Application").ToList(); if (applications.Count == 0) { return new AddExecutionAliasResult(AddExecutionAliasStatus.NoApplicationElement); @@ -562,13 +542,13 @@ public async Task AddExecutionAliasAsync( } // Check if the target Application already has any execution alias - var targetExtensions = targetApp.Element(AppxManifestDocument.DefaultNs + "Extensions"); + var targetExtensions = targetApp.Element(AppxDefaultNs + "Extensions"); if (targetExtensions != null) { var existingAliasElements = targetExtensions - .Elements(AppxManifestDocument.Uap5Ns + "Extension") + .Elements(Uap5Ns + "Extension") .Where(e => string.Equals(e.Attribute("Category")?.Value, "windows.appExecutionAlias", StringComparison.OrdinalIgnoreCase)) - .Descendants(AppxManifestDocument.Uap5Ns + "ExecutionAlias") + .Descendants(Uap5Ns + "ExecutionAlias") .Select(e => e.Attribute("Alias")?.Value) .Where(v => v != null) .ToList(); @@ -590,7 +570,7 @@ public async Task AddExecutionAliasAsync( // Ensure uap5 namespace is declared on the Package element if (root.GetNamespaceOfPrefix("uap5") == null) { - root.Add(new XAttribute(XNamespace.Xmlns + "uap5", AppxManifestDocument.Uap5Ns)); + root.Add(new XAttribute(XNamespace.Xmlns + "uap5", Uap5Ns)); } // Ensure uap5 is in IgnorableNamespaces @@ -605,19 +585,19 @@ public async Task AddExecutionAliasAsync( } // Build the ExecutionAlias element - var aliasElement = new XElement(AppxManifestDocument.Uap5Ns + "ExecutionAlias", + var aliasElement = new XElement(Uap5Ns + "ExecutionAlias", new XAttribute("Alias", aliasName)); // Find or create the Extensions > uap5:Extension > uap5:AppExecutionAlias hierarchy - var extensions = targetApp.Element(AppxManifestDocument.DefaultNs + "Extensions"); + var extensions = targetApp.Element(AppxDefaultNs + "Extensions"); if (extensions == null) { - extensions = new XElement(AppxManifestDocument.DefaultNs + "Extensions"); + extensions = new XElement(AppxDefaultNs + "Extensions"); targetApp.Add(extensions); } // Look for an existing uap5:Extension with Category="windows.appExecutionAlias" - var aliasExtension = extensions.Elements(AppxManifestDocument.Uap5Ns + "Extension") + var aliasExtension = extensions.Elements(Uap5Ns + "Extension") .FirstOrDefault(e => string.Equals( e.Attribute("Category")?.Value, "windows.appExecutionAlias", @@ -626,23 +606,23 @@ public async Task AddExecutionAliasAsync( if (aliasExtension != null) { // Add to existing AppExecutionAlias block - var appExecAlias = aliasExtension.Element(AppxManifestDocument.Uap5Ns + "AppExecutionAlias"); + var appExecAlias = aliasExtension.Element(Uap5Ns + "AppExecutionAlias"); if (appExecAlias != null) { appExecAlias.Add(aliasElement); } else { - var newAppExecAlias = new XElement(AppxManifestDocument.Uap5Ns + "AppExecutionAlias", aliasElement); + var newAppExecAlias = new XElement(Uap5Ns + "AppExecutionAlias", aliasElement); aliasExtension.Add(newAppExecAlias); } } else { // Create new Extension block - var newExtension = new XElement(AppxManifestDocument.Uap5Ns + "Extension", + var newExtension = new XElement(Uap5Ns + "Extension", new XAttribute("Category", "windows.appExecutionAlias"), - new XElement(AppxManifestDocument.Uap5Ns + "AppExecutionAlias", aliasElement)); + new XElement(Uap5Ns + "AppExecutionAlias", aliasElement)); extensions.Add(newExtension); } @@ -728,65 +708,4 @@ internal static string FormatXmlAttributes(string xml) return result.ToString(); } - - /// - /// Determines the output path for the generated ICO file. - /// If the assets directory already contains an .ico file, reuses its name so that - /// project-template icons (e.g. AppIcon.ico) are replaced rather than duplicated. - /// When multiple .ico files exist, a name-based heuristic picks the most likely app icon. - /// Falls back to "app.ico" when no existing .ico file is found. - /// - internal static string DetermineIcoOutputPath(DirectoryInfo assetsDir, TaskContext taskContext) - { - if (!assetsDir.Exists) - { - return Path.Combine(assetsDir.FullName, "app.ico"); - } - - var existingIcoFiles = assetsDir.GetFiles("*.ico"); - - if (existingIcoFiles.Length == 0) - { - return Path.Combine(assetsDir.FullName, "app.ico"); - } - - if (existingIcoFiles.Length == 1) - { - taskContext.AddDebugMessage($"Found existing ICO file: {existingIcoFiles[0].Name}, will replace it"); - return existingIcoFiles[0].FullName; - } - - // Multiple .ico files — pick the best candidate by name heuristic - var preferredNames = new[] { "appicon", "app", "icon" }; - foreach (var preferred in preferredNames) - { - var match = existingIcoFiles.FirstOrDefault(f => - Path.GetFileNameWithoutExtension(f.Name) - .Contains(preferred, StringComparison.OrdinalIgnoreCase)); - if (match != null) - { - taskContext.AddDebugMessage($"Found multiple ICO files, replacing best match: {match.Name}"); - return match.FullName; - } - } - - // No name heuristic matched — existing ICO files are likely unrelated, - // so create app.ico rather than overwriting an unknown file. - taskContext.AddDebugMessage($"Found {existingIcoFiles.Length} ICO files but none matched app icon heuristics, creating app.ico"); - return Path.Combine(assetsDir.FullName, "app.ico"); - } - - /// - /// Returns the relative path of the asset whose parent directory appears most often, - /// so the ICO file lands in the majority directory even for non-standard manifests. - /// - private static string GetMostCommonAssetDirectory(IReadOnlyList assetReferences) - { - return assetReferences - .GroupBy(r => Path.GetDirectoryName(r.RelativePath) ?? string.Empty, StringComparer.OrdinalIgnoreCase) - .OrderByDescending(g => g.Count()) - .First() - .First() - .RelativePath; - } } diff --git a/src/winapp-CLI/WinApp.Cli/Services/MsixService.Identity.cs b/src/winapp-CLI/WinApp.Cli/Services/MsixService.Identity.cs index b6dac4f6..e128d97e 100644 --- a/src/winapp-CLI/WinApp.Cli/Services/MsixService.Identity.cs +++ b/src/winapp-CLI/WinApp.Cli/Services/MsixService.Identity.cs @@ -88,8 +88,8 @@ public async Task AddSparseIdentityAsync(string? entryPointP var entryPointDir = Path.GetDirectoryName(entryPointPath); var externalLocation = new DirectoryInfo(string.IsNullOrEmpty(entryPointDir) ? currentDirectoryProvider.GetCurrentDirectory() : entryPointDir); - // Unregister any existing package first - await UnregisterExistingPackageAsync(debugIdentity.PackageName, taskContext, cancellationToken); + // Unregister any existing package first (preserving app data by default) + await UnregisterExistingPackageAsync(debugIdentity.PackageName, taskContext, cancellationToken: cancellationToken); // Register the new debug manifest with external location await RegisterSparsePackageAsync(debugManifestPath, externalLocation, taskContext, cancellationToken); @@ -98,7 +98,7 @@ public async Task AddSparseIdentityAsync(string? entryPointP return new MsixIdentityResult(debugIdentity.PackageName, debugIdentity.Publisher, debugIdentity.ApplicationId); } - public async Task AddLooseLayoutIdentityAsync(FileInfo appxManifestPath, DirectoryInfo inputDirectory, DirectoryInfo outputAppXDirectory, TaskContext taskContext, CancellationToken cancellationToken = default) + public async Task AddLooseLayoutIdentityAsync(FileInfo appxManifestPath, DirectoryInfo inputDirectory, DirectoryInfo outputAppXDirectory, TaskContext taskContext, bool clean = false, CancellationToken cancellationToken = default) { // Validate inputs if (!appxManifestPath.Exists) @@ -146,8 +146,8 @@ public async Task AddLooseLayoutIdentityAsync(FileInfo appxM var identity = ParseAppxManifestAsync(manifestContent); - // Unregister any existing package first - await UnregisterExistingPackageAsync(identity.PackageName, taskContext, cancellationToken); + // Unregister any existing package first (preserving app data by default) + await UnregisterExistingPackageAsync(identity.PackageName, taskContext, preserveAppData: !clean, cancellationToken); // Register from the AppX layout directory var registrationManifest = new FileInfo(Path.Combine(outputAppXDirectory.FullName, "AppxManifest.xml")); @@ -252,8 +252,8 @@ await priService.CreatePriConfigAsync( // Install the Windows App Runtime framework packages if not already present await EnsureWindowsAppRuntimeInstalledAsync(dotNetPackageList, taskContext, cancellationToken); - // Unregister any existing package first - await UnregisterExistingPackageAsync(identity.PackageName, taskContext, cancellationToken); + // Unregister any existing package first (preserving app data by default) + await UnregisterExistingPackageAsync(identity.PackageName, taskContext, preserveAppData: !clean, cancellationToken); // Register the new debug manifest with external location await RegisterLooseLayoutPackageAsync(copiedAppxManifestPath, taskContext, cancellationToken); @@ -767,19 +767,21 @@ private static MsixIdentityResult CreateDebugIdentity(MsixIdentityResult origina /// Checks if a package with the given name exists and unregisters it if found /// /// The name of the package to check and unregister + /// Task context for debug output + /// When true, preserves the package's application data during removal /// Cancellation token /// True if package was found and unregistered, false if no package was found - public async Task UnregisterExistingPackageAsync(string packageName, TaskContext taskContext, CancellationToken cancellationToken = default) + public async Task UnregisterExistingPackageAsync(string packageName, TaskContext taskContext, bool preserveAppData = true, CancellationToken cancellationToken = default) { taskContext.AddDebugMessage($"{UiSymbols.Trash} Checking for existing package..."); try { - var removed = await packageRegistrationService.UnregisterAsync(packageName, cancellationToken); + var removed = await packageRegistrationService.UnregisterAsync(packageName, preserveAppData, cancellationToken); if (removed) { - taskContext.AddDebugMessage($"{UiSymbols.Check} Existing package unregistered successfully"); + taskContext.AddDebugMessage($"{UiSymbols.Check} Existing package unregistered successfully{(preserveAppData ? " (app data preserved)" : "")}"); } else { diff --git a/src/winapp-CLI/WinApp.Cli/Services/PackageRegistrationService.cs b/src/winapp-CLI/WinApp.Cli/Services/PackageRegistrationService.cs index f61b4601..9bb74f60 100644 --- a/src/winapp-CLI/WinApp.Cli/Services/PackageRegistrationService.cs +++ b/src/winapp-CLI/WinApp.Cli/Services/PackageRegistrationService.cs @@ -88,7 +88,7 @@ public async Task RegisterSparseAsync(string manifestPath, string externalLocati } /// - public async Task UnregisterAsync(string packageName, CancellationToken cancellationToken = default) + public async Task UnregisterAsync(string packageName, bool preserveAppData = true, CancellationToken cancellationToken = default) { var pm = new PackageManager(); @@ -107,9 +107,13 @@ public async Task UnregisterAsync(string packageName, CancellationToken ca foreach (var pkg in matchingPackages) { var fullName = pkg.Id.FullName; - logger.LogDebug("Removing package: {PackageFullName}", fullName); + logger.LogDebug("Removing package: {PackageFullName} (preserveAppData={PreserveAppData})", fullName, preserveAppData); - var result = await pm.RemovePackageAsync(fullName).AsTask(cancellationToken); + var removalOptions = preserveAppData + ? RemovalOptions.PreserveApplicationData + : RemovalOptions.None; + + var result = await pm.RemovePackageAsync(fullName, removalOptions).AsTask(cancellationToken); if (!string.IsNullOrEmpty(result.ErrorText)) { diff --git a/src/winapp-CLI/WinApp.Cli/Services/PowerShellService.cs b/src/winapp-CLI/WinApp.Cli/Services/PowerShellService.cs new file mode 100644 index 00000000..ff448f3a --- /dev/null +++ b/src/winapp-CLI/WinApp.Cli/Services/PowerShellService.cs @@ -0,0 +1,245 @@ +// Copyright (c) Microsoft Corporation and Contributors. All rights reserved. +// Licensed under the MIT License. + +using System.Diagnostics; +using WinApp.Cli.ConsoleTasks; + +namespace WinApp.Cli.Services; + +/// +/// Service for executing PowerShell commands +/// +internal class PowerShellService : IPowerShellService +{ + /// + /// Runs a PowerShell command and returns the exit code and output + /// + /// The PowerShell command to run + /// Whether to run with elevated privileges (UAC prompt) + /// Optional dictionary of environment variables to set/override + /// Cancellation token + /// Tuple containing (exitCode, stdout, stderr) + public async Task<(int exitCode, string output, string error)> RunCommandAsync( + string command, + TaskContext taskContext, + bool elevated = false, + Dictionary? environmentVariables = null, + CancellationToken cancellationToken = default) + { + var elevatedText = elevated ? "elevated " : ""; + taskContext.AddDebugMessage($"Running {elevatedText}PowerShell: {command}"); + if (elevated) + { + taskContext.AddDebugMessage("UAC prompt may appear..."); + } + + // Build a safe, profile-less, non-interactive PowerShell invocation + // Prefix command to suppress progress records that can otherwise pollute stderr output. + var preparedCommand = $"$ProgressPreference='SilentlyContinue'; {command}"; + + static string ToEncodedCommand(string s) + { + var bytes = System.Text.Encoding.Unicode.GetBytes(s); // UTF-16LE + return Convert.ToBase64String(bytes); + } + var encoded = ToEncodedCommand(preparedCommand); + + var psi = new ProcessStartInfo + { + FileName = "powershell.exe", + Arguments = $"-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -EncodedCommand {encoded}", + UseShellExecute = elevated, // Required for elevation, must be true for Verb=runas + RedirectStandardOutput = !elevated, // Always redirect when not elevated so we can capture output + RedirectStandardError = !elevated, + RedirectStandardInput = !elevated, // close stdin so PS never waits for input + CreateNoWindow = !elevated, + WindowStyle = elevated ? ProcessWindowStyle.Normal : ProcessWindowStyle.Hidden + }; + + // Apply custom environment variables if provided (only when not elevated) + // When elevated, UseShellExecute=true which doesn't support environment variables + if (!elevated) + { + if (environmentVariables is not null) + { + foreach (var kvp in environmentVariables) + { + psi.Environment[kvp.Key] = kvp.Value; + } + } + + // Always clear PSModulePath to prevent PowerShell Core module conflicts when calling Windows PowerShell + // This fixes the issue where calling powershell.exe from PowerShell Core causes module loading errors + if (!psi.Environment.ContainsKey("PSModulePath")) + { + psi.Environment["PSModulePath"] = ""; + } + } + + if (elevated) + { + psi.Verb = "runas"; // This triggers UAC elevation + } + + using var process = Process.Start(psi); + if (process == null) + { + return (-1, string.Empty, "Failed to start PowerShell process"); + } + + string stdOut = string.Empty, stdErr = string.Empty; + + if (!elevated) + { + // Read both streams concurrently to avoid deadlocks + var outTask = process.StandardOutput.ReadToEndAsync(cancellationToken); + var errTask = process.StandardError.ReadToEndAsync(cancellationToken); + + // Close stdin immediately; we won’t provide input + await process.StandardInput.FlushAsync(cancellationToken); + process.StandardInput.Close(); + + await Task.WhenAll(outTask, errTask); + stdOut = outTask.Result.Trim(); + stdErr = NormalizePowerShellErrorStream(errTask.Result); + } + + await process.WaitForExitAsync(cancellationToken); + + if (process.ExitCode != 0 && !string.IsNullOrWhiteSpace(stdErr)) + { + taskContext.AddDebugMessage($"PowerShell error: {Environment.NewLine}{stdErr.Trim()}"); + } + else if (!string.IsNullOrWhiteSpace(stdOut)) + { + taskContext.AddDebugMessage($"PowerShell output: {Environment.NewLine}{stdOut.Trim().TrimStart(Environment.NewLine).TrimEnd(Environment.NewLine)}"); + } + + // For elevated commands, exit codes may not be reliable, so we return 0 if no exception occurred + var exitCode = elevated ? (process.ExitCode == 0 ? 0 : process.ExitCode) : process.ExitCode; + + return (exitCode, stdOut, stdErr); + } + + private static string NormalizePowerShellErrorStream(string stream) + { + if (string.IsNullOrWhiteSpace(stream)) + { + return string.Empty; + } + + var trimmed = stream.Trim(); + + if (!trimmed.Contains("= 0) + { + section = section[..noteIndex]; + } + + section = StripXmlTags(section); + section = DecodeClixmlEscapes(section); + section = section.Replace("#< CLIXML", string.Empty, StringComparison.OrdinalIgnoreCase); + + var lines = section + .Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Trim()) + .Where(line => !string.IsNullOrWhiteSpace(line)); + + var normalized = string.Join(" ", lines).Trim(); + return string.IsNullOrWhiteSpace(normalized) ? trimmed : normalized; + } + + private static string StripXmlTags(string value) + { + if (string.IsNullOrEmpty(value)) + { + return string.Empty; + } + + var sb = new System.Text.StringBuilder(value.Length); + var inTag = false; + + foreach (var character in value) + { + if (character == '<') + { + inTag = true; + continue; + } + + if (character == '>') + { + inTag = false; + sb.Append(' '); + continue; + } + + if (!inTag) + { + sb.Append(character); + } + } + + return sb.ToString(); + } + + private static string DecodeClixmlEscapes(string value) + { + if (string.IsNullOrEmpty(value)) + { + return string.Empty; + } + + var sb = new System.Text.StringBuilder(value.Length); + + for (var i = 0; i < value.Length; i++) + { + if (i + 6 < value.Length + && value[i] == '_' + && (value[i + 1] == 'x' || value[i + 1] == 'X') + && IsHex(value[i + 2]) + && IsHex(value[i + 3]) + && IsHex(value[i + 4]) + && IsHex(value[i + 5]) + && value[i + 6] == '_') + { + var hex = value.AsSpan(i + 2, 4); + var codePoint = Convert.ToInt32(hex.ToString(), 16); + + sb.Append(codePoint switch + { + 0x000D => '\r', + 0x000A => '\n', + 0x0009 => ' ', + _ => (char)codePoint + }); + + i += 6; + continue; + } + + sb.Append(value[i]); + } + + return sb.ToString(); + + static bool IsHex(char c) + => (c >= '0' && c <= '9') + || (c >= 'a' && c <= 'f') + || (c >= 'A' && c <= 'F'); + } +} diff --git a/src/winapp-CLI/WinApp.Cli/Services/WorkspaceSetupService.cs b/src/winapp-CLI/WinApp.Cli/Services/WorkspaceSetupService.cs index fc76c595..e4dbf0df 100644 --- a/src/winapp-CLI/WinApp.Cli/Services/WorkspaceSetupService.cs +++ b/src/winapp-CLI/WinApp.Cli/Services/WorkspaceSetupService.cs @@ -36,7 +36,7 @@ internal class WorkspaceSetupService( IBuildToolsService buildToolsService, ICppWinrtService cppWinrtService, IPackageLayoutService packageLayoutService, - IPackageRegistrationService packageRegistrationService, + IPowerShellService powerShellService, INugetService nugetService, IManifestService manifestService, IDevModeService devModeService, @@ -1069,38 +1069,6 @@ public class MsixPackageEntry return packageEntries; } - /// - /// Reads the actual package Name and Version from the AppxManifest.xml inside an MSIX file. - /// The MSIX inventory file can have incorrect package names (e.g., the DDLM), so we read - /// the real identity directly from the package to ensure correct installation checks. - /// - private static (string? Name, string? Version) ReadMsixIdentity(string msixFilePath, TaskContext taskContext) - { - try - { - using var zip = System.IO.Compression.ZipFile.OpenRead(msixFilePath); - var manifestEntry = zip.GetEntry("AppxManifest.xml"); - if (manifestEntry == null) - { - return (null, null); - } - - using var stream = manifestEntry.Open(); - var doc = System.Xml.Linq.XDocument.Load(stream); - var identityElement = doc.Root?.Elements() - .FirstOrDefault(e => e.Name.LocalName == "Identity"); - - var name = identityElement?.Attribute("Name")?.Value; - var version = identityElement?.Attribute("Version")?.Value; - return (name, version); - } - catch (Exception ex) - { - taskContext.AddDebugMessage($"{UiSymbols.Note} Could not read identity from {Path.GetFileName(msixFilePath)}: {ex.Message}"); - return (null, null); - } - } - /// /// Installs Windows App SDK runtime MSIX packages for the current system architecture /// @@ -1119,8 +1087,8 @@ private static (string? Name, string? Version) ReadMsixIdentity(string msixFileP var msixArchDir = Path.Combine(msixDir.FullName, $"win10-{architecture}"); - // Build list of packages to evaluate - var packagesToCheck = new List<(string FilePath, string PackageName, string NewVersion, string FileName)>(); + // Build package data for PowerShell script + var packageData = new List(); foreach (var entry in packageEntries) { var msixFilePath = Path.Combine(msixArchDir, entry.FileName); @@ -1130,57 +1098,117 @@ private static (string? Name, string? Version) ReadMsixIdentity(string msixFileP continue; } - // Read the actual package identity from the MSIX's AppxManifest.xml. - // The inventory file's PackageIdentity can differ from the real installed name. - var (packageName, newVersionString) = ReadMsixIdentity(msixFilePath, taskContext); - if (packageName == null) - { - // Fallback: parse from inventory identity string - var identityParts = entry.PackageIdentity.Split('_'); - packageName = identityParts[0]; - newVersionString = identityParts.Length >= 2 ? identityParts[1] : ""; - } + // Parse the PackageIdentity (format: Name_Version_Architecture_PublisherId) + var identityParts = entry.PackageIdentity.Split('_'); + var packageName = identityParts[0]; + var newVersionString = identityParts.Length >= 2 ? identityParts[1] : ""; - packagesToCheck.Add((msixFilePath, packageName, newVersionString ?? "", entry.FileName)); + packageData.Add($"@{{Path='{msixFilePath}';Identity='{entry.PackageIdentity}';Name='{packageName}';Version='{newVersionString}';FileName='{entry.FileName}'}}"); } - if (packagesToCheck.Count == 0) + if (packageData.Count == 0) { return (0, 0); } - taskContext.AddDebugMessage($"{UiSymbols.Info} Checking and installing {packagesToCheck.Count} MSIX packages"); + // Create compact PowerShell script with reusable function + var script = $@" +function Test-PackageNeedsInstall($pkg) {{ + $exactMatch = Get-AppxPackage | Where-Object {{ $_.PackageFullName -eq $pkg.Identity }} + if ($exactMatch) {{ return $false }} + + $existing = Get-AppxPackage -Name $pkg.Name -ErrorAction SilentlyContinue + if (-not $existing) {{ return $true }} + + $shouldUpgrade = $false + foreach ($p in $existing) {{ if ([version]$pkg.Version -gt [version]$p.Version) {{ $shouldUpgrade = $true; break }} }} + return $shouldUpgrade +}} + +$packages = @({string.Join(",", packageData)}) +$toInstall = @() + +foreach ($pkg in $packages) {{ + if (Test-PackageNeedsInstall $pkg) {{ + $toInstall += $pkg.Path + Write-Output ""INSTALL|$($pkg.FileName)|Will install"" + }} else {{ + Write-Output ""SKIP|$($pkg.FileName)|Already installed or newer version exists"" + }} +}} + +if ($toInstall.Count -gt 0) {{ + Write-Output ""INSTALLING|$($toInstall.Count) packages will be installed"" + foreach ($path in $toInstall) {{ + try {{ + Add-AppxPackage -Path $path -ForceApplicationShutdown -ErrorAction Stop + Write-Output ""SUCCESS|$(Split-Path $path -Leaf)|Installation successful"" + }} catch {{ + Write-Output ""ERROR|$(Split-Path $path -Leaf)|$($_.Exception.Message)"" + }} + }} +}} else {{ + Write-Output ""COMPLETE|No packages need to be installed"" +}}"; + + taskContext.AddDebugMessage($"{UiSymbols.Info} Checking and installing {packageEntries.Count} MSIX packages"); + + // Execute the batch script + var (exitCode, output, _) = await powerShellService.RunCommandAsync(script, taskContext, cancellationToken: cancellationToken); + + // Parse the output to provide user feedback + var outputLines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries) + .Select(line => line.Trim()) + .Where(line => !string.IsNullOrEmpty(line)); var installedCount = 0; var errorCount = 0; - foreach (var (filePath, packageName, newVersion, fileName) in packagesToCheck) + foreach (var line in outputLines) { - // Check if already installed with same or newer version - var installedVersion = packageRegistrationService.GetInstalledVersion(packageName); - if (installedVersion != null) + var parts = line.Split('|', 3); + if (parts.Length < 2) { - if (Version.TryParse(installedVersion, out var existing) && - Version.TryParse(newVersion, out var incoming) && - existing >= incoming) - { - taskContext.AddDebugMessage($"{UiSymbols.Check} {fileName}: Already installed or newer version exists"); - continue; - } + continue; } - taskContext.AddDebugMessage($"{UiSymbols.Info} {fileName}: Will install"); + var action = parts[0]; + var fileName = parts[1]; + var message = parts.Length > 2 ? parts[2] : ""; - try + switch (action) { - await packageRegistrationService.InstallPackageAsync(filePath, cancellationToken); - installedCount++; - taskContext.AddDebugMessage($"{UiSymbols.Check} {fileName}: Installation successful"); - } - catch (Exception ex) - { - errorCount++; - taskContext.AddDebugMessage($"{UiSymbols.Note} {fileName}: {ex.Message}"); + case "SKIP": + taskContext.AddDebugMessage($"{UiSymbols.Check} {fileName}: {message}"); + break; + + case "INSTALL": + taskContext.AddDebugMessage($"{UiSymbols.Info} {fileName}: {message}"); + break; + + case "INSTALLING": + if (!string.IsNullOrWhiteSpace(message)) + { + taskContext.AddDebugMessage($"{UiSymbols.Info} {message}"); + } + break; + + case "SUCCESS": + installedCount++; + taskContext.AddDebugMessage($"{UiSymbols.Check} {fileName}: {message}"); + break; + + case "ERROR": + errorCount++; + taskContext.AddDebugMessage($"{UiSymbols.Note} {fileName}: {message}"); + break; + + case "COMPLETE": + if (!string.IsNullOrWhiteSpace(message)) + { + taskContext.AddDebugMessage($"{UiSymbols.Check} {message}"); + } + break; } } diff --git a/src/winapp-CLI/WinApp.Cli/Templates/appxmanifest.packaged.xml b/src/winapp-CLI/WinApp.Cli/Templates/appxmanifest.packaged.xml index 62a7c33f..5bd3e076 100644 --- a/src/winapp-CLI/WinApp.Cli/Templates/appxmanifest.packaged.xml +++ b/src/winapp-CLI/WinApp.Cli/Templates/appxmanifest.packaged.xml @@ -40,9 +40,9 @@ DisplayName="{PackageName}" Description="{Description}" BackgroundColor="transparent" - Square150x150Logo="Assets\MedTile.png" - Square44x44Logo="Assets\AppList.png"> - + Square150x150Logo="Assets\Square150x150Logo.png" + Square44x44Logo="Assets\Square44x44Logo.png"> + diff --git a/src/winapp-CLI/WinApp.Cli/Templates/appxmanifest.sparse.xml b/src/winapp-CLI/WinApp.Cli/Templates/appxmanifest.sparse.xml index 2bbcf9c0..6695eb06 100644 --- a/src/winapp-CLI/WinApp.Cli/Templates/appxmanifest.sparse.xml +++ b/src/winapp-CLI/WinApp.Cli/Templates/appxmanifest.sparse.xml @@ -42,8 +42,8 @@ DisplayName="{PackageName}" Description="{Description}" BackgroundColor="transparent" - Square150x150Logo="Assets\MedTile.png" - Square44x44Logo="Assets\AppList.png"> + Square150x150Logo="Assets\Square150x150Logo.png" + Square44x44Logo="Assets\Square44x44Logo.png"> diff --git a/src/winapp-CLI/WinApp.Cli/WinApp.Cli.csproj b/src/winapp-CLI/WinApp.Cli/WinApp.Cli.csproj index 0a4a9753..98c2bf9e 100644 --- a/src/winapp-CLI/WinApp.Cli/WinApp.Cli.csproj +++ b/src/winapp-CLI/WinApp.Cli/WinApp.Cli.csproj @@ -26,7 +26,7 @@ full true true - + false false @@ -54,7 +54,6 @@ runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/src/winapp-GUI/winapp-GUI.sln b/src/winapp-GUI/winapp-GUI.sln new file mode 100644 index 00000000..09648573 --- /dev/null +++ b/src/winapp-GUI/winapp-GUI.sln @@ -0,0 +1,43 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36408.4 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "winapp-GUI", "winapp-GUI\winapp-GUI.csproj", "{8C1F6AEE-36CD-4871-A2AF-97566468E684}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|ARM64 = Debug|ARM64 + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|ARM64 = Release|ARM64 + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {8C1F6AEE-36CD-4871-A2AF-97566468E684}.Debug|ARM64.ActiveCfg = Debug|ARM64 + {8C1F6AEE-36CD-4871-A2AF-97566468E684}.Debug|ARM64.Build.0 = Debug|ARM64 + {8C1F6AEE-36CD-4871-A2AF-97566468E684}.Debug|ARM64.Deploy.0 = Debug|ARM64 + {8C1F6AEE-36CD-4871-A2AF-97566468E684}.Debug|x64.ActiveCfg = Debug|x64 + {8C1F6AEE-36CD-4871-A2AF-97566468E684}.Debug|x64.Build.0 = Debug|x64 + {8C1F6AEE-36CD-4871-A2AF-97566468E684}.Debug|x64.Deploy.0 = Debug|x64 + {8C1F6AEE-36CD-4871-A2AF-97566468E684}.Debug|x86.ActiveCfg = Debug|x86 + {8C1F6AEE-36CD-4871-A2AF-97566468E684}.Debug|x86.Build.0 = Debug|x86 + {8C1F6AEE-36CD-4871-A2AF-97566468E684}.Debug|x86.Deploy.0 = Debug|x86 + {8C1F6AEE-36CD-4871-A2AF-97566468E684}.Release|ARM64.ActiveCfg = Release|ARM64 + {8C1F6AEE-36CD-4871-A2AF-97566468E684}.Release|ARM64.Build.0 = Release|ARM64 + {8C1F6AEE-36CD-4871-A2AF-97566468E684}.Release|ARM64.Deploy.0 = Release|ARM64 + {8C1F6AEE-36CD-4871-A2AF-97566468E684}.Release|x64.ActiveCfg = Release|x64 + {8C1F6AEE-36CD-4871-A2AF-97566468E684}.Release|x64.Build.0 = Release|x64 + {8C1F6AEE-36CD-4871-A2AF-97566468E684}.Release|x64.Deploy.0 = Release|x64 + {8C1F6AEE-36CD-4871-A2AF-97566468E684}.Release|x86.ActiveCfg = Release|x86 + {8C1F6AEE-36CD-4871-A2AF-97566468E684}.Release|x86.Build.0 = Release|x86 + {8C1F6AEE-36CD-4871-A2AF-97566468E684}.Release|x86.Deploy.0 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {DAC9315A-1024-4379-BB63-9DD2BB81E2C5} + EndGlobalSection +EndGlobal diff --git a/src/winapp-GUI/winapp-GUI/App.xaml b/src/winapp-GUI/winapp-GUI/App.xaml new file mode 100644 index 00000000..6ab21cd0 --- /dev/null +++ b/src/winapp-GUI/winapp-GUI/App.xaml @@ -0,0 +1,17 @@ + + + + + + + + + + + + diff --git a/src/winapp-GUI/winapp-GUI/App.xaml.cs b/src/winapp-GUI/winapp-GUI/App.xaml.cs new file mode 100644 index 00000000..e88986dd --- /dev/null +++ b/src/winapp-GUI/winapp-GUI/App.xaml.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices.WindowsRuntime; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; +using Microsoft.UI.Xaml.Data; +using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; +using Microsoft.UI.Xaml.Navigation; +using Microsoft.UI.Xaml.Shapes; +using Windows.ApplicationModel; +using Windows.ApplicationModel.Activation; +using Windows.Foundation; +using Windows.Foundation.Collections; + +// To learn more about WinUI, the WinUI project structure, +// and more about our project templates, see: http://aka.ms/winui-project-info. + +namespace winapp_GUI +{ + /// + /// Provides application-specific behavior to supplement the default Application class. + /// + public partial class App : Application + { + private Window? _window; + + /// + /// Initializes the singleton application object. This is the first line of authored code + /// executed, and as such is the logical equivalent of main() or WinMain(). + /// + public App() + { + InitializeComponent(); + } + + /// + /// Invoked when the application is launched. + /// + /// Details about the launch request and process. + protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args) + { + _window = new MainWindow(); + _window.Activate(); + } + } +} diff --git a/src/winapp-GUI/winapp-GUI/Assets/AppIcon.ico b/src/winapp-GUI/winapp-GUI/Assets/AppIcon.ico new file mode 100644 index 00000000..d5b858de Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/AppIcon.ico differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/BadgeLogo.scale-100.png b/src/winapp-GUI/winapp-GUI/Assets/BadgeLogo.scale-100.png new file mode 100644 index 00000000..d6f0101b Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/BadgeLogo.scale-100.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/BadgeLogo.scale-125.png b/src/winapp-GUI/winapp-GUI/Assets/BadgeLogo.scale-125.png new file mode 100644 index 00000000..98d60f22 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/BadgeLogo.scale-125.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/BadgeLogo.scale-150.png b/src/winapp-GUI/winapp-GUI/Assets/BadgeLogo.scale-150.png new file mode 100644 index 00000000..48b28d21 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/BadgeLogo.scale-150.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/BadgeLogo.scale-200.png b/src/winapp-GUI/winapp-GUI/Assets/BadgeLogo.scale-200.png new file mode 100644 index 00000000..3347c923 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/BadgeLogo.scale-200.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/BadgeLogo.scale-400.png b/src/winapp-GUI/winapp-GUI/Assets/BadgeLogo.scale-400.png new file mode 100644 index 00000000..db8d1fac Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/BadgeLogo.scale-400.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/LargeTile.scale-100.png b/src/winapp-GUI/winapp-GUI/Assets/LargeTile.scale-100.png new file mode 100644 index 00000000..5ff0f08f Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/LargeTile.scale-100.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/LargeTile.scale-125.png b/src/winapp-GUI/winapp-GUI/Assets/LargeTile.scale-125.png new file mode 100644 index 00000000..e3d9a743 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/LargeTile.scale-125.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/LargeTile.scale-150.png b/src/winapp-GUI/winapp-GUI/Assets/LargeTile.scale-150.png new file mode 100644 index 00000000..08cf536a Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/LargeTile.scale-150.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/LargeTile.scale-200.png b/src/winapp-GUI/winapp-GUI/Assets/LargeTile.scale-200.png new file mode 100644 index 00000000..9399c09e Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/LargeTile.scale-200.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/LargeTile.scale-400.png b/src/winapp-GUI/winapp-GUI/Assets/LargeTile.scale-400.png new file mode 100644 index 00000000..447259e7 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/LargeTile.scale-400.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/LockScreenLogo.scale-200.png b/src/winapp-GUI/winapp-GUI/Assets/LockScreenLogo.scale-200.png new file mode 100644 index 00000000..16838912 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/LockScreenLogo.scale-200.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/SmallTile.scale-100.png b/src/winapp-GUI/winapp-GUI/Assets/SmallTile.scale-100.png new file mode 100644 index 00000000..79f1db60 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/SmallTile.scale-100.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/SmallTile.scale-125.png b/src/winapp-GUI/winapp-GUI/Assets/SmallTile.scale-125.png new file mode 100644 index 00000000..0f95a9b2 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/SmallTile.scale-125.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/SmallTile.scale-150.png b/src/winapp-GUI/winapp-GUI/Assets/SmallTile.scale-150.png new file mode 100644 index 00000000..3e25f2b0 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/SmallTile.scale-150.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/SmallTile.scale-200.png b/src/winapp-GUI/winapp-GUI/Assets/SmallTile.scale-200.png new file mode 100644 index 00000000..219846ec Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/SmallTile.scale-200.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/SmallTile.scale-400.png b/src/winapp-GUI/winapp-GUI/Assets/SmallTile.scale-400.png new file mode 100644 index 00000000..72a14faf Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/SmallTile.scale-400.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/SplashScreen.png b/src/winapp-GUI/winapp-GUI/Assets/SplashScreen.png new file mode 100644 index 00000000..16838912 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/SplashScreen.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/SplashScreen.scale-100.png b/src/winapp-GUI/winapp-GUI/Assets/SplashScreen.scale-100.png new file mode 100644 index 00000000..28e4f264 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/SplashScreen.scale-100.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/SplashScreen.scale-125.png b/src/winapp-GUI/winapp-GUI/Assets/SplashScreen.scale-125.png new file mode 100644 index 00000000..16ec99c0 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/SplashScreen.scale-125.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/SplashScreen.scale-150.png b/src/winapp-GUI/winapp-GUI/Assets/SplashScreen.scale-150.png new file mode 100644 index 00000000..e0d4a882 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/SplashScreen.scale-150.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/SplashScreen.scale-200.png b/src/winapp-GUI/winapp-GUI/Assets/SplashScreen.scale-200.png new file mode 100644 index 00000000..c3fa9c8a Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/SplashScreen.scale-200.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/SplashScreen.scale-400.png b/src/winapp-GUI/winapp-GUI/Assets/SplashScreen.scale-400.png new file mode 100644 index 00000000..83c4d252 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/SplashScreen.scale-400.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Square150x150Logo.scale-100.png b/src/winapp-GUI/winapp-GUI/Assets/Square150x150Logo.scale-100.png new file mode 100644 index 00000000..d762331c Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Square150x150Logo.scale-100.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Square150x150Logo.scale-125.png b/src/winapp-GUI/winapp-GUI/Assets/Square150x150Logo.scale-125.png new file mode 100644 index 00000000..744000c3 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Square150x150Logo.scale-125.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Square150x150Logo.scale-150.png b/src/winapp-GUI/winapp-GUI/Assets/Square150x150Logo.scale-150.png new file mode 100644 index 00000000..057484bf Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Square150x150Logo.scale-150.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Square150x150Logo.scale-200.png b/src/winapp-GUI/winapp-GUI/Assets/Square150x150Logo.scale-200.png new file mode 100644 index 00000000..4f135c81 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Square150x150Logo.scale-200.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Square150x150Logo.scale-400.png b/src/winapp-GUI/winapp-GUI/Assets/Square150x150Logo.scale-400.png new file mode 100644 index 00000000..bbcefd87 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Square150x150Logo.scale-400.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png new file mode 100644 index 00000000..93f157ef Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-16.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png new file mode 100644 index 00000000..921cd044 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-24.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png new file mode 100644 index 00000000..d040d1f8 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-256.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png new file mode 100644 index 00000000..fab08a59 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-32.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png new file mode 100644 index 00000000..f67fab7e Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-lightunplated_targetsize-48.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-unplated_targetsize-16.png b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-unplated_targetsize-16.png new file mode 100644 index 00000000..93f157ef Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-unplated_targetsize-16.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-unplated_targetsize-256.png b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-unplated_targetsize-256.png new file mode 100644 index 00000000..d040d1f8 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-unplated_targetsize-256.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-unplated_targetsize-32.png b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-unplated_targetsize-32.png new file mode 100644 index 00000000..fab08a59 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-unplated_targetsize-32.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-unplated_targetsize-48.png b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-unplated_targetsize-48.png new file mode 100644 index 00000000..f67fab7e Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.altform-unplated_targetsize-48.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.scale-100.png b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.scale-100.png new file mode 100644 index 00000000..ee966b77 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.scale-100.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.scale-125.png b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.scale-125.png new file mode 100644 index 00000000..9554a6a0 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.scale-125.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.scale-150.png b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.scale-150.png new file mode 100644 index 00000000..5cd4858d Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.scale-150.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.scale-200.png b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.scale-200.png new file mode 100644 index 00000000..b6b57e34 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.scale-200.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.scale-400.png b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.scale-400.png new file mode 100644 index 00000000..c5096483 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.scale-400.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.targetsize-16.png b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.targetsize-16.png new file mode 100644 index 00000000..71ef74a4 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.targetsize-16.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.targetsize-24.png b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.targetsize-24.png new file mode 100644 index 00000000..cbdcba70 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.targetsize-24.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png new file mode 100644 index 00000000..921cd044 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.targetsize-24_altform-unplated.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.targetsize-256.png b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.targetsize-256.png new file mode 100644 index 00000000..e28195cc Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.targetsize-256.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.targetsize-32.png b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.targetsize-32.png new file mode 100644 index 00000000..4288fffa Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.targetsize-32.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.targetsize-48.png b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.targetsize-48.png new file mode 100644 index 00000000..593545b2 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Square44x44Logo.targetsize-48.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/StoreLogo.backup.png b/src/winapp-GUI/winapp-GUI/Assets/StoreLogo.backup.png new file mode 100644 index 00000000..16838912 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/StoreLogo.backup.png differ diff --git a/samples/cpp-app-winui/Assets/StoreLogo.png b/src/winapp-GUI/winapp-GUI/Assets/StoreLogo.png similarity index 100% rename from samples/cpp-app-winui/Assets/StoreLogo.png rename to src/winapp-GUI/winapp-GUI/Assets/StoreLogo.png diff --git a/src/winapp-GUI/winapp-GUI/Assets/StoreLogo.scale-100.png b/src/winapp-GUI/winapp-GUI/Assets/StoreLogo.scale-100.png new file mode 100644 index 00000000..7484a99e Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/StoreLogo.scale-100.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/StoreLogo.scale-125.png b/src/winapp-GUI/winapp-GUI/Assets/StoreLogo.scale-125.png new file mode 100644 index 00000000..de04bfe5 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/StoreLogo.scale-125.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/StoreLogo.scale-150.png b/src/winapp-GUI/winapp-GUI/Assets/StoreLogo.scale-150.png new file mode 100644 index 00000000..c1d38730 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/StoreLogo.scale-150.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/StoreLogo.scale-200.png b/src/winapp-GUI/winapp-GUI/Assets/StoreLogo.scale-200.png new file mode 100644 index 00000000..34395843 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/StoreLogo.scale-200.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/StoreLogo.scale-400.png b/src/winapp-GUI/winapp-GUI/Assets/StoreLogo.scale-400.png new file mode 100644 index 00000000..39e3c989 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/StoreLogo.scale-400.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Upload.png b/src/winapp-GUI/winapp-GUI/Assets/Upload.png new file mode 100644 index 00000000..13342ffa Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Upload.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Wide310x150Logo.scale-100.png b/src/winapp-GUI/winapp-GUI/Assets/Wide310x150Logo.scale-100.png new file mode 100644 index 00000000..35fd0e71 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Wide310x150Logo.scale-100.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Wide310x150Logo.scale-125.png b/src/winapp-GUI/winapp-GUI/Assets/Wide310x150Logo.scale-125.png new file mode 100644 index 00000000..98d26de9 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Wide310x150Logo.scale-125.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Wide310x150Logo.scale-150.png b/src/winapp-GUI/winapp-GUI/Assets/Wide310x150Logo.scale-150.png new file mode 100644 index 00000000..41acca5e Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Wide310x150Logo.scale-150.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Wide310x150Logo.scale-200.png b/src/winapp-GUI/winapp-GUI/Assets/Wide310x150Logo.scale-200.png new file mode 100644 index 00000000..28e4f264 Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Wide310x150Logo.scale-200.png differ diff --git a/src/winapp-GUI/winapp-GUI/Assets/Wide310x150Logo.scale-400.png b/src/winapp-GUI/winapp-GUI/Assets/Wide310x150Logo.scale-400.png new file mode 100644 index 00000000..c3fa9c8a Binary files /dev/null and b/src/winapp-GUI/winapp-GUI/Assets/Wide310x150Logo.scale-400.png differ diff --git a/src/winapp-GUI/winapp-GUI/Controls/CertificatePickerControl.xaml b/src/winapp-GUI/winapp-GUI/Controls/CertificatePickerControl.xaml new file mode 100644 index 00000000..f74faa9e --- /dev/null +++ b/src/winapp-GUI/winapp-GUI/Controls/CertificatePickerControl.xaml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + diff --git a/src/winapp-GUI/winapp-GUI/Controls/FileNamePanelControl.xaml.cs b/src/winapp-GUI/winapp-GUI/Controls/FileNamePanelControl.xaml.cs new file mode 100644 index 00000000..2a73d4eb --- /dev/null +++ b/src/winapp-GUI/winapp-GUI/Controls/FileNamePanelControl.xaml.cs @@ -0,0 +1,26 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using System; + +namespace winapp_GUI.Controls +{ + public sealed partial class FileNamePanelControl : UserControl + { + public FileNamePanelControl() + { + this.InitializeComponent(); + } + + public string FileName + { + get => FileNameText.Text; + set => FileNameText.Text = value; + } + + public event RoutedEventHandler CancelClicked + { + add { CancelButton.Click += value; } + remove { CancelButton.Click -= value; } + } + } +} diff --git a/src/winapp-GUI/winapp-GUI/Controls/ProgressPanelControl.xaml b/src/winapp-GUI/winapp-GUI/Controls/ProgressPanelControl.xaml new file mode 100644 index 00000000..2b1f88d1 --- /dev/null +++ b/src/winapp-GUI/winapp-GUI/Controls/ProgressPanelControl.xaml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/winapp-GUI/winapp-GUI/Controls/ProgressPanelControl.xaml.cs b/src/winapp-GUI/winapp-GUI/Controls/ProgressPanelControl.xaml.cs new file mode 100644 index 00000000..143c85fa --- /dev/null +++ b/src/winapp-GUI/winapp-GUI/Controls/ProgressPanelControl.xaml.cs @@ -0,0 +1,21 @@ +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using System.Collections.ObjectModel; +using winapp_GUI; + +namespace winapp_GUI.Controls +{ + public sealed partial class ProgressPanelControl : UserControl + { + public ProgressPanelControl() + { + this.InitializeComponent(); + } + + public ObservableCollection ProgressCards + { + get => (ObservableCollection)ProgressItemsControl.ItemsSource; + set => ProgressItemsControl.ItemsSource = value; + } + } +} diff --git a/src/winapp-GUI/winapp-GUI/MainWindow.xaml b/src/winapp-GUI/winapp-GUI/MainWindow.xaml new file mode 100644 index 00000000..dbe9d6e4 --- /dev/null +++ b/src/winapp-GUI/winapp-GUI/MainWindow.xaml @@ -0,0 +1,257 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +