diff --git a/.github/workflows/build-package.yml b/.github/workflows/build-package.yml index 6662ee8a..336a4e16 100644 --- a/.github/workflows/build-package.yml +++ b/.github/workflows/build-package.yml @@ -173,34 +173,6 @@ jobs: }); core.info(`Marked comment ${existing.id} as stale.`); - # E2E test for Electron workflow - runs after build completes - e2e-test: - runs-on: windows-latest - needs: build-and-package - 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 - with: - node-version: '24' - - - name: Download npm package artifact - uses: actions/download-artifact@v4 - with: - name: npm-package - path: artifacts/npm - - - name: Run E2E Electron test - run: | - .\scripts\test-e2e-electron.ps1 -ArtifactsPath "artifacts/npm" -Verbose - # E2E test for winapp ui commands against WinUI 3 sample app e2e-test-ui: runs-on: windows-latest diff --git a/.github/workflows/test-samples.yml b/.github/workflows/test-samples.yml new file mode 100644 index 00000000..2f9a53e4 --- /dev/null +++ b/.github/workflows/test-samples.yml @@ -0,0 +1,190 @@ +name: Test Samples & Guides + +on: + pull_request: + branches: [ "main" ] + workflow_dispatch: + inputs: + sample: + description: 'Specific sample to test (or "all")' + required: false + default: 'all' + type: choice + options: + - all + - cpp-app + - dotnet-app + - electron + - flutter-app + - packaging-cli + - rust-app + - tauri-app + - wpf-app + +permissions: + contents: read + +jobs: + # Build npm (and NuGet) package for pull_request and workflow_dispatch events. + # This makes the workflow self-contained — no cross-workflow artifact chaining. + build: + if: github.event_name == 'pull_request' || github.event_name == 'workflow_dispatch' + runs-on: windows-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-dotnet@v5 + with: + dotnet-version: '10.0.x' + - uses: actions/setup-node@v5 + with: + node-version: '24' + - name: Build npm package + shell: pwsh + run: .\scripts\build-cli.ps1 -SkipTests -SkipMsix + - uses: actions/upload-artifact@v4 + with: + name: npm-package + path: artifacts/*.tgz + - uses: actions/upload-artifact@v4 + with: + name: nuget-package + path: artifacts/nuget/*.nupkg + if-no-files-found: ignore + + test-sample: + needs: [build] + if: needs.build.result == 'success' + strategy: + fail-fast: false + matrix: + sample: [cpp-app, dotnet-app, electron, flutter-app, packaging-cli, rust-app, tauri-app, wpf-app] + runs-on: windows-latest + name: ${{ matrix.sample }} + + steps: + - name: Check if sample should run + id: check + shell: pwsh + run: | + $requested = '${{ github.event.inputs.sample || 'all' }}' + if ($requested -ne 'all' -and $requested -ne '${{ matrix.sample }}') { + echo "skip=true" >> $env:GITHUB_OUTPUT + } else { + echo "skip=false" >> $env:GITHUB_OUTPUT + } + + - name: Checkout + if: steps.check.outputs.skip != 'true' + uses: actions/checkout@v5 + + # Download the npm package artifact built by the `build` job above + - name: Download npm package + if: steps.check.outputs.skip != 'true' + uses: actions/download-artifact@v4 + with: + name: npm-package + path: artifacts/npm + + # Download NuGet package for .NET samples + - name: Download NuGet package + if: >- + steps.check.outputs.skip != 'true' && + contains(fromJson('["dotnet-app", "wpf-app"]'), matrix.sample) + uses: actions/download-artifact@v4 + with: + name: nuget-package + path: artifacts/nuget + continue-on-error: true + + - name: Add local NuGet source + if: >- + steps.check.outputs.skip != 'true' && + contains(fromJson('["dotnet-app", "wpf-app"]'), matrix.sample) + shell: pwsh + run: | + $nugetPath = "artifacts/nuget" + if (Test-Path $nugetPath) { + $resolvedPath = (Resolve-Path $nugetPath).Path + dotnet nuget add source $resolvedPath --name WinAppLocal + Write-Host "Added local NuGet source: $resolvedPath" + } else { + Write-Warning "No NuGet artifacts found — .NET samples may fail to restore" + } + + # --- Toolchain setup (conditional per sample) --- + + - name: Setup .NET + if: >- + steps.check.outputs.skip != 'true' && + contains(fromJson('["dotnet-app", "wpf-app", "packaging-cli", "electron"]'), matrix.sample) + uses: actions/setup-dotnet@v5 + with: + dotnet-version: '10.0.x' + + - name: Setup Node.js + if: >- + steps.check.outputs.skip != 'true' && + contains(fromJson('["electron", "tauri-app", "cpp-app", "dotnet-app", "wpf-app", "rust-app", "flutter-app", "packaging-cli"]'), matrix.sample) + uses: actions/setup-node@v5 + with: + node-version: '24' + + - name: Setup Flutter + if: >- + steps.check.outputs.skip != 'true' && + matrix.sample == 'flutter-app' + uses: subosito/flutter-action@v2 + with: + channel: stable + + - name: Setup Rust + if: >- + steps.check.outputs.skip != 'true' && + contains(fromJson('["rust-app", "tauri-app"]'), matrix.sample) + uses: dtolnay/rust-toolchain@stable + + # --- Run the sample's self-contained Pester test --- + + - name: Install Pester + if: steps.check.outputs.skip != 'true' + shell: pwsh + run: | + Install-Module -Name Pester -Force -SkipPublisherCheck -Scope CurrentUser -MinimumVersion 5.0 + + - name: Run ${{ matrix.sample }} test + if: steps.check.outputs.skip != 'true' + shell: pwsh + run: | + $container = New-PesterContainer -Path "samples/${{ matrix.sample }}/test.Tests.ps1" -Data @{ + WinappPath = "artifacts/npm" + } + $config = New-PesterConfiguration + $config.Run.Container = $container + $config.Run.Exit = $true + $config.Output.Verbosity = 'Detailed' + $config.TestResult.Enabled = $true + $config.TestResult.OutputPath = "test-results-${{ matrix.sample }}.xml" + $config.TestResult.OutputFormat = 'JUnitXml' + Invoke-Pester -Configuration $config + + - name: Upload test results + if: always() && steps.check.outputs.skip != 'true' + uses: actions/upload-artifact@v4 + with: + name: test-results-${{ matrix.sample }} + path: test-results-${{ matrix.sample }}.xml + if-no-files-found: ignore + + # Summary job to provide a single check status for branch protection + test-samples-result: + if: always() + needs: [build, test-sample] + runs-on: ubuntu-latest + steps: + - name: Check results + run: | + if [ "${{ needs.test-sample.result }}" = "failure" ]; then + echo "::error::One or more sample tests failed" + exit 1 + fi + echo "All sample tests passed (or were skipped)" diff --git a/AGENTS.md b/AGENTS.md index 8f2b0a89..6e2f9d70 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -47,6 +47,47 @@ When adding or changing public facing features, ensure all documentation is also If a feature is big enough and requires its own docs page, add it under docs\ +## Sample & guide testing + +Each sample under `samples/` has a self-contained **Pester 5.x** test file (`test.Tests.ps1`) that validates the corresponding guide workflow from scratch (Phase 1) and verifies the existing sample code still builds (Phase 2). Tests share infrastructure via `samples/SampleTestHelpers.psm1`. + +### Running sample & guide tests locally + +```powershell +# Run all sample tests +.\scripts\test-samples.ps1 + +# Run a specific sample +.\scripts\test-samples.ps1 -Samples dotnet-app + +# Run with a locally built winapp npm tarball (package-npm.ps1 outputs to .\artifacts\) +.\scripts\test-samples.ps1 -WinappPath .\artifacts -Verbose + +# Or pass a specific .tgz / a directory containing one (e.g., a CI artifact download) +.\scripts\test-samples.ps1 -WinappPath .\artifacts\npm -Verbose +``` + +### Writing a new sample & guide test + +1. Create `test.Tests.ps1` in the sample directory (Pester naming convention) +2. Use `BeforeDiscovery` for skip logic (prerequisite checks run at discovery time) +3. Import shared helpers in `BeforeAll`: `Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force` +4. Accept `$WinappPath` and `$SkipCleanup` parameters via `param()` block +5. Phase 1 (`Context`): from-scratch guide workflow in a temp directory (scaffold, winapp init, build, cert, pack) +6. Phase 2 (`Context`): quick build of existing sample code to verify freshness +7. Add the sample name to the matrix in `.github/workflows/test-samples.yml` + +#### Pester conventions for sample tests + +- **`BeforeDiscovery`**: Set `$script:skip` using inline `Get-Command` checks (no module import). Pester evaluates `-Skip:$variable` during discovery, before `BeforeAll` runs. +- **`BeforeAll`**: Import `SampleTestHelpers.psm1`, install winapp, create temp directories. Guard with `if ($script:skip) { return }`. +- **`AfterAll`**: Clean up temp directories using `Remove-TempTestDirectory`. +- **`It` blocks**: Use `-Skip:$script:skip` for prerequisite gating. Use Pester `Should` assertions (`Should -Be 0`, `Should -Exist`, `Should -Not -BeNullOrEmpty`). When all setup happens in `BeforeAll` and depends on the prerequisite, you may apply `-Skip:$script:skip` to the enclosing `Context` instead. +- **Shared helpers**: `Invoke-WinappCommand` (throws on failure), `Test-Prerequisite` (returns bool), `New-TempTestDirectory`, `Remove-TempTestDirectory`, `Install-WinappGlobal`. + +### CI integration + +Sample & guide tests run via `.github/workflows/test-samples.yml` using a GitHub Actions matrix strategy. Each sample runs in its own parallel job after the main build completes. The workflow downloads the npm package artifact from the `Build and Package` workflow. Test results are uploaded as JUnit XML via `Invoke-Pester` with `TestResult` configuration. ## Where to look first diff --git a/docs/guides/cpp.md b/docs/guides/cpp.md index e71ed31f..cf6ce3f4 100644 --- a/docs/guides/cpp.md +++ b/docs/guides/cpp.md @@ -8,18 +8,18 @@ A standard executable (like one created with `cmake --build`) does not have pack ## Prerequisites -1. **Build Tools**: Use a compiler toolchain supported by CMake. This example uses Visual Studio. You can install the community edition with: +1. **Build Tools**: Use a compiler toolchain supported by CMake. This example uses Visual Studio. You can install the community edition with (or update if already installed): ```powershell winget install --id Microsoft.VisualStudio.Community --source winget --override "--add Microsoft.VisualStudio.Workload.NativeDesktop --includeRecommended --passive --wait" ``` Reboot after installation. -2. **CMake**: Install CMake: +2. **CMake**: Install CMake (or update if already installed): ```powershell winget install Kitware.CMake --source winget ``` -3. **winapp CLI**: Install the `winapp` cli via winget: +3. **winapp CLI**: Install the `winapp` cli via winget (or update if already installed): ```powershell winget install Microsoft.winappcli --source winget ``` @@ -67,24 +67,16 @@ cmake --build build --config Debug ## 2. Update Code to Check Identity -We'll update the app to check if it's running with package identity. We'll use the Windows Runtime C++ API to access the Package APIs. +We'll update the app to check if it's running with package identity. This will help us verify that identity is working correctly in later steps. We'll use the Windows Runtime C++ API to access the Package APIs. -First, update your `CMakeLists.txt` to link against the Windows App Model library: +First, add the following line to the end of your `CMakeLists.txt` to link against the Windows App Model library: ```cmake -cmake_minimum_required(VERSION 3.20) -project(cpp-app) - -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -add_executable(cpp-app main.cpp) - # Link Windows Runtime libraries target_link_libraries(cpp-app PRIVATE WindowsApp.lib OneCoreUap.lib) ``` -Next, replace the contents of `main.cpp` with the following code. This code attempts to retrieve the current package identity using the Windows Runtime API. If it succeeds, it prints the Package Family Name; otherwise, it prints "Not packaged". +Next, replace the entire contents of `main.cpp` with the following code. This code attempts to retrieve the current package identity using the Windows Runtime API. If it succeeds, it prints the Package Family Name; otherwise, it prints "Not packaged". ```cpp #include @@ -134,7 +126,7 @@ The `winapp init` command sets up everything you need in one go: app manifest, a Run the following command and follow the prompts: ```powershell -winapp init +winapp init . ``` When prompted: @@ -142,18 +134,54 @@ When prompted: - **Publisher name**: Press Enter to accept the default or enter your name - **Version**: Press Enter to accept 1.0.0.0 - **Entry point**: Press Enter to accept the default (cpp-app.exe) -- **Setup SDKs**: Select "Stable SDKs" to download Windows App SDK and generate headers +- **Setup SDKs**: Select "Stable SDKs" to download Windows App SDK and generate C++ headers This command will: -- Create `appxmanifest.xml` and `Assets` folder for your app identity +- Create `appxmanifest.xml` — the manifest that defines your app's identity +- Create `Assets` folder — icons required for MSIX packaging and Store submission - Create a `.winapp` folder with Windows App SDK headers and libraries -- Create a `winapp.yaml` configuration file for pinning sdk versions +- Create a `winapp.yaml` configuration file for pinning SDK versions You can open `appxmanifest.xml` to further customize properties like the display name, publisher, and capabilities. +### Add Execution Alias (for console apps) + +An execution alias lets users run your app by name from any terminal (like `cpp-app`). It also enables `winapp run --with-alias` during development, which keeps console output in the current terminal instead of opening a new window. + +You can add one automatically: + +```powershell +winapp manifest add-alias +``` + +Or manually: open `appxmanifest.xml` and add the `uap5` namespace to the `` tag if it's missing, and then add the extension inside `...`: + +```diff + + + ... + + + ... ++ ++ ++ ++ ++ ++ ++ + + + +``` + ## 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 run`. This registers a loose layout package (just like a real MSIX install) and launches the app in one step. No certificate or signing is needed for debugging. 1. **Build the executable**: ```powershell @@ -165,7 +193,9 @@ To test features that require identity (like Notifications) without fully packag winapp run .\build\Debug --with-alias ``` -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`. +The `--with-alias` flag launches the app via its execution alias so console output stays in the current terminal. This requires the `uap5:ExecutionAlias` we added in step 4. + +> **Tip**: `winapp run` also registers the package on your system. This is why the MSIX may appear as "already installed" when you try to install it later in step 8. Use `winapp unregister` to clean up development packages when done. You should now see output similar to: ``` @@ -186,34 +216,22 @@ winapp create-debug-identity .\build\Debug\cpp-app.exe ## 6. Using Windows App SDK (Optional) -If you selected to setup the SDKs during `winapp init`, you now have access to Windows App SDK headers in the `.winapp/include` folder. This gives you access to modern Windows APIs like notifications, windowing, and more. +If you selected to setup the SDKs during `winapp init`, you now have access to Windows App SDK headers in the `.winapp/include` folder. This gives you access to modern Windows APIs like notifications, windowing, on-device AI, and more. If you just need package identity for distribution, you can skip to step 7. Let's add a simple example that prints the Windows App Runtime version. ### Update CMakeLists.txt -Add the Windows App SDK include directory and link the necessary libraries: +Add the following line to the end of your `CMakeLists.txt` to include the Windows App SDK headers: ```cmake -cmake_minimum_required(VERSION 3.20) -project(cpp-app) - -set(CMAKE_CXX_STANDARD 20) -set(CMAKE_CXX_STANDARD_REQUIRED ON) - -add_executable(cpp-app main.cpp) - -# Link Windows Runtime libraries -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) - ``` ### Update main.cpp -Replace the contents of `main.cpp` to use the Windows App Runtime API: +Replace the entire contents of `main.cpp` to use the Windows App Runtime API: ```cpp #include @@ -382,7 +400,7 @@ With this setup: ## 8. Package with MSIX -Once you're ready to distribute your app, you can package it as an MSIX using the same manifest. +Once you're ready to distribute your app, you can package it as an MSIX using the same manifest. MSIX provides clean install/uninstall, auto-updates, and a trusted installation experience. ### Prepare the Package Directory First, build your application in release mode for optimal performance: @@ -391,51 +409,23 @@ First, build your application in release mode for optimal performance: cmake --build build --config Release ``` -Then, create a directory to hold your package files and copy your release executable: +Then, create a directory with just the files needed for distribution and copy your release executable: ```powershell mkdir dist copy .\build\Release\cpp-app.exe .\dist\ ``` -### Add Execution Alias -To allow users to run your app from the command line after installation (like `cpp-app`), add an execution alias to the `appxmanifest.xml`. - -Open `appxmanifest.xml` and add the `uap5` namespace to the `` tag if it's missing, and then add the extension inside `...`: - -```xml - - - ... - - - ... - - - - - - - - - ... - - - -``` - ### Generate a Development Certificate -Before packaging, you need a development certificate for signing. Generate one if you haven't already: +MSIX packages must be signed. For local testing, generate a self-signed development certificate: ```powershell winapp cert generate --if-exists skip ``` +> **Tip**: The certificate's publisher must match the `Publisher` in your `appxmanifest.xml`. The `cert generate` command reads this automatically from your manifest. + ### Sign and Pack Now you can package and sign: @@ -445,23 +435,28 @@ Now you can package and sign: winapp pack .\dist --cert .\devcert.pfx ``` -> Note: The appxmanifest.xml and assets need to be in the target folder for packaging. To simplify, the `pack` command by default uses the appxmanifest.xml in your current directory and copies it to the target folder before packaging. +> **Tip**: 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. ### Install the Certificate -Before you can install the MSIX package, you need to install the development certificate. Run this command as administrator (you only need to do this once): +Before you can install the MSIX package, you need to trust the development certificate on your machine. Run this command as administrator (you only need to do this once per certificate): ```powershell winapp cert install .\devcert.pfx ``` ### Install and Run -The `winapp pack` command generates the MSIX file in your project root directory. You can install the package using PowerShell: + +> **Tip**: If you used `winapp run` in step 5, the package may already be registered on your system. Use `winapp unregister` first to remove the development registration, then install the release package. + +The `winapp pack` command generates the MSIX file in your project root directory. Install the package by double-clicking the generated `.msix` file, or using PowerShell: ```powershell -Add-AppxPackage .\cpp-app.msix +Add-AppxPackage .\cpp-app_1.0.0.0_x64.msix ``` +> **Tip**: The MSIX filename includes the version and architecture (e.g., `cpp-app_1.0.0.0_arm64.msix`). Check your directory for the exact filename. + Now you can run your app from anywhere in the terminal by typing: ```powershell @@ -470,8 +465,17 @@ cpp-app You should see the "Package Family Name" output, confirming it's installed and running with identity. -### Tips: +> **Tip**: If you need to repackage your app (e.g., after code changes), increment the `Version` in your `appxmanifest.xml` before running `winapp pack` again. Windows requires a higher version number to update an installed package. + +## Tips 1. Once you are ready for distribution, you can sign your MSIX with a code signing certificate from a Certificate Authority so your users don't have to install a self-signed certificate. 2. The [Azure Trusted Signing](https://azure.microsoft.com/products/trusted-signing) service is a great way to manage your certificates securely and integrate signing into your CI/CD pipeline. 3. The Microsoft Store will sign the MSIX for you, no need to sign before submission. 4. You might need to create multiple MSIX packages, one for each architecture you support (x64, Arm64). Configure CMake with the appropriate generator and architecture flags. + +## Next Steps + +- **Distribute via winget**: Submit your MSIX to the [Windows Package Manager Community Repository](https://github.com/microsoft/winget-pkgs) +- **Publish to the Microsoft Store**: Use `winapp store` to submit your package +- **Set up CI/CD**: Use the [`setup-WinAppCli`](https://github.com/microsoft/setup-WinAppCli) GitHub Action to automate packaging in your pipeline +- **Explore Windows APIs**: With package identity, you can now use [Notifications](https://learn.microsoft.com/windows/apps/develop/notifications/app-notifications/app-notifications-quickstart), [on-device AI](https://learn.microsoft.com/windows/ai/apis/), and other [identity-dependent APIs](https://learn.microsoft.com/windows/apps/desktop/modernize/desktop-to-uwp-extensions) diff --git a/docs/guides/dotnet.md b/docs/guides/dotnet.md index d5311676..af963321 100644 --- a/docs/guides/dotnet.md +++ b/docs/guides/dotnet.md @@ -10,12 +10,12 @@ A standard executable (like one created with `dotnet build`) does not have packa ## Prerequisites -1. **.NET SDK**: Install the .NET SDK: +1. **.NET SDK**: Install the .NET SDK (requires a restart after installation): ```powershell winget install Microsoft.DotNet.SDK.10 --source winget ``` -2. **winapp CLI**: Install the `winapp` tool via winget: +2. **winapp CLI**: Install the `winapp` tool via winget (or update if already installed): ```powershell winget install Microsoft.winappcli --source winget ``` @@ -104,6 +104,14 @@ This command will: You can open `appxmanifest.xml` to further customize properties like the display name, publisher, and capabilities. +To verify the packages were added to your project: + +```powershell +dotnet list package +``` + +You should see `Microsoft.WindowsAppSDK` and `Microsoft.Windows.SDK.BuildTools` in the output. + ## 5. Debug with Identity Since `winapp init` added the `Microsoft.Windows.SDK.BuildTools.WinApp` NuGet package to your project, you can simply run: @@ -114,6 +122,8 @@ dotnet run This automatically invokes `winapp run` under the hood — creating a loose layout package, registering it with Windows, and launching your app with full package identity. +> **Note**: You may see NuGet vulnerability warnings (NU1900) about package sources. These are safe to ignore — they don't affect your build. + > **Console apps:** By default, AUMID activation opens a new window. For console applications that need stdin/stdout in the current terminal, add `true` to your `.csproj` and ensure your manifest has a `uap5:ExecutionAlias`. You can add one with `winapp manifest add-alias`. You should see output similar to: @@ -165,10 +175,13 @@ With this configuration, `dotnet build` applies the debug identity and you can r > **Tip:** For advanced debugging workflows (attaching debuggers, IDE setup, startup debugging), see the [Debugging Guide](../debugging.md). +> **When to skip this**: If you prefer explicit control over when identity is applied, or if you're working on code that doesn't need identity for most of your development cycle, the manual approach above may be simpler. -## 6. Using Windows App SDK +## 6. Using Windows App SDK (Optional) -If you ran `winapp init` (Step 4), `Microsoft.WindowsAppSDK` was already added as a NuGet package reference to your `.csproj`. If you skipped SDK setup during init, or need to add it manually, run: +The Windows App SDK gives you access to modern Windows APIs beyond what the base Windows SDK provides — things like the notification system, windowing APIs, app lifecycle management, and on-device AI. If your app needs any of these capabilities, this step is for you. If you just need package identity for distribution, you can skip to step 7. + +If you ran `winapp init` (Step 4), `Microsoft.WindowsAppSDK` was already added as a NuGet package reference to your `.csproj`. You can verify with `dotnet list package`. If you skipped SDK setup during init, or need to add it manually, run: ```powershell dotnet add package Microsoft.WindowsAppSDK @@ -176,7 +189,7 @@ dotnet add package Microsoft.WindowsAppSDK ### Update Program.cs -Let's update the app to use the Windows App Runtime API to get the runtime version: +Replace the entire contents of `Program.cs` with the following code, which adds a Windows App Runtime version check: ```csharp using Windows.ApplicationModel; @@ -206,7 +219,7 @@ class Program ### Build and Run -Rebuild and run the application with Windows App SDK. Since we've added the WinAppSDK, we need to re-register with identity so `winapp` adds the runtime dependency. If you added the WinApp NuGet package (recommended), simply run `dotnet run`. Otherwise: +Rebuild and run the application with Windows App SDK. Since we've added the WinAppSDK, we need to re-register with identity so `winapp` adds the runtime dependency. If you added the WinApp NuGet package (recommended), simply run `dotnet run`. Otherwise (replace `dotnet-app` with your project name): ```powershell dotnet build -c Debug @@ -238,8 +251,10 @@ First, build your application in release mode for optimal performance: dotnet build -c Release ``` +> **Note**: You may see NuGet vulnerability warnings (NU1900). These are safe to ignore and don't affect your build output. + ### Add Execution Alias (for console apps) -To allow users to run your app from the command line after installation (like `dotnet-app`), add an execution alias to the `appxmanifest.xml`. If you are building a WPF or WinForms app, this step is not necessary. +To allow users to run your app from the command line after installation (like `dotnet-app`), add an execution alias to the `appxmanifest.xml`. If you are building a WPF or WinForms app, this step is not necessary — those apps launch from the Start menu instead. Open `appxmanifest.xml` and add the `uap5` namespace to the `` tag if it's missing, and then add the extension inside `...`: @@ -281,7 +296,7 @@ winapp cert generate --if-exists skip ### Sign and Pack -Now you can package and sign. Point the pack command to your build output folder: +Now you can package and sign. Point the pack command to your build output folder (replace `dotnet-app` and the TFM path with your project's values): ```powershell # package and sign the app with the generated certificate @@ -309,7 +324,9 @@ dotnet-app You should see the "Package Family Name" output, confirming it's installed and running with identity. -### Tips: +> **Tip**: If you need to repackage your app (e.g., after code changes), increment the `Version` in your `appxmanifest.xml` before running `winapp pack` again. Windows requires a higher version number to update an installed package. + +## Tips 1. Once you are ready for distribution, you can sign your MSIX with a code signing certificate from a Certificate Authority so your users don't have to install a self-signed certificate. 2. The Microsoft Store will sign the MSIX for you, no need to sign before submission. 3. You might need to create multiple MSIX packages, one for each architecture you support (x64, Arm64). Use the `-r` flag with `dotnet build` to target specific architectures: `dotnet build -c Release -r win-x64` or `dotnet build -c Release -r win-arm64`. @@ -334,3 +351,10 @@ With this configuration: - The final `.msix` file will be in the root of the project You can also create a custom configuration (e.g., `PackagedRelease`) by modifying the condition to `'$(Configuration)' == 'PackagedRelease'`. + +## Next Steps + +- **Distribute via winget**: Submit your MSIX to the [Windows Package Manager Community Repository](https://github.com/microsoft/winget-pkgs) +- **Publish to the Microsoft Store**: Use `winapp store` to submit your package +- **Set up CI/CD**: Use the [`setup-WinAppCli`](https://github.com/microsoft/setup-WinAppCli) GitHub Action to automate packaging in your pipeline +- **Explore Windows APIs**: With package identity, you can now use [Notifications](https://learn.microsoft.com/windows/apps/develop/notifications/app-notifications/app-notifications-quickstart), [on-device AI](https://learn.microsoft.com/windows/ai/apis/), and other [identity-dependent APIs](https://learn.microsoft.com/windows/apps/desktop/modernize/desktop-to-uwp-extensions) diff --git a/docs/guides/electron/packaging.md b/docs/guides/electron/packaging.md index f4869b1a..1d340980 100644 --- a/docs/guides/electron/packaging.md +++ b/docs/guides/electron/packaging.md @@ -11,17 +11,40 @@ Before packaging, make sure you've: ## Prepare for Packaging -> **📝 Note:** Before packaging, make sure to configure your build tool (Electron Forge, webpack, etc.) to exclude temporary files from the final build: -> - `.winapp/` folder -> - `winapp.yaml` -> - Certificate files (`.pfx`) -> - Debug symbols (`.pdb`) -> - C# build artifacts (`obj/`, `bin/` folders) -> - MSIX packages (*.msix) -> -> **⚠️ Important:** Verify that your `appxmanifest.xml` matches your packaged app structure: +Configure Electron Forge to exclude temporary files from the final build. Add an `ignore` array to your `packagerConfig` in `forge.config.js`: + +```javascript +module.exports = { + packagerConfig: { + asar: true, + ignore: [ + /^\/\.winapp($|\/)/, // SDK packages and headers + /^\/winapp\.yaml$/, // SDK config + /\.pfx$/, // Certificate files + /\.pdb$/, // Debug symbols + /\/obj($|\/)/, // C# build artifacts + /\/bin($|\/)/, // C# build artifacts + /\.msix$/ // MSIX packages + ] + }, + // ... rest of your config +}; +``` + +> [!IMPORTANT] +> Verify that your `appxmanifest.xml` matches your packaged app structure: > - The `Executable` attribute should point to the correct .exe file in your packaged output +## Generate a Development Certificate + +Before creating a signed MSIX package, generate a development certificate: + +```bash +npx winapp cert generate +``` + +This creates a `devcert.pfx` file in your project root that will be used to sign the MSIX package. + ## Packaging Options You have two options for creating an MSIX package for your Electron app: @@ -66,7 +89,8 @@ The `--out` option is also optional. If not provided, the current directory will The MSIX package will be created as `./out/.msix`. -> **💡 Tip:** You can add these commands to your `package.json` scripts for convenience: +> [!TIP] +> You can add these commands to your `package.json` scripts for convenience: > ```json > { > "scripts": { @@ -113,7 +137,7 @@ module.exports = { #### Update appxmanifest.xml -The Electron Forge MSIX maker uses a different folder layout than the winapp CLI approach. Update the `Executable` path in your `appxmanifest.xml` to point to the `app` folder: +The Electron Forge MSIX maker uses a different folder layout than the winapp CLI approach. It places your app inside an `app\` folder in the MSIX. This folder is created automatically during packaging — you don't need to create it yourself. Update the `Executable` path in your `appxmanifest.xml` to point to the `app` folder: ```xml @@ -125,19 +149,23 @@ The Electron Forge MSIX maker uses a different folder layout than the winapp CLI ``` -Replace `my-app.exe` with your actual executable name. +Replace `my-app.exe` with your actual executable name. This is based on the `productName` (or `name`) field in your `package.json`. + +> [!NOTE] +> The Forge MSIX maker looks for Windows SDK tools based on the `MinVersion` in your `appxmanifest.xml`. If you get an error about WindowsKit not being found, ensure the SDK version specified in `MinVersion` is installed on your machine, or update `MinVersion` to match an installed SDK version. #### Create the MSIX Package -Now you can create the MSIX package with a single command: +Now you can create the MSIX package. Use the `--targets` flag to run only the MSIX maker (otherwise Forge will run all configured makers): ```bash -npm run make +npx electron-forge make --targets @electron-forge/maker-msix ``` -The MSIX package will be created in the `./out/make/msix/` folder. +The MSIX package will be created in the `./out/make/msix//` folder (e.g., `./out/make/msix/arm64/` or `./out/make/msix/x64/`). -> **💡 Tip:** This approach is more integrated with the Electron Forge workflow and automatically handles packaging and MSIX creation in one step. +> [!TIP] +> This approach is more integrated with the Electron Forge workflow and automatically handles packaging and MSIX creation in one step. ## Install and Test the MSIX @@ -151,9 +179,15 @@ npx winapp cert install .\devcert.pfx Now install the MSIX package. Double click the msix file or run the following command: ```bash -Add-AppxPackage .\my-windows-app.msix +# Option 1 output: +Add-AppxPackage .\out\.msix + +# Option 2 output: +Add-AppxPackage .\out\make\msix\\.msix ``` +Replace `` and `` with the actual values from your build output. + Your app will appear in the Start Menu! Launch it and test your Windows API features. ## Distribution Options diff --git a/docs/guides/electron/phi-silica-addon.md b/docs/guides/electron/phi-silica-addon.md index 1138e3c7..846c6b98 100644 --- a/docs/guides/electron/phi-silica-addon.md +++ b/docs/guides/electron/phi-silica-addon.md @@ -24,11 +24,12 @@ This creates a `csAddon/` folder with: - `csAddon.csproj` - Project file with references to Windows SDK and Windows App SDK - `README.md` - Documentation on how to use the addon -The command also adds a `build-csAddon` script to your `package.json` for building the addon: +The command also adds a `build-csAddon` script to your `package.json` for building the addon, and a `clean-csAddon` script for cleaning build artifacts: ```json { "scripts": { - "build-csAddon": "dotnet publish ./csAddon/csAddon.csproj -c Release" + "build-csAddon": "dotnet publish ./csAddon/csAddon.csproj -c Release", + "clean-csAddon": "dotnet clean ./csAddon/csAddon.csproj" } } ``` @@ -42,7 +43,8 @@ Let's verify everything is set up correctly by building the addon: npm run build-csAddon ``` -> **Note:** You can also create a C++ addon using `npx winapp node create-addon` (without the `--template` flag). C++ addons use [node-addon-api](https://github.com/nodejs/node-addon-api) and provide direct access to Windows APIs with maximum performance. See the [C++ Notification Addon guide](cpp-notification-addon.md) for a walkthrough or the [full command documentation](../../usage.md#node-create-addon) for more options. +> [!NOTE] +> You can also create a C++ addon using `npx winapp node create-addon` (without the `--template` flag). C++ addons use [node-addon-api](https://github.com/nodejs/node-addon-api) and provide direct access to Windows APIs with maximum performance. See the [C++ Notification Addon guide](cpp-notification-addon.md) for a walkthrough or the [full command documentation](../../usage.md#node-create-addon) for more options. ## Step 2: Add AI Capabilities with Phi Silica @@ -99,14 +101,15 @@ namespace csAddon } ``` -> **📝 Note:** Phi Silica requires Windows 11 with an NPU-equipped device (Copilot+ PC). If you don't have compatible hardware, the API will return a message indicating the model is not available. You can still complete this tutorial and package the app - it will gracefully handle devices without NPU support. +> [!NOTE] +> Phi Silica requires Windows 11 with an NPU-equipped device (Copilot+ PC). If you don't have compatible hardware, the API will return a message indicating the model is not available. You can still complete this tutorial and package the app - it will gracefully handle devices without NPU support. ## Step 3: Build the C# Addon Now build the addon again: ```bash -npm run build-addon +npm run build-csAddon ``` This compiles your C# code using **Native AOT** (Ahead-of-Time compilation), which: @@ -115,11 +118,11 @@ This compiles your C# code using **Native AOT** (Ahead-of-Time compilation), whi - Requires **no .NET runtime** on target machines - Provides native performance -The compiled addon will be in `csAddon/bin/Release/net10.0/win-/publish/csAddon.node` . +The compiled addon will be in `csAddon/dist/csAddon.node`. ## Step 4: Test the Windows API -Now let's verify the addon works by calling it from the main process. Open `src/index.js` and follow these steps: +Now let's verify the addon works by calling it from the main process. Open `src/main.js` and follow these steps: ### 4.1. Load the C# Addon @@ -162,7 +165,8 @@ Before you can use the Phi Silica API, you need to declare the required capabili ``` -> **💡 Tip:** Different Windows APIs require different capabilities. Always check the API documentation to see what capabilities are needed. Common ones include `microphone`, `webcam`, `location`, and `bluetooth`. +> [!TIP] +> Different Windows APIs require different capabilities. Always check the API documentation to see what capabilities are needed. Common ones include `microphone`, `webcam`, `location`, and `bluetooth`. ## Step 6: Update Debug Identity @@ -177,7 +181,8 @@ This command: 2. Registers `electron.exe` in your `node_modules` with a temporary identity 3. Enables you to test identity-required APIs without full MSIX packaging -> **📝 Note:** This command is already part of the `postinstall` script we added in the setup guide, so it runs automatically after `npm install`. However, you need to run it manually whenever you: +> [!NOTE] +> This command is already part of the `postinstall` script we added in the setup guide, so it runs automatically after `npm install`. However, you need to run it manually whenever you: > - Modify `appxmanifest.xml` (change capabilities, identity, or properties) > - Update app assets (icons, logos, etc.) > - Reinstall or update dependencies diff --git a/docs/guides/electron/setup.md b/docs/guides/electron/setup.md index 6990dd5c..42c6f593 100644 --- a/docs/guides/electron/setup.md +++ b/docs/guides/electron/setup.md @@ -20,6 +20,12 @@ npm create electron-app@latest my-windows-app cd my-windows-app ``` +When prompted by Electron Forge: +- **Bundler**: Select **None** (recommended — native addons work without extra configuration) +- **Language**: Select **JavaScript** (this guide uses JS; TypeScript works too) +- **Electron version**: Select **latest** +- **Initialize git**: Your preference + Verify the app runs: ```bash @@ -30,6 +36,8 @@ You should see the default Electron Forge window. Close it and let's add Windows ## Step 2: Install winapp CLI +The Electron workflow requires the **npm package** (`@microsoft/winappcli`) rather than the standalone CLI installed from winget. The npm package includes Node.js-specific helpers (like `add-electron-debug-identity` and `create-addon`) that are not available in the native CLI. If you already have winapp installed from winget, that's fine — the npm package adds Node.js-specific tools as a project dependency and won't conflict with your system installation. + ```bash npm install --save-dev @microsoft/winappcli ``` @@ -41,7 +49,7 @@ The `winapp init` command sets up everything you need in one go: app manifest, a Run the following command and follow the prompts: ```bash -npx winapp init +npx winapp init . ``` When prompted: @@ -66,16 +74,17 @@ This command sets up everything you need for Windows development: 4. **Creates `winapp.yaml`** - Tracks SDK versions and project configuration -6. **Installs Windows App SDK runtime** - Required runtime components for modern APIs +5. **Installs Windows App SDK runtime** - Required runtime components for modern APIs -7. **Enables Developer Mode in Windows** - Required for debugging our application +6. **Enables Developer Mode in Windows** - Required for debugging our application > [!NOTE] > The `.winapp/` folder is automatically added to `.gitignore` and should not be checked in to source. You can open `appxmanifest.xml` to further customize properties like the display name, publisher, and capabilities. -> **💡 About the Windows SDKs:** +> [!TIP] +> **About the Windows SDKs:** > > - **[Windows SDK](https://developer.microsoft.com/windows/downloads/windows-sdk/)** - A development platform that lets you build Win32/desktop apps. It's designed around Windows APIs that are coupled to particular versions of the OS. Use this to access core Win32 APIs like file system, networking, and system services. > @@ -100,7 +109,14 @@ This script automatically runs after `npm install` and does two things: 1. **`winapp restore`** - Downloads and restores all Windows SDK packages to the `.winapp/` folder 2. **`winapp node add-electron-debug-identity`** - Registers your Electron app with debug identity (more on this in the next steps) -Now whenever someone runs `npm install`, the Windows environment is automatically configured! +Now run `npm install` to trigger the postinstall script and configure the Windows environment: + +```bash +npm install +``` + +> [!NOTE] +> The `postinstall` script runs automatically after every `npm install`. This means the Windows environment will be configured automatically whenever someone clones your project and runs `npm install`.
💡 Cross-Platform Development (click to expand) @@ -138,7 +154,7 @@ This ensures Windows-specific setup only runs on Windows machines, allowing deve ## Step 5: Understanding Debug Identity -The `postinstall` script in Step 4 includes the `winapp node add-electron-debug-identity` command, which enables you to test Windows APIs that require app identity during development. +The `npm install` you ran in Step 4 triggered the `postinstall` script, which ran `winapp node add-electron-debug-identity`. This gives your app a temporary debug identity so you can test Windows APIs that require app identity during development. ### What Does Debug Identity Do? @@ -147,7 +163,7 @@ This command: 2. Registers `electron.exe` in your `node_modules` with a temporary identity 3. Enables you to test identity-required APIs without creating a full MSIX package -The debug identity is automatically applied when you run `npm install` thanks to the `postinstall` script. +The debug identity was applied automatically when you ran `npm install` in Step 4. Going forward, it will be reapplied whenever anyone runs `npm install`. ### When to Manually Update Debug Identity @@ -165,6 +181,8 @@ You can now test your Electron app with the debug identity applied: npm start ``` +You should see a **desktop application window** open (not a browser tab) — this is how Electron apps run. +
⚠️ Known Issue: App Crashes or Blank Window (click to expand) diff --git a/docs/guides/electron/winml-addon.md b/docs/guides/electron/winml-addon.md index 5860b02c..17fbb25b 100644 --- a/docs/guides/electron/winml-addon.md +++ b/docs/guides/electron/winml-addon.md @@ -11,6 +11,9 @@ Before starting this guide, make sure you've: > [!NOTE] > WinML runs on any Windows 10 (1809+) or Windows 11 device. For best performance, devices with GPUs or NPUs are recommended, but the API works on CPU as well. +> [!IMPORTANT] +> The WinML addon requires the **experimental** Windows App SDK. If you selected "Stable SDKs" during `winapp init` in the setup guide, you'll need to update your SDK version. Edit `winapp.yaml` and change the `Microsoft.WindowsAppSDK` version to `2.0.0-experimental3`, then run `npx winapp restore` to update. + ## Step 1: Create a C# Native Addon Let's create a native addon that will use WinML APIs. We'll use a C# template that leverages [node-api-dotnet](https://github.com/microsoft/node-api-dotnet) to bridge JavaScript and C#. @@ -24,11 +27,12 @@ This creates a `winMlAddon/` folder with: - `winMlAddon.csproj` - Project file with references to Windows SDK and Windows App SDK - `README.md` - Documentation on how to use the addon -The command also adds a `build-winMlAddon` script to your `package.json` for building the addon: +The command also adds a `build-winMlAddon` script to your `package.json` for building the addon, and a `clean-winMlAddon` script for cleaning build artifacts: ```json { "scripts": { - "build-winMlAddon": "dotnet publish ./winMlAddon/winMlAddon.csproj -c Release" + "build-winMlAddon": "dotnet publish ./winMlAddon/winMlAddon.csproj -c Release", + "clean-winMlAddon": "dotnet clean ./winMlAddon/winMlAddon.csproj" } } ``` @@ -42,7 +46,8 @@ Let's verify everything is set up correctly by building the addon: npm run build-winMlAddon ``` -> **Note:** You can also create a C++ addon using `npx winapp node create-addon` (without the `--template` flag). C++ addons use [node-addon-api](https://github.com/nodejs/node-addon-api) and provide direct access to Windows APIs with maximum performance. See the [C++ Notification Addon guide](cpp-notification-addon.md) for a walkthrough or the [full command documentation](../../usage.md#node-create-addon) for more options. +> [!NOTE] +> You can also create a C++ addon using `npx winapp node create-addon` (without the `--template` flag). C++ addons use [node-addon-api](https://github.com/nodejs/node-addon-api) and provide direct access to Windows APIs with maximum performance. See the [C++ Notification Addon guide](cpp-notification-addon.md) for a walkthrough or the [full command documentation](../../usage.md#node-create-addon) for more options. ## Step 2: Download the SqueezeNet Model and Get Sample Code @@ -64,7 +69,7 @@ We'll use the **Classify Image** sample from the [AI Dev Gallery](https://aka.ms ## Step 3: Add Required NuGet Packages -Before adding the WinML code, we need to add two additional NuGet packages that are required for image processing and ONNX Runtime extensions. +Before adding the WinML code, we need to add additional NuGet packages required for image processing, ONNX Runtime, and GenAI support. ### 3.1. Update Directory.packages.props @@ -79,9 +84,12 @@ Add the following package versions to the `Directory.packages.props` file in the - + + + ++ ++ ++ @@ -98,9 +106,12 @@ Open `winMlAddon/winMlAddon.csproj` and add the package references to the ` - + + + ++ ++ ++ @@ -110,6 +121,9 @@ Open `winMlAddon/winMlAddon.csproj` and add the package references to the ` [!IMPORTANT] +> You must copy the **entire folder**, not just `addon.cs`. The addon depends on helper files in the `Utils/` subfolder (`Prediction.cs`, `ImageNet.cs`, `BitmapFunctions.cs`, etc.). ### Key Implementation Details @@ -212,7 +223,8 @@ module.exports = { - `.msix` files - Packaged outputs - `winMlAddon/` source files - Keeps only the `dist/` folder with compiled binaries -> **📝 Note:** If you're using a different packaging tool (electron-builder, etc.), you'll need to configure similar settings for unpacking native dependencies and excluding development files. Check your packager's documentation for ASAR unpacking options. +> [!NOTE] +> If you're using a different packaging tool (electron-builder, etc.), you'll need to configure similar settings for unpacking native dependencies and excluding development files. Check your packager's documentation for ASAR unpacking options. #### 4. Image Classification @@ -232,7 +244,8 @@ The complete implementation handles: - Running the model inference - Post-processing results to get top predictions with labels and confidence scores -> **📝 Note:** The full source code includes image preprocessing, tensor creation, and result parsing. Check the [sample implementation](../../../samples/electron-winml/winMlAddon/addon.cs) for all the details. +> [!NOTE] +> The full source code includes image preprocessing, tensor creation, and result parsing. Check the [sample implementation](../../../samples/electron-winml/winMlAddon/addon.cs) for all the details. ### Understanding the Code @@ -261,7 +274,7 @@ The compiled addon will be in `winMlAddon/dist/winMlAddon.node`. ## Step 6: Test the Addon -Now let's test the addon works by calling it from the main process. Open `src/index.js` and follow these steps: +Now let's test the addon works by calling it from the main process. Open `src/main.js` and follow these steps: ### 6.1. Load the Addon @@ -320,12 +333,13 @@ testWinML(); To test image classification: 1. Create a `test-images/` folder in your project root -2. Add some test images (e.g., `sample.jpg`, `cat.jpg`, `dog.jpg`) -3. The SqueezeNet model recognizes 1000 different ImageNet classes +2. Add a test image named `sample.jpg` (the code expects this exact filename) +3. The SqueezeNet model recognizes 1000 different ImageNet classes (animals, objects, scenes, etc.) When you run the app, you'll see the classification results in the console! -> **💡 Tip:** For a complete implementation with IPC handlers, file selection dialogs, and a UI, see the [electron-winml sample](../../../samples/electron-winml/src/index.js). +> [!TIP] +> For a complete implementation with IPC handlers, file selection dialogs, and a UI, see the [electron-winml sample](../../../samples/electron-winml/src/index.js). ## Step 7: Update Debug Identity @@ -340,7 +354,8 @@ This command: 2. Registers `electron.exe` in your `node_modules` with a temporary identity 3. Enables you to test identity-required APIs without full MSIX packaging -> **📝 Note:** This command is already part of the `postinstall` script we added in the setup guide, so it runs automatically after `npm install`. However, you need to run it manually whenever you: +> [!NOTE] +> This command is already part of the `postinstall` script we added in the setup guide, so it runs automatically after `npm install`. However, you need to run it manually whenever you: > - Modify `appxmanifest.xml` (change capabilities, identity, or properties) > - Update app assets (icons, logos, etc.) @@ -390,6 +405,43 @@ To fully integrate your ONNX model, you'll need to: - **[Windows App SDK Samples](https://github.com/microsoft/WindowsAppSDK-Samples/tree/main/Samples/WindowsML)** - Collection of Windows App SDK samples - **[node-api-dotnet](https://github.com/microsoft/node-api-dotnet)** - C# ↔ JavaScript interop library +### Troubleshooting + +
+Build fails with NU1010: PackageReference items do not define a corresponding PackageVersion + +Ensure all packages referenced in `winMlAddon.csproj` have matching entries in `Directory.packages.props`. See Step 3 for the complete list of required packages. + +
+ +
+"not a valid Win32 application" when loading the addon + +This means the addon was built for a different architecture than your Node.js/Electron runtime. Check your Node.js architecture: + +```bash +node -e "console.log(process.arch)" +``` + +Then rebuild the addon with the matching target: + +```bash +# For x64 Node.js: +dotnet publish ./winMlAddon/winMlAddon.csproj -c Release -r win-x64 + +# For ARM64 Node.js: +dotnet publish ./winMlAddon/winMlAddon.csproj -c Release -r win-arm64 +``` + +If you recently changed your Node.js installation, also reinstall `node_modules` to get the matching Electron binary: + +```bash +rm -rf node_modules package-lock.json +npm install +``` + +
+ ### Get Help - **Found a bug?** [File an issue](https://github.com/microsoft/WinAppCli/issues) diff --git a/docs/guides/flutter.md b/docs/guides/flutter.md index 37248239..020a72ab 100644 --- a/docs/guides/flutter.md +++ b/docs/guides/flutter.md @@ -12,7 +12,7 @@ A standard Flutter Windows build does not have package identity. This guide show 1. **Flutter SDK**: Install Flutter following the [official guide](https://docs.flutter.dev/install/quick). -2. **winapp CLI**: Install the `winapp` CLI via winget: +2. **winapp CLI**: Install the `winapp` CLI via winget (or update if already installed): ```powershell winget install Microsoft.winappcli --source winget ``` @@ -176,14 +176,22 @@ Now, build and run the app as usual: ```powershell flutter build windows +``` + +Run the executable directly (replace `flutter_app` with your project name if different): + +```powershell .\build\windows\x64\runner\Release\flutter_app.exe ``` +> [!TIP] +> The build output is in the `x64` folder regardless of your machine's architecture — this is expected for Flutter's Windows build. + You should see the app with an orange "Not packaged" indicator. This confirms that the standard executable is running without any package identity. ## 4. Initialize Project with winapp CLI -The `winapp init` command sets up everything you need in one go: app manifest, assets, and optionally Windows App SDK headers for C++ development. +The `winapp init` command sets up everything you need in one go: app manifest, assets, and optionally Windows App SDK headers for C++ development. The manifest defines your app's identity (name, publisher, version) which Windows uses to grant API access. Run the following command and follow the prompts: @@ -192,22 +200,23 @@ winapp init ``` When prompted: -- **Package name**: Press Enter to accept the default (flutterapp) +- **Package name**: Press Enter to accept the default (derived from your project name) - **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 (Windows Application) -- **Setup SDKs**: Select "Stable SDKs" to download Windows App SDK and generate C++ headers +- **Setup SDKs**: Select "Stable SDKs" to download Windows App SDK and generate C++ headers (needed for step 6) This command will: -- Create `appxmanifest.xml` and `Assets` folder for your app identity +- Create `appxmanifest.xml` — the manifest that defines your app's identity +- Create `Assets` folder — icons required for MSIX packaging and Store submission - Create a `.winapp` folder with Windows App SDK headers and libraries -- Create a `winapp.yaml` configuration file for pinning sdk versions +- Create a `winapp.yaml` configuration file for pinning SDK versions You can open `appxmanifest.xml` to further customize properties like the display name, publisher, and capabilities. ## 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 run`. This registers a loose layout package (just like a real MSIX install) and launches the app in one step. No certificate or signing is needed for debugging. 1. **Build the app**: ```powershell @@ -219,6 +228,8 @@ To test features that require identity (like Notifications) without fully packag winapp run .\build\windows\x64\runner\Release ``` +> **Tip**: `winapp run` also registers the package on your system. This is why the MSIX may appear as "already installed" when you try to install it later in step 7. Use `winapp unregister` to clean up development packages when done. + You should now see the app with a green indicator showing: ``` Package Family Name: flutterapp.debug_xxxxxxxx @@ -229,7 +240,7 @@ This confirms your app is running with a valid package identity! ## 6. Using Windows App SDK (Optional) -If you selected to setup the SDKs during `winapp init`, you now have access to Windows App SDK C++ headers in the `.winapp/include` folder. Since Flutter's Windows runner is C++, you can call Windows App SDK APIs from native code and expose them to Dart via a method channel. +If you selected to setup the SDKs during `winapp init`, you now have access to Windows App SDK C++ headers in the `.winapp/include` folder. Since Flutter's Windows runner is C++, you can call Windows App SDK APIs from native code and expose them to Dart via a method channel. If you just need package identity for distribution, you can skip to step 7. Let's add a simple example that displays the Windows App Runtime version. @@ -294,23 +305,24 @@ void RegisterWinAppSdkPlugin(flutter::FlutterEngine* engine) { ### Update CMakeLists.txt -Edit `windows/runner/CMakeLists.txt` to add the new source file, include the Windows App SDK headers, and link the required libraries: +Edit `windows/runner/CMakeLists.txt` to make three changes. Find the `add_executable` block and add `"winapp_sdk_plugin.cpp"` to the source file list: ```cmake -# Add the new source file to the executable add_executable(${BINARY_NAME} WIN32 "flutter_window.cpp" "main.cpp" "utils.cpp" "win32_window.cpp" - "winapp_sdk_plugin.cpp" + "winapp_sdk_plugin.cpp" # <-- add this line "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" "Runner.rc" "runner.exe.manifest" ) +``` -# ... existing settings ... +Then add these two lines at the end of the file to link WinRT libraries and include the Windows App SDK headers: +```cmake # Link Windows Runtime libraries for WinRT target_link_libraries(${BINARY_NAME} PRIVATE "WindowsApp.lib") @@ -321,23 +333,30 @@ target_include_directories(${BINARY_NAME} PRIVATE ### Register the Plugin -In `windows/runner/flutter_window.cpp`, include the header and register the plugin: +In `windows/runner/flutter_window.cpp`, add the include at the top of the file with the other includes: ```cpp #include "winapp_sdk_plugin.h" +``` + +Then find the `RegisterPlugins` call in `FlutterWindow::OnCreate()` and add `RegisterWinAppSdkPlugin` on the line right after it: -// In FlutterWindow::OnCreate(), after RegisterPlugins: -RegisterPlugins(flutter_controller_->engine()); -RegisterWinAppSdkPlugin(flutter_controller_->engine()); +```cpp + RegisterPlugins(flutter_controller_->engine()); + RegisterWinAppSdkPlugin(flutter_controller_->engine()); // <-- add this line ``` ### Update main.dart -Add a method channel call in Dart to query the runtime version and display it: +Add the following import at the top of `lib/main.dart`, alongside the existing imports: ```dart import 'package:flutter/services.dart'; +``` + +Add this function below the existing `getPackageFamilyName()` function (outside any class): +```dart /// Queries the Windows App Runtime version via a native method channel. Future getWindowsAppRuntimeVersion() async { if (!Platform.isWindows) return null; @@ -351,7 +370,41 @@ Future getWindowsAppRuntimeVersion() async { } ``` -Call it in `initState()` and display it in the UI alongside the package identity indicator. +In the `_MyHomePageState` class, add a new field next to the existing `_packageFamilyName`: + +```dart + late final String? _packageFamilyName; + String? _runtimeVersion; // <-- add this line +``` + +Update `initState()` to call the new function: + +```dart + @override + void initState() { + super.initState(); + _packageFamilyName = getPackageFamilyName(); + // Fetch the runtime version asynchronously + getWindowsAppRuntimeVersion().then((version) { + setState(() { + _runtimeVersion = version; + }); + }); + } +``` + +Finally, display the runtime version in the `build` method. Add this widget inside the `Column` children list, right after the `Container` that shows the package identity: + +```dart + if (_runtimeVersion != null) + Padding( + padding: const EdgeInsets.only(bottom: 16), + child: Text( + 'Windows App Runtime: $_runtimeVersion', + style: Theme.of(context).textTheme.bodyLarge, + ), + ), +``` ### Build and Run @@ -418,7 +471,7 @@ winapp pack .\dist --cert .\devcert.pfx ### Install the Certificate -Before you can install the MSIX package, you need to install the development certificate. Run this command as administrator (you only need to do this once): +Before you can install the MSIX package, you need to trust the development certificate on your machine. Run this command as administrator (you only need to do this once per certificate): ```powershell winapp cert install .\devcert.pfx @@ -426,14 +479,25 @@ winapp cert install .\devcert.pfx ### Install and Run -Install the package by double-clicking the generated `flutterapp.msix` file, or using PowerShell: +> **Tip**: If you used `winapp run` in step 5, the package may already be registered on your system. Use `winapp unregister` first to remove the development registration, then install the release package. + +Install the package by double-clicking the generated `.msix` file, or using PowerShell: ```powershell Add-AppxPackage .\flutterapp.msix ``` -### Tips +> **Tip**: The MSIX filename includes the version and architecture (e.g., `flutterapplication1_1.0.0.0_x64.msix`). Check your directory for the exact filename. If you need to repackage after code changes, increment the `Version` in your `appxmanifest.xml` — Windows requires a higher version number to update an installed package. + +## Tips 1. Once you are ready for distribution, you can sign your MSIX with a code signing certificate from a Certificate Authority so your users don't have to install a self-signed certificate. 2. The [Azure Trusted Signing](https://azure.microsoft.com/products/trusted-signing) service is a great way to manage your certificates securely and integrate signing into your CI/CD pipeline. 3. The Microsoft Store will sign the MSIX for you, no need to sign before submission. + +## Next Steps + +- **Distribute via winget**: Submit your MSIX to the [Windows Package Manager Community Repository](https://github.com/microsoft/winget-pkgs) +- **Publish to the Microsoft Store**: Use `winapp store` to submit your package +- **Set up CI/CD**: Use the [`setup-WinAppCli`](https://github.com/microsoft/setup-WinAppCli) GitHub Action to automate packaging in your pipeline +- **Explore Windows APIs**: With package identity, you can now use [Notifications](https://learn.microsoft.com/windows/apps/develop/notifications/app-notifications/app-notifications-quickstart), [on-device AI](https://learn.microsoft.com/windows/ai/apis/), and other [identity-dependent APIs](https://learn.microsoft.com/windows/apps/desktop/modernize/desktop-to-uwp-extensions) diff --git a/docs/guides/packaging-cli.md b/docs/guides/packaging-cli.md index 69bd84bc..23050bc8 100644 --- a/docs/guides/packaging-cli.md +++ b/docs/guides/packaging-cli.md @@ -22,9 +22,10 @@ cd MyCliPackage ### 2. Install winapp CLI -The quickest way to get started is to install winapp CLI via Windows Package Manager: +Install the winapp CLI via Windows Package Manager, or update to the latest version if you already have it: ```powershell +# Install (or update if already installed) winget install microsoft.winappcli --source winget ``` @@ -40,14 +41,11 @@ This command creates an `appxmanifest.xml` file in the current directory with de ### 4. Configure the Manifest -You'll need to edit the generated `appxmanifest.xml` to: -- Add an execution alias so users can run your CLI from any directory -- Hide the app from the Start menu app list -- Update application details to match your CLI +Edit the generated `appxmanifest.xml` to customize your package. Each sub-step below explains what to change and why. #### 4.1 Add Required Namespace -Add the `uap5` namespace to the `Package` element if it's not already present: +Add the `uap5` namespace to the `Package` element if it's not already present. This is needed for the execution alias in step 4.3: ```xml ` element, add `AppListEntry="none"` to prevent the app from appearing in the Start menu: +In the `` element, add `AppListEntry="none"` to hide the app from the Start menu. CLI tools are invoked from the terminal, so they don't need a Start menu entry: ```xml ` element, add `AppListEntry="none"` to prevent the #### 4.3 Add Execution Alias Extension -Add the execution alias extension within the `` element (after ``): +Add an execution alias so users can run your CLI by name from any terminal window. Add this within the `` element (after ``): ```xml @@ -92,7 +90,9 @@ Replace `yourcli.exe` with the desired command name for your CLI. Once a user in #### 4.4 Update Application Metadata -Update the following fields to match your CLI application: +Update the following fields to match your CLI application. + +> **Important**: The `Publisher` value in your manifest must match the publisher in your signing certificate. If you generate a certificate later (step 5), it will use the publisher from your manifest. If you change the publisher after generating a certificate, you'll need to regenerate the certificate to match. - **Identity**: Update `Name`, `Publisher`, and `Version` ```xml @@ -131,7 +131,7 @@ Update the following fields to match your CLI application: For local testing and distribution outside the Microsoft Store, you'll need to sign your MSIX package with a certificate. -Generate a development certificate: +Generate a development certificate. Keep it outside your CLI folder to avoid accidentally including it in the package: ```powershell # Navigate to a location outside your CLI folder (e.g., your home directory) @@ -139,28 +139,55 @@ cd ~ winapp cert generate ``` -This creates a `devcert.pfx` file. To trust this certificate on your development machine, install it (requires administrator privileges): +This creates a `devcert.pfx` file in your home directory (e.g., `C:\Users\yourname\devcert.pfx`). + +To trust this certificate on your development machine, install it (requires administrator privileges): ```powershell # Run PowerShell as Administrator -winapp cert install +winapp cert install ~\devcert.pfx ``` -**Important**: Keep your development certificate outside the folder containing your CLI executable to avoid accidentally including it in the package. - ### 6. Package Your CLI Now you're ready to create the MSIX package: ```powershell -# Run from outside CLI folder +# Navigate back outside of your project folder # Package with dev certificate (for local testing/distribution) -winapp pack .\MyCliPackage --cert path\to\devcert.pfx +winapp pack .\path\to\MyCliPackage --cert .\path\to\devcert.pfx +``` + +This creates an `.msix` file in the current directory. + +### 7. Install and Verify + +Install the MSIX package to verify everything works: + +```powershell +Add-AppxPackage .\MyCliPackage.msix ``` -This creates an `.msix` file in the current directory +If you added an execution alias in step 4.3, you can now run your CLI from any terminal: + +```powershell +yourcli --help +``` + +To uninstall later: + +```powershell +Get-AppxPackage *YourCLI* | Remove-AppxPackage +``` + +## Tips -### Tips: 1. Once you are ready for distribution, you can sign your MSIX with a code signing certificate from a Certificate Authority so your users don't have to install a self-signed certificate 2. The Microsoft Store will sign the MSIX for you, no need to sign before submission. -3. You might need to create multiple MSIX packages, one for each architecture you support (x64, Arm64) \ No newline at end of file +3. You might need to create multiple MSIX packages, one for each architecture you support (x64, Arm64) + +## Next Steps + +- **Distribute via winget**: Submit your MSIX to the [Windows Package Manager Community Repository](https://github.com/microsoft/winget-pkgs) +- **Publish to the Microsoft Store**: Use `winapp store` to submit your package +- **Set up CI/CD**: Use the [`setup-WinAppCli`](https://github.com/microsoft/setup-WinAppCli) GitHub Action to automate packaging in your pipeline \ No newline at end of file diff --git a/docs/guides/rust.md b/docs/guides/rust.md index 0e5fad27..2962ccfc 100644 --- a/docs/guides/rust.md +++ b/docs/guides/rust.md @@ -10,12 +10,12 @@ A standard executable (like one created with `cargo build`) does not have packag ## Prerequisites -1. **Rust Toolchain**: Install Rust using [rustup](https://rustup.rs/) or winget: +1. **Rust Toolchain**: Install Rust using [rustup](https://rustup.rs/) or winget (or update if already installed): ```powershell winget install Rustlang.Rustup --source winget ``` -2. **winapp CLI**: Install the `winapp` tool via winget: +2. **winapp CLI**: Install the `winapp` tool via winget (or update if already installed): ```powershell winget install microsoft.winappcli --source winget ``` @@ -38,7 +38,7 @@ cargo run ## 2. Update Code to Check Identity -We'll update the app to check if it's running with package identity. We'll use the `windows` crate to access Windows APIs. +We'll update the app to check if it's running with package identity. This will help us verify that identity is working correctly in later steps. We'll use the `windows` crate to access Windows APIs. First, add the `windows` dependency to your `Cargo.toml` by running: @@ -46,7 +46,9 @@ First, add the `windows` dependency to your `Cargo.toml` by running: cargo add windows --features ApplicationModel ``` -Next, replace the contents of `src/main.rs` with the following code. This code attempts to retrieve the current package identity. If it succeeds, it prints the Package Family Name; otherwise, it prints "Not packaged". +This adds the Windows API bindings with the `ApplicationModel` feature, which gives us access to the `Package` API for checking identity. + +Next, replace the entire contents of `src/main.rs` with the following code. This code attempts to retrieve the current package identity. If it succeeds, it prints the Package Family Name; otherwise, it prints "Not packaged". > **Note**: The [full sample](../../samples/rust-app) also includes code to show a Windows Notification if identity is present, but for this guide, we'll focus on the identity check. @@ -81,7 +83,7 @@ You should see the output "Not packaged". This confirms that the standard execut ## 4. Initialize Project with winapp CLI -The `winapp init` command sets up everything you need in one go: app manifest and assets. +The `winapp init` command sets up everything you need in one go: app manifest and assets. The manifest defines your app's identity (name, publisher, version) which Windows uses to grant API access. Run the following command and follow the prompts: @@ -94,15 +96,16 @@ When prompted: - **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" +- **Setup SDKs**: Select "Do not setup SDKs" (Rust uses its own `windows` crate, not the C++ SDK headers) This command will: -- Create `appxmanifest.xml` and `Assets` folder for your app identity +- Create `appxmanifest.xml` — the manifest that defines your app's identity +- Create `Assets` folder — icons required for MSIX packaging and Store submission You can open `appxmanifest.xml` to further customize properties like the display name, publisher, and capabilities. ### Add Execution Alias (for console apps) -To allow users to run your app from the command line after installation (like `rust-app`), and to use `winapp run --with-alias` during development (which keeps console output in the current terminal), add an execution alias to the `appxmanifest.xml`. +An execution alias lets users run your app by name from any terminal (like `rust-app`). It also enables `winapp run --with-alias` during development, which keeps console output in the current terminal instead of opening a new window. You can add one automatically: @@ -137,7 +140,7 @@ Or manually: open `appxmanifest.xml` and add the `uap5` namespace to the ` **Note**: `winapp run` also registers the package on your system. This is why the MSIX may appear as "already installed" when you try to install it later in step 6. Use `winapp unregister` to clean up development packages when done. You should now see output similar to: ``` @@ -161,7 +166,7 @@ This confirms your app is running with a valid package identity! ## 6. Package with MSIX -Once you're ready to distribute your app, you can package it as an MSIX using the same manifest. +Once you're ready to distribute your app, you can package it as an MSIX using the same manifest. MSIX provides clean install/uninstall, auto-updates, and a trusted installation experience. ### Prepare the Package Directory First, build your application in release mode for optimal performance: @@ -175,7 +180,7 @@ winapp manifest add-alias cargo build --release ``` -Then, create a directory to hold your package files and copy your release executable. +Then, create a directory with just the files needed for distribution. The `target\release` folder contains build artifacts that aren't part of your app — we only need the executable: ```powershell mkdir dist @@ -184,15 +189,17 @@ copy .\target\release\rust-app.exe .\dist\ ### Generate a Development Certificate -Before packaging, you need a development certificate for signing. Generate one if you haven't already: +MSIX packages must be signed. For local testing, generate a self-signed development certificate: ```powershell winapp cert generate --if-exists skip ``` +> **Important**: The certificate's publisher must match the `Publisher` in your `appxmanifest.xml`. The `cert generate` command reads this automatically from your manifest. + ### Sign and Pack -Now you can package and sign: +Now you can package and sign in one step: ```powershell winapp pack .\dist --cert .\devcert.pfx @@ -202,14 +209,21 @@ winapp pack .\dist --cert .\devcert.pfx ### Install the Certificate -Before you can install the MSIX package, you need to install the development certificate. Run this command as administrator: +Before you can install the MSIX package, you need to trust the development certificate on your machine. Run this command as administrator (you only need to do this once per certificate): ```powershell winapp cert install .\devcert.pfx ``` ### Install and Run -Install the package by double-clicking the generated *.msix file + +> **Note**: If you used `winapp run` in step 5, the package may already be registered on your system. Use `winapp unregister` first to remove the development registration, then install the release package. + +Install the package by double-clicking the generated `.msix` file, or via PowerShell: + +```powershell +Add-AppxPackage .\rust-app.msix +``` Now you can run your app from anywhere in the terminal by typing: @@ -219,7 +233,16 @@ rust-app You should see the "Package Family Name" output, confirming it's installed and running with identity. -### Tips: +> **Tip**: If you need to repackage your app (e.g., after code changes), increment the `Version` in your `appxmanifest.xml` before running `winapp pack` again. Windows requires a higher version number to update an installed package. + +## Tips 1. Once you are ready for distribution, you can sign your MSIX with a code signing certificate from a Certificate Authority so your users don't have to install a self-signed certificate 2. The Microsoft Store will sign the MSIX for you, no need to sign before submission. -3. You might need to create multiple MSIX packages, one for each architecture you support (x64, Arm64) \ No newline at end of file +3. You might need to create multiple MSIX packages, one for each architecture you support (x64, Arm64) + +## Next Steps + +- **Distribute via winget**: Submit your MSIX to the [Windows Package Manager Community Repository](https://github.com/microsoft/winget-pkgs) +- **Publish to the Microsoft Store**: Use `winapp store` to submit your package +- **Set up CI/CD**: Use the [`setup-WinAppCli`](https://github.com/microsoft/setup-WinAppCli) GitHub Action to automate packaging in your pipeline +- **Explore Windows APIs**: With package identity, you can now use [Notifications](https://learn.microsoft.com/windows/apps/develop/notifications/app-notifications/app-notifications-quickstart), [on-device AI](https://learn.microsoft.com/windows/ai/apis/), and other [identity-dependent APIs](https://learn.microsoft.com/windows/apps/desktop/modernize/desktop-to-uwp-extensions) \ No newline at end of file diff --git a/docs/guides/tauri.md b/docs/guides/tauri.md index e337df86..aa3f4640 100644 --- a/docs/guides/tauri.md +++ b/docs/guides/tauri.md @@ -10,8 +10,11 @@ For a complete working example, check out the [Tauri sample](../../samples/tauri 1. **Windows 11** 1. **Node.js** - `winget install OpenJS.NodeJS --source winget` +1. **Rust Toolchain** - Install Rust using [rustup](https://rustup.rs/) or `winget install Rustlang.Rustup --source winget` 1. **winapp CLI** - `winget install microsoft.winappcli --source winget` +> **Tip**: If you already have these installed, run the `winget install` commands anyway to check for updates. + ## 1. Create a New Tauri App Start by creating a new Tauri application using the official scaffolding tool: @@ -19,7 +22,12 @@ Start by creating a new Tauri application using the official scaffolding tool: ```powershell npm create tauri-app@latest ``` -Follow the prompts (e.g., Project name: `tauri-app`, Frontend language: `JavaScript`, Package manager: `npm`). +Follow the prompts: +- **Project name**: `tauri-app` (or your preferred name) +- **Frontend language**: `JavaScript` +- **Package manager**: `npm` +- **UI template**: `Vanilla` +- **UI flavor**: `JavaScript` Navigate to your project directory and install dependencies: @@ -40,14 +48,14 @@ We'll update the app to check if it's running with package identity. We'll use t ### Backend Changes (Rust) -1. **Add Dependency**: Open `src-tauri/Cargo.toml` and add the `windows` dependency for the Windows target: +1. **Add Dependency**: Open `src-tauri/Cargo.toml` and add the following lines at the end of the file. This adds the Windows API bindings so we can check for package identity: ```toml [target.'cfg(windows)'.dependencies] windows = { version = "0.58", features = ["ApplicationModel"] } ``` -2. **Add Command**: Open `src-tauri/src/lib.rs` and add the `get_package_family_name` command. This function attempts to retrieve the current package identity. +2. **Add Command**: Open `src-tauri/src/lib.rs` and add the `get_package_family_name` function. Place it before the `pub fn run()` function: ```rust #[tauri::command] @@ -130,7 +138,7 @@ We'll update the app to check if it's running with package identity. We'll use t ## 3. Initialize Project with winapp CLI -The `winapp init` command sets up everything you need in one go: app manifest and assets. +The `winapp init` command sets up everything you need in one go: app manifest and assets. The manifest defines your app's identity (name, publisher, version) which Windows uses to grant API access. Run the following command and follow the prompts: @@ -143,16 +151,17 @@ When prompted: - **Publisher name**: Press Enter to accept the default or enter your name - **Version**: Press Enter to accept 1.0.0.0 - **Entry point**: Press Enter to accept the default (tauri-app.exe) -- **Setup SDKs**: Select "Do not setup SDKs" +- **Setup SDKs**: Select "Do not setup SDKs" (Tauri uses Rust's `windows` crate, not the C++ SDK headers) This command will: -- Create `appxmanifest.xml` and `Assets` folder for your app identity +- Create `appxmanifest.xml` — the manifest that defines your app's identity +- Create `Assets` folder — icons required for MSIX packaging and Store submission You can open `appxmanifest.xml` to further customize properties like the display name, publisher, and capabilities. ## 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 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. No certificate or signing is needed for debugging. 1. **Add Script**: Open `package.json` and add a new script `tauri:dev:withidentity`: @@ -174,7 +183,11 @@ To debug with identity, we need to build the Rust backend and run it with `winap npm run tauri:dev:withidentity ``` -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**: You may see a terminal/console window appear behind the app window — this is normal for Tauri debug builds (it's the Rust process's console). + +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**: `winapp run` also registers the package on your system. This is why the MSIX may appear as "already installed" when you try to install it later in step 5. Use `winapp unregister` to clean up development packages when done. > **Tip:** For advanced debugging workflows (attaching debuggers, IDE setup, startup debugging), see the [Debugging Guide](../debugging.md). @@ -199,30 +212,53 @@ First, add a `pack:msix` script to your `package.json`: ### Generate a Development Certificate -Before packaging, you need a development certificate for signing. Generate one if you haven't already: +MSIX packages must be signed. For local testing, generate a self-signed development certificate: ```powershell winapp cert generate --if-exists skip ``` +> **Tip**: The certificate's publisher must match the `Publisher` in your `appxmanifest.xml`. The `cert generate` command reads this automatically from your manifest. + ### Build, Stage, and Pack ```powershell npm run pack:msix ``` -> 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. +> **Tip**: 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. ### Install the Certificate -Before you can install the MSIX package, you need to install the development certificate. Run this command as administrator: +Before you can install the MSIX package, you need to trust the development certificate on your machine. Run this command as administrator (you only need to do this once per certificate): ```powershell winapp cert install .\devcert.pfx ``` ### Install and Run -Install the package by double-clicking the generated `.msix` file. Once installed, you can launch your app from the start menu. +> **Tip**: If you used `winapp run` in step 4, the package may already be registered on your system. Use `winapp unregister` first to remove the development registration, then install the release package. + +Install the package by double-clicking the generated `.msix` file, or using PowerShell: + +```powershell +Add-AppxPackage .\tauri-app.msix +``` + +> **Tip**: The MSIX filename includes the version and architecture (e.g., `tauri-app_1.0.0.0_x64.msix`). Check your directory for the exact filename. If you need to repackage after code changes, increment the `Version` in your `appxmanifest.xml` — Windows requires a higher version number to update an installed package. + +Once installed, you can launch your app from the Start menu. You should see the app running with identity. + +## Tips + +1. Once you are ready for distribution, you can sign your MSIX with a code signing certificate from a Certificate Authority so your users don't have to install a self-signed certificate. +2. The Microsoft Store will sign the MSIX for you, no need to sign before submission. +3. You might need to create multiple MSIX packages, one for each architecture you support (x64, Arm64). + +## Next Steps -You should see the app running with identity. +- **Distribute via winget**: Submit your MSIX to the [Windows Package Manager Community Repository](https://github.com/microsoft/winget-pkgs) +- **Publish to the Microsoft Store**: Use `winapp store` to submit your package +- **Set up CI/CD**: Use the [`setup-WinAppCli`](https://github.com/microsoft/setup-WinAppCli) GitHub Action to automate packaging in your pipeline +- **Explore Windows APIs**: With package identity, you can now use [Notifications](https://learn.microsoft.com/windows/apps/develop/notifications/app-notifications/app-notifications-quickstart), [on-device AI](https://learn.microsoft.com/windows/ai/apis/), and other [identity-dependent APIs](https://learn.microsoft.com/windows/apps/desktop/modernize/desktop-to-uwp-extensions) diff --git a/samples/SampleTestHelpers.psm1 b/samples/SampleTestHelpers.psm1 new file mode 100644 index 00000000..d91a70f8 --- /dev/null +++ b/samples/SampleTestHelpers.psm1 @@ -0,0 +1,189 @@ +<# +.SYNOPSIS +Shared PowerShell helpers for sample & guide Pester tests. + +.DESCRIPTION +This module provides setup and CLI helper functions used by each sample's +test.Tests.ps1 Pester test file. Assertion and reporting functions are handled +by Pester's built-in Should assertions — this module only provides: + - CLI path resolution and installation + - Prerequisite checks + - Temp directory management + - winapp invocation helpers + +Import with: Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force +#> + +# ============================================================================ +# Winapp CLI Helpers +# ============================================================================ + +function Resolve-WinappCliPath { + <# + .SYNOPSIS + Resolves the winapp CLI path from artifacts or local build. + Returns the resolved absolute path to a .tgz or package directory. + #> + param( + [string]$WinappPath + ) + + $repoRoot = (Resolve-Path "$PSScriptRoot\..").Path + + if (-not $WinappPath) { + # Default search order: CI artifact dir, local package-npm.ps1 output dir, then source dir. + $defaultCandidates = @( + (Join-Path $repoRoot "artifacts\npm"), + (Join-Path $repoRoot "artifacts"), + (Join-Path $repoRoot "src\winapp-npm") + ) + $WinappPath = $defaultCandidates | Where-Object { Test-Path $_ } | Select-Object -First 1 + } + + if (-not $WinappPath -or -not (Test-Path $WinappPath)) { + throw "Winapp path not found: $WinappPath" + } + + $resolved = (Resolve-Path $WinappPath).Path + + if (Test-Path $resolved -PathType Container) { + $tgz = Get-ChildItem -Path $resolved -Filter "*.tgz" -ErrorAction SilentlyContinue | + Sort-Object -Property LastWriteTime -Descending | + Select-Object -First 1 + if ($tgz) { return $tgz.FullName } + if (Test-Path (Join-Path $resolved "package.json")) { return $resolved } + throw "No .tgz or package.json found in $resolved" + } + + return $resolved +} + +function Invoke-WinappCommand { + <# + .SYNOPSIS + Invokes the winapp CLI with the given arguments and returns stdout lines. + Resolution order: local node_modules/.bin/winapp -> winapp on PATH -> + dotnet run against the repo CLI project (only when WINAPP_TEST_USE_DOTNET=1 + or no other winapp is available). Throws on non-zero exit code. + #> + param( + [Parameter(Mandatory)] + [string]$Arguments, + [string]$FailMessage = "winapp $Arguments failed" + ) + + $npxWinapp = Join-Path (Get-Location) "node_modules\.bin\winapp.cmd" + $pathWinapp = Get-Command winapp -ErrorAction SilentlyContinue + $cliProject = Join-Path $PSScriptRoot "..\src\winapp-CLI\WinApp.Cli\WinApp.Cli.csproj" + $useDotnet = $env:WINAPP_TEST_USE_DOTNET -eq '1' + + if (Test-Path $npxWinapp) { + $cmd = "npx winapp $Arguments" + } elseif ($pathWinapp -and -not $useDotnet) { + $cmd = "winapp $Arguments" + } elseif (Test-Path $cliProject) { + # Fall back to dotnet run when no installed winapp is on PATH, or when explicitly requested. + $cmd = "dotnet run --project `"$cliProject`" -- $Arguments" + } else { + $cmd = "winapp $Arguments" + } + + Write-Verbose "Running: $cmd" + $output = Invoke-Expression $cmd + if ($LASTEXITCODE -ne 0) { throw $FailMessage } + return $output +} + +function Install-WinappNpmPackage { + <# + .SYNOPSIS + Installs the winapp npm package into the current project as a devDependency. + #> + param( + [Parameter(Mandatory)] + [string]$PackagePath + ) + Write-Verbose "Installing winapp from: $PackagePath" + Invoke-Expression "npm install `"$PackagePath`" --save-dev" + if ($LASTEXITCODE -ne 0) { throw "Failed to install winapp npm package" } +} + +function Install-WinappGlobal { + <# + .SYNOPSIS + Installs the winapp npm package globally so 'winapp' is available on PATH. + #> + param( + [Parameter(Mandatory)] + [string]$PackagePath + ) + Write-Verbose "Installing winapp globally from: $PackagePath" + Invoke-Expression "npm install -g `"$PackagePath`"" + if ($LASTEXITCODE -ne 0) { throw "Failed to install winapp globally" } +} + +# ============================================================================ +# Prerequisite Checks +# ============================================================================ + +function Test-Prerequisite { + <# + .SYNOPSIS + Tests whether a command-line tool is available on PATH. Returns $true/$false. + #> + param( + [Parameter(Mandatory)] + [string]$Command + ) + $null = Get-Command $Command -ErrorAction SilentlyContinue + return $? +} + +# ============================================================================ +# Temp Directory Helpers +# ============================================================================ + +function New-TempTestDirectory { + <# + .SYNOPSIS + Creates a temporary directory for from-scratch guide workflow tests. + Returns the absolute path. + #> + param( + [Parameter(Mandatory)] + [string]$Prefix + ) + $tempBase = Join-Path ([System.IO.Path]::GetTempPath()) "winapp-test" + $null = New-Item -ItemType Directory -Path $tempBase -Force + $tempDir = Join-Path $tempBase "$Prefix-$([System.IO.Path]::GetRandomFileName())" + $null = New-Item -ItemType Directory -Path $tempDir -Force + return $tempDir +} + +function Remove-TempTestDirectory { + <# + .SYNOPSIS + Removes a temporary test directory created by New-TempTestDirectory. + #> + param( + [Parameter(Mandatory)] + [string]$Path + ) + if (Test-Path $Path) { + Remove-Item -Path $Path -Recurse -Force -ErrorAction SilentlyContinue + } +} + +# ============================================================================ +# Exports +# ============================================================================ + +Export-ModuleMember -Function @( + 'Resolve-WinappCliPath' + 'Invoke-WinappCommand' + 'Install-WinappNpmPackage' + 'Install-WinappGlobal' + 'Test-Prerequisite' + 'New-TempTestDirectory' + 'Remove-TempTestDirectory' +) diff --git a/samples/cpp-app/test.Tests.ps1 b/samples/cpp-app/test.Tests.ps1 new file mode 100644 index 00000000..0da097e8 --- /dev/null +++ b/samples/cpp-app/test.Tests.ps1 @@ -0,0 +1,147 @@ +<# +.SYNOPSIS +Pester 5.x tests for the cpp-app sample and C++/CMake guide workflow. + +.DESCRIPTION +Phase 1: Follows the docs/guides/cpp.md guide from scratch — creates a minimal + C++ project, runs winapp init, builds with CMake, and packages as MSIX. +Phase 2: Quick build of the existing sample to verify it is not stale. + +.PARAMETER WinappPath +Path to the winapp npm package (.tgz or directory) to install. + +.PARAMETER SkipCleanup +Keep generated artifacts after test completes. +#> + +param( + [string]$WinappPath, + [switch]$SkipCleanup +) + +BeforeDiscovery { + $script:skip = $null -eq (Get-Command cmake -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) +} + +Describe "cpp-app sample" { + BeforeAll { + Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force + $script:skip = $null -eq (Get-Command cmake -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) + + $script:sampleDir = $PSScriptRoot + $script:tempDir = $null + + if (-not $script:skip) { + $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath + Install-WinappGlobal -PackagePath $resolvedPkg + } + } + + AfterAll { + if (-not $SkipCleanup) { + if ($script:tempDir) { Remove-TempTestDirectory -Path $script:tempDir } + Remove-Item -Path (Join-Path $script:sampleDir "build") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $script:sampleDir ".winapp") -Recurse -Force -ErrorAction SilentlyContinue + } + } + + Context "Phase 1: C++/CMake Guide Workflow (from scratch)" { + BeforeAll { + if (-not $script:skip) { + $script:tempDir = New-TempTestDirectory -Prefix "cpp-guide" + Push-Location $script:tempDir + + @' +#include +#include +int main() { + std::cout << "Hello from C++ app" << std::endl; + return 0; +} +'@ | Set-Content "main.cpp" + + @' +cmake_minimum_required(VERSION 3.20) +project(test-cpp-app LANGUAGES CXX) +set(CMAKE_CXX_STANDARD 20) +add_executable(test-cpp-app main.cpp) +'@ | Set-Content "CMakeLists.txt" + } + } + + AfterAll { + if (-not $script:skip) { + Pop-Location + } + } + + It "winapp init creates config files" -Skip:$script:skip { + Invoke-WinappCommand -Arguments "init . --use-defaults --setup-sdks=stable" + "winapp.yaml" | Should -Exist + "Package.appxmanifest" | Should -Exist + ".winapp" | Should -Exist + } + + It "adds execution alias to manifest" -Skip:$script:skip { + Invoke-WinappCommand -Arguments "manifest add-alias" + } + + It "CMake configures successfully" -Skip:$script:skip { + $output = cmake -B build -DCMAKE_BUILD_TYPE=Debug 2>&1 + $LASTEXITCODE | Should -Be 0 -Because "CMake configure failed: $output" + } + + It "CMake builds debug successfully" -Skip:$script:skip { + $output = cmake --build build --config Debug 2>&1 + $LASTEXITCODE | Should -Be 0 -Because "CMake build failed: $output" + } + + It "runs app with identity via winapp run" -Skip:$script:skip { + Invoke-WinappCommand -Arguments "run build\Debug --unregister-on-exit" + } + + It "CMake builds release successfully" -Skip:$script:skip { + $output = cmake --build build --config Release 2>&1 + $LASTEXITCODE | Should -Be 0 -Because "CMake build failed: $output" + } + + It "generates a dev certificate" -Skip:$script:skip { + Invoke-WinappCommand -Arguments "cert generate --if-exists skip" + "devcert.pfx" | Should -Exist + } + + It "packages as MSIX" -Skip:$script:skip { + Invoke-WinappCommand -Arguments "pack build\Release --manifest Package.appxmanifest --cert devcert.pfx" + Get-ChildItem -Filter "*.msix" | Should -Not -BeNullOrEmpty -Because "MSIX package should be created" + } + } + + Context "Phase 2: Sample Build Check" { + BeforeAll { + if (-not $script:skip) { + Push-Location $script:sampleDir + } + } + + AfterAll { + if (-not $script:skip) { + Pop-Location + } + } + + It "winapp restore succeeds" -Skip:$script:skip { + Invoke-WinappCommand -Arguments "restore" + } + + It "sample CMake configures successfully" -Skip:$script:skip { + $output = cmake -B build -DCMAKE_BUILD_TYPE=Release 2>&1 + $LASTEXITCODE | Should -Be 0 -Because "Sample CMake configure failed: $output" + } + + It "sample builds and produces cpp-app.exe" -Skip:$script:skip { + $output = cmake --build build --config Release 2>&1 + $LASTEXITCODE | Should -Be 0 -Because "Sample CMake build failed: $output" + "build\Release\cpp-app.exe" | Should -Exist + } + } +} diff --git a/samples/dotnet-app/test.Tests.ps1 b/samples/dotnet-app/test.Tests.ps1 new file mode 100644 index 00000000..276a17ff --- /dev/null +++ b/samples/dotnet-app/test.Tests.ps1 @@ -0,0 +1,180 @@ +param( + [string]$WinappPath, + [switch]$SkipCleanup +) + +BeforeDiscovery { + $script:skip = $null -eq (Get-Command dotnet -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) +} + +Describe ".NET App Guide Workflow" { + BeforeAll { + Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force + $script:skip = $null -eq (Get-Command dotnet -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) + $script:tempDir = $null + $script:sampleDir = $PSScriptRoot + + if ($script:skip) { return } + + $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath + Install-WinappGlobal -PackagePath $resolvedPkg + + $script:tempDir = New-TempTestDirectory -Prefix "dotnet-guide" + $script:projectDir = Join-Path $script:tempDir "test-dotnet-app" + } + + AfterAll { + if (-not $SkipCleanup) { + if ($script:tempDir) { Remove-TempTestDirectory -Path $script:tempDir } + if ($script:sampleDir) { + Remove-Item -Path (Join-Path $script:sampleDir "bin") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $script:sampleDir "obj") -Recurse -Force -ErrorAction SilentlyContinue + } + } + } + + Context "Prerequisites" { + It "Should have dotnet available" -Skip:$script:skip { + Test-Prerequisite 'dotnet' | Should -Be $true + } + + It "Should have npm available" -Skip:$script:skip { + Test-Prerequisite 'npm' | Should -Be $true + } + } + + Context "Phase 1: .NET Guide Workflow (from scratch)" { + + Context "Project Creation" { + It "Should create a new .NET console project" -Skip:$script:skip { + Push-Location $script:tempDir + try { + Invoke-Expression "dotnet new console -n test-dotnet-app" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should have created the project directory" -Skip:$script:skip { + $script:projectDir | Should -Exist + } + } + + Context "Winapp Init" { + It "Should run winapp init successfully" -Skip:$script:skip { + Push-Location $script:projectDir + try { + Invoke-WinappCommand -Arguments "init --use-defaults" + } finally { Pop-Location } + } + + It "Should have created Package.appxmanifest" -Skip:$script:skip { + Join-Path $script:projectDir "Package.appxmanifest" | Should -Exist + } + } + + Context "Debug with Identity" { + It "Should build in Debug mode" -Skip:$script:skip { + Push-Location $script:projectDir + try { + Invoke-Expression "dotnet build -c Debug" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should apply debug identity with create-debug-identity" -Skip:$script:skip { + Push-Location $script:projectDir + try { + $exeFile = Get-ChildItem -Path (Join-Path $script:projectDir "bin\Debug") -Filter "*.exe" -Recurse | + Select-Object -First 1 + $exeFile | Should -Not -BeNullOrEmpty + Invoke-WinappCommand -Arguments "create-debug-identity `"$($exeFile.FullName)`"" + } finally { Pop-Location } + } + + It "Should add execution alias to manifest" -Skip:$script:skip { + Push-Location $script:projectDir + try { + Invoke-WinappCommand -Arguments "manifest add-alias" + } finally { Pop-Location } + } + + It "Should run app with identity via winapp run" -Skip:$script:skip { + Push-Location $script:projectDir + try { + $debugDir = Get-ChildItem -Path (Join-Path $script:projectDir "bin\Debug") -Filter "*.exe" -Recurse | + Select-Object -First 1 + Invoke-WinappCommand -Arguments "run `"$($debugDir.DirectoryName)`" --unregister-on-exit" + } finally { Pop-Location } + } + } + + Context "Certificate Generation" { + It "Should generate dev certificate" -Skip:$script:skip { + Push-Location $script:projectDir + try { + Invoke-WinappCommand -Arguments "cert generate --if-exists skip" + } finally { Pop-Location } + } + + It "Should have created devcert.pfx" -Skip:$script:skip { + Join-Path $script:projectDir "devcert.pfx" | Should -Exist + } + + It "Should report certificate info" -Skip:$script:skip { + Push-Location $script:projectDir + try { + $output = Invoke-WinappCommand -Arguments "cert info devcert.pfx" + $output | Should -Not -BeNullOrEmpty + } finally { Pop-Location } + } + } + + Context "Release Build and MSIX Packaging" { + It "Should build in Release mode" -Skip:$script:skip { + Push-Location $script:projectDir + try { + Invoke-Expression "dotnet build -c Release" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should produce a Release executable" -Skip:$script:skip { + $exeFile = Get-ChildItem -Path (Join-Path $script:projectDir "bin\Release") -Filter "*.exe" -Recurse | + Select-Object -First 1 + $exeFile | Should -Not -BeNullOrEmpty + $script:outputDir = $exeFile.DirectoryName + } + + It "Should package MSIX with winapp pack" -Skip:$script:skip { + Push-Location $script:projectDir + try { + Invoke-WinappCommand -Arguments "pack `"$($script:outputDir)`" --manifest Package.appxmanifest --cert devcert.pfx" + } finally { Pop-Location } + } + + It "Should have created an MSIX file" -Skip:$script:skip { + $msix = Get-ChildItem -Path $script:projectDir -Filter "*.msix" | + Select-Object -First 1 + $msix | Should -Not -BeNullOrEmpty + } + } + } + + Context "Phase 2: Sample Build Check" { + It "Should restore sample dependencies" -Skip:$script:skip { + Push-Location $script:sampleDir + try { + Invoke-Expression "dotnet restore" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should build sample in Debug mode" -Skip:$script:skip { + Push-Location $script:sampleDir + try { + Invoke-Expression "dotnet build -c Debug /p:ApplyDebugIdentity=false" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + } +} diff --git a/samples/electron/addon/binding.gyp b/samples/electron/addon/binding.gyp index f2c4fcf6..5d1540c1 100644 --- a/samples/electron/addon/binding.gyp +++ b/samples/electron/addon/binding.gyp @@ -6,12 +6,12 @@ "include_dirs": [ " + +param( + [string]$WinappPath, + [switch]$SkipCleanup +) + +BeforeDiscovery { + $script:skip = $null -eq (Get-Command node -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) +} + +Describe "Electron Sample" { + BeforeAll { + Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force + $script:skip = $null -eq (Get-Command node -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) + + $script:sampleDir = $PSScriptRoot + $script:tempDir = $null + $script:appDir = $null + $script:resolvedPkg = $null + + if (-not $script:skip) { + $script:resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath + } + } + + AfterAll { + Set-Location $script:sampleDir + + if (-not $SkipCleanup) { + if ($script:tempDir) { Remove-TempTestDirectory -Path $script:tempDir } + Remove-Item -Path (Join-Path $script:sampleDir 'node_modules') -Recurse -Force -ErrorAction SilentlyContinue + } + } + + Context "Phase 1: Electron Guide Workflow (from scratch)" { + BeforeAll { + if (-not $script:skip) { + $script:tempDir = New-TempTestDirectory -Prefix "electron-guide" + + # Use a dedicated npm cache to avoid ECOMPROMISED errors in CI + $npmCacheDir = Join-Path $script:tempDir ".npm-cache" + $null = New-Item -ItemType Directory -Path $npmCacheDir -Force + $env:npm_config_cache = $npmCacheDir + } + } + + It "Should create a new Electron app" -Skip:$script:skip { + Push-Location $script:tempDir + try { + $maxRetries = 3 + $created = $false + for ($i = 1; $i -le $maxRetries; $i++) { + if ($i -gt 1) { + Remove-Item -Path (Join-Path $script:tempDir "electron-app") -Recurse -Force -ErrorAction SilentlyContinue + Invoke-Expression "npm cache clean --force" 2>$null + Start-Sleep -Seconds 2 + } + Invoke-Expression "npx -y create-electron-app@latest electron-app" + if ($LASTEXITCODE -eq 0) { $created = $true; break } + } + $created | Should -Be $true -Because "Electron app creation should succeed within $maxRetries attempts" + $script:appDir = Join-Path $script:tempDir "electron-app" + $script:appDir | Should -Exist + } finally { Pop-Location } + } + + It "Should configure package.json for MSIX" -Skip:$script:skip { + $pkgPath = Join-Path $script:appDir "package.json" + $pkg = Get-Content $pkgPath | ConvertFrom-Json + $pkg | Add-Member -MemberType NoteProperty -Name "displayName" -Value "WinApp Electron Test" -Force + $pkg | Add-Member -MemberType NoteProperty -Name "description" -Value "Test app for winapp CLI" -Force + if ([string]::IsNullOrEmpty($pkg.version)) { $pkg.version = "1.0.0" } + $pkg | ConvertTo-Json -Depth 10 | Set-Content $pkgPath + $pkgPath | Should -Exist + } + + It "Should install winapp as a local devDependency" -Skip:$script:skip { + Push-Location $script:appDir + try { + Install-WinappNpmPackage -PackagePath $script:resolvedPkg + Join-Path $script:appDir "node_modules\.bin\winapp.cmd" | Should -Exist + } finally { Pop-Location } + } + + It "Should initialize winapp workspace" -Skip:$script:skip { + Push-Location $script:appDir + try { + Invoke-WinappCommand -Arguments "init . --use-defaults --setup-sdks=stable" + } finally { Pop-Location } + } + + It "Should create workspace files" -Skip:$script:skip { + Join-Path $script:appDir ".winapp" | Should -Exist + Join-Path $script:appDir "winapp.yaml" | Should -Exist + Join-Path $script:appDir "Package.appxmanifest" | Should -Exist + } + + It "Should create a C++ native addon" -Skip:$script:skip { + Push-Location $script:appDir + try { + Invoke-WinappCommand -Arguments "node create-addon --template cpp --name testCppAddon" + Join-Path $script:appDir "testCppAddon" | Should -Exist + Join-Path $script:appDir "testCppAddon\binding.gyp" | Should -Exist + } finally { Pop-Location } + } + + It "Should create a C# native addon" -Skip:$script:skip { + Push-Location $script:appDir + try { + Invoke-WinappCommand -Arguments "node create-addon --template cs --name testCsAddon" + Join-Path $script:appDir "testCsAddon" | Should -Exist + Join-Path $script:appDir "testCsAddon\testCsAddon.csproj" | Should -Exist + } finally { Pop-Location } + } + + It "Should build the C++ addon" -Skip:$script:skip { + Push-Location $script:appDir + try { + $output = Invoke-Expression "npx node-gyp clean configure build --directory=testCppAddon --verbose 2>&1" + $output | ForEach-Object { Write-Host $_ } + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should build the C# addon" -Skip:$script:skip { + Push-Location $script:appDir + try { + Invoke-Expression "npm run build-testCsAddon" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should add Electron debug identity" -Skip:$script:skip { + Push-Location $script:appDir + try { + Invoke-WinappCommand -Arguments "node add-electron-debug-identity --no-install" + } finally { Pop-Location } + } + + It "Should package the Electron app" -Skip:$script:skip { + Push-Location $script:appDir + try { + Invoke-Expression "npm run package" + $LASTEXITCODE | Should -Be 0 + $script:outDir = Join-Path $script:appDir "out" + $script:outDir | Should -Exist + $script:appPackageDir = (Get-ChildItem -Path $script:outDir -Directory | Select-Object -First 1).FullName + $script:appPackageDir | Should -Not -BeNullOrEmpty + } finally { Pop-Location } + } + + It "Should register app with winapp run --no-launch" -Skip:$script:skip { + Push-Location $script:appDir + try { + Invoke-WinappCommand -Arguments "run `"$($script:appPackageDir)`" --no-launch" + } finally { Pop-Location } + } + + It "Should generate a development certificate" -Skip:$script:skip { + Push-Location $script:appDir + try { + Invoke-WinappCommand -Arguments "cert generate" + Join-Path $script:appDir "devcert.pfx" | Should -Exist + } finally { Pop-Location } + } + + It "Should package as MSIX" -Skip:$script:skip { + Push-Location $script:appDir + try { + $certPath = Join-Path $script:appDir "devcert.pfx" + Invoke-WinappCommand -Arguments "pack `"$($script:appPackageDir)`" --cert `"$certPath`"" + Get-ChildItem -Path $script:appDir -Filter "*.msix" | Should -Not -BeNullOrEmpty + } finally { Pop-Location } + } + } + + Context "Phase 2: Sample Build Check" { + It "Should install sample dependencies" -Skip:$script:skip { + Push-Location $script:sampleDir + try { + Invoke-Expression "npm install --ignore-scripts" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should have node_modules" -Skip:$script:skip { + Join-Path $script:sampleDir 'node_modules' | Should -Exist + } + + It "Should have package.json" -Skip:$script:skip { + Join-Path $script:sampleDir 'package.json' | Should -Exist + } + + It "Should have forge.config.js" -Skip:$script:skip { + Join-Path $script:sampleDir 'forge.config.js' | Should -Exist + } + + It "Should have appxmanifest.xml" -Skip:$script:skip { + Join-Path $script:sampleDir 'appxmanifest.xml' | Should -Exist + } + + It "Should build the C# addon" -Skip:$script:skip { + Push-Location $script:sampleDir + try { + Invoke-Expression "npm run build-csAddon" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + } +} diff --git a/samples/flutter-app/test.Tests.ps1 b/samples/flutter-app/test.Tests.ps1 new file mode 100644 index 00000000..c84ffe80 --- /dev/null +++ b/samples/flutter-app/test.Tests.ps1 @@ -0,0 +1,129 @@ +<# +.SYNOPSIS +Pester 5.x tests for the flutter-app sample and Flutter guide workflow. + +.DESCRIPTION +Phase 1: Follows the docs/guides/flutter.md guide from scratch — creates a new + Flutter project, runs winapp init, builds, and packages as MSIX. +Phase 2: Quick build of the existing sample to verify it is not stale. + +.PARAMETER WinappPath +Path to the winapp npm package (.tgz or directory) to install. + +.PARAMETER SkipCleanup +Keep generated artifacts after test completes. +#> + +param( + [string]$WinappPath, + [switch]$SkipCleanup +) + +BeforeDiscovery { + $script:skip = $null -eq (Get-Command flutter -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) +} + +Describe "flutter-app sample" { + BeforeAll { + Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force + $script:skip = $null -eq (Get-Command flutter -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) + + $script:sampleDir = $PSScriptRoot + $script:tempDir = $null + $script:projectDir = $null + + if (-not $script:skip) { + $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath + Install-WinappGlobal -PackagePath $resolvedPkg + } + } + + AfterAll { + Set-Location $script:sampleDir + if (-not $SkipCleanup -and -not $script:skip) { + if ($script:tempDir) { Remove-TempTestDirectory -Path $script:tempDir } + Remove-Item -Path (Join-Path $script:sampleDir "build") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $script:sampleDir ".winapp") -Recurse -Force -ErrorAction SilentlyContinue + } + } + + Context "Phase 1: Flutter Guide Workflow (from scratch)" -Skip:$script:skip { + BeforeAll { + $script:tempDir = New-TempTestDirectory -Prefix "flutter-guide" + Set-Location $script:tempDir + } + + It "Should create a new Flutter project" { + flutter create test_flutter_app --platforms=windows + $LASTEXITCODE | Should -Be 0 + $script:projectDir = Join-Path $script:tempDir "test_flutter_app" + $script:projectDir | Should -Exist + } + + It "Should run winapp init successfully" { + Set-Location $script:projectDir + Invoke-WinappCommand -Arguments "init --use-defaults --setup-sdks=stable" + } + + It "Should create winapp.yaml after init" { + Join-Path $script:projectDir "winapp.yaml" | Should -Exist + } + + It "Should create Package.appxmanifest after init" { + Join-Path $script:projectDir "Package.appxmanifest" | Should -Exist + } + + It "Should create .winapp directory after init" { + Join-Path $script:projectDir ".winapp" | Should -Exist + } + + It "Should build Flutter app for Windows" { + Set-Location $script:projectDir + flutter build windows + $LASTEXITCODE | Should -Be 0 + $script:buildOutput = Join-Path $script:projectDir "build\windows\x64\runner\Release" + $script:buildOutput | Should -Exist + } + + It "Should run app with identity via winapp run" { + Set-Location $script:projectDir + Invoke-WinappCommand -Arguments "run $($script:buildOutput) --no-launch" + } + + It "Should generate a dev certificate" { + Set-Location $script:projectDir + Invoke-WinappCommand -Arguments "cert generate --if-exists skip" + Join-Path $script:projectDir "devcert.pfx" | Should -Exist + } + + It "Should prepare dist directory" { + Set-Location $script:projectDir + Copy-Item $script:buildOutput -Destination (Join-Path $script:projectDir "dist") -Recurse + Join-Path $script:projectDir "dist" | Should -Exist + } + + It "Should package as MSIX" { + Set-Location $script:projectDir + Invoke-WinappCommand -Arguments "pack dist --cert devcert.pfx" + Get-ChildItem -Path $script:projectDir -Filter "*.msix" | Should -Not -BeNullOrEmpty + } + } + + Context "Phase 2: Sample Build Check" -Skip:$script:skip { + BeforeAll { + Set-Location $script:sampleDir + + flutter pub get + if ($LASTEXITCODE -ne 0) { throw "flutter pub get failed" } + + Invoke-WinappCommand -Arguments "restore" + + flutter build windows + if ($LASTEXITCODE -ne 0) { throw "flutter build windows failed" } + } + + It "Should build flutter_app.exe" { + Join-Path $script:sampleDir "build\windows\x64\runner\Release\flutter_app.exe" | Should -Exist + } + } +} diff --git a/samples/packaging-cli/test.Tests.ps1 b/samples/packaging-cli/test.Tests.ps1 new file mode 100644 index 00000000..17432e9a --- /dev/null +++ b/samples/packaging-cli/test.Tests.ps1 @@ -0,0 +1,99 @@ +param( + [string]$WinappPath, + [switch]$SkipCleanup +) + +BeforeDiscovery { + $script:skip = $null -eq (Get-Command npm -ErrorAction SilentlyContinue) +} + +Describe "Packaging CLI Guide Workflow" { + BeforeAll { + Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force + $script:skip = $null -eq (Get-Command npm -ErrorAction SilentlyContinue) + $script:tempDir = $null + + if ($script:skip) { return } + + $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath + Install-WinappGlobal -PackagePath $resolvedPkg + + $script:tempDir = New-TempTestDirectory -Prefix "packaging-cli-guide" + $script:packageDir = Join-Path $script:tempDir "MyCliPackage" + $null = New-Item -ItemType Directory -Path $script:packageDir -Force + Copy-Item "$env:SystemRoot\System32\cmd.exe" -Destination (Join-Path $script:packageDir "mycli.exe") + } + + AfterAll { + if (-not $SkipCleanup -and $script:tempDir) { + Remove-TempTestDirectory -Path $script:tempDir + } + } + + Context "Prerequisites" { + It "Should have npm available" -Skip:$script:skip { + Test-Prerequisite 'npm' | Should -Be $true + } + + It "Should have dummy CLI executable" -Skip:$script:skip { + Join-Path $script:packageDir "mycli.exe" | Should -Exist + } + } + + Context "Manifest Generation" { + It "Should generate manifest from executable" -Skip:$script:skip { + Push-Location $script:packageDir + try { + Invoke-WinappCommand -Arguments "manifest generate --executable mycli.exe" + } finally { Pop-Location } + } + + It "Should have created Package.appxmanifest" -Skip:$script:skip { + Join-Path $script:packageDir "Package.appxmanifest" | Should -Exist + } + } + + Context "Certificate Generation" { + It "Should generate dev certificate" -Skip:$script:skip { + Push-Location $script:packageDir + try { + Invoke-WinappCommand -Arguments "cert generate --if-exists skip" + } finally { Pop-Location } + } + + It "Should have created devcert.pfx" -Skip:$script:skip { + Join-Path $script:packageDir "devcert.pfx" | Should -Exist + } + + It "Should report certificate info" -Skip:$script:skip { + Push-Location $script:packageDir + try { + $output = Invoke-WinappCommand -Arguments "cert info devcert.pfx" + $output | Should -Not -BeNullOrEmpty + } finally { Pop-Location } + } + } + + Context "MSIX Packaging and Signing" { + It "Should package as MSIX" -Skip:$script:skip { + Push-Location $script:packageDir + try { + Invoke-WinappCommand -Arguments "pack . --cert devcert.pfx" + } finally { Pop-Location } + } + + It "Should have created an MSIX file" -Skip:$script:skip { + $msix = Get-ChildItem -Path $script:packageDir -Filter "*.msix" | + Select-Object -First 1 + $msix | Should -Not -BeNullOrEmpty + $script:msixPath = $msix.FullName + } + + It "Should sign the MSIX" -Skip:$script:skip { + Push-Location $script:packageDir + try { + Invoke-WinappCommand -Arguments "sign `"$($script:msixPath)`" devcert.pfx" + } finally { Pop-Location } + } + } +} diff --git a/samples/rust-app/test.Tests.ps1 b/samples/rust-app/test.Tests.ps1 new file mode 100644 index 00000000..d1537761 --- /dev/null +++ b/samples/rust-app/test.Tests.ps1 @@ -0,0 +1,151 @@ +param( + [string]$WinappPath, + [switch]$SkipCleanup +) + +BeforeDiscovery { + $script:skip = $null -eq (Get-Command cargo -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) +} + +Describe "Rust App Sample" { + BeforeAll { + Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force + $script:skip = $null -eq (Get-Command cargo -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) + $script:sampleDir = $PSScriptRoot + $script:tempDir = $null + + if ($script:skip) { return } + + $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath + Install-WinappGlobal -PackagePath $resolvedPkg + + $script:tempDir = New-TempTestDirectory -Prefix "rust-guide" + } + + AfterAll { + if (-not $SkipCleanup) { + if ($script:tempDir) { Remove-TempTestDirectory -Path $script:tempDir } + Remove-Item -Path (Join-Path $script:sampleDir "target") -Recurse -Force -ErrorAction SilentlyContinue + } + } + + Context "Prerequisites" { + It "Should have cargo available" -Skip:$script:skip { + Test-Prerequisite 'cargo' | Should -Be $true + } + + It "Should have npm available" -Skip:$script:skip { + Test-Prerequisite 'npm' | Should -Be $true + } + } + + Context "Rust Guide Workflow (from scratch)" { + It "Should create a new Rust project" -Skip:$script:skip { + Push-Location $script:tempDir + try { + Invoke-Expression "cargo new test-rust-app" + $LASTEXITCODE | Should -Be 0 + $script:rustProjectDir = Join-Path $script:tempDir "test-rust-app" + $script:rustProjectDir | Should -Exist + } finally { Pop-Location } + } + + It "Should run winapp init" -Skip:$script:skip { + Push-Location $script:rustProjectDir + try { + Invoke-WinappCommand -Arguments "init --use-defaults --setup-sdks=none" + } finally { Pop-Location } + } + + It "Should have created Package.appxmanifest" -Skip:$script:skip { + Join-Path $script:rustProjectDir "Package.appxmanifest" | Should -Exist + } + + It "Should add execution alias to manifest" -Skip:$script:skip { + Push-Location $script:rustProjectDir + try { + Invoke-WinappCommand -Arguments "manifest add-alias" + } finally { Pop-Location } + } + + It "Should build Rust app in debug mode" -Skip:$script:skip { + Push-Location $script:rustProjectDir + try { + Invoke-Expression "cargo build" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should run app with identity via winapp run" -Skip:$script:skip { + Push-Location $script:rustProjectDir + try { + Invoke-WinappCommand -Arguments "run .\target\debug --with-alias --unregister-on-exit" + } finally { Pop-Location } + } + + It "Should build Rust app in release mode" -Skip:$script:skip { + Push-Location $script:rustProjectDir + try { + Invoke-Expression "cargo build --release" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should have produced release executable" -Skip:$script:skip { + Join-Path $script:rustProjectDir "target\release\test-rust-app.exe" | Should -Exist + } + + It "Should prepare MSIX layout" -Skip:$script:skip { + $distDir = Join-Path $script:rustProjectDir "dist" + $null = New-Item -ItemType Directory -Path $distDir -Force + Copy-Item (Join-Path $script:rustProjectDir "target\release\test-rust-app.exe") -Destination $distDir + Join-Path $distDir "test-rust-app.exe" | Should -Exist + } + + It "Should generate dev certificate" -Skip:$script:skip { + Push-Location $script:rustProjectDir + try { + Invoke-WinappCommand -Arguments "cert generate --if-exists skip" + } finally { Pop-Location } + } + + It "Should have created devcert.pfx" -Skip:$script:skip { + Join-Path $script:rustProjectDir "devcert.pfx" | Should -Exist + } + + It "Should report certificate info" -Skip:$script:skip { + Push-Location $script:rustProjectDir + try { + $output = Invoke-WinappCommand -Arguments "cert info devcert.pfx" + $output | Should -Not -BeNullOrEmpty + } finally { Pop-Location } + } + + It "Should package as MSIX" -Skip:$script:skip { + Push-Location $script:rustProjectDir + try { + Invoke-WinappCommand -Arguments "pack dist --manifest Package.appxmanifest --cert devcert.pfx" + } finally { Pop-Location } + } + + It "Should have created an MSIX file" -Skip:$script:skip { + $msix = Get-ChildItem -Path $script:rustProjectDir -Filter "*.msix" | + Select-Object -First 1 + $msix | Should -Not -BeNullOrEmpty + } + } + + Context "Sample Build Check" { + It "Should build existing sample with cargo" -Skip:$script:skip { + Push-Location $script:sampleDir + try { + Invoke-Expression "cargo build" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should have produced debug executable" -Skip:$script:skip { + Join-Path $script:sampleDir "target\debug\rust-app.exe" | Should -Exist + } + } +} diff --git a/samples/tauri-app/src-tauri/Cargo.lock b/samples/tauri-app/src-tauri/Cargo.lock index 21cf00af..e98aa13c 100644 --- a/samples/tauri-app/src-tauri/Cargo.lock +++ b/samples/tauri-app/src-tauri/Cargo.lock @@ -3538,7 +3538,7 @@ dependencies = [ "tauri", "tauri-build", "tauri-plugin-opener", - "windows 0.58.0", + "windows 0.62.2", ] [[package]] @@ -4407,8 +4407,8 @@ dependencies = [ "webview2-com-sys", "windows 0.61.3", "windows-core 0.61.2", - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", ] [[package]] @@ -4481,25 +4481,27 @@ dependencies = [ [[package]] name = "windows" -version = "0.58.0" +version = "0.61.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" +checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" dependencies = [ - "windows-core 0.58.0", - "windows-targets 0.52.6", + "windows-collections 0.2.0", + "windows-core 0.61.2", + "windows-future 0.2.1", + "windows-link 0.1.3", + "windows-numerics 0.2.0", ] [[package]] name = "windows" -version = "0.61.3" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" dependencies = [ - "windows-collections", - "windows-core 0.61.2", - "windows-future", - "windows-link 0.1.3", - "windows-numerics", + "windows-collections 0.3.2", + "windows-core 0.62.2", + "windows-future 0.3.2", + "windows-numerics 0.3.1", ] [[package]] @@ -4512,16 +4514,12 @@ dependencies = [ ] [[package]] -name = "windows-core" -version = "0.58.0" +name = "windows-collections" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" dependencies = [ - "windows-implement 0.58.0", - "windows-interface 0.58.0", - "windows-result 0.2.0", - "windows-strings 0.1.0", - "windows-targets 0.52.6", + "windows-core 0.62.2", ] [[package]] @@ -4530,8 +4528,8 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", "windows-link 0.1.3", "windows-result 0.3.4", "windows-strings 0.4.2", @@ -4543,8 +4541,8 @@ version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", + "windows-implement", + "windows-interface", "windows-link 0.2.1", "windows-result 0.4.1", "windows-strings 0.5.1", @@ -4558,18 +4556,18 @@ checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", "windows-link 0.1.3", - "windows-threading", + "windows-threading 0.1.0", ] [[package]] -name = "windows-implement" -version = "0.58.0" +name = "windows-future" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bbd5b46c938e506ecbce286b6628a02171d56153ba733b6c741fc627ec9579b" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", + "windows-core 0.62.2", + "windows-link 0.2.1", + "windows-threading 0.2.1", ] [[package]] @@ -4583,17 +4581,6 @@ dependencies = [ "syn 2.0.111", ] -[[package]] -name = "windows-interface" -version = "0.58.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053c4c462dc91d3b1504c6fe5a726dd15e216ba718e84a0e46a88fbe5ded3515" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.111", -] - [[package]] name = "windows-interface" version = "0.59.3" @@ -4628,12 +4615,13 @@ dependencies = [ ] [[package]] -name = "windows-result" -version = "0.2.0" +name = "windows-numerics" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" dependencies = [ - "windows-targets 0.52.6", + "windows-core 0.62.2", + "windows-link 0.2.1", ] [[package]] @@ -4654,16 +4642,6 @@ dependencies = [ "windows-link 0.2.1", ] -[[package]] -name = "windows-strings" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" -dependencies = [ - "windows-result 0.2.0", - "windows-targets 0.52.6", -] - [[package]] name = "windows-strings" version = "0.4.2" @@ -4775,6 +4753,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link 0.2.1", +] + [[package]] name = "windows-version" version = "0.1.7" diff --git a/samples/tauri-app/test.Tests.ps1 b/samples/tauri-app/test.Tests.ps1 new file mode 100644 index 00000000..cbee2b91 --- /dev/null +++ b/samples/tauri-app/test.Tests.ps1 @@ -0,0 +1,163 @@ +param( + [string]$WinappPath, + [switch]$SkipCleanup +) + +BeforeDiscovery { + $script:skip = $null -eq (Get-Command node -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) -or $null -eq (Get-Command cargo -ErrorAction SilentlyContinue) +} + +Describe "Tauri App Sample" { + BeforeAll { + Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force + $script:skip = $null -eq (Get-Command node -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) -or $null -eq (Get-Command cargo -ErrorAction SilentlyContinue) + $script:sampleDir = $PSScriptRoot + $script:tempDir = $null + + if ($script:skip) { return } + + $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath + Install-WinappGlobal -PackagePath $resolvedPkg + + $script:tempDir = New-TempTestDirectory -Prefix "tauri-guide" + $script:tempApp = Join-Path $script:tempDir "tauri-app" + } + + AfterAll { + Set-Location $script:sampleDir + + if (-not $SkipCleanup) { + if ($script:tempDir) { Remove-TempTestDirectory -Path $script:tempDir } + Remove-Item -Path (Join-Path $script:sampleDir "node_modules") -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $script:sampleDir "src-tauri\target") -Recurse -Force -ErrorAction SilentlyContinue + } + } + + Context "Prerequisites" { + It "Should have Node.js available" -Skip:$script:skip { + Test-Prerequisite 'node' | Should -Be $true + } + + It "Should have npm available" -Skip:$script:skip { + Test-Prerequisite 'npm' | Should -Be $true + } + + It "Should have Rust/Cargo available" -Skip:$script:skip { + Test-Prerequisite 'cargo' | Should -Be $true + } + } + + Context "Tauri Guide Workflow (from scratch)" { + It "Should copy sample to temp directory" -Skip:$script:skip { + Copy-Item -Path $script:sampleDir -Destination $script:tempApp -Recurse -Exclude @('.gitignore', 'node_modules', 'src-tauri\target') + $script:tempApp | Should -Exist + } + + It "Should install npm dependencies" -Skip:$script:skip { + Push-Location $script:tempApp + try { + Invoke-Expression "npm install" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should run winapp init" -Skip:$script:skip { + Push-Location $script:tempApp + try { + Invoke-WinappCommand -Arguments "init --use-defaults --setup-sdks=none" + } finally { Pop-Location } + } + + It "Should have appxmanifest" -Skip:$script:skip { + # winapp init preserves the existing appxmanifest.xml copied from the sample + Join-Path $script:tempApp "appxmanifest.xml" | Should -Exist + } + + It "Should build Tauri app in debug mode" -Skip:$script:skip { + Push-Location $script:tempApp + try { + Invoke-Expression "cargo build --manifest-path src-tauri\Cargo.toml" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should run app with identity via winapp run" -Skip:$script:skip { + Push-Location $script:tempApp + try { + $distDir = Join-Path $script:tempApp "dist" + $null = New-Item -ItemType Directory -Path $distDir -Force + Copy-Item (Join-Path $script:tempApp "src-tauri\target\debug\tauri-app.exe") -Destination $distDir + Invoke-WinappCommand -Arguments "run dist --no-launch" + } finally { Pop-Location } + } + + It "Should build Tauri app in release mode" -Skip:$script:skip { + Push-Location $script:tempApp + try { + Invoke-Expression "cargo build --release --manifest-path src-tauri\Cargo.toml" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should have produced release executable" -Skip:$script:skip { + Join-Path $script:tempApp "src-tauri\target\release\tauri-app.exe" | Should -Exist + } + + It "Should prepare MSIX layout" -Skip:$script:skip { + Push-Location $script:tempApp + try { + $layoutDir = Join-Path $script:tempApp "msix-layout" + $null = New-Item -ItemType Directory -Path $layoutDir -Force + $tauriExe = Join-Path $script:tempApp "src-tauri\target\release\tauri-app.exe" + Copy-Item $tauriExe -Destination $layoutDir + Join-Path $layoutDir "tauri-app.exe" | Should -Exist + } finally { Pop-Location } + } + + It "Should generate dev certificate" -Skip:$script:skip { + Push-Location $script:tempApp + try { + Invoke-WinappCommand -Arguments "cert generate --if-exists skip --manifest appxmanifest.xml" + } finally { Pop-Location } + } + + It "Should have created devcert.pfx" -Skip:$script:skip { + Join-Path $script:tempApp "devcert.pfx" | Should -Exist + } + + It "Should package as MSIX" -Skip:$script:skip { + Push-Location $script:tempApp + try { + Invoke-WinappCommand -Arguments "pack msix-layout --manifest appxmanifest.xml --cert devcert.pfx" + } finally { Pop-Location } + } + + It "Should have created an MSIX file" -Skip:$script:skip { + $msix = Get-ChildItem -Path $script:tempApp -Filter "*.msix" | + Select-Object -First 1 + $msix | Should -Not -BeNullOrEmpty + } + } + + Context "Sample Build Check" { + It "Should install sample npm dependencies" -Skip:$script:skip { + Push-Location $script:sampleDir + try { + Invoke-Expression "npm install" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should build sample Rust backend" -Skip:$script:skip { + Push-Location $script:sampleDir + try { + Invoke-Expression "cargo build --manifest-path src-tauri\Cargo.toml" + $LASTEXITCODE | Should -Be 0 + } finally { Pop-Location } + } + + It "Should have produced debug executable" -Skip:$script:skip { + Join-Path $script:sampleDir "src-tauri\target\debug\tauri-app.exe" | Should -Exist + } + } +} diff --git a/samples/wpf-app/test.Tests.ps1 b/samples/wpf-app/test.Tests.ps1 new file mode 100644 index 00000000..b883278e --- /dev/null +++ b/samples/wpf-app/test.Tests.ps1 @@ -0,0 +1,140 @@ +param( + [string]$WinappPath, + [switch]$SkipCleanup +) + +BeforeDiscovery { + $script:skip = $null -eq (Get-Command dotnet -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) +} + +Describe 'wpf-app sample' { + + BeforeAll { + Import-Module "$PSScriptRoot\..\SampleTestHelpers.psm1" -Force + $script:skip = $null -eq (Get-Command dotnet -ErrorAction SilentlyContinue) -or $null -eq (Get-Command npm -ErrorAction SilentlyContinue) + + $script:sampleDir = $PSScriptRoot + $script:tempDir = $null + $script:originalLocation = Get-Location + + if (-not $script:skip) { + $resolvedPkg = Resolve-WinappCliPath -WinappPath $WinappPath + Install-WinappGlobal -PackagePath $resolvedPkg + } + } + + AfterAll { + Set-Location $script:sampleDir + + if (-not $SkipCleanup) { + if ($script:tempDir) { Remove-TempTestDirectory -Path $script:tempDir } + Remove-Item -Path (Join-Path $script:sampleDir 'bin') -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item -Path (Join-Path $script:sampleDir 'obj') -Recurse -Force -ErrorAction SilentlyContinue + } + } + + Context 'Phase 1: WPF Guide Workflow (from scratch)' { + + BeforeAll { + if (-not $script:skip) { + $script:tempDir = New-TempTestDirectory -Prefix 'wpf-guide' + Push-Location $script:tempDir + + Invoke-Expression 'dotnet new wpf -n test-wpf-app' + $script:dotnetNewExit = $LASTEXITCODE + + if ($script:dotnetNewExit -eq 0) { + Push-Location 'test-wpf-app' + } + } + } + + AfterAll { + if (-not $script:skip) { + # Unwind any Push-Location calls made during this context + Set-Location $script:originalLocation + } + } + + It 'Creates a new WPF project' -Skip:$script:skip { + $script:dotnetNewExit | Should -Be 0 + } + + It 'Runs winapp init successfully' -Skip:$script:skip { + Invoke-WinappCommand -Arguments 'init --use-defaults' + } + + It 'Generates Package.appxmanifest from winapp init' -Skip:$script:skip { + 'Package.appxmanifest' | Should -Exist + } + + It 'Builds in Debug mode' -Skip:$script:skip { + Invoke-Expression 'dotnet build -c Debug /p:ApplyDebugIdentity=false' + $LASTEXITCODE | Should -Be 0 + } + + It 'Applies debug identity with create-debug-identity' -Skip:$script:skip { + $exeFile = Get-ChildItem -Path 'bin\Debug' -Filter '*.exe' -Recurse | Select-Object -First 1 + $exeFile | Should -Not -BeNullOrEmpty + Invoke-WinappCommand -Arguments "create-debug-identity `"$($exeFile.FullName)`"" + } + + It 'Registers app with winapp run --no-launch' -Skip:$script:skip { + $exeFile = Get-ChildItem -Path 'bin\Debug' -Filter '*.exe' -Recurse | Select-Object -First 1 + Invoke-WinappCommand -Arguments "run `"$($exeFile.DirectoryName)`" --no-launch" + } + + It 'Generates a dev certificate' -Skip:$script:skip { + Invoke-WinappCommand -Arguments 'cert generate --if-exists skip' + 'devcert.pfx' | Should -Exist + } + + It 'Shows certificate info without error' -Skip:$script:skip { + Invoke-WinappCommand -Arguments 'cert info devcert.pfx' + } + + It 'Builds in Release mode with RID' -Skip:$script:skip { + $rid = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq 'Arm64') { 'win-arm64' } else { 'win-x64' } + Invoke-Expression "dotnet build -c Release -r $rid" + $LASTEXITCODE | Should -Be 0 + } + + It 'Packages MSIX with winapp pack' -Skip:$script:skip { + $exeFile = Get-ChildItem -Path 'bin\Release' -Filter '*.exe' -Recurse | Select-Object -First 1 + $exeFile | Should -Not -BeNullOrEmpty -Because 'Release build should produce an .exe' + $script:outputDir = $exeFile.DirectoryName + + Invoke-WinappCommand -Arguments "pack `"$($script:outputDir)`" --manifest Package.appxmanifest --cert devcert.pfx" + } + + It 'Produces an MSIX file' -Skip:$script:skip { + $msix = Get-ChildItem -Path '.' -Filter '*.msix' -ErrorAction SilentlyContinue | Select-Object -First 1 + $msix | Should -Not -BeNullOrEmpty -Because 'winapp pack should create an .msix file' + } + } + + Context 'Phase 2: Sample Build Check' { + + BeforeAll { + if (-not $script:skip) { + Push-Location $script:sampleDir + } + } + + AfterAll { + if (-not $script:skip) { + Set-Location $script:originalLocation + } + } + + It 'Restores NuGet packages' -Skip:$script:skip { + Invoke-Expression 'dotnet restore' + $LASTEXITCODE | Should -Be 0 + } + + It 'Builds existing sample in Debug mode' -Skip:$script:skip { + Invoke-Expression 'dotnet build -c Debug /p:ApplyDebugIdentity=false' + $LASTEXITCODE | Should -Be 0 + } + } +} diff --git a/scripts/test-e2e-electron.ps1 b/scripts/test-e2e-electron.ps1 deleted file mode 100644 index b5c3ae8a..00000000 --- a/scripts/test-e2e-electron.ps1 +++ /dev/null @@ -1,457 +0,0 @@ -<# -.SYNOPSIS -End-to-end test for WinApp CLI with Electron framework. - -.DESCRIPTION -This script tests the complete WinApp CLI workflow with Electron: -1. Creates a new Electron application -2. Installs the locally-built winapp npm package -3. Runs 'winapp init' with non-interactive mode -4. Creates C++ and C# native addons -5. Builds the addons to validate they compile -6. Adds Electron debug identity -7. Packages the app to MSIX -8. Signs the MSIX package - -The test creates a 'test-wd' directory in the repo root for the test project and cleans it up after completion. - -.PARAMETER ArtifactsPath -Path to the artifacts folder containing the built winapp npm package. -Default: "$PSScriptRoot\..\artifacts\npm" - -.PARAMETER NpmPackagePath -Path to the winapp npm package. If not specified, uses the one from ArtifactsPath. -Default: "$PSScriptRoot\..\src\winapp-npm" - -.PARAMETER SkipCleanup -If specified, does not delete the test project after completion (useful for debugging). - -.PARAMETER Verbose -Enable verbose output for debugging. - -.EXAMPLE -.\test-e2e-electron.ps1 -Run the test with default settings. - -.EXAMPLE -.\test-e2e-electron.ps1 -SkipCleanup -Verbose -Run the test, keep the project folder, and show detailed output. -#> - -param( - [string]$ArtifactsPath = "$PSScriptRoot\..\artifacts\npm", - [string]$NpmPackagePath = "$PSScriptRoot\..\src\winapp-npm", - [switch]$SkipCleanup, - [switch]$Verbose -) - -# Enable strict mode -Set-StrictMode -Version Latest -$ErrorActionPreference = 'Stop' - -$VerbosePreference = if ($Verbose) { 'Continue' } else { 'SilentlyContinue' } - -# ============================================================================ -# Helper Functions -# ============================================================================ - -function Write-TestHeader { - param([string]$Message) - Write-Host "`n$('='*80)" -ForegroundColor Cyan - Write-Host "TEST: $Message" -ForegroundColor Cyan - Write-Host "$('='*80)`n" -ForegroundColor Cyan -} - -function Write-TestStep { - param([string]$Message, [int]$Step) - Write-Host "[$Step] $Message" -ForegroundColor Yellow -} - -function Write-TestSuccess { - param([string]$Message) - Write-Host "[PASS] $Message" -ForegroundColor Green -} - -function Write-TestError { - param([string]$Message) - Write-Host "[FAIL] $Message" -ForegroundColor Red -} - -function Assert-Command { - param( - [string]$Command, - [string]$FailMessage - ) - Write-Verbose "Running: $Command" - $result = Invoke-Expression $Command - if ($LASTEXITCODE -ne 0) { - Write-TestError $FailMessage - throw $FailMessage - } - Write-TestSuccess "$Command" - return $result -} - -function Assert-FileExists { - param( - [string]$Path, - [string]$Description - ) - if (-not (Test-Path $Path)) { - Write-TestError "$Description not found at $Path" - throw "$Description not found at $Path" - } - Write-TestSuccess "$Description exists: $Path" -} - -function Assert-DirectoryExists { - param( - [string]$Path, - [string]$Description - ) - if (-not (Test-Path $Path -PathType Container)) { - Write-TestError "$Description not found at $Path" - throw "$Description not found at $Path" - } - Write-TestSuccess "$Description exists: $Path" -} - -# ============================================================================ -# Validation -# ============================================================================ - -Write-TestHeader "E2E Electron Test - Validation Phase" - -Write-TestStep "Validating prerequisites..." 1 - -# Check Node.js -try { - $nodeVersion = node --version - Write-TestSuccess "Node.js found: $nodeVersion" -} catch { - Write-TestError "Node.js is not installed or not in PATH" - throw "Node.js is required but not found" -} - -# Check npm -try { - $npmVersion = npm --version - Write-TestSuccess "npm found: $npmVersion" -} catch { - Write-TestError "npm is not installed or not in PATH" - throw "npm is required but not found" -} - - - -# Verify artifacts path or npm package path -if ($ArtifactsPath -and (Test-Path $ArtifactsPath)) { - # Convert to absolute path to ensure it works after directory changes - $resolvedArtifactsPath = (Resolve-Path $ArtifactsPath).Path - - # Check if this is a directory containing .tgz files (from CI artifact download) - # or a directory with package.json (local npm package) - $tgzFiles = Get-ChildItem -Path $resolvedArtifactsPath -Filter "*.tgz" -ErrorAction SilentlyContinue - if ($tgzFiles) { - # Use the first .tgz file found - $localNpmPackagePath = $tgzFiles[0].FullName - Write-TestSuccess "Found npm tarball: $localNpmPackagePath" - } elseif (Test-Path (Join-Path $resolvedArtifactsPath "package.json")) { - # It's a directory with package.json (local development) - $localNpmPackagePath = $resolvedArtifactsPath - Write-TestSuccess "Found npm package directory: $localNpmPackagePath" - } else { - Write-TestError "Artifacts path exists but contains no .tgz files or package.json: $resolvedArtifactsPath" - throw "Invalid artifacts path - no installable npm package found" - } -} elseif (Test-Path $NpmPackagePath) { - Write-TestSuccess "npm package found: $NpmPackagePath" - # Convert to absolute path to ensure it works after directory changes - $localNpmPackagePath = (Resolve-Path $NpmPackagePath).Path -} else { - Write-TestError "Neither artifacts path nor npm package path exists" - throw "Cannot find winapp npm package at $ArtifactsPath or $NpmPackagePath" -} - -Write-Verbose "Using npm package path: $localNpmPackagePath" - -# ============================================================================ -# Setup Test Environment -# ============================================================================ - -Write-TestHeader "E2E Electron Test - Setup Phase" - -Write-TestStep "Creating test directory..." 2 - -$repoRoot = (Resolve-Path "$PSScriptRoot\..").Path -$testDir = Join-Path $repoRoot "test-wd" - -# Clean up any existing test directory -if (Test-Path $testDir) { - Remove-Item -Path $testDir -Recurse -Force -ErrorAction SilentlyContinue -} - -$null = New-Item -ItemType Directory -Path $testDir -Force -Write-TestSuccess "Test directory created: $testDir" - -# Save original location to restore on exit -$originalLocation = Get-Location - -try { - Push-Location $testDir - Write-Verbose "Working directory: $(Get-Location)" - - # ======================================================================== - # Configure npm for CI environment - # ======================================================================== - - # Set a unique npm cache directory to avoid ECOMPROMISED errors in CI - # This prevents conflicts with concurrent builds and stale cache issues - $npmCacheDir = Join-Path $testDir ".npm-cache" - $null = New-Item -ItemType Directory -Path $npmCacheDir -Force - $env:npm_config_cache = $npmCacheDir - Write-Verbose "npm cache directory set to: $npmCacheDir" - - # ======================================================================== - # Create Electron Application - # ======================================================================== - - Write-TestHeader "E2E Electron Test - Create Electron App Phase" - - Write-TestStep "Creating new Electron app..." 3 - - # Use Electron Forge to scaffold basic app (no webpack) - # Retry logic for CI environments where npm can have transient failures - $maxRetries = 3 - $retryCount = 0 - $electronAppCreated = $false - - while (-not $electronAppCreated -and $retryCount -lt $maxRetries) { - $retryCount++ - Write-Verbose "Attempt $retryCount of $maxRetries to create Electron app..." - - try { - # Use --prefer-offline to reduce network issues and clear package-lock before retry - if ($retryCount -gt 1) { - Write-Verbose "Cleaning up failed attempt..." - Remove-Item -Path (Join-Path $testDir "electron-app") -Recurse -Force -ErrorAction SilentlyContinue - npm cache clean --force 2>$null - Start-Sleep -Seconds 2 - } - - $electronCommand = "npx -y create-electron-app@7.11.1 electron-app --template=webpack" - Write-Verbose "Running: $electronCommand" - Invoke-Expression $electronCommand - - if ($LASTEXITCODE -eq 0) { - $electronAppCreated = $true - Write-TestSuccess "Electron app created successfully" - } else { - Write-Verbose "npx command failed with exit code $LASTEXITCODE" - } - } catch { - Write-Verbose "Exception during Electron app creation: $_" - } - } - - if (-not $electronAppCreated) { - Write-TestError "Failed to create Electron app after $maxRetries attempts" - throw "Failed to create Electron app" - } - - $electronAppDir = Join-Path $testDir "electron-app" - Assert-DirectoryExists $electronAppDir "Electron app directory" - - Push-Location $electronAppDir - - # Update package.json to add required fields for MSIX - Write-TestStep "Configuring package.json for Windows packaging..." 4 - - $packageJsonPath = Join-Path $electronAppDir "package.json" - $packageJson = Get-Content $packageJsonPath | ConvertFrom-Json - - # Add required fields for MSIX packaging - $packageJson | Add-Member -MemberType NoteProperty -Name "displayName" -Value "WinApp Electron Test" -Force - $packageJson | Add-Member -MemberType NoteProperty -Name "description" -Value "E2E test application for WinApp CLI" -Force - - # Ensure version is set - if ([string]::IsNullOrEmpty($packageJson.version)) { - $packageJson.version = "1.0.0" - } - - $packageJson | ConvertTo-Json -Depth 10 | Set-Content $packageJsonPath - Write-TestSuccess "package.json configured" - - # ======================================================================== - # Install WinApp npm package - # ======================================================================== - - Write-TestHeader "E2E Electron Test - Install WinApp Phase" - - Write-TestStep "Installing winapp npm package from local artifacts..." 5 - - # Install the local winapp package - $installCommand = "npm install $localNpmPackagePath --save-dev" - Assert-Command $installCommand "Failed to install winapp npm package" - - # Verify winapp is installed - $nodeModulesPath = Join-Path $electronAppDir "node_modules" ".bin" "winapp" - $winappCli = Join-Path $electronAppDir "node_modules" ".bin" "winapp.cmd" - Assert-FileExists $winappCli "winapp CLI" - - # ======================================================================== - # Initialize WinApp Workspace - # ======================================================================== - - Write-TestHeader "E2E Electron Test - Initialize Workspace Phase" - - Write-TestStep "Running 'winapp init' with non-interactive mode..." 6 - - # Use --use-defaults for non-interactive initialization - # Setup stable SDKs for packaging - $initCommand = "npx winapp init . --use-defaults --setup-sdks=stable" - Assert-Command $initCommand "Failed to initialize winapp workspace" - - # Verify workspace was created - Assert-DirectoryExists ".winapp" ".winapp directory" - Assert-FileExists "winapp.yaml" "winapp.yaml configuration file" - Assert-FileExists "Package.appxmanifest" "Package.appxmanifest manifest file" - - # ======================================================================== - # Create Native Addons - # ======================================================================== - - Write-TestHeader "E2E Electron Test - Create Native Addons Phase" - - Write-TestStep "Creating C++ addon..." 7 - - $addCppCommand = "npx winapp node create-addon --template cpp --name testCppAddon" - Assert-Command $addCppCommand "Failed to create C++ addon" - - Assert-DirectoryExists "testCppAddon" "C++ addon directory" - Assert-FileExists "testCppAddon\binding.gyp" "C++ addon binding.gyp file" - - Write-TestSuccess "C++ addon created" - - Write-TestStep "Creating C# addon..." 8 - - $addCsharpCommand = "npx winapp node create-addon --template cs --name testCsAddon" - Assert-Command $addCsharpCommand "Failed to create C# addon" - - Assert-DirectoryExists "testCsAddon" "C# addon directory" - Assert-FileExists "testCsAddon\testCsAddon.csproj" "C# addon project file" - - Write-TestSuccess "C# addon created" - - # ======================================================================== - # Build Native Addons - # ======================================================================== - - Write-TestHeader "E2E Electron Test - Build Addons Phase" - - Write-TestStep "Building C++ addon..." 9 - - $buildCppCommand = "npm run build-testCppAddon" - Assert-Command $buildCppCommand "Failed to build C++ addon" - - Write-TestSuccess "C++ addon built successfully" - - Write-TestStep "Building C# addon..." 10 - - $buildCsCommand = "npm run build-testCsAddon" - Assert-Command $buildCsCommand "Failed to build C# addon" - - Write-TestSuccess "C# addon built successfully" - - # ======================================================================== - # Add Electron Debug Identity - # ======================================================================== - - Write-TestHeader "E2E Electron Test - Debug Identity Phase" - - Write-TestStep "Adding Electron debug identity..." 11 - - $addIdentityCommand = "npx winapp node add-electron-debug-identity" - Assert-Command $addIdentityCommand "Failed to add Electron debug identity" - - # ======================================================================== - # Package Application - # ======================================================================== - - Write-TestHeader "E2E Electron Test - Package Phase" - - Write-TestStep "Building Electron application package..." 12 - - # First, run npm package to create the packaged app - $packageCommand = "npm run package" - Assert-Command $packageCommand "Failed to package Electron app" - - # Find the output directory created by electron-forge - $outDir = Join-Path $electronAppDir "out" - if (-not (Test-Path $outDir)) { - Write-TestError "Electron package output directory not found at $outDir" - throw "Electron app packaging did not create output directory" - } - - # Find the app package directory (typically 'out/' or similar) - $appPackageDirs = Get-ChildItem -Path $outDir -Directory -ErrorAction SilentlyContinue - if (-not $appPackageDirs) { - Write-TestError "No app package directories found in $outDir" - throw "Electron app package not created" - } - - $appPackageDir = $appPackageDirs[0].FullName - Write-TestSuccess "Electron app packaged to: $appPackageDir" - - Write-TestStep "Generating development certificate..." 13 - - $certGenCommand = "npx winapp cert generate" - Assert-Command $certGenCommand "Failed to generate development certificate" - - $certPath = Join-Path $electronAppDir "devcert.pfx" - Assert-FileExists $certPath "Development certificate" - - Write-TestStep "Packaging app to MSIX..." 14 - - $packCommand = "npx winapp pack `"$appPackageDir`" --cert `"$certPath`"" - Assert-Command $packCommand "Failed to package app to MSIX" - - # Verify MSIX was created (winapp pack outputs to the project root) - $msixFiles = Get-ChildItem -Path $electronAppDir -Filter "*.msix" -ErrorAction SilentlyContinue - if ($msixFiles) { - Write-TestSuccess "MSIX package created and signed: $($msixFiles[0].Name)" - } else { - Write-TestError "No MSIX file found after packaging" - throw "MSIX packaging failed - no output file generated" - } - - # ======================================================================== - # Final Verification - # ======================================================================== - - Write-Host "`n$('='*80)" -ForegroundColor Green - Write-Host "E2E ELECTRON TEST COMPLETED SUCCESSFULLY" -ForegroundColor Green - Write-Host "$('='*80)`n" -ForegroundColor Green - -} finally { - # Restore to original location (handles any number of Push-Location calls) - Set-Location $originalLocation - - # ======================================================================== - # Cleanup - # ======================================================================== - - if (-not $SkipCleanup) { - Write-TestHeader "Cleanup" - Write-TestStep "Cleaning up temporary test directory..." 15 - - try { - Remove-Item -Path $testDir -Recurse -Force -ErrorAction SilentlyContinue - Write-TestSuccess "Test directory cleaned up: $testDir" - } catch { - Write-Verbose "Warning: Could not fully clean up test directory: $_" - } - } else { - Write-Host "Test directory preserved at: $testDir" -ForegroundColor Yellow - } -} diff --git a/scripts/test-samples.ps1 b/scripts/test-samples.ps1 new file mode 100644 index 00000000..ff8657d1 --- /dev/null +++ b/scripts/test-samples.ps1 @@ -0,0 +1,95 @@ +<# +.SYNOPSIS +Local orchestrator to run sample & guide Pester tests. + +.DESCRIPTION +Discovers and runs test.Tests.ps1 for each sample (or a specified subset) +using Invoke-Pester. Each test validates the corresponding guide workflow +from scratch and verifies the existing sample code still builds. + +Requires Pester 5.x: Install-Module -Name Pester -Force -MinimumVersion 5.0 + +.PARAMETER Samples +One or more sample names to test. Defaults to all samples that have a test.Tests.ps1. + +.PARAMETER WinappPath +Path to the winapp npm package (.tgz or directory) passed to each test. + +.PARAMETER SkipCleanup +Passed through to each test — keep build artifacts for debugging. + +.EXAMPLE +.\scripts\test-samples.ps1 +Run all sample & guide tests. + +.EXAMPLE +.\scripts\test-samples.ps1 -Samples dotnet-app,rust-app +Run only the dotnet-app and rust-app tests. +#> + +[CmdletBinding()] +param( + [string[]]$Samples, + [string]$WinappPath, + [switch]$SkipCleanup +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' + +# Ensure Pester 5.x is available +$pester = Get-Module -Name Pester -ListAvailable | Where-Object { $_.Version.Major -ge 5 } | Select-Object -First 1 +if (-not $pester) { + Write-Error "Pester 5.x is required. Install with: Install-Module -Name Pester -Force -MinimumVersion 5.0" + exit 1 +} + +$samplesRoot = Join-Path $PSScriptRoot "..\samples" + +# Discover samples with test.Tests.ps1 +$allTests = @(Get-ChildItem -Path $samplesRoot -Directory | + Where-Object { Test-Path (Join-Path $_.FullName "test.Tests.ps1") } | + Select-Object -ExpandProperty Name) + +if ($Samples) { + # Support comma-separated values (e.g., -Samples "dotnet-app,rust-app") + $Samples = @($Samples | ForEach-Object { $_ -split ',' } | ForEach-Object { $_.Trim() } | Where-Object { $_ }) + foreach ($s in $Samples) { + if ($s -notin $allTests) { + Write-Warning "Sample '$s' does not have a test.Tests.ps1 — skipping" + } + } + $testList = @($Samples | Where-Object { $_ -in $allTests }) +}else { + $testList = $allTests +} + +if (-not $testList) { + Write-Host "No sample tests to run." -ForegroundColor Yellow + exit 0 +} + +# Resolve WinappPath to absolute before passing to containers +if ($WinappPath) { + if (-not [System.IO.Path]::IsPathRooted($WinappPath)) { + $WinappPath = (Resolve-Path $WinappPath -ErrorAction Stop).Path + } +} + +# Build Pester containers for each sample +$containers = @() +foreach ($sample in $testList) { + $testFile = Join-Path $samplesRoot $sample "test.Tests.ps1" + $data = @{} + if ($WinappPath) { $data['WinappPath'] = $WinappPath } + if ($SkipCleanup) { $data['SkipCleanup'] = $true } + $containers += New-PesterContainer -Path $testFile -Data $data +} + +# Configure and run Pester +$config = New-PesterConfiguration +$config.Run.Container = $containers +$config.Run.Exit = $true +$config.Output.Verbosity = if ($VerbosePreference -eq 'Continue') { 'Detailed' } else { 'Normal' } + +Invoke-Pester -Configuration $config diff --git a/src/winapp-CLI/WinApp.Cli/Services/NugetService.cs b/src/winapp-CLI/WinApp.Cli/Services/NugetService.cs index c7ed8c29..5e95d0fa 100644 --- a/src/winapp-CLI/WinApp.Cli/Services/NugetService.cs +++ b/src/winapp-CLI/WinApp.Cli/Services/NugetService.cs @@ -165,9 +165,11 @@ private async Task ResolveDependenciesAsync(DirectoryInfo packageDir, string pac } } } - catch + catch (Exception ex) { - // Dependency resolution failures are non-fatal; the main package is installed + // Dependency resolution failures are non-fatal; the main package is installed. + // Log so transitive dependency issues are visible in verbose/debug output. + taskContext.AddDebugMessage($"{UiSymbols.Note} Dependency resolution for {package} {version}: {ex.Message}"); } } diff --git a/src/winapp-CLI/WinApp.Cli/Services/PackageInstallationService.cs b/src/winapp-CLI/WinApp.Cli/Services/PackageInstallationService.cs index 3ec6188c..05bdd982 100644 --- a/src/winapp-CLI/WinApp.Cli/Services/PackageInstallationService.cs +++ b/src/winapp-CLI/WinApp.Cli/Services/PackageInstallationService.cs @@ -119,7 +119,7 @@ public async Task> InstallPackagesAsync( // Add the main package to installed versions allInstalledVersions[packageName] = version; - // Try to get package information about what else is installed with this package + // Resolve transitive dependencies and ensure they are also present on disk try { var cachedPackages = await nugetService.GetPackageDependenciesAsync(packageName, version, cancellationToken); @@ -128,16 +128,40 @@ public async Task> InstallPackagesAsync( var depVersion = NugetService.ParseMinimumVersion(packageVersion); if (!string.IsNullOrEmpty(depVersion)) { - if (allInstalledVersions.TryGetValue(packageId, out var existingVersion)) + // Check if the dependency actually exists on disk — if not, install it + var depDir = nugetService.GetNuGetPackageDir(packageId, depVersion); + if (!depDir.Exists) { - if (NugetService.CompareVersions(depVersion, existingVersion) > 0) + logger.LogDebug("Transitive dependency {PackageId} {Version} missing from cache, installing", packageId, depVersion); + var depInstalledVersions = await nugetService.InstallPackageAsync(packageId, depVersion, taskContext, cancellationToken); + foreach (var (depPkg, depVer) in depInstalledVersions) { - allInstalledVersions[packageId] = depVersion; + if (allInstalledVersions.TryGetValue(depPkg, out var existingDepVersion)) + { + if (NugetService.CompareVersions(depVer, existingDepVersion) > 0) + { + allInstalledVersions[depPkg] = depVer; + } + } + else + { + allInstalledVersions[depPkg] = depVer; + } } } else { - allInstalledVersions[packageId] = depVersion; + if (allInstalledVersions.TryGetValue(packageId, out var existingVersion)) + { + if (NugetService.CompareVersions(depVersion, existingVersion) > 0) + { + allInstalledVersions[packageId] = depVersion; + } + } + else + { + allInstalledVersions[packageId] = depVersion; + } } } } diff --git a/src/winapp-npm/addon-template/binding.gyp b/src/winapp-npm/addon-template/binding.gyp index abd73c32..6d611427 100644 --- a/src/winapp-npm/addon-template/binding.gyp +++ b/src/winapp-npm/addon-template/binding.gyp @@ -6,12 +6,12 @@ "include_dirs": [ "