mirror of
https://github.com/libretro/virtualjaguar-libretro.git
synced 2026-11-07 04:36:00 -10:00
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:
1 parent
efa7141796
commit
aaa20f8e9a
14 files changed
+605
No files matched your search
@@ -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
|
||||
@@ -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`.
|
||||
|
||||
@@ -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 |
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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"
|
||||
@@ -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
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user