<# .SYNOPSIS Release build + packaging for Windows: a portable .exe and an .msi installer. .DESCRIPTION The Unix side is scripts/build.sh; this is the half that needs MSVC and WiX. libmpv is the one thing that makes a Windows build of this repo different from `cargo build`. libmpv2-sys emits `cargo:rustc-link-lib=mpv`, so the linker wants an import library called mpv.lib, and the process wants libmpv-2.dll at run time. Neither ships with mpv's Windows *player* build — they come from the separate "mpv-dev" archive: https://sourceforge.net/projects/mpv-player-windows/files/libmpv/ Unpack it somewhere and point this script at it with -MpvDev or $env:MPV_DEV_DIR. If it contains mpv.def but no mpv.lib, the import library is generated here with lib.exe. There is no static libmpv worth shipping, so "standalone .exe" means the exe plus libmpv-2.dll next to it — which is what the portable .zip contains. .EXAMPLE scripts\build.ps1 -MpvDev C:\mpv-dev scripts\build.ps1 -Artifacts msi -SkipBuild #> [CmdletBinding()] param( # Directory holding libmpv-2.dll plus mpv.lib or mpv.def. [string] $MpvDev = $env:MPV_DEV_DIR, # exe | zip | msi | all [ValidateSet('exe', 'zip', 'msi', 'all')] [string[]] $Artifacts = @('all'), # Reuse target\release from a previous run. [switch] $SkipBuild, # Rust target triple; the default is the host. [string] $Target = 'x86_64-pc-windows-msvc', # Authenticode signing, if you have a certificate. Both or neither. [string] $SignCertThumbprint, [string] $TimestampUrl = 'http://timestamp.digicert.com' ) $ErrorActionPreference = 'Stop' Set-StrictMode -Version Latest $Root = Split-Path -Parent $PSScriptRoot $Dist = Join-Path $Root 'dist' $OutDir = Join-Path $Dist 'windows' $Pkg = Join-Path $Root 'packaging\windows' $Version = (Select-String -Path (Join-Path $Root 'Cargo.toml') -Pattern '^version\s*=\s*"([^"]+)"' ` | Select-Object -First 1).Matches[0].Groups[1].Value function Log ($m) { Write-Host "==> $m" -ForegroundColor Magenta } function Warn ($m) { Write-Host "warning: $m" -ForegroundColor Yellow } function Die ($m) { Write-Host "error: $m" -ForegroundColor Red; exit 1 } if ($Artifacts -contains 'all') { $Artifacts = @('exe', 'zip', 'msi') } # ------------------------------------------------------------------ libmpv --- function Resolve-MpvDev { if (-not $MpvDev) { Die @" no libmpv development files. Download the mpv-dev archive from https://sourceforge.net/projects/mpv-player-windows/files/libmpv/ unpack it, and pass -MpvDev or set MPV_DEV_DIR. "@ } if (-not (Test-Path $MpvDev)) { Die "-MpvDev path does not exist: $MpvDev" } $dll = Get-ChildItem -Path $MpvDev -Filter 'libmpv-2.dll' -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 if (-not $dll) { Die "libmpv-2.dll not found under $MpvDev" } $dir = $dll.DirectoryName $lib = Join-Path $dir 'mpv.lib' if (-not (Test-Path $lib)) { $def = Join-Path $dir 'mpv.def' if (-not (Test-Path $def)) { Die "neither mpv.lib nor mpv.def in $dir — the archive is not an mpv-dev build" } Log 'generating mpv.lib from mpv.def' Ensure-MsvcTools # /NAME matters: without it the import library would reference mpv.dll, # and the file that actually ships is libmpv-2.dll. & lib.exe /def:"$def" /name:libmpv-2.dll /out:"$lib" /machine:x64 | Out-Null if ($LASTEXITCODE -ne 0) { Die 'lib.exe failed to build mpv.lib' } } [pscustomobject]@{ Dir = $dir; Dll = $dll.FullName; Lib = $lib } } # lib.exe and link.exe come from a Developer prompt. If this is a plain # PowerShell session, find the toolchain with vswhere and import its environment. function Ensure-MsvcTools { if (Get-Command lib.exe -ErrorAction SilentlyContinue) { return } $vswhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' if (-not (Test-Path $vswhere)) { Die 'lib.exe not on PATH and vswhere.exe not found — run from a Developer PowerShell' } $vsPath = & $vswhere -latest -products * ` -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath if (-not $vsPath) { Die 'no Visual Studio C++ toolchain found' } $devCmd = Join-Path $vsPath 'Common7\Tools\VsDevCmd.bat' Log 'importing the MSVC environment' cmd /c "`"$devCmd`" -arch=amd64 -host_arch=amd64 >nul && set" | ForEach-Object { if ($_ -match '^([^=]+)=(.*)$') { Set-Item -Path "env:$($Matches[1])" -Value $Matches[2] } } if (-not (Get-Command lib.exe -ErrorAction SilentlyContinue)) { Die 'still no lib.exe after importing VsDevCmd' } } # --------------------------------------------------------------------- build --- function Invoke-Build ($mpv) { if ($SkipBuild) { Log 'skipping cargo build'; return } # The dev-only path patch; say so plainly rather than deep in a resolve error. $manifest = Get-Content (Join-Path $Root 'Cargo.toml') -Raw if ($manifest -match '(?m)^\[patch\.crates-io\]' -and -not (Test-Path (Join-Path $Root '..\proton-sdk-rs'))) { Die 'Cargo.toml still carries [patch.crates-io] -> ../proton-sdk-rs, but that checkout is missing.' } # MSVC reads the library search path out of LIB. $env:LIB = "$($mpv.Dir);$env:LIB" Log "cargo build --release --target $Target" Push-Location $Root try { & cargo build --release --locked --target $Target --bin proton-stream --bin pstr if ($LASTEXITCODE -ne 0) { Die 'cargo build failed' } } finally { Pop-Location } } function Stage ($mpv) { Log 'staging into dist\windows' $rel = Join-Path $Root "target\$Target\release" foreach ($f in 'proton-stream.exe', 'pstr.exe') { if (-not (Test-Path (Join-Path $rel $f))) { Die "$f missing from $rel — drop -SkipBuild" } } New-Item -ItemType Directory -Force -Path $OutDir | Out-Null Copy-Item (Join-Path $rel 'proton-stream.exe') $OutDir -Force Copy-Item (Join-Path $rel 'pstr.exe') $OutDir -Force Copy-Item $mpv.Dll $OutDir -Force Copy-Item (Join-Path $Root 'README.md') $OutDir -Force if ($SignCertThumbprint) { foreach ($f in 'proton-stream.exe', 'pstr.exe') { Invoke-Sign (Join-Path $OutDir $f) } } } function Invoke-Sign ($path) { Log "signing $(Split-Path -Leaf $path)" $cert = Get-ChildItem Cert:\CurrentUser\My, Cert:\LocalMachine\My | Where-Object Thumbprint -eq $SignCertThumbprint | Select-Object -First 1 if (-not $cert) { Die "no certificate with thumbprint $SignCertThumbprint" } Set-AuthenticodeSignature -FilePath $path -Certificate $cert ` -TimestampServer $TimestampUrl -HashAlgorithm SHA256 | Out-Null } # ----------------------------------------------------------------- portable --- function New-Zip { $out = Join-Path $Dist "proton-stream-$Version-x86_64-windows.zip" Log "zip -> $(Split-Path -Leaf $out)" if (Test-Path $out) { Remove-Item $out } Compress-Archive -Path (Join-Path $OutDir '*') -DestinationPath $out } # ---------------------------------------------------------------------- msi --- function Ensure-Wix { if (Get-Command wix -ErrorAction SilentlyContinue) { return } if (-not (Get-Command dotnet -ErrorAction SilentlyContinue)) { Die 'the .msi needs the WiX toolset: install the .NET SDK, then `dotnet tool install --global wix`' } Log 'installing the WiX toolset' & dotnet tool install --global wix | Out-Null $env:PATH = "$env:USERPROFILE\.dotnet\tools;$env:PATH" if (-not (Get-Command wix -ErrorAction SilentlyContinue)) { Die 'wix still not on PATH after install' } } function New-Msi ($mpv) { Ensure-Wix # WixUI_InstallDir lives in the UI extension, which is per-installation state # rather than per-project; adding it twice is a no-op. & wix extension add -g WixToolset.UI.wixext 2>&1 | Out-Null # The RTF the license dialog shows. Prefer the repository's own LICENSE, so # the installer cannot drift from it; fall back to the checked-in copy. $rtf = Join-Path $Pkg 'license.rtf' $repoLicense = Join-Path $Root 'LICENSE' if (Test-Path $repoLicense) { $rtf = Join-Path $Dist 'license.rtf' # RTF escapes are backslash, brace-open and brace-close; paragraphs are # \par. Done per line, because a lookbehind over already-rewritten line # endings is a good way to emit \par twice. $body = ((Get-Content $repoLicense) | ForEach-Object { ($_ -replace '\\', '\\') -replace '([{}])', '\$1' }) -join "\par`r`n" @" {\rtf1\ansi\ansicpg1252\deff0\nouicompat{\fonttbl{\f0\fnil\fcharset0 Segoe UI;}} \viewkind4\uc1\pard\f0\fs20 $body } "@ | Set-Content -Path $rtf -Encoding ASCII } else { Warn 'no LICENSE in the repo root; the installer shows packaging\windows\license.rtf' } $out = Join-Path $Dist "proton-stream-$Version-x64.msi" Log "msi -> $(Split-Path -Leaf $out)" # Every -d value is built into a string first: PowerShell does not evaluate a # parenthesised expression glued to the middle of a bare argument token, so # `-d MpvDll=(Join-Path ...)` would be passed through literally. $wxs = Join-Path $Pkg 'proton-stream.wxs' $mpvDll = Join-Path $OutDir (Split-Path -Leaf $mpv.Dll) $icon = Join-Path $Pkg 'proton-stream.ico' $readme = Join-Path $Root 'README.md' $wixArgs = @( 'build', '-arch', 'x64', '-ext', 'WixToolset.UI.wixext', $wxs, '-d', "Version=$Version", '-d', "BinDir=$OutDir", '-d', "MpvDll=$mpvDll", '-d', "IconFile=$icon", '-d', "LicenseRtf=$rtf", '-d', "ReadMe=$readme", '-o', $out ) & wix @wixArgs if ($LASTEXITCODE -ne 0) { Die 'wix build failed' } if ($SignCertThumbprint) { Invoke-Sign $out } } # --------------------------------------------------------------------- main --- New-Item -ItemType Directory -Force -Path $Dist | Out-Null $mpv = Resolve-MpvDev Invoke-Build $mpv Stage $mpv foreach ($a in $Artifacts) { switch ($a) { 'exe' { } # Stage already produced dist\windows\proton-stream.exe 'zip' { New-Zip } 'msi' { New-Msi $mpv } } } Log 'done — dist\' Get-ChildItem $Dist -File | ForEach-Object { ' {0,10:N0} KB {1}' -f ($_.Length / 1KB), $_.Name }