| 1 | <# |
| 2 | .SYNOPSIS |
| 3 | Pack the built Windows app into an MSIX for Microsoft Store submission. |
| 4 | |
| 5 | .DESCRIPTION |
| 6 | Runs after `pnpm tauri build -c src-tauri/tauri.prod-windows-microsoftstore.conf.json`. |
| 7 | Tauri emits an .exe plus .msi/.exe installers; this assembles a full-trust |
| 8 | MSIX layout around the raw .exe and calls makeappx. |
| 9 | |
| 10 | The MSIX is the Store artifact. The .msi/.exe stay useful for direct |
| 11 | download, but a Store MSI/EXE submission gets no package identity, so |
| 12 | in-app purchase only works from this MSIX. |
| 13 | |
| 14 | Leave the package unsigned for Partner Center — the Store re-signs it with |
| 15 | the publisher certificate. Sign it locally only to sideload for testing. |
| 16 | |
| 17 | .PARAMETER IdentityName |
| 18 | Package/Identity/@Name from Partner Center (Product identity page), |
| 19 | e.g. "SplitfireAB.SitiAI". Defaults to $env:MSIX_IDENTITY_NAME. |
| 20 | |
| 21 | .PARAMETER Publisher |
| 22 | Package/Identity/@Publisher from Partner Center, the full subject string, |
| 23 | e.g. "CN=1234ABCD-....". Defaults to $env:MSIX_PUBLISHER. |
| 24 | |
| 25 | .PARAMETER Version |
| 26 | Four-part version. The Store requires the revision (fourth) part to be 0. |
| 27 | Defaults to the version in tauri.prod-windows-microsoftstore.conf.json. |
| 28 | |
| 29 | .PARAMETER ValidateOnly |
| 30 | Check the manifest against makeappx's schema using stub payload files, |
| 31 | without needing a real build. Manifest schema errors are otherwise only |
| 32 | discovered after the ~50-minute Rust build, so CI runs this first to fail |
| 33 | in seconds instead. |
| 34 | #> |
| 35 | [CmdletBinding()] |
| 36 | param( |
| 37 | [string]$IdentityName = $env:MSIX_IDENTITY_NAME, |
| 38 | [string]$Publisher = $env:MSIX_PUBLISHER, |
| 39 | [string]$Version, |
| 40 | [string]$Configuration = "release", |
| 41 | [switch]$ValidateOnly |
| 42 | ) |
| 43 | |
| 44 | $ErrorActionPreference = "Stop" |
| 45 | |
| 46 | $repoRoot = Split-Path -Parent $PSScriptRoot |
| 47 | $tauriDir = Join-Path $repoRoot "src-tauri" |
| 48 | $confPath = Join-Path $tauriDir "tauri.prod-windows-microsoftstore.conf.json" |
| 49 | $conf = Get-Content $confPath -Raw | ConvertFrom-Json |
| 50 | $productName = $conf.productName |
| 51 | |
| 52 | # Cargo names the raw binary in target/release/ after Cargo.toml's [package] |
| 53 | # name, NOT tauri.conf.json's productName — that friendly name only gets |
| 54 | # applied to the bundled .msi/.exe Tauri's bundler produces afterwards. Read |
| 55 | # it from Cargo.toml directly rather than hardcoding, since it's the actual |
| 56 | # source of truth for what cargo build emits. |
| 57 | $cargoTomlPath = Join-Path $tauriDir "Cargo.toml" |
| 58 | $cargoNameLine = Select-String -Path $cargoTomlPath -Pattern '^\s*name\s*=\s*"([^"]+)"' | Select-Object -First 1 |
| 59 | if (-not $cargoNameLine) { |
| 60 | throw "Could not find [package] name in $cargoTomlPath." |
| 61 | } |
| 62 | $cargoBinName = $cargoNameLine.Matches[0].Groups[1].Value |
| 63 | |
| 64 | if (-not $IdentityName) { |
| 65 | throw "IdentityName is required. Pass -IdentityName or set MSIX_IDENTITY_NAME. Find it in Partner Center under Product identity." |
| 66 | } |
| 67 | if (-not $Publisher) { |
| 68 | throw "Publisher is required. Pass -Publisher or set MSIX_PUBLISHER. It is the full 'CN=...' string from Partner Center, not the display name." |
| 69 | } |
| 70 | |
| 71 | if (-not $Version) { |
| 72 | # tauri.conf.json carries a three-part version; MSIX wants four parts with a |
| 73 | # trailing 0, since the Store reserves the revision part for itself. |
| 74 | $Version = "$($conf.version).0" |
| 75 | } |
| 76 | if ($Version -notmatch '^\d+\.\d+\.\d+\.0$') { |
| 77 | throw "Version must be four parts ending in 0 (got '$Version'). The Microsoft Store rejects packages whose revision part is non-zero." |
| 78 | } |
| 79 | |
| 80 | $targetDir = Join-Path $tauriDir "target\$Configuration" |
| 81 | $exePath = Join-Path $targetDir "$cargoBinName.exe" |
| 82 | if (-not $ValidateOnly -and -not (Test-Path $exePath)) { |
| 83 | throw "Built executable not found at $exePath. Run 'pnpm tauri build -c src-tauri/tauri.prod-windows-microsoftstore.conf.json' first." |
| 84 | } |
| 85 | |
| 86 | $layoutDir = Join-Path $targetDir "msix-layout" |
| 87 | $assetsDir = Join-Path $layoutDir "Assets" |
| 88 | if (Test-Path $layoutDir) { Remove-Item $layoutDir -Recurse -Force } |
| 89 | New-Item -ItemType Directory -Path $assetsDir -Force | Out-Null |
| 90 | |
| 91 | # Renamed to the friendly name here (not $cargoBinName) so it matches the |
| 92 | # Executable= attribute AppxManifest.xml already declares — the source |
| 93 | # filename and the packaged filename don't have to match. |
| 94 | if ($ValidateOnly) { |
| 95 | # A stub stands in for the real binary: makeappx validates the manifest |
| 96 | # against its schema regardless of what the payload files actually contain, |
| 97 | # which is the whole point of running this before the build. |
| 98 | Set-Content (Join-Path $layoutDir "$productName.exe") "stub" -Encoding ASCII |
| 99 | } else { |
| 100 | Copy-Item $exePath (Join-Path $layoutDir "$productName.exe") |
| 101 | } |
| 102 | |
| 103 | # Tauri's WebView2 install modes are installer-only concepts; an MSIX cannot |
| 104 | # run a bootstrapper, so the package relies on the Evergreen WebView2 runtime |
| 105 | # that ships with Windows 11 and is serviced onto Windows 10. Any WebView2 |
| 106 | # loader emitted next to the .exe still needs to travel with it. |
| 107 | Get-ChildItem $targetDir -Filter "WebView2Loader.dll" -ErrorAction SilentlyContinue | |
| 108 | ForEach-Object { Copy-Item $_.FullName $layoutDir } |
| 109 | |
| 110 | # Exactly the logos AppxManifest.xml references. |
| 111 | $logos = @( |
| 112 | "Square44x44Logo.png", |
| 113 | "Square71x71Logo.png", |
| 114 | "Square150x150Logo.png", |
| 115 | "StoreLogo.png" |
| 116 | ) |
| 117 | foreach ($logo in $logos) { |
| 118 | $source = Join-Path $tauriDir "icons\$logo" |
| 119 | if (-not (Test-Path $source)) { |
| 120 | throw "Missing icon $source. Regenerate the Windows icon set with 'pnpm tauri icon'." |
| 121 | } |
| 122 | Copy-Item $source (Join-Path $assetsDir $logo) |
| 123 | } |
| 124 | |
| 125 | $manifest = Get-Content (Join-Path $tauriDir "msix\AppxManifest.xml") -Raw |
| 126 | $manifest = $manifest.Replace("__IDENTITY_NAME__", $IdentityName) |
| 127 | $manifest = $manifest.Replace("__PUBLISHER__", $Publisher) |
| 128 | $manifest = $manifest.Replace("__VERSION__", $Version) |
| 129 | Set-Content (Join-Path $layoutDir "AppxManifest.xml") $manifest -Encoding UTF8 |
| 130 | |
| 131 | $makeappx = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin" -Recurse -Filter "makeappx.exe" -ErrorAction SilentlyContinue | |
| 132 | Where-Object { $_.FullName -match "x64" } | |
| 133 | Sort-Object FullName -Descending | |
| 134 | Select-Object -First 1 |
| 135 | if (-not $makeappx) { |
| 136 | throw "makeappx.exe not found. Install the Windows 10/11 SDK." |
| 137 | } |
| 138 | |
| 139 | $outputDir = Join-Path $targetDir "bundle\msix" |
| 140 | New-Item -ItemType Directory -Path $outputDir -Force | Out-Null |
| 141 | $msixPath = Join-Path $outputDir "$productName-$Version.msix" |
| 142 | if (Test-Path $msixPath) { Remove-Item $msixPath -Force } |
| 143 | |
| 144 | & $makeappx.FullName pack /d $layoutDir /p $msixPath /o |
| 145 | if ($LASTEXITCODE -ne 0) { |
| 146 | throw "makeappx pack failed with exit code $LASTEXITCODE" |
| 147 | } |
| 148 | |
| 149 | if ($ValidateOnly) { |
| 150 | # Throw the stub package away — it contains a 4-byte "exe" and would be |
| 151 | # actively dangerous to mistake for a real artifact later in the job. |
| 152 | Remove-Item $msixPath -Force |
| 153 | Remove-Item $layoutDir -Recurse -Force |
| 154 | Write-Host "AppxManifest.xml passed makeappx schema validation." |
| 155 | } else { |
| 156 | Write-Host "MSIX written to $msixPath" |
| 157 | } |