#Requires -Version 5.1 <# .SYNOPSIS Pack EvilFox.exe into per-arch MSIX (and optional .msixbundle) for Microsoft Store. .DESCRIPTION Builds Store assets, stages AppxManifest + EvilFox.exe per architecture, runs MakeAppx. Default output: dist\microsoft-store\EvilFox__.msix Prefer MSIX over PWA: EvilFox needs Win32/WebView2 VPN and system-proxy access. resource.syso note (arm64 cross-build): goversioninfo -64 emits an amd64 .syso. For GOARCH=arm64 builds, temporarily rename cmd/vpnapp/resource.syso aside, then restore. .EXAMPLE .\scripts\pack-msix.ps1 .\scripts\pack-msix.ps1 -Arch x64 -Version 4.2.8.0 .\scripts\pack-msix.ps1 -Arch arm64 -ExePath dist\navis-release\windows\arm64\EvilFox.exe .\scripts\pack-msix.ps1 -Arch all -OutDir dist\microsoft-store Store note: Identity Version MUST end with .0 (revision). Partner Center rejects 4.2.8.1. #> [CmdletBinding()] param( [ValidateSet("x64", "arm64", "all")] [string]$Arch = "all", [string]$ExePath = "", [string]$OutDir = "", [string]$Version = "4.2.8.0", [string]$Name = "EvilFox.evilfox", [string]$Publisher = "CN=9D58DFF7-7918-460E-845B-B3904A30562A", [string]$DisplayName = "EvilFox", [string]$PublisherDisplayName = "EvilFox", [string]$CertPath = "", [string]$CertPassword = "", [switch]$SkipAssets, [switch]$SkipBundle ) $ErrorActionPreference = "Stop" $Root = Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path) Set-Location $Root if (-not $OutDir) { $OutDir = Join-Path $Root "dist\microsoft-store" } if (-not [System.IO.Path]::IsPathRooted($OutDir)) { $OutDir = Join-Path $Root $OutDir } $OutDir = [System.IO.Path]::GetFullPath($OutDir) function Find-SdkTool([string]$name) { $candidates = @() $kits = Join-Path ${env:ProgramFiles(x86)} "Windows Kits\10\bin" if (Test-Path $kits) { Get-ChildItem -Path $kits -Directory -ErrorAction SilentlyContinue | Sort-Object Name -Descending | ForEach-Object { $candidates += (Join-Path $_.FullName "x64\$name") $candidates += (Join-Path $_.FullName "x86\$name") } } # MSIX Toolkit via winget (MakeAppx without full Windows SDK) $wingetPkgs = Join-Path $env:LOCALAPPDATA "Microsoft\WinGet\Packages" if (Test-Path $wingetPkgs) { Get-ChildItem -Path $wingetPkgs -Directory -Filter "Microsoft.MSIX-Toolkit*" -ErrorAction SilentlyContinue | ForEach-Object { $candidates += (Join-Path $_.FullName "MSIX-Toolkit.x64\$name") $candidates += (Join-Path $_.FullName "MSIX-Toolkit.x86\$name") Get-ChildItem -Path $_.FullName -Recurse -Filter $name -ErrorAction SilentlyContinue | ForEach-Object { $candidates += $_.FullName } } } foreach ($p in $candidates) { if (Test-Path -LiteralPath $p) { return $p } } $cmd = Get-Command $name -ErrorAction SilentlyContinue if ($cmd) { return $cmd.Source } return $null } function Resolve-ArchExe([string]$arch, [string]$explicit) { if ($explicit) { return $explicit } switch ($arch) { "x64" { return Join-Path $Root "dist\navis-release\windows\EvilFox.exe" } "arm64" { return Join-Path $Root "dist\navis-release\windows\arm64\EvilFox.exe" } default { throw "Unsupported arch: $arch" } } } function Find-WintunDll([string]$arch) { $candidates = @() if ($arch -eq "arm64") { $candidates += @( (Join-Path $Root "assets\wintun\arm64\wintun.dll"), (Join-Path $Root "dist\navis-release\windows\arm64\wintun.dll") ) } else { $candidates += @( (Join-Path $Root "assets\wintun\x64\wintun.dll"), (Join-Path $Root "assets\wintun\amd64\wintun.dll"), (Join-Path $Root "dist\navis-release\windows\wintun.dll") ) } $candidates += @( (Join-Path $Root "assets\wintun\$arch\wintun.dll"), (Join-Path $Root "assets\wintun\wintun.dll"), (Join-Path $Root "wintun.dll") ) foreach ($p in $candidates) { if (Test-Path -LiteralPath $p) { return $p } } return $null } function Write-StoreReadme( [string]$dir, [string]$version, [string[]]$stagedArchs, [string[]]$msixPaths, [string]$bundlePath, [string]$makeAppxPath, [hashtable]$makeAppxCmds, [bool]$missingWintun ) { $tplPath = Join-Path $Root "packaging\msix\STORE_README_TEMPLATE.txt" if (-not (Test-Path -LiteralPath $tplPath)) { throw "Missing template: $tplPath" } $text = [System.IO.File]::ReadAllText($tplPath, [System.Text.Encoding]::UTF8) $text = $text.Replace("{{VERSION}}", $version) $text = $text.Replace("{{GENERATED}}", (Get-Date -Format "yyyy-MM-dd HH:mm:ss")) $artifacts = New-Object System.Collections.Generic.List[string] if ($bundlePath -and (Test-Path -LiteralPath $bundlePath)) { $artifacts.Add("Bundle: $bundlePath") } else { $artifacts.Add("Bundle: (not created)") } foreach ($p in $msixPaths) { if (Test-Path -LiteralPath $p) { $artifacts.Add("MSIX: $p") } } foreach ($a in $stagedArchs) { $stg = Join-Path $Root ("packaging\msix\_staging_" + $a) $artifacts.Add("Staging ${a}: $stg") } $text = $text.Replace("{{ARTIFACTS}}", ($artifacts -join "`r`n")) $makeLines = New-Object System.Collections.Generic.List[string] if ($makeAppxPath) { $makeLines.Add("Found: $makeAppxPath") } else { $makeLines.Add("MakeAppx.exe NOT FOUND on this machine.") $makeLines.Add("Install Windows 10/11 SDK, then run:") foreach ($a in $stagedArchs) { if ($makeAppxCmds.ContainsKey($a)) { $makeLines.Add(" $($makeAppxCmds[$a])") } } if ($stagedArchs.Count -ge 2) { $bundleDir = Join-Path $dir "_bundle_input" $bundleOut = Join-Path $dir ("EvilFox_" + $version + ".msixbundle") $makeLines.Add(" makeappx bundle /d `"$bundleDir`" /p `"$bundleOut`" /o") $makeLines.Add(" (copy both .msix into _bundle_input first)") } } $text = $text.Replace("{{MAKEAPPX}}", ($makeLines -join "`r`n")) if ($missingWintun) { $wintun = @( "wintun.dll was NOT found under assets/ or dist/. Packaging continued.", "VPN (TUN) mode needs wintun.dll next to EvilFox.exe.", "Store users can still use proxy mode without it.", "Add arch-specific wintun.dll later under assets/wintun// if needed." ) -join "`r`n" } else { $wintun = "wintun.dll was included in staging where found for the target arch." } $text = $text.Replace("{{WINTUN}}", $wintun) $utf8 = New-Object System.Text.UTF8Encoding $false [System.IO.File]::WriteAllText((Join-Path $dir "README.txt"), $text, $utf8) } $MakeAppx = Find-SdkTool "makeappx.exe" New-Item -ItemType Directory -Force -Path $OutDir | Out-Null $archList = @() if ($Arch -eq "all") { $archList = @("x64", "arm64") } else { $archList = @($Arch) } # When -Arch all, prefer per-arch default exe paths (ignore -ExePath). $explicitExe = "" if ($Arch -ne "all") { $explicitExe = $ExePath } if (-not $SkipAssets) { Write-Host "Generating Store assets..." go run ./tools/mkstoreassets if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } $producedMsix = @() $stagedArchs = @() $makeAppxCmds = @{} $anyMissingExe = $false $missingWintun = $false $packFailed = $false $makeAppxMissing = -not [bool]$MakeAppx foreach ($a in $archList) { $exe = Resolve-ArchExe $a $explicitExe if (-not (Test-Path -LiteralPath $exe)) { Write-Host "SKIP ${a}: EvilFox.exe not found at '$exe'" -ForegroundColor Yellow $anyMissingExe = $true continue } $Staging = Join-Path $Root ("packaging\msix\_staging_" + $a) if (Test-Path $Staging) { Remove-Item -Recurse -Force $Staging } New-Item -ItemType Directory -Path $Staging | Out-Null New-Item -ItemType Directory -Path (Join-Path $Staging "Assets") | Out-Null $AssetsSrc = Join-Path $Root "packaging\msix\Assets" if (-not (Test-Path $AssetsSrc)) { throw "Store assets missing at $AssetsSrc (run without -SkipAssets)." } Copy-Item -Path (Join-Path $AssetsSrc "*") -Destination (Join-Path $Staging "Assets") -Force Copy-Item -LiteralPath $exe -Destination (Join-Path $Staging "EvilFox.exe") -Force $wintun = Find-WintunDll $a if ($wintun) { Copy-Item -LiteralPath $wintun -Destination (Join-Path $Staging "wintun.dll") -Force Write-Host "Included wintun.dll for $a from $wintun" } else { $missingWintun = $true Write-Host "No wintun.dll for $a (proxy mode still OK; TUN needs dll next to exe)." -ForegroundColor DarkYellow } $ManifestSrc = Join-Path $Root "packaging\msix\AppxManifest.xml" $ManifestDst = Join-Path $Staging "AppxManifest.xml" $manifestText = Get-Content -LiteralPath $ManifestSrc -Raw -Encoding UTF8 # Prefer exact placeholder swaps so we never touch . $manifestText = $manifestText.Replace('Name="EvilFox.evilfox"', "Name=`"$Name`"") $manifestText = $manifestText.Replace('Name="EvilFox.EvilFox"', "Name=`"$Name`"") $manifestText = $manifestText.Replace('Publisher="CN=9D58DFF7-7918-460E-845B-B3904A30562A"', "Publisher=`"$Publisher`"") $manifestText = $manifestText.Replace('Publisher="CN=EvilFox"', "Publisher=`"$Publisher`"") # Store requires Identity Version revision (4th component) to be 0. if ($Version -notmatch '^\d+\.\d+\.\d+\.0$') { Write-Host "WARNING: Partner Center rejects non-zero revision; use e.g. 4.2.8.0 (got '$Version')." -ForegroundColor Yellow } $manifestText = $manifestText.Replace('Version="4.2.8.0"', "Version=`"$Version`"") $manifestText = $manifestText.Replace('Version="4.2.8.1"', "Version=`"$Version`"") # Also accept older template version if someone reverts the manifest placeholder. $manifestText = $manifestText.Replace('Version="4.2.3.1"', "Version=`"$Version`"") $manifestText = $manifestText.Replace('ProcessorArchitecture="x64"', "ProcessorArchitecture=`"$a`"") $manifestText = $manifestText.Replace('ProcessorArchitecture="arm64"', "ProcessorArchitecture=`"$a`"") $manifestText = $manifestText.Replace('EvilFox', "$DisplayName") $manifestText = $manifestText.Replace('EvilFox', "$PublisherDisplayName") $manifestText = $manifestText.Replace('DisplayName="EvilFox"', "DisplayName=`"$DisplayName`"") [System.IO.File]::WriteAllText($ManifestDst, $manifestText, (New-Object System.Text.UTF8Encoding $false)) $MsixPath = Join-Path $OutDir ("EvilFox_" + $Version + "_" + $a + ".msix") $makeAppxCmds[$a] = ("makeappx pack /d `"{0}`" /p `"{1}`" /o" -f $Staging, $MsixPath) $stagedArchs += $a if ($makeAppxMissing) { Write-Host "Staged $a (MakeAppx missing): $Staging" -ForegroundColor Yellow continue } if (Test-Path $MsixPath) { Remove-Item -Force $MsixPath } Write-Host "Packing $MsixPath ..." & $MakeAppx pack /d $Staging /p $MsixPath /o if ($LASTEXITCODE -ne 0) { Write-Host "MakeAppx pack failed for $a (exit $LASTEXITCODE)" -ForegroundColor Red $packFailed = $true continue } if ($CertPath) { $SignTool = Find-SdkTool "signtool.exe" if (-not $SignTool) { throw "signtool.exe not found (needed for -CertPath)." } Write-Host "Signing $a with $CertPath ..." if ($CertPassword) { & $SignTool sign /fd SHA256 /a /f $CertPath /p $CertPassword $MsixPath } else { & $SignTool sign /fd SHA256 /a /f $CertPath $MsixPath } if ($LASTEXITCODE -ne 0) { Write-Host "Sign failed for $a" -ForegroundColor Red $packFailed = $true continue } } $producedMsix += $MsixPath Write-Host "Packed: $MsixPath" } $BundlePath = Join-Path $OutDir ("EvilFox_" + $Version + ".msixbundle") if (-not $SkipBundle -and -not $makeAppxMissing -and $producedMsix.Count -ge 2) { $bundleInput = Join-Path $OutDir "_bundle_input" if (Test-Path $bundleInput) { Remove-Item -Recurse -Force $bundleInput } New-Item -ItemType Directory -Path $bundleInput | Out-Null foreach ($p in $producedMsix) { Copy-Item -LiteralPath $p -Destination (Join-Path $bundleInput (Split-Path $p -Leaf)) -Force } if (Test-Path $BundlePath) { Remove-Item -Force $BundlePath } Write-Host "Bundling $BundlePath ..." & $MakeAppx bundle /d $bundleInput /p $BundlePath /o if ($LASTEXITCODE -ne 0) { Write-Host "makeappx bundle failed (exit $LASTEXITCODE)" -ForegroundColor Yellow $BundlePath = "" } else { Write-Host "Bundle: $BundlePath" } } elseif ($producedMsix.Count -eq 1) { $BundlePath = "" } Write-StoreReadme -dir $OutDir -version $Version -stagedArchs $stagedArchs ` -msixPaths $producedMsix -bundlePath $BundlePath -makeAppxPath $MakeAppx ` -makeAppxCmds $makeAppxCmds -missingWintun $missingWintun # Copy Partner Center listing inputs when present in the repo. $listingDir = Join-Path $OutDir "listing" $listingScreen = Join-Path $listingDir "screen" New-Item -ItemType Directory -Force -Path $listingScreen | Out-Null $storeCsv = Join-Path $Root "store.csv" if (Test-Path -LiteralPath $storeCsv) { Copy-Item -LiteralPath $storeCsv -Destination (Join-Path $listingDir "store.csv") -Force } $screenSrc = Join-Path $Root "screen" if (Test-Path -LiteralPath $screenSrc) { Copy-Item -Path (Join-Path $screenSrc "*") -Destination $listingScreen -Force } Write-Host "" Write-Host "Store folder: $OutDir" Write-Host "README: $(Join-Path $OutDir 'README.txt')" Write-Host "Staged: $($stagedArchs -join ', ')" if ($producedMsix.Count -gt 0) { Write-Host "MSIX: $($producedMsix -join ', ')" } # Exit non-zero only after staging both (when requested) if MakeAppx missing, # or if packing failed / required exes missing. if ($makeAppxMissing) { if ($Arch -eq "all" -and $stagedArchs.Count -lt 2) { Write-Host "MakeAppx missing and could not stage both arches." -ForegroundColor Red exit 2 } if ($stagedArchs.Count -eq 0) { Write-Host "Nothing staged." -ForegroundColor Red exit 2 } Write-Host "MakeAppx missing - staging complete; see README.txt for pack commands." -ForegroundColor Yellow exit 2 } if ($packFailed) { exit 1 } if ($anyMissingExe -and $Arch -eq "all" -and $producedMsix.Count -lt 2) { exit 1 } if ($producedMsix.Count -eq 0) { exit 1 } exit 0