fix windows tui packing error

This commit is contained in:
Boof2015
2026-08-17 20:44:19 -04:00
parent 0db46f7bb3
commit 325ec155c0
8 changed files with 363 additions and 24 deletions
+31
View File
@@ -267,6 +267,37 @@ jobs:
call "%VSINSTALL%\VC\Auxiliary\Build\vcvars64.bat"
cmake --build plugin/build --target PrismInstallerPlugins --config Release --parallel 4
- name: Stage plugins for Windows installer
if: runner.os == 'Windows'
shell: bash
run: |
rm -rf plugin/dist-installer
mkdir -p plugin/dist-installer/VST3
for art in PrismSpectrum PrismOscilloscope PrismVUMeter PrismLUFSMeter PrismVectorscope PrismSpectrogram PrismWaveform; do
vst3_dir="plugin/build/${art}_artefacts/Release/VST3"
[ -d "$vst3_dir" ] && find "$vst3_dir" -maxdepth 1 -name "*.vst3" -exec cp -R {} plugin/dist-installer/VST3/ \;
done
vst3_count="$(find plugin/dist-installer/VST3 -maxdepth 1 -name "*.vst3" | wc -l | tr -d ' ')"
if [ "$vst3_count" -ne 7 ]; then
echo "ERROR: expected 7 VST3 plugins, found $vst3_count"
exit 1
fi
- name: Stage Prism TUI for Windows installer
if: runner.os == 'Windows'
run: npm run stage:tui
- name: Build Windows NSIS smoke artifact
if: runner.os == 'Windows'
run: npx electron-builder --win nsis --publish never
env:
CSC_IDENTITY_AUTO_DISCOVERY: 'false'
- name: Smoke-test Windows installer PATH integration
if: runner.os == 'Windows'
shell: pwsh
run: ./scripts/test-windows-installer.ps1
- name: Show sccache stats
if: runner.os != 'Linux'
run: sccache --show-stats
+5
View File
@@ -281,6 +281,11 @@ jobs:
env:
CSC_IDENTITY_AUTO_DISCOVERY: 'false'
- name: Smoke-test Windows installer PATH integration
if: runner.os == 'Windows'
shell: pwsh
run: ./scripts/test-windows-installer.ps1
- name: Build Linux packages with VST3 plugins
if: runner.os == 'Linux'
shell: bash
+2
View File
@@ -114,6 +114,8 @@ Prebuilt versions of Prism for Windows, macOS, and Linux are available on the [R
Installable packages include the desktop application and the components supported by that package, including DAW plugins and `prism-tui` where applicable.
On Windows, the NSIS installer adds `prism-tui` to the machine `PATH`. Open a new terminal after installation so it inherits the updated environment. The portable Windows package does not modify `PATH`.
## Building from Source
**Prerequisites:** Node.js 18+, npm, CMake 3.22+, and a C++ compiler toolchain.
+1 -1
View File
@@ -40,7 +40,7 @@
"postinstall": "node scripts/build/postinstall-native.cjs",
"dist": "node scripts/build/dist-current.cjs",
"dist:mac": "npm run stage:tui && npm run build && electron-builder --mac --publish never",
"dist:win": "npm run stage:tui && npm run build && electron-builder --win --publish never",
"dist:win": "powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -File scripts/build/check-windows-dist-output.ps1 && npm run stage:tui && npm run build && electron-builder --win --publish never",
"dist:linux": "npm run dist:linux:appimage && npm run stage:tui && electron-builder --linux deb rpm tar.gz --publish never",
"dist:linux:appimage": "npm run build && electron-builder --config electron-builder.appimage.cjs --linux AppImage --publish never",
"dist:linux:packages": "npm run stage:tui && electron-builder --linux deb rpm tar.gz --publish never"
+25 -9
View File
@@ -61,10 +61,20 @@
Abort
${EndIf}
; Pass the path through the installer's process environment. Supplying it
; after PowerShell's -Command argument loses quotes at the native command
; line boundary and truncates the default path to "C:\Program".
System::Call 'Kernel32::SetEnvironmentVariable(t, t)i ("PRISM_TUI_INSTALL_DIR", "$INSTDIR\resources\tui").r0'
${If} $0 == 0
DetailPrint "ERROR: could not prepare the Prism TUI PATH update"
MessageBox MB_OK|MB_ICONSTOP "Prism could not prepare the prism-tui PATH update." /SD IDOK
Abort
${EndIf}
; PowerShell avoids NSIS string-length truncation on machines with a large
; PATH. It removes exact duplicates, appends Prism once, and leaves every
; other entry intact. PowerShell comparisons are case-insensitive here.
nsExec::ExecToLog `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "& { param([string]$$entry) $$path = [Environment]::GetEnvironmentVariable('Path', 'Machine'); $$entries = @($$path -split ';' | Where-Object { $$_ -and $$_ -ine $$entry }); $$entries += $$entry; [Environment]::SetEnvironmentVariable('Path', ($$entries -join ';'), 'Machine') }" "$INSTDIR\resources\tui"`
; PATH. It removes exact duplicates, safely repairs the truncated entry
; written by older installers, appends Prism once, and verifies the write.
nsExec::ExecToLog `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "& { $$ErrorActionPreference = 'Stop'; $$entry = $$env:PRISM_TUI_INSTALL_DIR; if ([string]::IsNullOrWhiteSpace($$entry)) { exit 10 }; $$legacyEntry = $$null; $$removeLegacy = $$false; if ($$entry -match '\s') { $$legacyEntry = ($$entry -split '\s', 2)[0]; $$removeLegacy = $$legacyEntry -ine $$entry -and -not (Test-Path -LiteralPath $$legacyEntry -PathType Container) }; $$path = [Environment]::GetEnvironmentVariable('Path', 'Machine'); $$entries = @($$path -split ';' | Where-Object { $$_ -and $$_ -ine $$entry -and (-not $$removeLegacy -or $$_ -ine $$legacyEntry) }); $$entries += $$entry; [Environment]::SetEnvironmentVariable('Path', ($$entries -join ';'), 'Machine'); $$updatedPath = [Environment]::GetEnvironmentVariable('Path', 'Machine'); $$matches = @($$updatedPath -split ';' | Where-Object { $$_ -ieq $$entry }); if ($$matches.Count -ne 1) { exit 11 } }"`
Pop $0
${If} $0 != 0
DetailPrint "ERROR: could not add Prism TUI to the machine PATH (exit code $0)"
@@ -78,11 +88,11 @@
DetailPrint "Installing Prism VST3 plugins to $COMMONFILES64\VST3"
${IfNot} ${FileExists} "$INSTDIR\resources\plugins\VST3\*.vst3"
DetailPrint "ERROR: bundled Prism VST3 plugins were not found at $INSTDIR\resources\plugins\VST3"
MessageBox MB_OK|MB_ICONSTOP "Prism VST3 plugins were selected, but the bundled plugin files were not found.$\r$\n$\r$\nMissing path:$\r$\n$INSTDIR\resources\plugins\VST3" /SD IDOK
Abort
${EndIf}
; Plugins are optional. Local app/TUI builds may intentionally omit the
; staged bundles, so do not roll back an otherwise valid installation.
DetailPrint "WARNING: bundled Prism VST3 plugins were not found; skipping plugin installation"
MessageBox MB_OK|MB_ICONEXCLAMATION "Prism was installed, but this package does not contain the optional VST3 plugins.$\r$\n$\r$\nprism-tui and the desktop app are ready to use." /SD IDOK
${Else}
CreateDirectory "$COMMONFILES64\VST3"
; xcopy /E recurse, /I treat dest as dir, /Y overwrite without prompt.
; The trailing "\*" + "/E" copies each *.vst3 bundle subfolder verbatim.
@@ -93,6 +103,7 @@
MessageBox MB_OK|MB_ICONSTOP "Prism VST3 plugin installation failed while copying files to:$\r$\n$COMMONFILES64\VST3$\r$\n$\r$\nxcopy exit code: $0" /SD IDOK
Abort
${EndIf}
${EndIf}
${Else}
DetailPrint "Prism VST3 plugins: skipped (opted out)."
${EndIf}
@@ -102,13 +113,18 @@
!macro customUnInstall
; Remove only Prism's exact machine-PATH entry. A failure is non-fatal so an
; otherwise valid uninstall is never blocked.
nsExec::ExecToLog `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "& { param([string]$$entry) $$path = [Environment]::GetEnvironmentVariable('Path', 'Machine'); $$entries = @($$path -split ';' | Where-Object { $$_ -and $$_ -ine $$entry }); [Environment]::SetEnvironmentVariable('Path', ($$entries -join ';'), 'Machine') }" "$INSTDIR\resources\tui"`
System::Call 'Kernel32::SetEnvironmentVariable(t, t)i ("PRISM_TUI_INSTALL_DIR", "$INSTDIR\resources\tui").r0'
${If} $0 == 0
DetailPrint "WARNING: could not prepare the Prism TUI PATH removal"
${Else}
nsExec::ExecToLog `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command "& { $$ErrorActionPreference = 'Stop'; $$entry = $$env:PRISM_TUI_INSTALL_DIR; if ([string]::IsNullOrWhiteSpace($$entry)) { exit 10 }; $$legacyEntry = $$null; $$removeLegacy = $$false; if ($$entry -match '\s') { $$legacyEntry = ($$entry -split '\s', 2)[0]; $$removeLegacy = $$legacyEntry -ine $$entry -and -not (Test-Path -LiteralPath $$legacyEntry -PathType Container) }; $$path = [Environment]::GetEnvironmentVariable('Path', 'Machine'); $$entries = @($$path -split ';' | Where-Object { $$_ -and $$_ -ine $$entry -and (-not $$removeLegacy -or $$_ -ine $$legacyEntry) }); [Environment]::SetEnvironmentVariable('Path', ($$entries -join ';'), 'Machine'); $$updatedPath = [Environment]::GetEnvironmentVariable('Path', 'Machine'); $$matches = @($$updatedPath -split ';' | Where-Object { $$_ -ieq $$entry }); if ($$matches.Count -ne 0) { exit 11 } }"`
Pop $0
${If} $0 == 0
SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000
${Else}
DetailPrint "WARNING: could not remove Prism TUI from the machine PATH (exit code $0)"
${EndIf}
${EndIf}
; Always attempt removal — harmless RMDir if the bundle isn't there (user opted
; out at install or removed manually).
@@ -0,0 +1,42 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
$packageJson = Get-Content -LiteralPath (Join-Path $repoRoot 'package.json') -Raw | ConvertFrom-Json
$productName = [string]$packageJson.build.productName
$version = [string]$packageJson.version
$outputDirectory = Join-Path $repoRoot ([string]$packageJson.build.directories.output)
$outputFiles = @(
(Join-Path $outputDirectory "$productName Setup $version.exe"),
(Join-Path $outputDirectory "$productName $version.exe"),
(Join-Path $outputDirectory "win-unpacked\$productName.exe")
)
$lockedFiles = New-Object Collections.Generic.List[string]
foreach ($outputFile in $outputFiles) {
if (-not (Test-Path -LiteralPath $outputFile -PathType Leaf)) {
continue
}
try {
$stream = [IO.File]::Open(
$outputFile,
[IO.FileMode]::Open,
[IO.FileAccess]::ReadWrite,
[IO.FileShare]::None
)
$stream.Close()
} catch {
$lockedFiles.Add($outputFile)
}
}
if ($lockedFiles.Count -gt 0) {
[Console]::Error.WriteLine('Cannot rebuild the Windows distribution because an output executable is still in use.')
[Console]::Error.WriteLine('Close the Prism app and installer, then run `npm run dist` again:')
foreach ($lockedFile in $lockedFiles) {
[Console]::Error.WriteLine(" $lockedFile")
}
exit 1
}
+17 -2
View File
@@ -12,6 +12,21 @@ if (!script) {
process.exit(1)
}
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm'
const result = spawnSync(npmCommand, ['run', script], { stdio: 'inherit' })
const npmCli = process.env.npm_execpath
if (!npmCli) {
console.error('Could not locate the npm CLI. Run this command through `npm run dist`.')
process.exit(1)
}
// Node 22 rejects direct child-process launches of Windows .cmd shims with
// EINVAL. Reuse the npm CLI that launched this script through the current Node
// executable instead, which is portable and avoids an extra command shell.
const result = spawnSync(process.execPath, [npmCli, 'run', script], {
stdio: 'inherit',
})
if (result.error) {
console.error(`Could not start npm run ${script}: ${result.error.message}`)
process.exit(1)
}
process.exit(result.status ?? 1)
+228
View File
@@ -0,0 +1,228 @@
[CmdletBinding()]
param(
[string]$InstallerPath
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
if ($env:OS -ne 'Windows_NT') {
throw 'The Windows installer smoke test can only run on Windows.'
}
if ($env:GITHUB_ACTIONS -ne 'true') {
throw 'This test changes the machine PATH and is restricted to the disposable GitHub Actions runner.'
}
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
throw 'The Windows installer smoke test requires an elevated process.'
}
$repoRoot = Split-Path -Parent $PSScriptRoot
if ([string]::IsNullOrWhiteSpace($InstallerPath)) {
$installerCandidates = @(
Get-ChildItem -LiteralPath (Join-Path $repoRoot 'dist') -Filter '*.exe' -File |
Where-Object { $_.Name -like '*Setup*.exe' }
)
if ($installerCandidates.Count -ne 1) {
throw "Expected one NSIS setup executable in dist, found $($installerCandidates.Count)."
}
$InstallerPath = $installerCandidates[0].FullName
}
$InstallerPath = (Resolve-Path -LiteralPath $InstallerPath).Path
$tempRoot = if ([string]::IsNullOrWhiteSpace($env:RUNNER_TEMP)) {
[IO.Path]::GetTempPath()
} else {
$env:RUNNER_TEMP
}
$testId = [Guid]::NewGuid().ToString('N')
$installRoot = Join-Path $tempRoot "Prism-$testId Installer Smoke"
$tuiDirectory = Join-Path $installRoot 'resources\tui'
$tuiExecutable = Join-Path $tuiDirectory 'prism-tui.exe'
$uninstallerPath = Join-Path $installRoot 'Uninstall Prism.exe'
$legacyEntry = ($tuiDirectory -split '\s', 2)[0]
$machinePathBefore = [Environment]::GetEnvironmentVariable('Path', 'Machine')
$uninstalled = $false
$legacyDirectoryCreated = $false
function Get-PathEntries {
param([AllowNull()][string]$Value)
return @($Value -split ';' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
}
function Assert-PathEntriesEqual {
param(
[string[]]$Expected,
[string[]]$Actual,
[string]$Context
)
if ($Expected.Count -ne $Actual.Count) {
throw "$Context entry count mismatch. Expected $($Expected.Count), got $($Actual.Count)."
}
for ($index = 0; $index -lt $Expected.Count; $index += 1) {
if (-not [string]::Equals($Expected[$index], $Actual[$index], [StringComparison]::OrdinalIgnoreCase)) {
throw "$Context differs at index $index. Expected '$($Expected[$index])', got '$($Actual[$index])'."
}
}
}
function Invoke-CheckedProcess {
param(
[Parameter(Mandatory)][string]$FilePath,
[string]$Arguments = '',
[hashtable]$Environment = @{},
[switch]$CaptureOutput
)
$startInfo = New-Object Diagnostics.ProcessStartInfo
$startInfo.FileName = $FilePath
$startInfo.Arguments = $Arguments
$startInfo.UseShellExecute = $false
$startInfo.CreateNoWindow = $true
$startInfo.WorkingDirectory = $repoRoot
$startInfo.RedirectStandardOutput = $CaptureOutput
$startInfo.RedirectStandardError = $CaptureOutput
foreach ($name in $Environment.Keys) {
$startInfo.EnvironmentVariables[$name] = [string]$Environment[$name]
}
$process = New-Object Diagnostics.Process
$process.StartInfo = $startInfo
if (-not $process.Start()) {
throw "Could not start $FilePath."
}
$stdout = ''
$stderr = ''
if ($CaptureOutput) {
$stdout = $process.StandardOutput.ReadToEnd()
$stderr = $process.StandardError.ReadToEnd()
}
$process.WaitForExit()
if ($process.ExitCode -ne 0) {
throw "$FilePath exited with code $($process.ExitCode).`n$stdout`n$stderr"
}
return [pscustomobject]@{
Stdout = $stdout
Stderr = $stderr
}
}
try {
if ([string]::Equals($legacyEntry, $tuiDirectory, [StringComparison]::OrdinalIgnoreCase)) {
throw "The smoke-test installation path must contain whitespace: $installRoot"
}
if (Test-Path -LiteralPath $legacyEntry -PathType Container) {
throw "Refusing to use an existing directory as the legacy PATH fixture: $legacyEntry"
}
$originalEntries = @(Get-PathEntries $machinePathBefore)
foreach ($entry in $originalEntries) {
if ([string]::Equals($entry, $tuiDirectory, [StringComparison]::OrdinalIgnoreCase) -or
[string]::Equals($entry, $legacyEntry, [StringComparison]::OrdinalIgnoreCase)) {
throw "The machine PATH already contains a smoke-test entry: $entry"
}
}
$seededEntries = @($originalEntries) + $legacyEntry
[Environment]::SetEnvironmentVariable('Path', ($seededEntries -join ';'), 'Machine')
# NSIS requires /D to be the final argument and consumes the remainder of the
# command line as the install directory, including spaces.
Invoke-CheckedProcess -FilePath $InstallerPath -Arguments "/S /D=$installRoot" | Out-Null
if (-not (Test-Path -LiteralPath $tuiExecutable -PathType Leaf)) {
throw "The installer did not install prism-tui.exe at $tuiExecutable."
}
if (-not (Test-Path -LiteralPath $uninstallerPath -PathType Leaf)) {
throw "The installer did not create the expected uninstaller at $uninstallerPath."
}
$installedEntries = @(Get-PathEntries ([Environment]::GetEnvironmentVariable('Path', 'Machine')))
$expectedInstalledEntries = @($originalEntries) + $tuiDirectory
Assert-PathEntriesEqual -Expected $expectedInstalledEntries -Actual $installedEntries -Context 'Installed machine PATH'
$matchingTuiEntries = @(
$installedEntries | Where-Object {
[string]::Equals($_, $tuiDirectory, [StringComparison]::OrdinalIgnoreCase)
}
)
if ($matchingTuiEntries.Count -ne 1) {
throw "Expected one Prism TUI PATH entry, found $($matchingTuiEntries.Count)."
}
if ($installedEntries -icontains $legacyEntry) {
throw "The installer left the legacy truncated PATH entry in place: $legacyEntry"
}
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
$launchPath = (@($installedEntries) + @(Get-PathEntries $userPath)) -join ';'
$launchResult = Invoke-CheckedProcess -FilePath $env:ComSpec `
-Arguments '/D /C prism-tui.exe --version' `
-Environment @{ Path = $launchPath } `
-CaptureOutput
if ($launchResult.Stdout.Trim() -notmatch '^prism-tui\s+') {
throw "prism-tui did not return the expected version output: $($launchResult.Stdout.Trim())"
}
# Reinstall with the legacy prefix backed by a real directory. The safe
# migration must now preserve that entry while continuing to deduplicate the
# complete Prism TUI path.
New-Item -ItemType Directory -Path $legacyEntry | Out-Null
$legacyDirectoryCreated = $true
[Environment]::SetEnvironmentVariable('Path', ((@($installedEntries) + $legacyEntry) -join ';'), 'Machine')
Invoke-CheckedProcess -FilePath $InstallerPath -Arguments "/S /D=$installRoot" | Out-Null
$reinstalledEntries = @(Get-PathEntries ([Environment]::GetEnvironmentVariable('Path', 'Machine')))
$expectedReinstalledEntries = @($originalEntries) + $legacyEntry + $tuiDirectory
Assert-PathEntriesEqual -Expected $expectedReinstalledEntries -Actual $reinstalledEntries -Context 'Reinstalled machine PATH'
Invoke-CheckedProcess -FilePath $uninstallerPath -Arguments '/S' | Out-Null
$uninstalled = $true
$uninstalledEntries = @(Get-PathEntries ([Environment]::GetEnvironmentVariable('Path', 'Machine')))
$expectedUninstalledEntries = @($originalEntries) + $legacyEntry
Assert-PathEntriesEqual -Expected $expectedUninstalledEntries -Actual $uninstalledEntries -Context 'Uninstalled machine PATH'
if ($uninstalledEntries -icontains $tuiDirectory) {
throw "The uninstaller left the Prism TUI PATH entry in place: $tuiDirectory"
}
Write-Host "Windows installer PATH smoke test passed for $InstallerPath"
} finally {
if (-not $uninstalled -and (Test-Path -LiteralPath $uninstallerPath -PathType Leaf)) {
try {
Invoke-CheckedProcess -FilePath $uninstallerPath -Arguments '/S' | Out-Null
} catch {
Write-Warning "Smoke-test uninstall cleanup failed: $($_.Exception.Message)"
}
}
[Environment]::SetEnvironmentVariable('Path', $machinePathBefore, 'Machine')
if (Test-Path -LiteralPath $installRoot) {
$fullTempRoot = [IO.Path]::GetFullPath($tempRoot).TrimEnd('\') + '\'
$fullInstallRoot = [IO.Path]::GetFullPath($installRoot)
if ($fullInstallRoot.StartsWith($fullTempRoot, [StringComparison]::OrdinalIgnoreCase)) {
Remove-Item -LiteralPath $fullInstallRoot -Recurse -Force
} else {
Write-Warning "Refusing to remove unexpected smoke-test directory: $fullInstallRoot"
}
}
if ($legacyDirectoryCreated -and (Test-Path -LiteralPath $legacyEntry -PathType Container)) {
$fullTempRoot = [IO.Path]::GetFullPath($tempRoot).TrimEnd('\') + '\'
$fullLegacyEntry = [IO.Path]::GetFullPath($legacyEntry)
if ($fullLegacyEntry.StartsWith($fullTempRoot, [StringComparison]::OrdinalIgnoreCase)) {
Remove-Item -LiteralPath $fullLegacyEntry -Recurse -Force
} else {
Write-Warning "Refusing to remove unexpected legacy fixture directory: $fullLegacyEntry"
}
}
}