release/windows-microsoft-store
ps1 157 lines 6.53 KB
Raw
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 }