diff --git a/pwiz-sharp/build.bat b/pwiz-sharp/build.bat index cfea8e432e..62c26c0bad 100644 --- a/pwiz-sharp/build.bat +++ b/pwiz-sharp/build.bat @@ -8,7 +8,7 @@ REM # REM # Usage: REM # build.bat [Debug|Release] [--i-agree-to-the-vendor-licenses] REM # [--require-vendor-support] [--without-mascot] -REM # [--automated] [--coverage] +REM # [--automated] [--coverage] [--with-vendor-sdks] REM # REM # Flags: REM # --i-agree-to-the-vendor-licenses @@ -23,6 +23,16 @@ REM # Fail the build if vendor support isn't enabled. Use in CI to make REM # sure --i-agree was passed, instead of silently building a stripped REM # artifact. REM # +REM # --with-vendor-sdks +REM # Also produce ProteoWizard-WithVendorSdks-Setup-.exe: the +REM # bundled-runtime installer plus every Windows vendor SDK, extracted +REM # into VendorSdkLoader's cache. That build needs no network at run +REM # time (it carries the .NET runtime too), which is what the wine +REM # container needs. Costs ~30 s and ~26 MB over the default variants, +REM # so it is opt-in here and passed by tcbuild.bat on CI. No effect +REM # without --i-agree-to-the-vendor-licenses: the whole installer step +REM # is skipped in that case. +REM # REM # --without-mascot REM # Disable Mascot (.dat) support in BiblioSpec. msparser is bundled REM # under libraries\msparser_3_1_0_x86_win64\, so Mascot support is ON @@ -60,6 +70,7 @@ set REQUIRE_VENDOR=0 set AUTOMATED=0 set COVERAGE=0 set WITHOUT_MASCOT=0 +set WITH_VENDOR_SDKS=0 set ERROR_TEXT= REM # Parse args. First non-flag arg is the configuration (Debug|Release). @@ -68,6 +79,7 @@ if "%~1"=="" goto endparse if /i "%~1"=="--i-agree-to-the-vendor-licenses" (set IAGREE=1) else ^ if /i "%~1"=="--require-vendor-support" (set REQUIRE_VENDOR=1) else ^ if /i "%~1"=="--without-mascot" (set WITHOUT_MASCOT=1) else ^ +if /i "%~1"=="--with-vendor-sdks" (set WITH_VENDOR_SDKS=1) else ^ if /i "%~1"=="--automated" (set AUTOMATED=1) else ^ if /i "%~1"=="--coverage" (set COVERAGE=1) else ^ if /i "%~1"=="Debug" (set CONFIG=Debug) else ^ @@ -136,8 +148,10 @@ if %IAGREE%==1 ( if exist "%ProgramFiles%\Inno Setup 6\ISCC.exe" set HAVE_ISCC=1 if exist "%ProgramFiles(x86)%\Inno Setup 6\ISCC.exe" set HAVE_ISCC=1 if !HAVE_ISCC!==1 ( - echo ##teamcity[progressMessage 'installer/build.ps1 -SkipBuild ^(uses Release artifacts^)'] - pwsh -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%\installer\build.ps1" -SkipBuild + set INSTALLER_ARGS=-SkipBuild + if !WITH_VENDOR_SDKS!==1 set INSTALLER_ARGS=!INSTALLER_ARGS! -WithVendorSdks + echo ##teamcity[progressMessage 'installer/build.ps1 !INSTALLER_ARGS! ^(uses Release artifacts^)'] + pwsh -NoProfile -ExecutionPolicy Bypass -File "%SCRIPT_DIR%\installer\build.ps1" !INSTALLER_ARGS! if !ERRORLEVEL! NEQ 0 ( echo ##teamcity[message text='installer build failed; Installer.Tests will skip' status='WARNING'] ) diff --git a/pwiz-sharp/installer/Setup.iss b/pwiz-sharp/installer/Setup.iss index 734d399f23..806090c5d8 100644 --- a/pwiz-sharp/installer/Setup.iss +++ b/pwiz-sharp/installer/Setup.iss @@ -1,24 +1,31 @@ ; ----------------------------------------------------------------------------- -; Inno Setup script for ProteoWizard-Sharp. +; Inno Setup script for ProteoWizard. ; -; build.ps1 invokes ISCC twice from this one script to produce two variants: +; build.ps1 invokes ISCC from this one script to produce up to three variants: ; -; ProteoWizard-Sharp-Setup.exe (~62 MB) +; ProteoWizard-Setup.exe (~78 MB) ; Bundles the .NET 10 Desktop Runtime installer. On install, checks for ; .NET 10; if missing, silently invokes the bundled runtime installer ; (which triggers UAC for its per-machine install). ; -; ProteoWizard-Sharp-NoNetRuntime-Setup.exe (~5 MB) +; ProteoWizard-NoNetRuntime-Setup.exe (~22 MB) ; Same payload minus the bundled runtime. On install, aborts with a ; dialog + download link if .NET 10 isn't already present. For users ; who manage their own runtime install (corp deployments, dev boxes ; that already have it, etc.). ; +; ProteoWizard-WithVendorSdks-Setup.exe (~104 MB, opt-in: build.ps1 -WithVendorSdks) +; The bundled-runtime variant PLUS every Windows vendor SDK, pre-extracted +; into VendorSdkLoader's cache. Carries the .NET runtime too -- the name +; says only what it adds over the default variant, not what it inherits. +; Nothing it needs is fetched at run time, so it is the one to use where +; there is no network: an offline site install, or the wine container. +; ; Both variants: ; - Ask the user to pick "for me" (per-user, no admin) or "for everyone" ; (per-machine, admin) at install time -; - Install to %LOCALAPPDATA%\Programs\ProteoWizard-Sharp\ or -; %ProgramFiles%\ProteoWizard-Sharp\ accordingly +; - Install to %LOCALAPPDATA%\Programs\ProteoWizard\ or +; %ProgramFiles%\ProteoWizard\ accordingly ; - Create Start Menu shortcuts for MSConvertGUI and SeeMS ; - Register Windows Explorer right-click verbs ; - Standard uninstall via Programs and Features @@ -35,7 +42,7 @@ ; `winget install JRSoftware.InnoSetup`). ; ----------------------------------------------------------------------------- -#define MyAppName "ProteoWizard-Sharp" +#define MyAppName "ProteoWizard" ; Version comes in from build.ps1 via /DMyAppVersion=4.0.YYDOY-gitsha. The ; fallback below keeps direct ISCC invocations buildable for local debugging. #ifndef MyAppVersion @@ -53,7 +60,13 @@ #define OutputDir "build" #endif #ifndef OutputBaseFilename - #define OutputBaseFilename "ProteoWizard-Sharp-Setup" + #define OutputBaseFilename "ProteoWizard-Setup" +#endif +; WithVendorSdks: bundle the vendor SDKs and pre-populate VendorSdkLoader's cache, so the +; installed app never reaches raw.githubusercontent.com. build.ps1 -WithVendorSdks sets this +; and points VendorCacheDir at a tree it extracted into the runtime's own cache layout. +#ifndef VendorCacheDir + #define VendorCacheDir "build\vendor-cache" #endif [Setup] @@ -64,7 +77,7 @@ ; own uninstaller log. Same-version reinstalls still upgrade in place. ; ; The base GUID stays stable so future migration code (or scripts iterating -; "all ProteoWizard-Sharp installs") can match on the prefix. +; "all ProteoWizard installs") can match on the prefix. ; ; Shared resources policy (last-installed-wins, no automatic cleanup): ; - Explorer context-menu verbs are SHARED across versions: each install @@ -138,6 +151,24 @@ Source: "cache\windowsdesktop-runtime-10.0-win-x64.exe"; DestDir: "{tmp}"; \ Flags: deleteafterinstall; Check: not IsDotNetDesktopInstalled #endif +#ifdef WithVendorSdks +; Vendor SDKs, pre-extracted by build.ps1 into exactly the layout +; VendorSdkLoader.EnsureExtracted would have produced on first use — one +; - directory per pin, flattened, each holding a .ok marker. +; The marker is the whole point: with it present the loader neither downloads +; nor extracts, so the app works offline and needs no write access to the cache. +; +; Destination is {commonappdata}, not {localappdata}: one copy shared by every +; user on the machine, which is what the cache-root override below is for. +; +; uninsneveruninstall because the cache is keyed by SDK version, not by app +; version, so side-by-side pwiz-sharp installs share these directories — the +; same last-installed-wins / leave-it-alone policy the Explorer verbs use. +; Removing every version therefore leaves the cache behind on purpose. +Source: "{#VendorCacheDir}\*"; DestDir: "{commonappdata}\ProteoWizard\vendor"; \ + Flags: ignoreversion recursesubdirs createallsubdirs uninsneveruninstall +#endif + [Icons] Name: "{group}\MSConvertGUI"; Filename: "{app}\MSConvertGUI-sharp.exe"; \ WorkingDir: "{app}"; Tasks: startmenu_msconvertgui; \ @@ -285,6 +316,37 @@ begin end; end; +#ifdef WithVendorSdks +{ ----- Point VendorSdkLoader at the bundled cache ----- + The loader's default cache root is %LOCALAPPDATA%\ProteoWizard\vendor, but this + variant installs one shared copy under %PROGRAMDATA%. GetCacheRoot() reads a path + out of %PROGRAMDATA%\ProteoWizard\vendor-cache-root.txt when that file exists, so + writing it is what makes the bundled cache the one the app actually consults. + Without this the SDKs would sit on disk unused and the app would still try to + download them. + + ProgramData is writable by standard users for new subdirectories, so this works + for a per-user install too; if it ever does not, failing loudly beats installing + a cache nothing reads. } +procedure StampVendorCacheRoot(); +var + dir: String; +begin + dir := ExpandConstant('{commonappdata}\ProteoWizard'); + if not ForceDirectories(dir) then + RaiseException('Could not create ' + dir); + if not SaveStringToFile(dir + '\vendor-cache-root.txt', + ExpandConstant('{commonappdata}\ProteoWizard\vendor'), False) then + RaiseException('Could not write vendor-cache-root.txt in ' + dir); +end; + +procedure CurStepChanged(CurStep: TSetupStep); +begin + if CurStep = ssPostInstall then + StampVendorCacheRoot(); +end; +#endif + { ----- Pre-flight check (NoNetRuntime variant only) ----- When this installer was built without the bundled .NET runtime (build.ps1's second ISCC pass), we can't install .NET ourselves. Abort @@ -302,9 +364,9 @@ begin if not IsDotNetDesktopInstalled() then begin rc := MsgBox( - 'ProteoWizard-Sharp requires the .NET 10 Desktop Runtime (x64), which is not installed on this machine.' + #13#10#13#10 + + 'ProteoWizard requires the .NET 10 Desktop Runtime (x64), which is not installed on this machine.' + #13#10#13#10 + 'Click OK to open the Microsoft download page in your browser, then re-run this installer after installing the runtime.' + #13#10#13#10 + - 'If you prefer an installer that bundles the runtime, use ProteoWizard-Sharp-Setup.exe instead.', + 'If you prefer an installer that bundles the runtime, use ProteoWizard-Setup.exe instead.', mbError, MB_OKCANCEL); if rc = IDOK then ShellExec('', 'https://dotnet.microsoft.com/download/dotnet/10.0/runtime', diff --git a/pwiz-sharp/installer/build-linux.sh b/pwiz-sharp/installer/build-linux.sh index 452d31fbfb..9bd7461e6f 100644 --- a/pwiz-sharp/installer/build-linux.sh +++ b/pwiz-sharp/installer/build-linux.sh @@ -4,9 +4,9 @@ # Mirrors the Windows pipeline and its two-variant split: # # Windows Linux -# ProteoWizard-Sharp-Setup-.exe ProteoWizard-Sharp-linux-x64-.tar.gz +# ProteoWizard-Setup-.exe ProteoWizard-linux-x64-.tar.gz # bundles the .NET desktop runtime self-contained: the runtime is IN the payload -# ProteoWizard-Sharp-NoNetRuntime-Setup-.exe ProteoWizard-Sharp-NoNetRuntime-linux-x64-.tar.gz +# ProteoWizard-NoNetRuntime-Setup-.exe ProteoWizard-NoNetRuntime-linux-x64-.tar.gz # needs .NET 8 already installed framework-dependent: same requirement # # "Setup" is dropped for the runtime identifier because a tarball is not an installer; the @@ -139,8 +139,8 @@ package() { } RC=0 -package self-contained true "ProteoWizard-Sharp-$RID-$APP_VERSION" || RC=1 -package framework-dependent false "ProteoWizard-Sharp-NoNetRuntime-$RID-$APP_VERSION" || RC=1 +package self-contained true "ProteoWizard-$RID-$APP_VERSION" || RC=1 +package framework-dependent false "ProteoWizard-NoNetRuntime-$RID-$APP_VERSION" || RC=1 # Same side-car build.ps1 writes, so packaging tests can pin the version without re-deriving it # from a filename. diff --git a/pwiz-sharp/installer/build.ps1 b/pwiz-sharp/installer/build.ps1 index fbb9c1cbce..4e9918acad 100644 --- a/pwiz-sharp/installer/build.ps1 +++ b/pwiz-sharp/installer/build.ps1 @@ -13,7 +13,7 @@ End-to-end packaging pipeline: 4. Download the .NET 10 desktop runtime installer EXE (cached under installer/cache/) so we can embed it in the Setup.exe 5. Compile installer/Setup.iss with Inno Setup's ISCC → installer/build/ - ProteoWizard-Sharp-Setup.exe (~58 MB, single self-contained installer) + ProteoWizard-Setup.exe (~58 MB, single self-contained installer) The Inno installer asks the user "Install for me / Install for everyone" at runtime — drops the dual-MSI complexity of the prior WiX build. .NET 10 prereq @@ -27,7 +27,12 @@ missing. #requires -Version 7.0 param( - [switch] $SkipBuild + [switch] $SkipBuild, + # Also produce ProteoWizard-WithVendorSdks-Setup-.exe: the bundled-runtime + # installer plus every Windows vendor SDK, pre-extracted into VendorSdkLoader's cache. + # That variant never contacts raw.githubusercontent.com, which is what makes it usable + # in an offline or containerised deployment (see ProteoWizard/container). + [switch] $WithVendorSdks ) $ErrorActionPreference = 'Stop' @@ -47,6 +52,7 @@ $msconvertGui = Join-Path $pwizSharp "Tools/MsConvertGUI/src/MsConvertGUI.csp $seems = Join-Path $pwizSharp "Tools/SeeMS/src/SeeMS.csproj" $msconvertGuiOut = Join-Path $pwizSharp "Tools/MsConvertGUI/src/bin/Release/net10.0-windows" $seemsOut = Join-Path $pwizSharp "Tools/SeeMS/src/bin/Release/net10.0-windows" +$msconvertOut = Join-Path $pwizSharp "Tools/Commandline/MsConvert/src/bin/Release/net10.0" $outDir = Join-Path $installerDir "build" $stagingDir = Join-Path $outDir "stage" $cacheDir = Join-Path $installerDir "cache" @@ -122,12 +128,13 @@ function Should-Skip([string] $relName) { return $false } -function Stage-From([string] $source) { +function Stage-From([string] $source, [switch] $TopLevelOnly, [string] $DestRoot = $stagingDir) { $copied = 0; $bytesCopied = 0L; $bytesSkipped = 0L; $dups = 0 - Get-ChildItem $source -Recurse -File | ForEach-Object { + $items = if ($TopLevelOnly) { Get-ChildItem $source -File } else { Get-ChildItem $source -Recurse -File } + $items | ForEach-Object { $rel = $_.FullName.Substring($source.Length + 1) if (Should-Skip $rel) { $bytesSkipped += $_.Length; return } - $dest = Join-Path $stagingDir $rel + $dest = Join-Path $DestRoot $rel if (Test-Path $dest) { $dups++; return } $destDir = Split-Path -Parent $dest if (-not (Test-Path $destDir)) { New-Item -ItemType Directory $destDir -Force | Out-Null } @@ -137,8 +144,159 @@ function Stage-From([string] $source) { Write-Host " from $((Split-Path -Leaf (Split-Path -Parent $source))): $copied new ($([math]::Round($bytesCopied/1MB, 2)) MB), $dups dup, $([math]::Round($bytesSkipped/1MB, 1)) MB skipped" } -Stage-From $msconvertGuiOut -Stage-From $seemsOut +function Build-Payload([string] $DestRoot) { +Stage-From $msconvertGuiOut -DestRoot $DestRoot +Stage-From $seemsOut -DestRoot $DestRoot +# msconvert's own bin, third: MSConvertGUI's chain builds msconvert.exe, but a referencing +# project only inherits the referenced project's assemblies, not every transitive package +# asset it resolved. MsConvert pulls five that MSConvertGUI does not -- notably +# System.Configuration.ConfigurationManager, which Agilent's MHDAC needs to read +# BaseDataAccess.dll.config. Staging the GUI alone shipped an msconvert.exe whose Agilent +# reader died at the first spectrum with "Could not load ... ConfigurationManager". +# +# Top level only: this bin also holds a nested win-x64/ RID tree that is a second copy of +# everything already staged above. Recursing it added 706 files / 238 MB and pushed the +# installer from 68 MB to 113 MB for five assemblies that all sit in the root. +Stage-From $msconvertOut -TopLevelOnly -DestRoot $DestRoot + +# ...plus msconvert's wiff2/ subdirectory, which MSConvertGUI's bin also lacks. Wiff2LoadContext +# loads its Cecil-patched Unity.Abstractions and the SDK-matched System.Data.SQLite 1.0.109 from +# AppContext.BaseDirectory\wiff2; without it the wiff2 ALC initialises against the wrong +# dependency versions and the read fails later, reaching for SCIEX.Apis.Control.v1 through the +# default ALC (which no archive supplies). Named explicitly rather than recursing $msconvertOut: +# the only other subtree there is a win-x64/ RID copy of everything already staged. +$wiff2Src = Join-Path $msconvertOut "wiff2" +if (Test-Path $wiff2Src) { + $wiff2Dest = Join-Path $DestRoot "wiff2" + New-Item -ItemType Directory $wiff2Dest -Force | Out-Null + $n = 0 + Get-ChildItem $wiff2Src -File | ForEach-Object { + if (Should-Skip "wiff2\$($_.Name)") { return } + Copy-Item $_.FullName (Join-Path $wiff2Dest $_.Name) + $n++ + } + Write-Host " from wiff2/: $n files" +} +} + +Build-Payload $stagingDir + +# 3b. Vendor SDK cache (only for -WithVendorSdks). +# +# Produces one - directory per Windows pin, laid out exactly as +# VendorSdkLoader.EnsureExtracted would have produced it on first use, each with the +# .ok marker that tells the loader not to download or extract. Mirrors that method +# and FlattenVendorArchiveLayout: 7za with the archive password, then hoist +# vendor_api//** to the top level, dropping wrong-arch subtrees. Keep the two +# in step — a layout the loader disagrees with fails as a silent re-download, not an +# error. Verify-VendorCache below is the guard against exactly that drift. +$vendorCacheDir = Join-Path $outDir "vendor-cache" + +function Build-VendorCache { + $sevenZa = Join-Path $msconvertGuiOut "7za.exe" + if (-not (Test-Path $sevenZa)) { throw "7za.exe not found at $sevenZa" } + # Same fixed licence-agreement password the runtime loader uses. + $pw = "i-agree-to-the-vendor-licenses" + $repoRoot = Split-Path -Parent $pwizSharp + + # Versions come from the pin table the generator just rewrote (step 1), so the cache + # keys cannot disagree with the table the installed app resolves against. + $pinsCs = Join-Path $pwizSharp "pwiz/src/Vendor/Common/VendorSdkPins.generated.cs" + $pinsCsText = Get-Content -Raw $pinsCs + $versions = @{} + foreach ($m in [regex]::Matches($pinsCsText, 'Name:\s*"([^"]+)",\s*[\r\n]+\s*Version:\s*"([^"]+)"')) { + $versions[$m.Groups[1].Value] = $m.Groups[2].Value + } + + if (Test-Path $vendorCacheDir) { Remove-Item $vendorCacheDir -Recurse -Force } + New-Item -ItemType Directory $vendorCacheDir -Force | Out-Null + + $vendors = (Get-Content -Raw $pinsJson | ConvertFrom-Json).vendors + foreach ($v in $vendors) { + # Windows installer: skip pins that exist only for a non-Windows runtime. + if ($v.os -and $v.os -ne 'windows') { + Write-Host " skip $($v.name) (os=$($v.os))" + continue + } + if (-not $versions.ContainsKey($v.name)) { + throw "no Version for '$($v.name)' in $pinsCs — did VendorPinsGenerator run?" + } + $archive = Join-Path $repoRoot $v.path + if (-not (Test-Path $archive)) { throw "vendor archive missing: $archive" } + + $dest = Join-Path $vendorCacheDir "$($v.name)-$($versions[$v.name])" + New-Item -ItemType Directory $dest -Force | Out-Null + & $sevenZa x -y "-p$pw" "-o$dest" $archive | Out-Null + if ($LASTEXITCODE -ne 0) { throw "7za failed extracting $archive (exit $LASTEXITCODE)" } + + # Flatten vendor_api//** onto $dest, dropping x86/mips; first file wins. + $nested = Join-Path $dest "vendor_api" + if (Test-Path $nested) { + foreach ($vendorDir in Get-ChildItem $nested -Directory) { + foreach ($f in Get-ChildItem $vendorDir.FullName -Recurse -File) { + $rel = $f.FullName.Substring($vendorDir.FullName.Length + 1) + $first = ($rel -split '[\\/]')[0] + if ($first -ieq 'x86' -or $first -ieq 'mips') { continue } + $target = Join-Path $dest $f.Name + if (-not (Test-Path $target)) { Move-Item $f.FullName $target } + } + } + Remove-Item $nested -Recurse -Force + } + + # Overlay any assembly the build patched. Agilent's BaseCommon / BaseDataAccess are + # Cecil-rewritten by Agilent.csproj to strip Delegate.BeginInvoke, which .NET 5+ removed; + # the archive still holds the originals. Extracting straight from the archive therefore + # caches an unpatched SDK, and Agilent dies on open with "MassSpecDataReader uses + # delegate.BeginInvoke ... cannot open Agilent .d files" — but only for files that reach + # the async metadata path, which is why a single-scan fixture does not catch it. + foreach ($f in Get-ChildItem $dest -File -Filter *.dll) { + $built = Join-Path $msconvertOut $f.Name + if ((Test-Path $built) -and ((Get-Item $built).Length -ne $f.Length)) { + Copy-Item $built $f.FullName -Force + Write-Host " overlaid build-patched $($f.Name)" + } + } + + # Shimadzu's natives (IOModuleQTFL and friends) are loaded from this cache directory by + # full path, so the loader resolves THEIR imports from here first, not from the directory + # holding the executable. The VC++ 2015-2022 runtime they link therefore has to be here + # too: app-local is enough on a machine carrying the redistributable, but not in a wine + # container, where the reader instead returns a structurally valid mzML with no spectra + # at all. MFC140 is the one wine has no builtin for. + if ($v.name -eq 'Shimadzu') { + foreach ($dll in @('mfc140.dll','msvcp140.dll','concrt140.dll','vcruntime140.dll','vcruntime140_1.dll')) { + $src = Join-Path $msconvertOut $dll + if (Test-Path $src) { Copy-Item $src (Join-Path $dest $dll) -Force } + } + Write-Host " staged VC140 runtime beside the Shimadzu natives" + } + + Set-Content -Path (Join-Path $dest ".ok") -NoNewline ` + -Value "staged by installer/build.ps1 -WithVendorSdks from $($v.path)" + $mb = [math]::Round((Get-ChildItem $dest -Recurse -File | Measure-Object Length -Sum).Sum / 1MB, 1) + Write-Host " $($v.name)-$($versions[$v.name]): $mb MB" + } +} + +function Verify-VendorCache { + # A cache the loader disagrees with degrades to a silent re-download, so assert the + # two invariants that would cause it: the .ok marker, and a flattened layout. + $dirs = Get-ChildItem $vendorCacheDir -Directory + if ($dirs.Count -eq 0) { throw "vendor cache is empty" } + foreach ($d in $dirs) { + if (-not (Test-Path (Join-Path $d.FullName ".ok"))) { throw "$($d.Name): no .ok marker" } + if (Test-Path (Join-Path $d.FullName "vendor_api")) { throw "$($d.Name): vendor_api not flattened" } + if (-not (Get-ChildItem $d.FullName -File -Filter *.dll)) { throw "$($d.Name): no DLLs at top level" } + } + Write-Host " verified $($dirs.Count) cache directories" +} + +if ($WithVendorSdks) { + Write-Host "`n==> vendor SDK cache (-WithVendorSdks)" -ForegroundColor Cyan + Build-VendorCache + Verify-VendorCache +} # 4. Cache the .NET 10 desktop runtime EXE (bundled into Setup.exe). The filename carries # the major version on purpose: the old version-agnostic name meant a machine holding a @@ -203,12 +361,13 @@ $iss = Join-Path $installerDir "Setup.iss" function Invoke-Iscc { param( [string] $OutputBaseFilename, - [string[]] $ExtraDefines = @() + [string[]] $ExtraDefines = @(), + [string] $Staging = $stagingDir ) Write-Host "`n==> ISCC compile: $OutputBaseFilename" -ForegroundColor Cyan $args = @( "/Q", - "/DStagingDir=$stagingDir", + "/DStagingDir=$Staging", "/DOutputDir=$outDir", "/DOutputBaseFilename=$OutputBaseFilename", "/DMyAppVersion=$appVersion" @@ -221,11 +380,20 @@ function Invoke-Iscc { # without overwriting each other (releases, nightlies, cherry-pick verifications, # etc. all drop side-by-side into installer/build/ instead of clobbering the # previous run's artifact). -$bundledName = "ProteoWizard-Sharp-Setup-$appVersion" -$lightName = "ProteoWizard-Sharp-NoNetRuntime-Setup-$appVersion" +$bundledName = "ProteoWizard-Setup-$appVersion" +$lightName = "ProteoWizard-NoNetRuntime-Setup-$appVersion" Invoke-Iscc -OutputBaseFilename $bundledName Invoke-Iscc -OutputBaseFilename $lightName -ExtraDefines @("/DNoNetRuntime") +# Pass 3 (opt-in): bundled runtime + the vendor SDK cache. Built on the bundled-runtime +# variant rather than the light one because its whole reason to exist is deployments that +# cannot reach the network, and those cannot fetch a .NET runtime either. +$vendorName = "ProteoWizard-WithVendorSdks-Setup-$appVersion" +if ($WithVendorSdks) { + Invoke-Iscc -OutputBaseFilename $vendorName -Staging $stagingDir ` + -ExtraDefines @("/DWithVendorSdks", "/DVendorCacheDir=$vendorCacheDir") +} + # Write the resolved version next to the .exes so Installer.Tests can pin to it # without re-deriving from the filename (the date+sha format is build.ps1's # internal convention, not a public contract). @@ -233,7 +401,9 @@ Set-Content -Path (Join-Path $outDir "installer-version.txt") -Value $appVersion # 7. Report. Write-Host "" -foreach ($base in @($bundledName, $lightName)) { +$reportNames = @($bundledName, $lightName) +if ($WithVendorSdks) { $reportNames += $vendorName } +foreach ($base in $reportNames) { $setupPath = Join-Path $outDir "$base.exe" if (-not (Test-Path $setupPath)) { Write-Host "MISSING: $setupPath" -ForegroundColor Red diff --git a/pwiz-sharp/pwiz/test/Installer.Tests/InstallerTests.cs b/pwiz-sharp/pwiz/test/Installer.Tests/InstallerTests.cs index 42113b2603..287b224f14 100644 --- a/pwiz-sharp/pwiz/test/Installer.Tests/InstallerTests.cs +++ b/pwiz-sharp/pwiz/test/Installer.Tests/InstallerTests.cs @@ -11,9 +11,9 @@ namespace Pwiz.Installer.Tests; /// -/// End-to-end tests for the Inno Setup-built ProteoWizard-Sharp-Setup.exe. -/// Each test runs a silent install, verifies file deployment + registry state + -/// a quick CLI smoke (msconvert.exe --help), then silently +/// End-to-end tests for the Inno Setup-built ProteoWizard-Setup.exe. +/// Each test runs a silent install, verifies file deployment + registry state, converts +/// one real fixture per vendor through the installed msconvert.exe, then silently /// uninstalls and verifies cleanup. /// /// Test policy: @@ -133,7 +133,7 @@ public void Install_PerMachine_DeploysAndConvertsVendorFile() /// /// Try to locate the built Setup.exe. Search order: PWIZ_INSTALLER_PATH /// env var (absolute path), then the newest - /// pwiz-sharp/installer/build/ProteoWizard-Sharp-Setup-*.exe (newest + /// pwiz-sharp/installer/build/ProteoWizard-Setup-*.exe (newest /// by file mtime, so the most recent build wins when older versioned /// installers are sitting alongside it). The NoNetRuntime variant is /// excluded — these tests target the bundled installer; the lightweight @@ -159,11 +159,11 @@ private static bool TryFindSetup(out string setupPath, out string reason) string buildDir = PwizSharpPaths.InstallerBuildDir; if (Directory.Exists(buildDir)) { - // Versioned bundled-variant filename: ProteoWizard-Sharp-Setup-.exe + // Versioned bundled-variant filename: ProteoWizard-Setup-.exe // Exclude the NoNetRuntime variant by name; pick the newest by mtime so // a fresh build wins over older artifacts in the same folder. var candidates = new DirectoryInfo(buildDir) - .GetFiles("ProteoWizard-Sharp-Setup-*.exe") + .GetFiles("ProteoWizard-Setup-*.exe") .Where(f => !f.Name.Contains("NoNetRuntime", StringComparison.Ordinal)) .OrderByDescending(f => f.LastWriteTimeUtc) .ToArray(); @@ -176,7 +176,7 @@ private static bool TryFindSetup(out string setupPath, out string reason) } setupPath = string.Empty; - reason = $"No ProteoWizard-Sharp-Setup-*.exe found in {buildDir}. " + + reason = $"No ProteoWizard-Setup-*.exe found in {buildDir}. " + "Run `pwsh -File pwiz-sharp/installer/build.ps1` first, " + "or set PWIZ_INSTALLER_PATH to its absolute path."; return false; @@ -399,15 +399,26 @@ private static void AssertRequiredFiles(string installDir) /// private static void AssertMsconvertSmokes(string installDir) { + // One fixture per vendor the installer can read, smallest available in each case. + // Every vendor has to be here: an installed build resolves its SDKs out of the + // VendorSdkLoader cache, a path no dev or CI build takes (those keep the vendor + // DLLs app-local, which the resolver prefers). A vendor missing from this list is + // therefore a vendor whose installed-from-a-package behaviour nothing checks. var fixtures = new[] { - ("Thermo", "FT-HCD-MSX.raw"), // managed SDK - ("Waters", "Minimal_DDA.raw"), // native: MassLynxRaw - ("Bruker", "20percLaser_100fold_1_0_H6_MS.d"), // native: timsdata (TSF) - ("Bruker", "CsI_Pos_0_G1_000003.d"), // native: baf2sql_c (BAF) + ("Thermo", "FT-HCD-MSX.raw"), // managed SDK + ("Waters", "Minimal_DDA.raw"), // native: MassLynxRaw + ("Bruker", "20percLaser_100fold_1_0_H6_MS.d"), // native: timsdata (TSF) + ("Bruker", "CsI_Pos_0_G1_000003.d"), // native: baf2sql_c (BAF) + ("Agilent", "Neg_MS_002_1scan.d"), // managed MHDAC + mixed-mode BaseTof + ("ABI", "swath.api.wiff2"), // managed Clearcore2 / SCIEX.Apis + ("Shimadzu", "10nmol_Negative_MS_ID_ON_055.lcd"), // native: IOModuleQTFL (MFC140) + ("Mobilion", "ExampleTuneMix_binned5.mbi"), // native: MBI_SDK via MobilionShim + ("UIMF", "BSA_10ugml_CID.UIMF"), // managed UIMFLibrary + SQLite }; var skipped = new List(); + var failures = new List(); int converted = 0; foreach ((string vendor, string name) in fixtures) { @@ -416,14 +427,29 @@ private static void AssertMsconvertSmokes(string installDir) skipped.Add(reason); continue; } - AssertMsconvertConverts(installDir, path); - converted++; + // Keep going after a failure and report every broken vendor at once. Throwing on + // the first one hides the rest behind whichever happens to be listed earliest, + // and these failures are per-vendor independent — one broken SDK resolution says + // nothing about the next. + try + { + AssertMsconvertConverts(installDir, path); + converted++; + } + catch (AssertFailedException ex) + { + failures.Add($"{vendor}/{name}: {ex.Message}"); + } } // Inconclusive only when nothing at all could run. Skipping an individual vendor whose // data is not checked out must not silently pass off the others as unverified. - if (converted == 0) + if (converted == 0 && failures.Count == 0) Assert.Inconclusive("No vendor fixtures available:\n " + string.Join("\n ", skipped)); + + if (failures.Count > 0) + Assert.Fail($"{failures.Count} of {failures.Count + converted} vendor fixtures failed " + + $"to convert from the installed build:\n\n" + string.Join("\n\n", failures)); } private static void AssertMsconvertConverts(string installDir, string fixturePath) diff --git a/pwiz-sharp/tcbuild.bat b/pwiz-sharp/tcbuild.bat index 25a6bdebfb..6601400783 100644 --- a/pwiz-sharp/tcbuild.bat +++ b/pwiz-sharp/tcbuild.bat @@ -22,7 +22,8 @@ REM # Usage: REM # tcbuild.bat [Debug|Release] [--i-agree-to-the-vendor-licenses] REM # [--require-vendor-support] [--automated] REM # -REM # Args are forwarded verbatim to build.bat; see that script for flag +REM # Args are forwarded verbatim to build.bat, plus --with-vendor-sdks; see that +REM # script for flag REM # semantics. TC should pass --i-agree-to-the-vendor-licenses REM # --require-vendor-support --automated for the standard CI run. REM # @@ -84,8 +85,14 @@ call "%SCRIPT_DIR%\clean.bat" set EXIT=%ERRORLEVEL% if %EXIT% NEQ 0 (set "ERROR_TEXT=clean.bat failed" & goto error) -echo ##teamcity[progressMessage 'pwiz-sharp build.bat %*'] -call "%SCRIPT_DIR%\build.bat" %* +REM # --with-vendor-sdks is added here rather than left to the caller: the +REM # vendor-bundled installer is a CI deliverable (the wine container installs +REM # it, and it is the only variant that needs no network at run time), while a +REM # developer running build.bat by hand should not pay ~30 s and ~26 MB for it. +REM # Appending after %* is safe — build.bat parses flags in any order, and the +REM # flag is inert unless --i-agree-to-the-vendor-licenses was also passed. +echo ##teamcity[progressMessage 'pwiz-sharp build.bat %* --with-vendor-sdks'] +call "%SCRIPT_DIR%\build.bat" %* --with-vendor-sdks set EXIT=%ERRORLEVEL% if %EXIT% NEQ 0 (set "ERROR_TEXT=build.bat failed" & goto error)