diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-11 17:47:42 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-11 17:51:51 +0200 |
| commit | edbff1768054afa80efda721cd1011b29c7fe355 (patch) | |
| tree | 4ae3d27877bd08e92cc0981b80bcc9502e82bff5 /packaging | |
| parent | d4935aa2a28fcbab8c3556e3532e53667092701e (diff) | |
| download | meshbay-edbff1768054afa80efda721cd1011b29c7fe355.tar.gz | |
feat(packaging): an MSIX target for Microsoft Store submission
Store certification of the NSIS "MSI/EXE" submission failed on three
checks (silent-install verification, Add/Remove Programs entry, bundleware
check) -- traced and reproduced live to one cause: SmartScreen blocks an
unsigned, internet-downloaded installer at the shell layer before
Microsoft's own unattended validation bot ever gets to run it. MSIX
sidesteps this class of failure entirely: submitted through the Store's
native pipeline, there is no browser-download-then-launch step for
SmartScreen to intercept, and Microsoft signs the package itself at
publish time -- free, and specific to this submission type (Trusted
Signing remains a paid service for the MSI/EXE path). Full plan and
findings: C:\Users\admin\devel\msix-installer.md (out of repo).
electron-builder.msix.yml carries the same bundle as Full (node runtime,
ffmpeg, both service scripts) -- an AppX/MSIX install never elevates, by
design, but that changes only *when* the two elevated operations can run,
not whether the daemon ships. No main.js changes were needed: the on-demand
elevation path for service-mode (winElevateServiceMode(), driven from the
Node page) already existed for a different reason and depends only on
service-mode.ps1 being present as an extraResource, true for any packaged
Windows target. identityName/publisher/publisherDisplayName are the real
values from Partner Center's app-identity reservation, not placeholders.
build-win-msix.ps1 points electron-builder at the system Windows 10 SDK
(auto-detected) instead of letting it download its own bundled copy --
that download's 7z extraction creates symlinks this target never uses and
fails without SeCreateSymbolicLinkPrivilege, reproduced on this machine.
build/appx/ carries the four tile images the AppX target requires
regardless of showNameOnTiles, generated once from the existing app icon
(see that directory's README) since the system-SDK redirect has no vendor
samples to fall back to. build/appx-extensions.xml declares
windows.startupTask by hand rather than via electron-builder's
addAutoLaunchExtension, which always targets the Electron shell -- this
points at the bundled node binary instead, matching what "starts at sign
in" already means for Full.
Verified live via a signed sideload install (self-signed test cert,
cleaned up after): the package installs and the app runs correctly. One
finding worth carrying forward -- the declared network capabilities
(internetClientServer, privateNetworkClientServer) do not create any
firewall exemption for this app, most likely because automatic
capability-based exemption is an AppContainer-sandbox property and this
app deliberately runs full-trust, outside any sandbox. Not a regression:
no install-time elevation was possible either way, so the cost is the same
one-time OS firewall prompt firewall.ps1's own header already documents as
its fallback today.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Diffstat (limited to 'packaging')
| -rw-r--r-- | packaging/win/build-win-msix.ps1 | 134 | ||||
| -rw-r--r-- | packaging/win/electron-builder.msix.yml | 113 |
2 files changed, 247 insertions, 0 deletions
diff --git a/packaging/win/build-win-msix.ps1 b/packaging/win/build-win-msix.ps1 new file mode 100644 index 0000000..9f7f667 --- /dev/null +++ b/packaging/win/build-win-msix.ps1 @@ -0,0 +1,134 @@ +<# +.SYNOPSIS + Build the Windows "MSIX" package: same feature set as Full (bundled node + + ffmpeg), packaged for Microsoft Store submission instead of NSIS. + +.DESCRIPTION + See C:\Users\admin\devel\msix-installer.md for the plan this implements. + Unlike Light, this target does NOT drop anything from Full's feature + set -- it exists because Store certification of the NSIS "MSI/EXE" + submission failed for a reason MSIX sidesteps entirely (an unsigned, + internet-downloaded installer never gets a chance to run under + Microsoft's own unattended validation bot; see msix-installer.md §2), + not because the bundled node/service-mode/autostart machinery needed + removing. The one real change is *when* the two elevated operations + (firewall rule, service-mode install) can happen: an AppX/MSIX install + never elevates, so neither can run at install time the way + build/installer.nsh's customInstall macro does. Both already have an + elevation path that does not depend on the installer at all -- + firewall.ps1's own per-first-use Windows prompt, and main.js's + winElevateServiceMode() ("the other door", already used by the Node + page today) -- so nothing in src/main.js needed to change for this + target to work. + + Steps 1-5 are identical to build-win.ps1 (build-win-common.ps1 for 1-4, + build-node-runtime.ps1 for 5 -- this target ships the same frozen daemon + Full does). Step 6 hands electron-builder a standalone config + (electron-builder.msix.yml) via --config, same reasoning as Light: the + two targets' extraResources and signing configuration must never merge. + + Output: packages/meshbay-client/dist-msix/MeshBay-<version>.appx + +.PARAMETER SkipFfmpeg + Passed through to build-node-runtime.ps1 -- skip bundling ffmpeg. Smaller, + streaming-less build for local iteration only. + +.PARAMETER NoElectronBump + Keep the pinned Electron instead of upgrading to the latest release. + +.PARAMETER SkipNodeRuntime + Reuse an existing packages/meshbay-client/node-runtime/ (faster iteration + on the electron-builder side). +#> +[CmdletBinding()] +param( + [switch]$SkipFfmpeg, + [switch]$NoElectronBump, + [switch]$SkipNodeRuntime +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest + +$WinDir = $PSScriptRoot +$Repo = (Resolve-Path (Join-Path $WinDir "..\..")).Path +$Client = Join-Path $Repo "packages\meshbay-client" +$Config = Join-Path $WinDir "electron-builder.msix.yml" + +. (Join-Path $WinDir "build-win-common.ps1") + +if (-not (Test-Path $Config)) { throw "missing $Config" } + +# electron-builder's AppX target downloads its own bundled Windows Kits tools +# (winCodeSign-*.7z) unless ELECTRON_BUILDER_WINDOWS_KITS_PATH already points +# at a real one. That archive's 7z extraction creates symlinks for binaries +# this target never uses (its macOS/Linux signing tools), and fails outright +# without SeCreateSymbolicLinkPrivilege -- which this machine's build shell +# does not hold. The Windows 10 SDK already installed here has everything +# actually needed (makeappx.exe, signtool.exe); point electron-builder at +# it instead of fighting that extraction. Only set if the caller hasn't +# already pointed it somewhere themselves. +if (-not $env:ELECTRON_BUILDER_WINDOWS_KITS_PATH) { + $kitsRoot = "C:\Program Files (x86)\Windows Kits\10\bin" + $kit = Get-ChildItem $kitsRoot -ErrorAction SilentlyContinue | + Where-Object { $_.PSIsContainer -and $_.Name -match '^\d+\.\d+\.\d+\.\d+$' ` + -and (Test-Path (Join-Path $_.FullName "x64\makeappx.exe")) } | + Sort-Object Name -Descending | Select-Object -First 1 + if (-not $kit) { + throw ("No Windows 10 SDK with makeappx.exe found under $kitsRoot -- " + + "install the Windows 10 SDK (the App Certification Kit / MSIX " + + "Packaging Tools component covers it), or set " + + "ELECTRON_BUILDER_WINDOWS_KITS_PATH yourself.") + } + $env:ELECTRON_BUILDER_WINDOWS_KITS_PATH = Join-Path $kit.FullName "x64" +} +Step "Windows Kits: $env:ELECTRON_BUILDER_WINDOWS_KITS_PATH" + +# --- 1. Node ------------------------------------------------------------- +Assert-NodeVersion + +Push-Location $Client +try { + # --- 2. deps ------------------------------------------------------ + Invoke-NpmCi + + # --- 3. Electron: build against the latest release -------------- + Invoke-ElectronBump -NoElectronBump:$NoElectronBump -WinDir $WinDir + + # --- 4. UI ----------------------------------------------------- + Invoke-SyncUi + + # --- 5. node runtime --------------------------------------- + $rtExe = Join-Path $Client "node-runtime\meshbay-node.exe" + if ($SkipNodeRuntime -and (Test-Path $rtExe)) { + Step "reusing existing node-runtime/" + } else { + Step "building the bundled node (PyInstaller)" + $rtArgs = @{} + if ($SkipFfmpeg) { $rtArgs["SkipFfmpeg"] = $true } + & (Join-Path $WinDir "build-node-runtime.ps1") @rtArgs + if ($LASTEXITCODE -ne 0) { throw "build-node-runtime.ps1 failed" } + } + + # --- 6. package --------------------------------------- + Step "electron-builder --win appx --config electron-builder.msix.yml" + & npx electron-builder --win appx --config $Config + if ($LASTEXITCODE -ne 0) { throw "electron-builder failed" } +} +finally { + Pop-Location +} + +$pkg = Get-ChildItem (Join-Path $Client "dist-msix") -Filter "*.appx" -ErrorAction SilentlyContinue | + Sort-Object LastWriteTime | Select-Object -Last 1 +Write-Host "" +if ($pkg) { + Write-Host "OK package: $($pkg.FullName)" -ForegroundColor Green + Write-Host (" ({0:N0} MB)" -f ($pkg.Length / 1MB)) + Write-Host " unsigned by design -- Microsoft signs it at publish time" -ForegroundColor DarkGray + Write-Host " (msix-installer.md 3)." -ForegroundColor DarkGray +} +else { + Write-Host "!! no *.appx found in $Client\dist-msix" -ForegroundColor Red + exit 1 +} diff --git a/packaging/win/electron-builder.msix.yml b/packaging/win/electron-builder.msix.yml new file mode 100644 index 0000000..4026d46 --- /dev/null +++ b/packaging/win/electron-builder.msix.yml @@ -0,0 +1,113 @@ +# Standalone electron-builder config for the "MSIX" Windows target: same +# feature set as Full (bundled node + ffmpeg), packaged for Microsoft Store +# submission instead of NSIS. See C:\Users\admin\devel\msix-installer.md for +# the plan this implements -- read that first, especially §4 (why the +# installer can carry none of installer.nsh's elevation logic: an AppX/MSIX +# install never elevates, by design) and §8 (what is still unverified here). +# +# Deliberately NOT layered onto package.json's `build` field, same reasoning +# as electron-builder.light.yml: --config reads ONLY this file, so nothing +# here can accidentally inherit or merge with Full's `nsis`/signing config. +# +# electron-builder's target key for this is `appx`, not `msix` -- this +# version (app-builder-lib's AppxTarget.js, checked against the installed +# ^26.15.3) has no separate `msix` target. It still produces a package +# Partner Center's MSIX upload flow accepts; the mismatch is a naming +# artifact of the tool, not a statement about which container format comes +# out. Re-check this against whatever version is installed if it is ever +# bumped -- do not assume the target name from memory. +# +# No CSC (code-signing cert) is configured for this target, and that is +# correct, not an oversight: per app-builder-lib's own +# windowsSignToolManager.js (computePublisherName), an AppX target built +# with no certificate configured is logged as "Windows Store only build" and +# left unsigned, with `publisher` written into the manifest as-is. Microsoft +# signs the package itself at publish time (msix-installer.md §3) -- signing +# it here first would be pointless work, not extra safety. + +appId: org.meshbay.client +productName: MeshBay + +directories: + # Full's own build lives in dist/, Light's in dist-light/ -- a third, + # separate output dir so no two targets ever race on or clobber each + # other's files. + output: dist-msix + +files: + - src/** + - ui/** + +win: + target: appx + icon: build/icon.ico + artifactName: "MeshBay-${version}.${ext}" + extraResources: + # Same bundle as Full (package.json's own build.win.extraResources) -- + # this target drops NSIS, not the node/ffmpeg runtime or the two service + # scripts. service-mode.ps1 in particular still works unmodified: it is + # invoked from main.js's winElevateServiceMode() on demand, from the + # running (unelevated) app, not from an installer step -- that path + # already existed before this target did (see "the other door" comment + # in src/main.js) and needs nothing new here beyond the file being + # present to find. + - from: node-runtime + to: node-runtime + filter: + - "**/*" + - from: ../../packaging/win/firewall.ps1 + to: firewall.ps1 + - from: ../../packaging/win/service.ps1 + to: service.ps1 + - from: ../../packaging/win/service-mode.ps1 + to: service-mode.ps1 + +appx: + # --- Real values, from Partner Center's "App identity" page (App + # management -> App identity), not chosen freely -- a mismatch here fails + # Store validation outright rather than warning. publisherDisplayName is + # "MeshBay" as Partner Center assigned it, NOT package.json's own author + # company name ("MeshBay Team") -- AppXOptions.d.ts's default (company + # name from app metadata) would silently pick the wrong one if this were + # left unset, so it must stay explicit even though it looks redundant + # next to `displayName`. + identityName: MeshBay.MeshBay + publisher: "CN=CE32BB0D-6B7C-4D3A-AA42-E259B778CAC9" + publisherDisplayName: MeshBay + applicationId: MeshBay + displayName: MeshBay + languages: + - en-US + - fr-FR + - es-ES + - pt-BR + - zh-CN + - ja-JP + - de-DE + - it-IT + - nl-NL + - pl-PL + # Both are the "common" (general, non-restricted) capability group per + # app-builder-lib's AppxCapabilities.js -- no special Store justification + # needed, unlike the `rescap`-namespaced restricted ones. Matches + # firewall.ps1's own rules, which are `-Profile Any` (private AND public + # network). Whether Windows Firewall actually auto-exempts a full-trust + # packaged app on the strength of these declarations -- eliminating the + # elevation firewall.ps1 exists for entirely -- is msix-installer.md §8's + # #1 open item: verify live before relying on it, the declaration alone + # only proves the manifest is well-formed. + capabilities: + - internetClientServer + - privateNetworkClientServer + # windows.startupTask, the MSIX-native equivalent of the NSIS "at sign in" + # mode's Startup-folder .vbs (main.js's WIN_STARTUP_VBS) -- but pointed at + # the node daemon, not the Electron shell, which is what `addAutoLaunchExtension: + # true` would do instead (it always targets the package's own main + # executable, per AppxTarget.js's `executable` macro -- there is no config + # switch to point it at a different bundled exe). build/appx-extensions.xml + # declares that extension by hand for exactly this reason. Whether + # Windows actually launches a *non-primary* bundled exe through this + # mechanism is the other open item in msix-installer.md §8 -- untested + # until sideloaded. + customExtensionsPath: build/appx-extensions.xml + showNameOnTiles: false |