feat(app): prepare Windows Store MSIX packaging and listing (#1111)

* feat(app): prepare Microsoft Store MSIX packages and listing draft

* test(windows): qualify installed MSIX and capture Store screenshots

* fix(windows): resolve qualification graphics from MSYS2 output

* test(windows): retain installed app logs and reuse qualified builds

* fix(windows): read installed process exit status reliably

* test(windows): keep MSIX CI focused on installed runtime qualification

* fix(windows): harden Store SDK discovery and workflow triggers
This commit is contained in:
Joe Mattiello authored and GitHub committed 2026-10-31 22:41:37 -04:00
1 parent efa7141796
commit aaa20f8e9a
14 files changed
+605

No files matched your search

+119
View File
@@ -0,0 +1,119 @@
name: Windows Store MSIX
# Store-only artifacts stay in a SEPARATE run: release.yml flattens all artifacts
# from its own run. These unsigned submissions must never become website downloads.
on:
pull_request:
branches: [develop, master, 'release/**']
paths:
- 'Makefile'
- 'app/packaging/icons/**'
- 'app/packaging/check-payload.sh'
- 'test/roms/yarc.j64'
- 'app/packaging/windows/msix/**'
- 'app/src/platform/platform_windows.cpp'
- '.github/workflows/windows-store.yml'
workflow_dispatch:
inputs:
candidate_run:
description: 'Existing successful packaging run to qualify without rebuilding (optional)'
type: string
default: ''
permissions:
contents: read
actions: read
concurrency:
group: windows-store-${{ github.ref }}
cancel-in-progress: true
jobs:
msix:
if: github.event_name != 'workflow_dispatch' || inputs.candidate_run == ''
name: Store MSIX ${{ matrix.arch }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- arch: x64
os: windows-latest
msystem: MINGW64
packages: mingw-w64-x86_64-gcc mingw-w64-x86_64-cmake mingw-w64-x86_64-ninja
cmake_extra: ''
- arch: arm64
os: windows-11-arm
msystem: CLANGARM64
packages: mingw-w64-clang-aarch64-clang mingw-w64-clang-aarch64-cmake mingw-w64-clang-aarch64-ninja
cmake_extra: '-DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++'
steps:
- uses: actions/checkout@v7
- name: Test MSIX staging and input rejection
shell: pwsh
run: ./app/packaging/windows/msix/test-package.ps1
- name: Read numeric app version
shell: pwsh
run: |
$line = Get-Content Makefile | Where-Object { $_ -match '^CORE_BASE_VERSION\s*:=' }
if ($line -notmatch ':=\s*v([0-9]+\.[0-9]+\.[0-9]+)\s*$') { throw 'Expected numeric CORE_BASE_VERSION' }
"STORE_VERSION=$($Matches[1])" >> $env:GITHUB_ENV
- uses: msys2/setup-msys2@v2
with:
msystem: ${{ matrix.msystem }}
update: false
install: ${{ matrix.packages }}
- name: Build the existing desktop app and install its payload
shell: 'msys2 {0}'
run: |
set -eu
cmake -S app -B build-store -G Ninja -DCMAKE_BUILD_TYPE=Release \
-DVJ_APP_WIN32_GUI=ON -DVJ_APP_BUILD_TESTS=OFF \
-DVJ_APP_FETCH_LIBRASHADER=ON -DVJ_TV_DEFAULT=manual -DVJ_BUILD_CHANNEL=release \
${{ matrix.cmake_extra }}
cmake --build build-store -j4
cmake --install build-store --prefix "$PWD/stage-store"
stage-store/bin/virtualjaguar.exe --headless --frames 300 --expect-hash f4c30ea3 test/roms/yarc.j64
bash app/packaging/check-payload.sh build-store stage-store/bin virtualjaguar.exe
- name: Pack and validate Store MSIX with the Windows SDK
shell: pwsh
run: |
./app/packaging/windows/msix/package.ps1 -InstallPrefix stage-store `
-Version $env:STORE_VERSION -Architecture '${{ matrix.arch }}' -OutputDirectory store-out
"Source commit: $env:GITHUB_SHA" >> $env:GITHUB_STEP_SUMMARY
'Unsigned Store submission only. Installation, upgrade and GUI qualification still required.' >> $env:GITHUB_STEP_SUMMARY
Get-FileHash store-out/*.msix -Algorithm SHA256 | Format-Table | Out-String | Add-Content $env:GITHUB_STEP_SUMMARY
$env:GITHUB_SHA | Set-Content store-out/SOURCE_COMMIT.txt
- uses: actions/upload-artifact@v7
with:
name: store-msix-windows-${{ matrix.arch }}
path: |
store-out/*.msix
store-out/*.msix.json
store-out/SOURCE_COMMIT.txt
if-no-files-found: error
retention-days: 14
qualify-x64:
name: Store MSIX installed x64 smoke
needs: msix
if: always() && (needs.msix.result == 'success' || (github.event_name == 'workflow_dispatch' && inputs.candidate_run != ''))
runs-on: windows-latest
steps:
- uses: actions/checkout@v7
- uses: actions/download-artifact@v8
with:
name: store-msix-windows-x64
path: candidate
run-id: ${{ inputs.candidate_run || github.run_id }}
github-token: ${{ github.token }}
- name: Install a test-signed copy and check the installed emulator
shell: powershell
run: ./app/packaging/windows/msix/qualify.ps1 -CandidateDirectory candidate -OutputDirectory qualification
- uses: actions/upload-artifact@v7
if: always()
with:
name: store-msix-qualification-x64
path: qualification/
if-no-files-found: error
retention-days: 14
+8
View File
@@ -9,6 +9,14 @@ CI builds every installable package from one reusable workflow,
| `app.yml` | pull requests | artifacts plus one sticky PR comment linking them; signed and notarized like a release when the secrets exist (same-repo PRs), ad-hoc signed for fork PRs |
| manual dispatch | on demand | artifacts only; the `sign` input exercises the signing path |
## Microsoft Store (MSIX)
The separate Windows Store MSIX workflow prepares unsigned x64/ARM64 submission
packages for the reserved product, outside the website release artifact run.
See [`windows/msix/README.md`](windows/msix/README.md) for identity, build commands,
validation and remaining Windows install/upgrade qualification. Website ZIP/EXE
packages retain their existing delivery path.
## Assets
`X.Y.Z` is the tag without `v`; nightlies use `nightly`, PRs `prNNN`, the `release/**` lane `prerelease`.
+4
View File
@@ -10,6 +10,10 @@ for s in 16 32 48 64 128 256 512; do
done
magick png/virtualjaguar-16.png png/virtualjaguar-32.png png/virtualjaguar-48.png \
png/virtualjaguar-64.png png/virtualjaguar-128.png png/virtualjaguar-256.png virtualjaguar.ico
mkdir -p ../windows/msix/Assets
for entry in Square44x44Logo:44 Square150x150Logo:150 StoreLogo:50; do
rsvg-convert -w "${entry#*:}" -h "${entry#*:}" "$svg" -o "../windows/msix/Assets/${entry%:*}.png"
done
if command -v iconutil >/dev/null; then
set_dir=$(mktemp -d)/virtualjaguar.iconset
mkdir -p "$set_dir"
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<Package xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
<Identity Name="@NAME@" Publisher="@PUBLISHER@" Version="@VERSION@" ProcessorArchitecture="@ARCH@" />
<Properties>
<DisplayName>@DISPLAY_NAME@</DisplayName>
<PublisherDisplayName>@PUBLISHER_DISPLAY_NAME@</PublisherDisplayName>
<Description>Atari Jaguar and Jaguar CD emulator</Description>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Resources><Resource Language="en-us" /></Resources>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.19041.0" MaxVersionTested="10.0.26100.0" />
</Dependencies>
<Applications>
<Application Id="@APP_ID@" Executable="virtualjaguar.exe" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements DisplayName="@DISPLAY_NAME@" Description="Atari Jaguar and Jaguar CD emulator"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png" BackgroundColor="transparent" />
</Application>
</Applications>
<Capabilities><rescap:Capability Name="runFullTrust" /></Capabilities>
</Package>
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

+82
View File
@@ -0,0 +1,82 @@
# Microsoft Store MSIX
Store ID: `9PP6567FHBMS`. Exact assigned identity is in `identity.json`; Partner
Center currently spells the publisher **Joseph Mattello**, and the package name
**JosephMattello.VirtualJaguar**. These are public manifest values, not credentials.
Do not correct that spelling in a manifest without first reconciling Partner Center.
This packages the existing Win32 app as a full-trust desktop MSIX, Windows 10
2004+ / Windows 11, x64 or ARM64. It requires `runFullTrust` because the app uses
ordinary desktop file dialogs, OpenGL, controllers and optional network features.
The capability needs an explanation in Partner Center. It does not add DRM or
change emulation. The app retains Windows' assigned shell identity when packaged.
## Build
Use PowerShell **7+** and a Windows SDK containing MakeAppx. Build the app using
its existing Windows toolchain, then install it into a fresh prefix:
```powershell
cmake --install build --prefix stage-store
./app/packaging/windows/msix/package.ps1 -InstallPrefix stage-store `
-Version 3.10.0 -Architecture x64 -OutputDirectory store-out
```
The script validates the input PE machine and version (`major.minor.patch.0`,
major > 0, components <= 65535, final component zero), places the contents of `bin/` at the
package root and preserves all installed documentation, renders `AppxManifest.xml`, packs
with SDK validation enabled, unpacks and checks every staged file's SHA256.
The output includes an **unsigned** `.msix` and checksum/identity summary JSON.
A fresh output folder avoids silently replacing packages from another source.
`-MakeAppx C:\path\to\makeappx.exe` overrides SDK discovery.
`-StageOnly` works on macOS/Linux with PowerShell 7 and produces the inspectable
layout without pretending to produce a valid MSIX. Test staging with:
```powershell
./app/packaging/windows/msix/test-package.ps1
```
`portable.txt` is rejected: this package uses the app's per-user data path rather
than trying to write into read-only WindowsApps. Input/output trees must be
separate. Symlink/reparse-point payloads are rejected.
The separate **Windows Store MSIX** workflow builds both architectures from the
selected source ref and uploads `store-msix-windows-*` artifacts. Its run is
separate from `release.yml`, whose artifact collector publishes everything in
its own run. No workflow uploads to Partner Center, signs with production keys,
changes prices or publishes. Numeric versions are the same for PR/manual builds;
only upload a qualified release candidate, not every artifact.
The x64 qualification job installs a test-signed copy on a disposable runner and
runs the installed app's public-ROM hash check. It removes the test installation
and certificates and retains diagnostic logs. This does not qualify GUI graphics,
controller/audio behaviour or upgrade retention; those need an interactive Windows
machine with a supported OpenGL driver. A workflow dispatch can reuse an earlier
packaging run's artifacts via `candidate_run` without rebuilding the app.
## Qualification before submission
- Upload the qualified x64 and ARM64 packages to the existing **draft** product.
Microsoft signs Store distribution; unsigned files are not end-user installers.
- For local testing, sign a COPY with a test certificate matching the publisher
and trust it on an isolated Windows test machine; never upload that private key
or turn off signature enforcement. Do not distribute test-signed copies.
- Test install/Start-menu launch, pinned taskbar identity, controller/audio/video,
import paths containing spaces/non-ASCII, external folders and multi-file CDs.
- Test normal saves/config storage, upgrade retention, coexistence with website
editions and uninstall. MSIX filesystem virtualization may isolate data from
the unpackaged app; automatic migration is **not** claimed.
- This initial manifest does not declare Explorer file associations or execution
aliases. Import through File > Open, library folders or drag/drop; qualify shell
activation separately before adding associations.
- Store screenshots, age ratings, privacy declarations and capability rationale
must match the exact package. `listing-en-us.md` is draft copy, not certification.
The Store version's last component is reserved for Microsoft. Any future release
must increase the first three components; do not publish nightly/PR suffixes or
upload multiple different builds under an already published version.
References: [manual packaging](https://learn.microsoft.com/en-us/windows/msix/desktop/desktop-to-uwp-manual-conversion),
[version rules](https://learn.microsoft.com/en-us/windows/apps/publish/publish-your-app/package-version-numbering),
[signing](https://learn.microsoft.com/en-us/windows/msix/package/signing-package-overview).
+8
View File
@@ -0,0 +1,8 @@
{
"storeId": "9PP6567FHBMS",
"name": "JosephMattello.VirtualJaguar",
"publisher": "CN=65735C6B-702F-4938-A980-C84C590F6DD9",
"publisherDisplayName": "Joseph Mattello",
"displayName": "Virtual Jaguar",
"applicationId": "App"
}
@@ -0,0 +1,80 @@
# Windows Store listing draft — English (United States)
Product: **Virtual Jaguar**, Store ID `9PP6567FHBMS`.
Submission: `1152921505702250954` (draft; never submitted by the packaging workflow).
Planning price: **USD 3.99**, regional prices to review in Partner Center.
## Short description
Play your Atari Jaguar and Jaguar CD games with controller support, save states and a desktop game library.
## Description
Virtual Jaguar brings Atari Jaguar and Jaguar CD emulation to your Windows PC.
Organize your game collection in a visual library, connect a controller, and play
in a desktop window or a controller-friendly TV interface.
Save states let you return to a moment in a game. Configure your controls,
use the on-screen Jaguar keypad, and customize the picture with shader presets.
Optional RetroAchievements support connects compatible games to your account.
Features include cartridge and CD image loading, CHD support, playlists for
multi-disc games, per-game settings, controller remapping, screenshots, cheats,
and built-in debugging tools for people exploring Jaguar software.
You supply your own game files. No commercial games are included. Compatibility
and performance vary by game and PC; see the project website for compatibility
information and documentation. An internet connection is used for optional
online features such as achievements and artwork downloads.
The Microsoft Store edition is a paid way to support continued development and
receive Store-managed installation and updates. Free desktop downloads with the
same core features are also available from the project website.
Virtual Jaguar is an independent open-source project and is not affiliated with
or endorsed by Atari. Atari and Jaguar names belong to their respective owners.
## Feature bullets
- Atari Jaguar cartridge and Jaguar CD emulation
- Game library with favorites, recently played games and artwork
- Save states and per-game configuration
- Gamepad remapping and on-screen Jaguar keypad
- Controller-friendly TV interface
- Shader presets and optional RetroAchievements support
- CHD disc images and multi-disc playlists
- Debugging and inspection tools for Jaguar software
## Links
- Website: https://jaguar.provenance-emu.com/
- Support: https://github.com/libretro/virtualjaguar-libretro/issues
- Privacy: https://jaguar.provenance-emu.com/privacy.html
- Source/license: https://github.com/libretro/virtualjaguar-libretro
## Keywords (draft)
Jaguar; Atari Jaguar; Jaguar CD; emulator; retro; gamepad; save states
## Certification notes (draft; qualify before sending)
This is a native Win32 SDL3/OpenGL desktop emulator packaged as MSIX, not a PWA
or Xbox console app. The runFullTrust capability supports ordinary desktop game
file/folder selection, controllers, OpenGL and optional networking. There is no
installer, elevation request or app self-updater inside the package. App code
updates are delivered by Microsoft Store.
Use File > Open or the library's folder selection to import test content.
Keyboard/gamepad controls and current limitations are documented at the project
website. Supply a verified redistributable test ROM and exact steps with the
final candidate; do not give reviewers private ROM corpus paths. Do not claim
that Store installation or GUI qualification passed until recorded evidence exists.
## Remaining before certification
- Qualified Windows x64/ARM64 MSIX artifacts and install/upgrade/GUI evidence.
- Accurate category, privacy declarations, IARC questionnaire and capability review.
- Screenshots from the actual Windows candidate and required Store image sizes.
- Review support/privacy URLs and price/market settings; await tax/payout validation.
- Confirm the assigned publisher display-name spelling in Partner Center.
- Explicit maintainer release decision; this document is not authorization to publish.
+120
View File
@@ -0,0 +1,120 @@
# Build an unsigned Microsoft Store submission package from `cmake --install`.
# -StageOnly is cross-platform; packing requires the Windows SDK MakeAppx tool.
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$InstallPrefix,
[Parameter(Mandatory)][string]$Version,
[Parameter(Mandatory)][ValidateSet('x64', 'arm64')][string]$Architecture,
[Parameter(Mandatory)][string]$OutputDirectory,
[string]$MakeAppx,
[switch]$StageOnly
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
# Store versions use a numeric quad; the Store owns the final (zero) component.
if ($Version -notmatch '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(\.0)?$') {
throw 'Version must be major.minor.patch or major.minor.patch.0 (no suffix).'
}
$parts = $Version.Split('.')
if ($parts[0] -eq '0' -or @($parts | Where-Object { [decimal]$_ -gt 65535 }).Count) {
throw 'Version major must be positive and all components must be <= 65535.'
}
$packageVersion = ($parts[0..2] -join '.') + '.0'
$prefix = (Resolve-Path -LiteralPath $InstallPrefix).Path
$out = [IO.Path]::GetFullPath($OutputDirectory)
# Never overwrite/delete an input tree, including a caller passing an ancestor as output.
$sep = [IO.Path]::DirectorySeparatorChar
$prefixRoot = $prefix.TrimEnd($sep) + $sep
$outRoot = $out.TrimEnd($sep) + $sep
if ($outRoot.StartsWith($prefixRoot, [StringComparison]::OrdinalIgnoreCase) -or
$prefixRoot.StartsWith($outRoot, [StringComparison]::OrdinalIgnoreCase)) {
throw 'InstallPrefix and OutputDirectory must not overlap.'
}
$bin = Join-Path $prefix 'bin'
$docs = Join-Path $prefix 'share/doc/virtualjaguar'
$exe = Join-Path $bin 'virtualjaguar.exe'
foreach ($required in @($exe, (Join-Path $docs 'LICENSE.txt'), (Join-Path $docs 'THIRD-PARTY-NOTICES.txt'))) {
if (-not (Test-Path -LiteralPath $required -PathType Leaf)) { throw "Missing required input: $required" }
}
if (Test-Path -LiteralPath (Join-Path $bin 'portable.txt')) {
throw 'Store input must not contain portable.txt; use a CMake install tree, not a portable ZIP.'
}
foreach ($dir in @($bin, $docs)) {
if (@(Get-ChildItem -LiteralPath $dir -Recurse -Force | Where-Object {
$_.Attributes -band [IO.FileAttributes]::ReparsePoint
}).Count) { throw "Input contains a symlink/reparse point: $dir" }
}
# Check PE machine independently of the filename/toolchain label.
$bytes = [IO.File]::ReadAllBytes($exe)
if ($bytes.Length -lt 64 -or $bytes[0] -ne 0x4d -or $bytes[1] -ne 0x5a) { throw 'Input is not a PE executable.' }
$pe = [BitConverter]::ToInt32($bytes, 0x3c)
if ($pe -lt 0 -or $pe -gt $bytes.Length - 6 -or [BitConverter]::ToUInt32($bytes, $pe) -ne 0x00004550) {
throw 'Invalid PE header.'
}
$machine = [BitConverter]::ToUInt16($bytes, $pe + 4)
$expected = if ($Architecture -eq 'x64') { 0x8664 } else { 0xaa64 }
if ($machine -ne $expected) { throw "PE machine does not match $Architecture." }
$identity = Get-Content -LiteralPath (Join-Path $PSScriptRoot 'identity.json') -Raw | ConvertFrom-Json
$layout = Join-Path $out "layout-$Architecture"
$package = Join-Path $out "VirtualJaguar-$packageVersion-windows-$Architecture.msix"
if ((Test-Path -LiteralPath $layout) -or (Test-Path -LiteralPath $package)) {
throw 'Output already exists; choose a fresh OutputDirectory.'
}
$null = New-Item -ItemType Directory -Path $layout -Force
Get-ChildItem -LiteralPath $bin -Force | Copy-Item -Destination $layout -Recurse -Force
Copy-Item -LiteralPath $docs -Destination (Join-Path $layout 'docs') -Recurse
Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'Assets') -Destination $layout -Recurse
$manifest = Get-Content -LiteralPath (Join-Path $PSScriptRoot 'AppxManifest.xml.in') -Raw
$values = @{
NAME = $identity.name; PUBLISHER = $identity.publisher; VERSION = $packageVersion
ARCH = $Architecture; DISPLAY_NAME = $identity.displayName
PUBLISHER_DISPLAY_NAME = $identity.publisherDisplayName; APP_ID = $identity.applicationId
}
foreach ($key in $values.Keys) {
$manifest = $manifest.Replace("@$key@", [Security.SecurityElement]::Escape([string]$values[$key]))
}
if ($manifest -match '@[A-Z_]+@') { throw 'Unexpanded manifest placeholder.' }
$manifestPath = Join-Path $layout 'AppxManifest.xml'
[IO.File]::WriteAllText($manifestPath, $manifest, [Text.UTF8Encoding]::new($false))
[xml]$xml = $manifest
# A manifest edit must not accidentally point to missing resources.
foreach ($file in @($xml.Package.Properties.Logo,
$xml.Package.Applications.Application.Executable,
$xml.Package.Applications.Application.VisualElements.Square150x150Logo,
$xml.Package.Applications.Application.VisualElements.Square44x44Logo)) {
if (-not (Test-Path -LiteralPath (Join-Path $layout ($file.Replace('\', '/'))) -PathType Leaf)) {
throw "Manifest references missing file: $file"
}
}
if ($StageOnly) { Write-Output $layout; return }
if (-not $IsWindows) { throw 'Packing requires Windows; use -StageOnly to inspect a layout on other hosts.' }
if (-not $MakeAppx) {
$sdk = Join-Path ${env:ProgramFiles(x86)} 'Windows Kits/10/bin'
$tools = @(Get-ChildItem -Path "$sdk/*/x64/makeappx.exe" -ErrorAction SilentlyContinue | Sort-Object { [version]$_.Directory.Parent.Name } -Descending)
if (-not $tools.Count) { throw 'Install the Windows SDK or pass -MakeAppx.' }
$MakeAppx = $tools[0].FullName
}
# Never use /nv: MakeAppx schema/content validation is part of the build gate.
& $MakeAppx pack /d $layout /p $package /o
if ($LASTEXITCODE -ne 0) { throw "MakeAppx pack failed ($LASTEXITCODE)." }
$unpacked = Join-Path $out "unpacked-$Architecture"
& $MakeAppx unpack /p $package /d $unpacked /o
if ($LASTEXITCODE -ne 0) { throw "MakeAppx unpack failed ($LASTEXITCODE)." }
foreach ($source in Get-ChildItem -LiteralPath $layout -File -Recurse) {
$relative = [IO.Path]::GetRelativePath($layout, $source.FullName)
$copy = Join-Path $unpacked $relative
if (-not (Test-Path -LiteralPath $copy) -or
(Get-FileHash -LiteralPath $copy).Hash -ne (Get-FileHash -LiteralPath $source.FullName).Hash) {
throw "MSIX round trip lost or changed $relative"
}
}
$metadata = [ordered]@{
storeId = $identity.storeId; identityName = $identity.name; publisher = $identity.publisher
version = $packageVersion; architecture = $Architecture
package = [IO.Path]::GetFileName($package); sha256 = (Get-FileHash -LiteralPath $package -Algorithm SHA256).Hash.ToLowerInvariant()
signed = $false; intendedDistribution = 'Microsoft Store submission only'
}
$metadata | ConvertTo-Json | Set-Content -LiteralPath "$package.json" -Encoding utf8
Write-Output "Unsigned Store submission package: $package"
+75
View File
@@ -0,0 +1,75 @@
# Run only on a disposable Windows CI machine. Never exports a private key.
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)][string]$CandidateDirectory,
[Parameter(Mandatory = $true)][string]$OutputDirectory
)
$ErrorActionPreference = 'Stop'
$candidate = (Resolve-Path $CandidateDirectory).Path
$out = (New-Item -ItemType Directory -Path $OutputDirectory -Force).FullName
Start-Transcript -Path (Join-Path $out 'qualification.log')
$identity = Get-Content "$PSScriptRoot/identity.json" -Raw | ConvertFrom-Json
$certificate = $null
$trusted = $null
$installed = $null
try {
if (Get-AppxPackage -Name $identity.name) { throw 'Refusing to replace an existing app installation' }
$metadata = Get-Content (Join-Path $candidate '*.msix.json') -Raw | ConvertFrom-Json
if ($metadata.architecture -ne 'x64') { throw 'This qualification job requires x64' }
$original = Join-Path $candidate $metadata.package
if ((Get-FileHash $original -Algorithm SHA256).Hash -ne $metadata.sha256) { throw 'Candidate checksum mismatch' }
$signedCopy = Join-Path $out 'test-only.msix'
Copy-Item $original $signedCopy
$certificate = New-SelfSignedCertificate -Type Custom -Subject $identity.publisher `
-KeyUsage DigitalSignature -FriendlyName 'Disposable Virtual Jaguar CI' `
-CertStoreLocation Cert:\CurrentUser\My -KeyExportPolicy NonExportable `
-TextExtension @('2.5.29.37={text}1.3.6.1.5.5.7.3.3', '2.5.29.19={text}')
$publicCertificate = Join-Path $out 'test-only.cer'
Export-Certificate -Cert $certificate -FilePath $publicCertificate | Out-Null
$trusted = Import-Certificate -FilePath $publicCertificate -CertStoreLocation Cert:\LocalMachine\TrustedPeople
$sdk = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\signtool.exe" -ErrorAction SilentlyContinue | Sort-Object { [version]$_.Directory.Parent.Name } -Descending | Select-Object -First 1
if (!$sdk) { throw 'Windows SDK SignTool missing' }
& $sdk.FullName sign /fd SHA256 /s My /sha1 $certificate.Thumbprint $signedCopy
if ($LASTEXITCODE -ne 0) { throw 'Test signing failed' }
Add-AppxPackage -Path $signedCopy
$installed = Get-AppxPackage -Name $identity.name
if (!$installed) { throw 'Package was not registered' }
$installed | Select-Object Name, Version, PackageFamilyName, InstallLocation | Format-List
$exe = Join-Path $installed.InstallLocation 'virtualjaguar.exe'
$rom = (Resolve-Path "$PSScriptRoot/../../../../test/roms/yarc.j64").Path
$script:invocationIndex = 0
function Run-Candidate([string[]]$Arguments) {
$script:invocationIndex++
$stdout = Join-Path $out "run-$script:invocationIndex.stdout.log"
$stderr = Join-Path $out "run-$script:invocationIndex.stderr.log"
$start = New-Object System.Diagnostics.ProcessStartInfo
$start.FileName = $exe
$start.Arguments = $Arguments -join ' '
$start.UseShellExecute = $false
$start.RedirectStandardOutput = $true
$start.RedirectStandardError = $true
$process = New-Object System.Diagnostics.Process
$process.StartInfo = $start
if (!$process.Start()) { throw 'Could not start installed app' }
$stdoutTask = $process.StandardOutput.ReadToEndAsync()
$stderrTask = $process.StandardError.ReadToEndAsync()
if (!$process.WaitForExit(90000)) { $process.Kill(); throw 'Installed app timed out' }
$process.WaitForExit()
$stdoutTask.GetAwaiter().GetResult() | Set-Content $stdout
$stderrTask.GetAwaiter().GetResult() | Set-Content $stderr
Get-Content $stdout, $stderr | Write-Output
$exitCode = $process.ExitCode
$process.Dispose()
if ($exitCode -ne 0) { throw "Installed app exited $exitCode" }
}
Run-Candidate @('--headless', '--frames', '300', '--expect-hash', 'f4c30ea3', "`"$rom`"")
if ((Get-FileHash $original -Algorithm SHA256).Hash -ne $metadata.sha256) { throw 'Original Store candidate changed' }
'PASS: test-signed installation and installed headless hash. Manual GUI/controller/audio/upgrade checks remain.' | Set-Content (Join-Path $out 'RESULT.txt')
} finally {
if ($installed) { Remove-AppxPackage -Package $installed.PackageFullName }
if ($trusted) { Remove-Item "Cert:\LocalMachine\TrustedPeople\$($trusted.Thumbprint)" }
if ($certificate) { Remove-Item "Cert:\CurrentUser\My\$($certificate.Thumbprint)" }
# Retain diagnostics only; never distribute test-signed packages/certs.
Remove-Item (Join-Path $out 'test-only.msix'), (Join-Path $out 'test-only.cer') -Force -ErrorAction SilentlyContinue
Stop-Transcript
}
@@ -0,0 +1,76 @@
# Hermetic staging tests. No Windows SDK, credentials or emulator build required.
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$root = Join-Path ([IO.Path]::GetTempPath()) ('vj-msix-test-' + [guid]::NewGuid())
$prefix = Join-Path $root 'install tree'
$bin = Join-Path $prefix 'bin'
$docs = Join-Path $prefix 'share/doc/virtualjaguar'
$null = New-Item -ItemType Directory -Path $bin, $docs, (Join-Path $bin 'cheats'), (Join-Path $bin 'shaders') -Force
$exe = [byte[]]::new(128)
$exe[0] = 0x4d; $exe[1] = 0x5a; $exe[0x3c] = 64
$exe[64] = 0x50; $exe[65] = 0x45; $exe[68] = 0x64; $exe[69] = 0x86
[IO.File]::WriteAllBytes((Join-Path $bin 'virtualjaguar.exe'), $exe)
Set-Content (Join-Path $bin 'cheats/example.cht') 'fixture cheat'
Set-Content (Join-Path $bin 'shaders/example.slang') 'fixture shader'
Set-Content (Join-Path $docs 'LICENSE.txt') 'fixture GPL notice'
Set-Content (Join-Path $docs 'THIRD-PARTY-NOTICES.txt') 'fixture notices'
Set-Content (Join-Path $docs 'extra-license.txt') 'retain extra notices'
$script:checks = 0
function Check([bool]$condition, [string]$message) {
if (-not $condition) { throw "FAIL: $message" }
$script:checks++
}
function Reject([string]$version, [string]$arch, [string]$output, [string]$reason) {
try {
& "$PSScriptRoot/package.ps1" -InstallPrefix $prefix -Version $version -Architecture $arch -OutputDirectory $output -StageOnly | Out-Null
} catch {
Check ($_.Exception.Message -like "*$reason*") "expected $reason, got $_"
return
}
throw "FAIL: accepted invalid input ($version, $arch, $reason)"
}
try {
$out = Join-Path $root 'valid output'
$layout = & "$PSScriptRoot/package.ps1" -InstallPrefix $prefix -Version '3.10.0' -Architecture x64 -OutputDirectory $out -StageOnly
[xml]$xml = Get-Content (Join-Path $layout 'AppxManifest.xml') -Raw
Check ($xml.Package.Identity.Name -eq 'JosephMattello.VirtualJaguar') 'reserved package identity'
Check ($xml.Package.Identity.Version -eq '3.10.0.0') 'Store numeric version'
Check ($xml.Package.Identity.Publisher -eq 'CN=65735C6B-702F-4938-A980-C84C590F6DD9') 'reserved publisher'
Check ($xml.Package.Identity.ProcessorArchitecture -eq 'x64') 'PE architecture'
Check ($xml.Package.Dependencies.TargetDeviceFamily.Name -eq 'Windows.Desktop') 'desktop only'
Check ($xml.Package.Capabilities.Capability.Name -eq 'runFullTrust') 'classic desktop capability'
foreach ($relative in @('virtualjaguar.exe', 'cheats/example.cht', 'shaders/example.slang', 'docs/LICENSE.txt',
'docs/THIRD-PARTY-NOTICES.txt', 'docs/extra-license.txt')) {
Check ((Get-FileHash (Join-Path $layout $relative)).Hash -eq
(Get-FileHash (Join-Path $(if ($relative.StartsWith('docs/')) { $docs } else { $bin }) ($relative -replace '^docs/', ''))).Hash) "payload retained: $relative"
}
Check (-not (Test-Path (Join-Path $layout 'portable.txt'))) 'non-portable package'
foreach ($entry in @(@('Square44x44Logo.png',44), @('Square150x150Logo.png',150), @('StoreLogo.png',50))) {
$png = [IO.File]::ReadAllBytes((Join-Path $layout "Assets/$($entry[0])"))
$width = $png[16] * 16777216 + $png[17] * 65536 + $png[18] * 256 + $png[19]
$height = $png[20] * 16777216 + $png[21] * 65536 + $png[22] * 256 + $png[23]
Check ($width -eq $entry[1] -and $height -eq $entry[1]) "manifest PNG size: $($entry[0])"
}
foreach ($version in @('3.10.0-nightly','3.10.0.1','0.1.0','65536.1.0','03.1.0')) {
Reject $version x64 (Join-Path $root 'bad') 'Version'
}
Reject '3.10.0' arm64 (Join-Path $root 'bad') 'PE machine'
Reject '3.10.0' x64 $prefix 'must not overlap'
Reject '3.10.0' x64 $root 'must not overlap'
Reject '3.10.0' x64 $out 'Output already exists'
Check (Test-Path (Join-Path $layout 'virtualjaguar.exe')) 'existing output preserved'
Set-Content (Join-Path $bin 'portable.txt') 'portable fixture'
Reject '3.10.0' x64 (Join-Path $root 'bad') 'portable.txt'
Remove-Item -LiteralPath (Join-Path $bin 'portable.txt')
$exe[68] = 0x64; $exe[69] = 0xaa
[IO.File]::WriteAllBytes((Join-Path $bin 'virtualjaguar.exe'), $exe)
$arm = & "$PSScriptRoot/package.ps1" -InstallPrefix $prefix -Version '3.10.0.0' -Architecture arm64 -OutputDirectory (Join-Path $root 'arm') -StageOnly
[xml]$armXml = Get-Content (Join-Path $arm 'AppxManifest.xml') -Raw
Check ($armXml.Package.Identity.ProcessorArchitecture -eq 'arm64') 'ARM64 input and manifest agree'
[IO.File]::WriteAllBytes((Join-Path $bin 'virtualjaguar.exe'), [byte[]]::new(64))
Reject '3.10.0' x64 (Join-Path $root 'bad') 'not a PE'
Write-Output "PASS: $script:checks MSIX staging checks"
} finally {
# Only our unique temporary fixture directory, never a checkout/input tree.
Remove-Item -LiteralPath $root -Recurse -Force
}
+8
View File
@@ -47,6 +47,14 @@ void initNetworking() {
}
void beforeWindow() {
// MSIX already supplies the package-family!application shell identity. An explicit
// unpackaged ID here would detach the running window from its Store Start-menu pin.
typedef LONG(WINAPI * GetPackageNameFn)(UINT32*, PWSTR);
HMODULE kernel = GetModuleHandleW(L"kernel32.dll");
GetPackageNameFn packageName = kernel ? (GetPackageNameFn)(void*)GetProcAddress(kernel, "GetCurrentPackageFullName") : nullptr;
UINT32 length = 0;
if (packageName && packageName(&length, nullptr) == ERROR_INSUFFICIENT_BUFFER) return;
typedef HRESULT(WINAPI * SetAumidFn)(PCWSTR);
HMODULE shell = LoadLibraryW(L"shell32.dll");
if (!shell) return;