Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .github/workflows/store-msix.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Microsoft Store MSIX

on:
pull_request:
paths:
- ".github/workflows/store-msix.yml"
- "packaging/msix/**"
- "scripts/**"
- "src-tauri/**"
- "src/**"
- "shaders/**"
- "package.json"
- "bun.lock"
push:
branches:
- master
paths:
- ".github/workflows/store-msix.yml"
- "packaging/msix/**"
- "scripts/**"
- "src-tauri/**"
- "src/**"
- "shaders/**"
- "package.json"
- "bun.lock"
workflow_dispatch:

permissions:
contents: read

jobs:
build-store-msix:
runs-on: windows-latest
steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Bun
uses: oven-sh/setup-bun@v2
with:
bun-version: 1.3.10

- name: Set up Rust
uses: dtolnay/rust-toolchain@stable

- name: Cache Rust build
uses: Swatinem/rust-cache@v2
with:
workspaces: "src-tauri -> target"

- name: Install JavaScript dependencies
shell: pwsh
run: bun install --frozen-lockfile

- name: Build unsigned Store MSIX
shell: pwsh
run: ./scripts/build-msix.ps1

- name: Upload Store package
uses: actions/upload-artifact@v4
with:
name: PLAY-Microsoft-Store-MSIX
path: artifacts/msix/*.msix
if-no-files-found: error
retention-days: 30
24 changes: 24 additions & 0 deletions docs/releasing.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,27 @@
8. Write curated release notes with Highlights, Fixes, Engineering, and Downloads sections as applicable.

Release notes should explain user impact. Commit messages and generated diffs are supporting material, not substitutes for release notes.

## Microsoft Store MSIX

PLAY. has a separate MSIX packaging path for Microsoft Store distribution. It does not replace the NSIS/GitHub release flow.

Partner Center identity used by the package:

- Package/Identity/Name: `DevJamStudio.622382E8743A9`
- Package/Identity/Publisher: `CN=300C6CAC-B727-40B2-8FF0-4C00EBAF31D9`
- Package/Properties/PublisherDisplayName: `Dev Jam Studio`
- Store ID: `9NXXT91JCTHX`

The Store package version is derived from PLAY.'s semantic version by adding one to the semantic major and reserving the fourth component for Store use. For example, PLAY. `0.16.0` produces package version `1.16.0.0`, and PLAY. `1.0.0` produces `2.0.0.0`.

Build locally on Windows with the Windows SDK installed:

```powershell
bun install --frozen-lockfile
bun run release:store
```

The unsigned package is written to `artifacts/msix/`. The build stages the Tauri executable, bundled libmpv runtime and licenses, shaders, generated Store logo assets, file associations, and the Partner Center identity into the MSIX, then unpacks the result to verify the required files are present.

For CI, run the `Microsoft Store MSIX` workflow manually or use the artifact produced by its pull request validation run. Upload the resulting `.msix` to the Packages section of the Microsoft Store submission. The Store signs the package after certification; no private signing certificate is required for the Store submission path.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"tauri:build": "tauri build",
"setup-lib": "bun ./scripts/setup-lib.mjs && bun ./scripts/setup-third-party-notices.mjs",
"release:win": "bun run setup-lib && tauri build --bundles nsis",
"release:store": "powershell -NoProfile -ExecutionPolicy Bypass -File ./scripts/build-msix.ps1",
"release:artifacts": "powershell -NoProfile -ExecutionPolicy Bypass -File ./scripts/release-artifacts.ps1",
"release": "bun run release:win && bun run release:artifacts"
},
Expand Down
86 changes: 86 additions & 0 deletions packaging/msix/AppxManifest.xml.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
<?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:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap uap3 rescap">
<Identity
Name="DevJamStudio.622382E8743A9"
Publisher="CN=300C6CAC-B727-40B2-8FF0-4C00EBAF31D9"
Version="__PACKAGE_VERSION__"
ProcessorArchitecture="x64" />

<Properties>
<DisplayName>PLAY.</DisplayName>
<PublisherDisplayName>Dev Jam Studio</PublisherDisplayName>
<Description>Premium video player for Windows.</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="PLAY"
Executable="playdot-player.exe"
EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements
DisplayName="PLAY."
Description="Premium video player for Windows."
BackgroundColor="#000000"
Square44x44Logo="Assets\Square44x44Logo.png"
Square150x150Logo="Assets\Square150x150Logo.png" />
<Extensions>
<uap:Extension Category="windows.fileTypeAssociation">
<uap3:FileTypeAssociation
Name="playvideo"
Parameters="&quot;%1&quot;"
MultiSelectModel="Player">
<uap:SupportedFileTypes>
<uap:FileType>.3g2</uap:FileType>
<uap:FileType>.3gp</uap:FileType>
<uap:FileType>.asf</uap:FileType>
<uap:FileType>.avi</uap:FileType>
<uap:FileType>.drc</uap:FileType>
<uap:FileType>.f4v</uap:FileType>
<uap:FileType>.flv</uap:FileType>
<uap:FileType>.m2ts</uap:FileType>
<uap:FileType>.m2v</uap:FileType>
<uap:FileType>.m4v</uap:FileType>
<uap:FileType>.mkv</uap:FileType>
<uap:FileType>.mov</uap:FileType>
<uap:FileType>.mp4</uap:FileType>
<uap:FileType>.mpeg</uap:FileType>
<uap:FileType>.mpg</uap:FileType>
<uap:FileType>.mxf</uap:FileType>
<uap:FileType>.mts</uap:FileType>
<uap:FileType>.ogm</uap:FileType>
<uap:FileType>.ogv</uap:FileType>
<uap:FileType>.qt</uap:FileType>
<uap:FileType>.rm</uap:FileType>
<uap:FileType>.rmvb</uap:FileType>
<uap:FileType>.ts</uap:FileType>
<uap:FileType>.vob</uap:FileType>
<uap:FileType>.webm</uap:FileType>
<uap:FileType>.wmv</uap:FileType>
</uap:SupportedFileTypes>
</uap3:FileTypeAssociation>
</uap:Extension>
</Extensions>
</Application>
</Applications>

<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>
208 changes: 208 additions & 0 deletions scripts/build-msix.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
[CmdletBinding()]
param(
[string]$Version,
[string]$OutputDirectory = (Join-Path $PSScriptRoot '..\artifacts\msix'),
[switch]$SkipBuild
)

$ErrorActionPreference = 'Stop'
$PSNativeCommandUseErrorActionPreference = $true

$projectRoot = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)
Set-Location $projectRoot

$identityName = 'DevJamStudio.622382E8743A9'
$publisher = 'CN=300C6CAC-B727-40B2-8FF0-4C00EBAF31D9'
$publisherDisplayName = 'Dev Jam Studio'

$tauriConfigPath = Join-Path $projectRoot 'src-tauri\tauri.conf.json'
$tauriConfig = Get-Content -LiteralPath $tauriConfigPath -Raw | ConvertFrom-Json
$appVersion = [string]$tauriConfig.version

if ([string]::IsNullOrWhiteSpace($Version)) {
$Version = $appVersion
} elseif ($Version -ne $appVersion) {
throw "Requested app version '$Version' does not match tauri.conf.json version '$appVersion'."
}

if ($Version -notmatch '^(\d+)\.(\d+)\.(\d+)$') {
throw "Version '$Version' must contain exactly three numeric components, such as 0.16.0."
}

$appMajor = [int]$Matches[1]
$appMinor = [int]$Matches[2]
$appPatch = [int]$Matches[3]
$packageMajor = $appMajor + 1

foreach ($component in @($packageMajor, $appMinor, $appPatch)) {
if ($component -lt 0 -or $component -gt 65535) {
throw "Version '$Version' cannot be mapped to a valid Microsoft Store package version."
}
}

# The Store requires a four-part package version whose first component is non-zero
# and reserves the fourth component. Offset the semantic major by one so pre-1.0
# PLAY. versions remain monotonic: 0.16.0 -> 1.16.0.0, 1.0.0 -> 2.0.0.0.
$packageVersion = "$packageMajor.$appMinor.$appPatch.0"

if (-not $SkipBuild) {
& bun run setup-lib
if ($LASTEXITCODE -ne 0) {
throw "bun run setup-lib failed with exit code $LASTEXITCODE."
}

& bun run tauri build --no-bundle
if ($LASTEXITCODE -ne 0) {
throw "Tauri build failed with exit code $LASTEXITCODE."
}
}

$makeAppxCommand = Get-Command makeappx.exe -ErrorAction SilentlyContinue | Select-Object -First 1
if ($makeAppxCommand) {
$makeAppx = $makeAppxCommand.Source
} else {
$makeAppx = Get-ChildItem `
-Path (Join-Path ${env:ProgramFiles(x86)} 'Windows Kits\10\bin') `
-Filter makeappx.exe `
-Recurse `
-File `
-ErrorAction SilentlyContinue |
Where-Object { $_.Directory.Name -eq 'x64' } |
Sort-Object FullName |
Select-Object -Last 1 -ExpandProperty FullName
}

if (-not $makeAppx) {
throw 'makeappx.exe was not found in the installed Windows SDK.'
}

$binary = Join-Path $projectRoot 'src-tauri\target\release\playdot-player.exe'
$libDirectory = Join-Path $projectRoot 'src-tauri\lib'
$shaderDirectory = Join-Path $projectRoot 'shaders'
$icon = Join-Path $projectRoot 'src-tauri\icons\128x128@2x.png'
$manifestTemplate = Join-Path $projectRoot 'packaging\msix\AppxManifest.xml.in'

foreach ($requiredFile in @($binary, $icon, $manifestTemplate)) {
if (-not (Test-Path -LiteralPath $requiredFile -PathType Leaf)) {
throw "Required MSIX input is missing: $requiredFile"
}
}
foreach ($requiredDirectory in @($libDirectory, $shaderDirectory)) {
if (-not (Test-Path -LiteralPath $requiredDirectory -PathType Container)) {
throw "Required MSIX input directory is missing: $requiredDirectory"
}
}

$requiredRuntimeFiles = @(
(Join-Path $libDirectory 'libmpv-wrapper.dll'),
(Join-Path $libDirectory 'libmpv-2.dll'),
(Join-Path $shaderDirectory 'FSR.glsl'),
(Join-Path $shaderDirectory 'adaptive-luma-ultra.glsl')
)
foreach ($requiredRuntimeFile in $requiredRuntimeFiles) {
if (-not (Test-Path -LiteralPath $requiredRuntimeFile -PathType Leaf)) {
throw "Required packaged runtime file is missing: $requiredRuntimeFile"
}
}

if ([System.IO.Path]::IsPathRooted($OutputDirectory)) {
$outputDirectory = [System.IO.Path]::GetFullPath($OutputDirectory)
} else {
$outputDirectory = [System.IO.Path]::GetFullPath((Join-Path $projectRoot $OutputDirectory))
}
New-Item -ItemType Directory -Path $outputDirectory -Force | Out-Null
$packagePath = Join-Path $outputDirectory "PLAY_$Version`_x64.msix"

$stagingRoot = Join-Path ([System.IO.Path]::GetTempPath()) "playdot-msix-$([guid]::NewGuid().ToString('N'))"
$verificationRoot = Join-Path ([System.IO.Path]::GetTempPath()) "playdot-msix-verify-$([guid]::NewGuid().ToString('N'))"
$stagingAssets = Join-Path $stagingRoot 'Assets'
$stagingLib = Join-Path $stagingRoot 'lib'
$stagingShaders = Join-Path $stagingRoot 'shaders'

try {
New-Item -ItemType Directory -Path $stagingAssets, $stagingLib, $stagingShaders -Force | Out-Null

Copy-Item -LiteralPath $binary -Destination (Join-Path $stagingRoot 'playdot-player.exe') -Force
Copy-Item -Path (Join-Path $libDirectory '*') -Destination $stagingLib -Recurse -Force
Copy-Item -Path (Join-Path $shaderDirectory '*') -Destination $stagingShaders -Recurse -Force

Add-Type -AssemblyName System.Drawing
function Write-ScaledPng([string]$SourcePath, [string]$DestinationPath, [int]$Width, [int]$Height) {
$sourceImage = [System.Drawing.Image]::FromFile($SourcePath)
$bitmap = [System.Drawing.Bitmap]::new($Width, $Height)
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
try {
$graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
$graphics.PixelOffsetMode = [System.Drawing.Drawing2D.PixelOffsetMode]::HighQuality
$graphics.SmoothingMode = [System.Drawing.Drawing2D.SmoothingMode]::HighQuality
$graphics.DrawImage($sourceImage, 0, 0, $Width, $Height)
$bitmap.Save($DestinationPath, [System.Drawing.Imaging.ImageFormat]::Png)
} finally {
$graphics.Dispose()
$bitmap.Dispose()
$sourceImage.Dispose()
}
}

Write-ScaledPng $icon (Join-Path $stagingAssets 'StoreLogo.png') 50 50
Write-ScaledPng $icon (Join-Path $stagingAssets 'Square44x44Logo.png') 44 44
Write-ScaledPng $icon (Join-Path $stagingAssets 'Square150x150Logo.png') 150 150

$manifest = Get-Content -LiteralPath $manifestTemplate -Raw
$manifest = $manifest.Replace('__PACKAGE_VERSION__', $packageVersion)
if ($manifest.Contains('__PACKAGE_VERSION__')) {
throw 'The MSIX manifest package version token was not replaced.'
}

$manifestPath = Join-Path $stagingRoot 'AppxManifest.xml'
$utf8NoBom = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText($manifestPath, $manifest, $utf8NoBom)

[xml]$manifestXml = $manifest
$identity = $manifestXml.Package.Identity
if ($identity.Name -ne $identityName -or $identity.Publisher -ne $publisher) {
throw 'The generated manifest identity does not match the reserved Microsoft Store identity.'
}
if ($manifestXml.Package.Properties.PublisherDisplayName -ne $publisherDisplayName) {
throw 'The generated manifest publisher display name does not match Partner Center.'
}

if (Test-Path -LiteralPath $packagePath) {
Remove-Item -LiteralPath $packagePath -Force
}

& $makeAppx pack /v /h SHA256 /d $stagingRoot /p $packagePath /o
if ($LASTEXITCODE -ne 0) {
throw "makeappx.exe failed with exit code $LASTEXITCODE."
}

New-Item -ItemType Directory -Path $verificationRoot -Force | Out-Null
& $makeAppx unpack /p $packagePath /d $verificationRoot /o
if ($LASTEXITCODE -ne 0) {
throw "makeappx.exe could not unpack the generated package (exit code $LASTEXITCODE)."
}

foreach ($packagedFile in @(
'playdot-player.exe',
'lib\libmpv-wrapper.dll',
'lib\libmpv-2.dll',
'shaders\FSR.glsl',
'shaders\adaptive-luma-ultra.glsl',
'AppxManifest.xml'
)) {
$verifiedPath = Join-Path $verificationRoot $packagedFile
if (-not (Test-Path -LiteralPath $verifiedPath -PathType Leaf)) {
throw "Generated MSIX is missing required file: $packagedFile"
}
}

Write-Host "Unsigned Microsoft Store MSIX written to $packagePath" -ForegroundColor Green
Write-Host "PLAY. version: $Version"
Write-Host "Store package version: $packageVersion"
Write-Host "Identity: $identityName"
Write-Host "Publisher: $publisher"
Write-Host 'The Microsoft Store will sign the package after certification.'
} finally {
Remove-Item -LiteralPath $stagingRoot -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item -LiteralPath $verificationRoot -Recurse -Force -ErrorAction SilentlyContinue
}
Loading