From 4a8bc5ae2dd07108ce21168f5fd1572b1325c354 Mon Sep 17 00:00:00 2001 From: Zaldaryon <273555259+Zaldaryon@users.noreply.github.com> Date: Tue, 15 Sep 2026 23:03:39 -0300 Subject: [PATCH] fix(installer): replace innounp with crazy-max/innoextract v1.13.0 across platforms Inno Setup 6.4.3 archives fail to unpack with innounp (exit code 64). Migrate bootstrap and packaging scripts across Windows, Linux, and macOS to download and use crazy-max/innoextract v1.13.0 with SHA-256 validation. --- .../Prerequisites/PrerequisiteScanner.cs | 5 +- scripts/_hostcaps.ps1 | 17 +- scripts/bootstrap.ps1 | 172 +++++++++++++----- scripts/bootstrap.sh | 42 +++++ scripts/package.ps1 | 5 +- 5 files changed, 185 insertions(+), 56 deletions(-) diff --git a/Optimum.Bootstrap.Core/Prerequisites/PrerequisiteScanner.cs b/Optimum.Bootstrap.Core/Prerequisites/PrerequisiteScanner.cs index aeaeb015..8ccf1199 100644 --- a/Optimum.Bootstrap.Core/Prerequisites/PrerequisiteScanner.cs +++ b/Optimum.Bootstrap.Core/Prerequisites/PrerequisiteScanner.cs @@ -160,7 +160,10 @@ private PrerequisiteResult DetectIlspycmd(PrerequisiteDefinition def) private PrerequisiteResult DetectInnoextract(PrerequisiteDefinition def) { - string? path = CommandSearch.Which(probe, "innoextract"); + string binaryName = probe.Os == OsKind.Windows ? "innoextract.exe" : "innoextract"; + string? path = CommandSearch.Which(probe, "innoextract") + ?? ExecutableOrNull(Path.Combine(repoRoot, ".tools", binaryName)) + ?? ExecutableOrNull(Path.Combine(probe.HomeDirectory, ".tools", binaryName)); if (path is null) return Missing(def, AcquisitionKind.DownloadPage, null, "https://github.com/crazy-max/innoextract/releases"); diff --git a/scripts/_hostcaps.ps1 b/scripts/_hostcaps.ps1 index a08823de..2fbf82cd 100644 --- a/scripts/_hostcaps.ps1 +++ b/scripts/_hostcaps.ps1 @@ -1,4 +1,4 @@ -<# +<# Shared host-capability detection for the packaging scripts. Dot-source it: . "$PSScriptRoot/_hostcaps.ps1" @@ -20,14 +20,23 @@ function Get-HostOS { function Test-Cmd { param([string]$Name) [bool](Get-Command $Name -ErrorAction SilentlyContinue) } -function Get-InnoextractVersion { +function Get-InnoextractPath { + $repoRoot = Split-Path -Parent $PSScriptRoot + $toolsBinary = Join-Path (Join-Path $repoRoot '.tools') ($(if ($IsWindows -or ($env:OS -eq 'Windows_NT')) { 'innoextract.exe' } else { 'innoextract' })) + if (Test-Path $toolsBinary) { return $toolsBinary } $command = Get-Command innoextract -ErrorAction SilentlyContinue - if (-not $command) { return $null } + if ($command) { return $command.Path } + return $null +} + +function Get-InnoextractVersion { + $commandPath = Get-InnoextractPath + if (-not $commandPath) { return $null } $previousEap = $ErrorActionPreference $ErrorActionPreference = 'Continue' try { - $output = @(& $command.Path --version 2>&1) + $output = @(& $commandPath --version 2>&1) $exitCode = $LASTEXITCODE } catch { return $null diff --git a/scripts/bootstrap.ps1 b/scripts/bootstrap.ps1 index 9ab87845..a19365b0 100644 --- a/scripts/bootstrap.ps1 +++ b/scripts/bootstrap.ps1 @@ -526,7 +526,7 @@ try { # a) .vanilla/win-x64/vintagestory already has Vintagestory.exe -> skip # b) -ClientArchive '__skip__' -> the caller (install-windows.ps1) already # placed .vanilla via junction; verify it exists and move on - # c) Normal: download the installer and extract with innounp + # c) Normal: download the installer and extract with innoextract $skipDownload = ($ClientArchive -eq '__skip__') $freshExtract = $false @@ -566,7 +566,7 @@ try { # succeeded, so an interrupted download leaves nothing rather than a short file # at the cache path. Without this, stopping the bootstrap during the ~570 MB # download poisons the cache: the next run finds the file, reports "Using - # cached", and fails in innounp instead. + # cached", and fails in innoextract instead. $partial = "$ClientArchive.partial" Remove-Item -Force -ErrorAction SilentlyContinue $partial @@ -581,66 +581,138 @@ try { Write-Host "Using cached $ClientArchive" } - # Extract using innounp (download if missing). Supports InnoSetup 6.x. + # Extract using innoextract >= 1.11 (download crazy-max fork if missing). + # Supports Inno Setup 6.4.3 through 7.1.0. $toolsDir = Join-Path $repoRoot '.tools' - $innounp = Join-Path $toolsDir 'innounp.exe' - if (-not (Test-Path $innounp)) { - New-Item -ItemType Directory -Force -Path $toolsDir | Out-Null - $innounpZip = Join-Path $toolsDir 'innounp-2.zip' - Write-Host "Downloading innounp" - - # Same reasoning as the client archive above: small enough that the window is - # narrow, and a short zip here fails in Expand-Archive on every later run. - $innounpPartial = "$innounpZip.partial" - Remove-Item -Force -ErrorAction SilentlyContinue $innounpPartial - - Invoke-NativeStep { curl.exe -L --fail --silent -o $innounpPartial "https://github.com/jrathlev/InnoUnpacker-Windows-GUI/releases/download/ui_2_2_9/innounp-2.zip" } - if ($LASTEXITCODE -ne 0) { - Remove-Item -Force -ErrorAction SilentlyContinue $innounpPartial - throw "Download failed: innounp-2.zip" + New-Item -ItemType Directory -Force -Path $toolsDir | Out-Null + $isWin = ($env:OS -eq 'Windows_NT') -or (($null -ne $IsWindows) -and $IsWindows) + $isMac = ($null -ne $IsMacOS) -and $IsMacOS + $exeSuffix = if ($isWin) { '.exe' } else { '' } + $localInno = Join-Path $toolsDir "innoextract$exeSuffix" + + # Check if local .tools/innoextract exists, or if system innoextract is >= 1.11 + $innoextract = $null + if (Test-Path $localInno) { + $innoextract = $localInno + } else { + $sysCmd = Get-Command innoextract -ErrorAction SilentlyContinue + if ($sysCmd) { + $prevEap = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + $verOut = @(& $sysCmd.Path --version 2>&1) -join [Environment]::NewLine + if ($verOut -match '(?im)^\s*innoextract\s+(\d+)\.(\d+)') { + $verMajor = [int]$Matches[1] + $verMinor = [int]$Matches[2] + if ($verMajor -gt 1 -or ($verMajor -eq 1 -and $verMinor -ge 11)) { + $innoextract = $sysCmd.Path + } + } + } catch { } finally { + $ErrorActionPreference = $prevEap + } } + } - Move-Item -Force $innounpPartial $innounpZip - Expand-Archive -Path $innounpZip -DestinationPath $toolsDir -Force - $found = Get-ChildItem -Path $toolsDir -Recurse -Filter 'innounp.exe' | Select-Object -First 1 - if ($found -and $found.FullName -ne $innounp) { - Copy-Item -Force $found.FullName $innounp + if (-not $innoextract) { + Write-Host "Downloading innoextract 1.13.0 (crazy-max fork)" + $arch = if ([System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture -eq [System.Runtime.InteropServices.Architecture]::Arm64) { + 'arm64' + } else { + 'amd64' } - Remove-Item -Force $innounpZip -ErrorAction SilentlyContinue - } - $extractTarget = $winVanillaDir - New-Item -ItemType Directory -Force -Path $extractTarget | Out-Null - Write-Host "Extracting with innounp to $extractTarget" - Get-Process -Name 'innounp' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue - $innounpProc = Start-Process -FilePath $innounp -ArgumentList "-x -d`"$extractTarget`" -c`"{app}`" `"$ClientArchive`"" -NoNewWindow -PassThru - $exited = $innounpProc.WaitForExit(300000) - if (-not $exited) { - $innounpProc.Kill() - Get-Process -Name 'innounp' -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue - throw "innounp timed out after 5 minutes. Kill any innounp.exe in Task Manager and retry." - } - $innounpExitCode = $innounpProc.ExitCode - $appDir = Join-Path $extractTarget '{app}' - if (Test-Path $appDir) { - Get-ChildItem -Path $appDir | Move-Item -Destination $extractTarget -Force - Remove-Item -Force $appDir - } - if ($innounpExitCode -ne 0 -and -not (Test-Path (Join-Path $extractTarget 'Vintagestory.exe'))) { - throw "innounp failed (exit $innounpExitCode)." + $assetName = if ($isWin) { + 'innoextract-windows-amd64.exe' + } elseif ($isMac) { + "innoextract-darwin-$arch" + } else { + "innoextract-linux-$arch" + } + + $expectedHashes = @{ + 'innoextract-windows-amd64.exe' = '5700fb1e82e6812bb341b964470537161e07a127d29eecfe176f5198cf215a59' + 'innoextract-linux-amd64' = 'c898db2ecc282ff8d943e9868d03dd6f1fc6069ddd40430341b211f93826b360' + 'innoextract-linux-arm64' = 'e3852c6225f1b6043025fbfb459d6f8c38c5418f6c3a18720f525a0161d37969' + 'innoextract-darwin-amd64' = '55a384d8f077508b04c888a9f68a40c5fb4345565586632124d80b6efec7629d' + 'innoextract-darwin-arm64' = '6b8f11062e4feb153141e85d2f2cd70fc8f1d6fc4354d59e81933308536d3164' + } + + $innoUrl = "https://github.com/crazy-max/innoextract/releases/download/v1.13.0/$assetName" + $innoPartial = "$localInno.partial" + Remove-Item -Force -ErrorAction SilentlyContinue $innoPartial + + if (Get-Command curl.exe -ErrorAction SilentlyContinue) { + Invoke-NativeStep { curl.exe -L --fail --silent -o $innoPartial $innoUrl } + } elseif (Get-Command curl -ErrorAction SilentlyContinue) { + Invoke-NativeStep { curl -L --fail --silent -o $innoPartial $innoUrl } + } else { + Invoke-WebRequest -Uri $innoUrl -OutFile $innoPartial + } + + if (-not (Test-Path $innoPartial) -or (Get-Item $innoPartial).Length -eq 0) { + Remove-Item -Force -ErrorAction SilentlyContinue $innoPartial + throw "Failed to download innoextract from $innoUrl" + } + + $expected = $expectedHashes[$assetName] + if ($expected) { + $actual = (Get-FileHash -Path $innoPartial -Algorithm SHA256).Hash.ToLowerInvariant() + if ($actual -ne $expected) { + Remove-Item -Force -ErrorAction SilentlyContinue $innoPartial + throw "SHA-256 verification failed for $assetName: expected $expected, got $actual" + } + } + + Move-Item -Force $innoPartial $localInno + if (-not $isWin) { + Invoke-NativeStep { chmod +x $localInno } + } + $innoextract = $localInno } - if ($innounpExitCode -ne 0) { - Write-Warning "innounp exited with code $innounpExitCode after extracting Vintagestory.exe; continuing." + + # Inspect installer with innoextract + $info = @(Invoke-NativeStep { & $innoextract --info $ClientArchive 2>&1 }) + $infoExitCode = $LASTEXITCODE + if ($infoExitCode -ne 0 -or (($info -join "`n") -notmatch '(?i)setup data version')) { + throw "innoextract could not inspect the installer '$ClientArchive'. The installer uses an unsupported Inno Setup format." } - if (-not (Test-Path (Join-Path $extractTarget 'Vintagestory.exe'))) { - throw "Extraction failed: Vintagestory.exe not found" + + # Extract into a clean staging folder beside the target + $stageParent = Split-Path -Parent $winVanillaDir + New-Item -ItemType Directory -Force -Path $stageParent | Out-Null + $extractRoot = Join-Path $stageParent ".innoextract-stage-$([Guid]::NewGuid().ToString('N'))" + New-Item -ItemType Directory -Force -Path $extractRoot | Out-Null + + try { + Write-Host "Extracting with innoextract to $winVanillaDir" + Invoke-NativeStep { & $innoextract --silent --extract --output-dir $extractRoot $ClientArchive } + if ($LASTEXITCODE -ne 0) { + throw "innoextract failed (exit $LASTEXITCODE) for $ClientArchive" + } + + $sourceRoot = Join-Path $extractRoot 'app' + if (-not (Test-Path (Join-Path $sourceRoot 'Vintagestory.exe'))) { + $sourceRoot = $extractRoot + } + if (-not (Test-Path (Join-Path $sourceRoot 'Vintagestory.exe'))) { + throw "Extraction failed: Vintagestory.exe not found in $ClientArchive payload" + } + + New-Item -ItemType Directory -Force -Path $winVanillaDir | Out-Null + Get-ChildItem -Path $sourceRoot -Force | ForEach-Object { + Move-Item -Path $_.FullName -Destination $winVanillaDir -Force + } + } finally { + if (Test-Path $extractRoot) { Remove-Item -Recurse -Force $extractRoot -ErrorAction SilentlyContinue } } + $freshExtract = $true Write-Host "Extraction complete." } # Validate the vanilla tree before building against it. A tolerated - # partial innounp extraction (nonzero exit above) or a stale .vanilla + # partial innoextract extraction (nonzero exit above) or a stale .vanilla # cache left by an older failed run carries zero-byte or truncated # assets that only surface later, in-game, as opaque GL crashes # ("blur.vsh ... unexpected $end at "). Catch them here instead. @@ -661,7 +733,7 @@ try { # Wipe the poisoned extraction so the next run re-extracts # instead of reusing it via the "Using existing" fast path. Remove-Item -Recurse -Force $winVanillaDir -ErrorAction SilentlyContinue - throw "innounp produced $($corrupt.Count) empty/truncated file(s); the extraction was discarded. Re-run to retry.`n $names" + throw "innoextract produced $($corrupt.Count) empty/truncated file(s); the extraction was discarded. Re-run to retry.`n $names" } throw "Vanilla client files are corrupt ($($corrupt.Count) empty/truncated file(s)):`n $names`nIf $winVanillaDir is Optimum's own cache, delete it and retry; if it points at your Vintage Story install, repair or reinstall Vintage Story $Version first." } diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index c4609a37..0acb504d 100644 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -191,6 +191,48 @@ extract_archive() { python3 -c "import zipfile,sys; zipfile.ZipFile(sys.argv[1]).extractall(sys.argv[2])" "$archive" "$dest" fi ;; + *.exe) + local innoextract_bin="" + if [[ -x "$repo_root/.tools/innoextract" ]]; then + innoextract_bin="$repo_root/.tools/innoextract" + elif command -v innoextract >/dev/null 2>&1; then + local ver + ver="$(innoextract --version 2>/dev/null | sed -n 's/^innoextract \([0-9][0-9]*\)\.\([0-9][0-9]*\).*/\1.\2/p' | head -n 1 || true)" + if [[ -n "$ver" ]] && awk -v v="$ver" 'BEGIN { exit (v >= 1.11 ? 0 : 1) }'; then + innoextract_bin="$(command -v innoextract)" + fi + fi + if [[ -z "$innoextract_bin" ]]; then + echo "Downloading innoextract 1.13.0 (crazy-max fork)..." >&2 + mkdir -p "$repo_root/.tools" + local os_type="$(uname -s)" + local machine="$(uname -m)" + local arch="linux-amd64" + if [[ "$os_type" == "Darwin" ]]; then + if [[ "$machine" == "arm64" ]]; then arch="darwin-arm64"; else arch="darwin-amd64"; fi + elif [[ "$machine" == "aarch64" || "$machine" == "arm64" ]]; then + arch="linux-arm64" + fi + local inno_url="https://github.com/crazy-max/innoextract/releases/download/v1.13.0/innoextract-$arch" + curl -sSL -o "$repo_root/.tools/innoextract" "$inno_url" || { + echo "Failed to download innoextract from $inno_url" >&2 + exit 1 + } + chmod +x "$repo_root/.tools/innoextract" + innoextract_bin="$repo_root/.tools/innoextract" + fi + local stage_dir="$dest/.innoextract-stage-$RANDOM" + rm -rf "$stage_dir" + mkdir -p "$stage_dir" + "$innoextract_bin" --silent --extract --output-dir "$stage_dir" "$archive" + local app_dir="$stage_dir/app" + if [[ ! -d "$app_dir" ]]; then + app_dir="$stage_dir" + fi + mkdir -p "$dest/vintagestory" + cp -a "$app_dir"/* "$dest/vintagestory/" + rm -rf "$stage_dir" + ;; *) echo "Unsupported archive: $archive" >&2; exit 1 ;; esac diff --git a/scripts/package.ps1 b/scripts/package.ps1 index 2e2258d6..cd71cc2e 100644 --- a/scripts/package.ps1 +++ b/scripts/package.ps1 @@ -134,7 +134,10 @@ function Resolve-WindowsVanilla { Write-Host "Using cached $InstallerPath" } - $innoextract = (Get-Command innoextract -ErrorAction Stop).Path + $innoextract = Get-InnoextractPath + if (-not $innoextract) { + throw "innoextract not found. Install a current release from https://github.com/crazy-max/innoextract/releases." + } $info = @(Invoke-NativeStep { & $innoextract --info $InstallerPath 2>&1 }) $infoExitCode = $LASTEXITCODE if ($infoExitCode -ne 0 -or (($info -join "`n") -notmatch '(?i)setup data version')) {