|
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
|
} |