diff --git a/README.md b/README.md index 9c551001..4868ef0c 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@ curl -fsSL https://raw.githubusercontent.com/SOFTNETWORK-APP/SoftClient4ES/main/ irm https://raw.githubusercontent.com/SOFTNETWORK-APP/SoftClient4ES/main/install.ps1 | iex ``` -**Windows (cmd.exe, when `.ps1` files are blocked):** one file — it fetches `install.ps1` if it is not beside it, takes the same flags, and only launches it with `-ExecutionPolicy Bypass`, for that one process. +**Windows (cmd.exe, when `.ps1` files are blocked):** one file — it fetches `install.ps1` if it is not beside it (from a pinned release tag, SHA-256 verified), takes the same flags, and only launches it with `-ExecutionPolicy Bypass`, for that one process. ```bat curl -O https://raw.githubusercontent.com/SOFTNETWORK-APP/SoftClient4ES/main/install.cmd && install.cmd ``` diff --git a/documentation/client/repl.md b/documentation/client/repl.md index ec4c1c47..a3cbca3f 100644 --- a/documentation/client/repl.md +++ b/documentation/client/repl.md @@ -54,20 +54,33 @@ It provides: **On Windows you do not have to install Java yourself.** Since `0.20.4`, `install.ps1` (and therefore `install.cmd`) resolves Java in this order: -1. `%JAVA_HOME%\bin\java.exe` — when `JAVA_HOME` is set, that is the JVM tested, +1. `\jdk\bin\java.exe` — a JDK a previous run bootstrapped into the + same install directory. It is reused as it stands, so re-running the + installer (to upgrade, to add `-NoExtensions`, to retry a failed download) + never downloads the ~180 MB Temurin zip a second time. +2. `%JAVA_HOME%\bin\java.exe` — when `JAVA_HOME` is set, that is the JVM tested, not whatever `java` happens to be first on `PATH`; the two frequently differ. -2. the `java` on `PATH`, when `JAVA_HOME` is not set. -3. Neither is present, or the one found is **below the floor** for your ES +3. the `java` on `PATH`, when `JAVA_HOME` is not set. +4. None of those is present, or the one found is **below the floor** for your ES version ⇒ the installer downloads a portable **Temurin 17** JDK (a zip from Adoptium, never an MSI, so it needs **no administrator rights**) and unpacks - it to `\jdk`. + it to `\jdk`. The new JDK is unpacked and checked before any + existing one is removed, so a failed download never leaves you without a JVM. Java 17 satisfies both floors, so there is only ever one JDK to think about. The bootstrapped JDK lives **inside the install directory**: `uninstall.ps1` removes it along with everything else, and nothing machine-wide is modified — the installer sets `JAVA_HOME` and `PATH` **for its own session only**. Later sessions do not need them, because the generated launcher applies the same order -and finds `\jdk` by relative path. +and finds `\jdk` by relative path — and refuses to start, naming the +version it found, if that JVM turns out to be below the floor (a stale +`JAVA_HOME` is the usual cause) rather than letting the JVM fail with +`UnsupportedClassVersionError`. + +If an install fails part way through — a version that does not exist, an +unreachable repository — a bootstrapped JDK is left in place on purpose and the +installer says where it is: the next attempt reuses it. Delete the install +directory if you are not going to retry. On Linux and macOS `install.sh` still expects a suitable Java to be present. @@ -126,8 +139,22 @@ install.cmd ``` A local `install.ps1` always wins, so a downloaded pair stays self-consistent — -put both files in the same directory when you want a pinned copy rather than -whatever is on `main`. +put both files in the same directory when you want a specific `install.ps1`. + +What the fallback fetches is **pinned to a release tag, not to `main`**, and is +checked against a SHA-256 recorded in `install.cmd` before it is run; a mismatch +is a hard failure, and the download goes to a fresh temporary directory that is +removed when the run ends. So `install.cmd` on its own installs with the +installer of the release it belongs to, never with whatever is currently on +`main`. To use `main`'s installer instead: + +```bat +set SOFTCLIENT4ES_INSTALL_REF=main +install.cmd +``` + +That drops the integrity check — the pinned hash belongs to the pinned tag — +and `install.cmd` says so unless you also set `SOFTCLIENT4ES_INSTALL_SHA256`. `install.cmd` accepts exactly the flags `install.ps1` does and forwards them verbatim, so every option, default and fallback documented below applies diff --git a/install.cmd b/install.cmd index 3754490e..46dab175 100644 --- a/install.cmd +++ b/install.cmd @@ -11,14 +11,15 @@ rem THIS process only, changes nothing on the machine, and needs no elevation. rem rem It is a wrapper and nothing else - every option, default, fallback and rem message lives in install.ps1, so the two entry points can never drift. -rem When install.ps1 is not sitting next to it, it downloads one, so -rem install.cmd on its own is a complete install. Pass the same flags you -rem would pass to install.ps1: +rem When install.ps1 is not sitting next to it, it downloads one - from a +rem pinned release tag, checked against a pinned SHA-256 before it is run - so +rem install.cmd on its own is a complete install. Pass the same flags you would +rem pass to install.ps1: rem rem install.cmd rem install.cmd -ListVersions -EsVersion 8 rem install.cmd -Target "C:\tools\softclient4es" -EsVersion 8 -Version 1.0.0 -rem install.cmd -EsVersion 7 -Version 0.20.4 -NoExtensions +rem install.cmd -EsVersion 7 -Version 0.21.0 -NoExtensions rem install.cmd -Help rem rem Caveat worth knowing: -ExecutionPolicy Bypass is overridden when the policy @@ -30,8 +31,40 @@ rem =========================================================================== setlocal -set "PS1_URL=https://raw.githubusercontent.com/SOFTNETWORK-APP/SoftClient4ES/refs/heads/main/install.ps1" +rem --------------------------------------------------------------------------- +rem Where a missing install.ps1 is fetched from. +rem +rem PINNED TO A RELEASE TAG, not to main. This file also travels inside release +rem bundles, and one that quietly pulled whatever happens to be on main would +rem produce exactly the mix that the "a local install.ps1 always wins" rule +rem below exists to prevent - silently, since a user who ran a released .cmd has +rem no reason to think they are running main. A tag is also immutable, which is +rem what makes the SHA-256 worth checking at all: the script is about to be run +rem with -ExecutionPolicy Bypass, and "it arrived over TLS" only says the +rem transport was sound, not that the file is the one the release intended. +rem +rem RELEASE RITUAL: bump BOTH lines together when cutting a release. +rem curl -fsSL https://raw.githubusercontent.com/SOFTNETWORK-APP/SoftClient4ES//install.ps1 | shasum -a 256 +rem A mismatch is a hard failure, so a bumped tag with a stale hash breaks the +rem fallback loudly instead of running something unverified. +rem +rem To track development instead: set SOFTCLIENT4ES_INSTALL_REF=main +rem The pinned hash belongs to the pinned tag, so overriding the ref drops the +rem integrity check unless SOFTCLIENT4ES_INSTALL_SHA256 supplies another one - +rem and says so when it does not. +rem --------------------------------------------------------------------------- +set "PS1_REF=v0.21.0" +set "PS1_SHA256=e1275bd269c8bb922d28827b6c74e965bbe7b5139b4e16138bacaac7c03912ae" + +rem One `if` per line, never a parenthesised block: cmd expands every %VAR% in a +rem block in ONE parse pass, before running any line in it. +if defined SOFTCLIENT4ES_INSTALL_REF set "PS1_REF=%SOFTCLIENT4ES_INSTALL_REF%" +if defined SOFTCLIENT4ES_INSTALL_REF set "PS1_SHA256=%SOFTCLIENT4ES_INSTALL_SHA256%" + +set "PS1_URL=https://raw.githubusercontent.com/SOFTNETWORK-APP/SoftClient4ES/%PS1_REF%/install.ps1" set "PS1=%~dp0install.ps1" +set "PS1_TMPDIR=" +set "RC=1" rem A local install.ps1 always wins: a downloaded pair must stay self-consistent, rem and a released bundle must never be silently mixed with main. @@ -40,17 +73,23 @@ rem how cmd binds `||` inside an `if` body is ambiguous, and this is not a rem platform where a subtlety can be settled by running it. if exist "%PS1%" goto run call :fetch_ps1 -if errorlevel 1 exit /b 1 +if errorlevel 1 goto cleanup :run powershell -NoProfile -ExecutionPolicy Bypass -File "%PS1%" %* -exit /b %ERRORLEVEL% +set "RC=%ERRORLEVEL%" + +rem Single exit point, so a downloaded install.ps1 is removed however this ends - +rem including the paths that never got as far as running it. +:cleanup +if defined PS1_TMPDIR if exist "%PS1_TMPDIR%" rd /s /q "%PS1_TMPDIR%" +exit /b %RC% rem --------------------------------------------------------------------------- -rem Each line of a subroutine is parsed when it is reached, so %PS1% below sees -rem the value assigned on the previous line. The same code inside the `if not -rem exist (...)` block above would NOT: cmd expands every %VAR% in a -rem parenthesised block in ONE parse pass, before running any line in it. +rem Each line of a subroutine is parsed when it is reached, so %PS1_TMPDIR% below +rem sees the value assigned on the previous line. The same code inside an `if not +rem exist (...)` block would NOT: cmd expands every %VAR% in a parenthesised block +rem in ONE parse pass, before running any line in it. rem --------------------------------------------------------------------------- :fetch_ps1 where curl.exe >nul 2>&1 @@ -60,7 +99,20 @@ if errorlevel 1 ( echo [ERROR] Download install.ps1 manually into the same directory as install.cmd. 1>&2 exit /b 1 ) -set "PS1=%TEMP%\softclient4es-install.ps1" + +rem A fresh directory of our own, rather than the fixed %TEMP%\softclient4es-install.ps1 +rem this used to write: that was a predictable path in a shared directory, executed +rem with -ExecutionPolicy Bypass, and left behind after every run. mkdir fails when +rem the name already exists, so nothing pre-created can be substituted for it. +set "PS1_TMPDIR=%TEMP%\softclient4es-install-%RANDOM%%RANDOM%" +mkdir "%PS1_TMPDIR%" 2>nul +if errorlevel 1 ( + echo [ERROR] Could not create a temporary directory: %PS1_TMPDIR% 1>&2 + set "PS1_TMPDIR=" + exit /b 1 +) +set "PS1=%PS1_TMPDIR%\install.ps1" + echo [INFO] install.ps1 not found next to install.cmd - downloading it... echo [INFO] URL: %PS1_URL% curl.exe -fsSL -o "%PS1%" "%PS1_URL%" @@ -68,5 +120,46 @@ if errorlevel 1 ( echo [ERROR] Could not download install.ps1 from %PS1_URL% 1>&2 exit /b 1 ) + +call :verify_ps1 +if errorlevel 1 exit /b 1 + echo [INFO] Using %PS1% exit /b 0 + +rem --------------------------------------------------------------------------- +rem Integrity check, before the file is handed to powershell.exe. +rem --------------------------------------------------------------------------- +:verify_ps1 +if not defined PS1_SHA256 ( + echo [WARN] SOFTCLIENT4ES_INSTALL_REF is set without SOFTCLIENT4ES_INSTALL_SHA256, 1>&2 + echo [WARN] so the downloaded install.ps1 was NOT checked against a known hash. 1>&2 + exit /b 0 +) + +where certutil.exe >nul 2>&1 +if errorlevel 1 ( + echo [ERROR] certutil.exe is unavailable, so install.ps1 cannot be verified - it was NOT run. 1>&2 + echo [ERROR] Download install.ps1 manually into the same directory as install.cmd. 1>&2 + exit /b 1 +) + +rem certutil prints the digest on the second line. The FOR must set PS1_HASH on a +rem line of its own, and the space-stripping must live OUTSIDE any block, or the +rem one-parse-pass rule above would have it read the previous value. +set "PS1_HASH=" +for /f "skip=1 tokens=* delims=" %%h in ('certutil -hashfile "%PS1%" SHA256') do if not defined PS1_HASH set "PS1_HASH=%%h" +rem Older certutil builds print the digest as space-separated byte pairs. +set "PS1_HASH=%PS1_HASH: =%" + +if /i not "%PS1_HASH%"=="%PS1_SHA256%" ( + echo [ERROR] install.ps1 failed its integrity check - it was NOT run. 1>&2 + echo [ERROR] expected: %PS1_SHA256% 1>&2 + echo [ERROR] actual: %PS1_HASH% 1>&2 + echo [ERROR] url: %PS1_URL% 1>&2 + echo [ERROR] Download install.ps1 manually into the same directory as install.cmd. 1>&2 + exit /b 1 +) + +echo [INFO] install.ps1 SHA-256 verified against %PS1_REF% +exit /b 0 diff --git a/install.ps1 b/install.ps1 index 7a43fcdd..ab044cac 100644 --- a/install.ps1 +++ b/install.ps1 @@ -174,11 +174,28 @@ $EMBEDDED_JDK_DIR = Join-Path $Target "jdk" # `java`, which is frequently a different and older JVM. # Set by Resolve-Java; read by Check-Prerequisites, the launcher writer and the -# summary. +# summary. $script:EmbeddedJdkHome is also what tells the failure path that this +# install depends on a JDK under $Target. $script:JavaMajor = 0 $script:JavaSource = "not found" $script:EmbeddedJdkHome = $null +# Point this run at a JDK inside the install tree - the one the launcher will use. +# SESSION scope only, deliberately not [Environment]::SetEnvironmentVariable(...,"User"): +# a machine-wide JAVA_HOME would silently repoint every other tool on the box, and +# would dangle after uninstall. Later sessions need nothing - the launcher finds +# \jdk by relative path. +function Use-EmbeddedJdk { + param([string]$JdkHome, [int]$Major) + + $script:EmbeddedJdkHome = $JdkHome + $script:JavaMajor = $Major + $script:JavaSource = "bundled JDK ($JdkHome)" + + $env:JAVA_HOME = $JdkHome + $env:PATH = (Join-Path $JdkHome "bin") + ";" + $env:PATH +} + function Install-EmbeddedJdk { Write-Info "Installing a portable Temurin $BOOTSTRAP_JAVA_VERSION JDK (zip, no administrator rights)..." @@ -198,9 +215,16 @@ function Install-EmbeddedJdk { # to a staging dir and MOVE that one level up, so the final JAVA_HOME is the # fixed path \jdk. The launcher hard-codes `%BASE_DIR%\jdk\bin`, and # it must not have to glob for a name that changes with every Temurin build. + # + # Stage, verify, THEN swap: an existing \jdk is removed only once a + # usable replacement is on disk. The unpack needs the 180 MB zip plus ~300 MB + # expanded plus the ~309 MB jar in the same tree, so running out of disk lands + # squarely in this window, and deleting first left a previously working + # install with no JVM at all — broken by the very installer the user would + # then re-run to fix it (issue #234). $staging is scratch by construction, so + # clearing that one up front is fine. $staging = "$EMBEDDED_JDK_DIR.unpack" - if (Test-Path $staging) { Remove-Item -Recurse -Force $staging } - if (Test-Path $EMBEDDED_JDK_DIR) { Remove-Item -Recurse -Force $EMBEDDED_JDK_DIR } + if (Test-Path $staging) { Remove-Item -Recurse -Force $staging } New-Item -ItemType Directory -Force -Path $staging | Out-Null try { @@ -211,6 +235,13 @@ function Install-EmbeddedJdk { Write-Err "The Temurin archive did not unpack as expected (no directory inside $staging)" return $null } + if (-not (Test-Path (Join-Path (Join-Path $inner.FullName "bin") "java.exe"))) { + Write-Err "The Temurin archive did not unpack as expected (no bin\java.exe under $($inner.FullName))" + return $null + } + + # Known-good from here: the previous JDK, if any, can go. + if (Test-Path $EMBEDDED_JDK_DIR) { Remove-Item -Recurse -Force $EMBEDDED_JDK_DIR } Move-Item -Path $inner.FullName -Destination $EMBEDDED_JDK_DIR } finally { @@ -237,35 +268,58 @@ function Install-EmbeddedJdk { function Resolve-Java { Write-Info "Resolving Java (ES$EsVersion requires ${REQUIRED_JAVA_VERSION}+)..." - # JAVA_HOME first, exactly as the launcher will. Probing the PATH `java` when - # JAVA_HOME is set would validate a JVM the REPL is never going to run. - $javaHomeExe = if ($env:JAVA_HOME) { Join-Path (Join-Path $env:JAVA_HOME "bin") "java.exe" } else { "" } - $jhMajor = if ($javaHomeExe -and (Test-Path $javaHomeExe)) { Get-JavaMajorFromExe -Exe $javaHomeExe } else { 0 } + # \jdk first — the JDK a previous run of this installer bootstrapped, + # and the first thing both generated launchers look at. Without this probe every + # re-run downloads the ~180 MB Temurin zip again and unpacks it over a perfectly + # good JDK (JAVA_HOME is set for the session only, so a later shell has nothing + # pointing at it either), and the installer contradicts the resolution order its + # own launchers document (issue #233). + $bundledExe = Join-Path (Join-Path $EMBEDDED_JDK_DIR "bin") "java.exe" + $bundledMajor = if (Test-Path $bundledExe) { Get-JavaMajorFromExe -Exe $bundledExe } else { 0 } + + if ($bundledMajor -ge $REQUIRED_JAVA_VERSION) { + Use-EmbeddedJdk -JdkHome $EMBEDDED_JDK_DIR -Major $bundledMajor + Write-Success "Java $bundledMajor found via $($script:JavaSource) (required: ${REQUIRED_JAVA_VERSION}+) — nothing to download" + return $true + } - if ($jhMajor -gt 0) { - $script:JavaMajor = $jhMajor - $script:JavaSource = "JAVA_HOME ($env:JAVA_HOME)" + if ($bundledMajor -gt 0) { + # The launcher prefers \jdk whenever java.exe is there, so a bundled + # JDK below the floor cannot be left in place and worked around with JAVA_HOME + # — it has to be replaced. Straight to the bootstrap, which swaps it out. + Write-Warn "The JDK bundled at $EMBEDDED_JDK_DIR is Java $bundledMajor — below the required ${REQUIRED_JAVA_VERSION}+; replacing it" } else { - if ($env:JAVA_HOME) { - Write-Warn "JAVA_HOME is set to '$env:JAVA_HOME' but no usable java.exe was found under it" + # Then JAVA_HOME, exactly as the launcher will. Probing the PATH `java` when + # JAVA_HOME is set would validate a JVM the REPL is never going to run. + $javaHomeExe = if ($env:JAVA_HOME) { Join-Path (Join-Path $env:JAVA_HOME "bin") "java.exe" } else { "" } + $jhMajor = if ($javaHomeExe -and (Test-Path $javaHomeExe)) { Get-JavaMajorFromExe -Exe $javaHomeExe } else { 0 } + + if ($jhMajor -gt 0) { + $script:JavaMajor = $jhMajor + $script:JavaSource = "JAVA_HOME ($env:JAVA_HOME)" } - $pathMajor = Get-JavaMajorVersion - if ($pathMajor -gt 0) { - $script:JavaMajor = $pathMajor - $script:JavaSource = "PATH" + else { + if ($env:JAVA_HOME) { + Write-Warn "JAVA_HOME is set to '$env:JAVA_HOME' but no usable java.exe was found under it" + } + $pathMajor = Get-JavaMajorVersion + if ($pathMajor -gt 0) { + $script:JavaMajor = $pathMajor + $script:JavaSource = "PATH" + } } - } - if ($script:JavaMajor -ge $REQUIRED_JAVA_VERSION) { - Write-Success "Java $($script:JavaMajor) found via $($script:JavaSource) (required: ${REQUIRED_JAVA_VERSION}+)" - return $true - } + if ($script:JavaMajor -ge $REQUIRED_JAVA_VERSION) { + Write-Success "Java $($script:JavaMajor) found via $($script:JavaSource) (required: ${REQUIRED_JAVA_VERSION}+)" + return $true + } - if ($script:JavaMajor -eq 0) { - Write-Warn "No usable Java found" - } else { - Write-Warn "Java $($script:JavaMajor) found via $($script:JavaSource) — below the required ${REQUIRED_JAVA_VERSION}+" + if ($script:JavaMajor -eq 0) { + Write-Warn "No usable Java found" + } else { + Write-Warn "Java $($script:JavaMajor) found via $($script:JavaSource) — below the required ${REQUIRED_JAVA_VERSION}+" + } } $jdkHome = Install-EmbeddedJdk @@ -275,15 +329,7 @@ function Resolve-Java { return $false } - $script:EmbeddedJdkHome = $jdkHome - $script:JavaMajor = Get-JavaMajorFromExe -Exe (Join-Path (Join-Path $jdkHome "bin") "java.exe") - $script:JavaSource = "bundled JDK ($jdkHome)" - - # SESSION scope only — deliberately not [Environment]::SetEnvironmentVariable(...,"User"). - # A machine-wide JAVA_HOME would silently repoint every other tool on the box. - # Future sessions do not need it: the launcher prefers \jdk directly. - $env:JAVA_HOME = $jdkHome - $env:PATH = (Join-Path $jdkHome "bin") + ";" + $env:PATH + Use-EmbeddedJdk -JdkHome $jdkHome -Major (Get-JavaMajorFromExe -Exe (Join-Path (Join-Path $jdkHome "bin") "java.exe")) Write-Success "Java $($script:JavaMajor) ready — JAVA_HOME and PATH updated for THIS session" return $true @@ -293,6 +339,16 @@ function Resolve-Java { # List Available Versions # ============================================================================= +# One listing per artifact per run. The pre-flight below consults the same lists +# the bundle-selection block does, and an HTTP call that has already been answered +# must not be paid for - or answered differently - twice in one run. +$script:VersionListings = @{} + +# Why a listing failed, kept for the pre-flight: -Quiet has to stay silent (a +# missing -all bundle is a normal, expected 404) but a genuine outage must not +# reach the user as a bare "no versions found". +$script:LastListingError = $null + function Get-AvailableVersions { param( # Default to the PLAIN artifact, never $ARTIFACT_NAME: that one is @@ -303,6 +359,10 @@ function Get-AvailableVersions { [switch]$Quiet ) + if ($script:VersionListings.ContainsKey($Artifact)) { + return $script:VersionListings[$Artifact] + } + $apiUrl = "${JFROG_API_URL}/${Artifact}" try { @@ -315,9 +375,15 @@ function Get-AvailableVersions { Where-Object { $_ -notmatch '^\.' } | Sort-Object { [Version]($_ -replace '-SNAPSHOT', '.0' -replace '[^0-9.]', '') } + # Successes only: a listing that failed keeps its own semantics (fail hard, + # or empty under -Quiet) if it is asked for again. + $versions = @($versions) + if ($versions.Count -gt 0) { $script:VersionListings[$Artifact] = $versions } + return $versions } catch { + $script:LastListingError = $_.Exception.Message if ($Quiet) { return @() } Write-Err "Failed to fetch versions from repository" Write-Err "Artifact: $Artifact" @@ -418,11 +484,48 @@ function Resolve-LatestVersion { return @($versions)[-1] } +# ============================================================================= +# Pre-flight: settle the inputs before anything large is downloaded +# ============================================================================= +# Resolving Java can write ~300 MB into $Target, and it used to be the FIRST thing +# to touch the disk - so a typo in -Version, an artifact that does not exist for +# the chosen -EsVersion / -ScalaVersion, or an unreachable repository was only +# discovered by Download-Jar, leaving an orphaned jdk\ behind that nothing in the +# failure output even mentioned (issue #236). The listings are the same two cheap +# calls bundle selection makes below, memoised, so this costs nothing. +function Test-RequestedVersion { + $artifacts = @() + if ($WITH_EXTENSIONS) { $artifacts += $BUNDLE_ARTIFACT_NAME } + $artifacts += $PLAIN_ARTIFACT_NAME + + $known = @() + foreach ($artifact in $artifacts) { + $known += @(Get-AvailableVersions -Artifact $artifact -Quiet) + } + + if ($known.Count -eq 0) { + Write-Err "No versions found for $($artifacts -join ' or ')" + Write-Err "Check -EsVersion $EsVersion and -ScalaVersion $ScalaVersion, and that the repository is reachable:" + Write-Err $JFROG_API_URL + if ($script:LastListingError) { Write-Err $script:LastListingError } + exit 1 + } + + if ($Version -ne "latest" -and $known -notcontains $Version) { + Write-Err "Version '$Version' is not published for $($artifacts -join ' or ')" + Write-Err "Run with -ListVersions to see available versions." + exit 1 + } +} + +Test-RequestedVersion + # ============================================================================= # Resolve Java before anything else that depends on it # ============================================================================= # Runs AFTER the -ListVersions early exit (listing versions must not download a -# JDK) and BEFORE bundle selection, which reads the resolved major. +# JDK), AFTER the pre-flight above (a bad -Version must not cost a JDK download) +# and BEFORE bundle selection, which reads the resolved major. if (-not (Resolve-Java)) { exit 1 } # ============================================================================= @@ -875,6 +978,20 @@ if not defined JVER ( ) for /f "tokens=1,2 delims=." %%a in ("%JVER%") do if "%%a"=="1" (set JAVA_MAJOR=%%b) else (set JAVA_MAJOR=%%a) +REM Refuse a JVM below the floor instead of letting it fail with +REM UnsupportedClassVersionError, which names a class-file version rather than a +REM Java one and reads as a broken install. The .ps1 launcher has always had this +REM check; the .bat computed JAVA_MAJOR and spent it only on --add-opens, so a +REM stale %JAVA_HOME% - which outranks the PATH here, deliberately, because that +REM is the JVM the installer probed - silently won (issue #235). +REM The "not 0" guard keeps parity with the .ps1 launcher: refuse only a version +REM we positively read as too low, never one we failed to parse. +if not "%JAVA_MAJOR%"=="0" if %JAVA_MAJOR% LSS %REQUIRED_JAVA% ( + echo Error: Java %REQUIRED_JAVA%+ is required. Found: Java %JAVA_MAJOR% >&2 + if defined JAVA_HOME echo JAVA_HOME takes precedence over the PATH here - point it at a Java %REQUIRED_JAVA%+ JDK, or clear it. >&2 + exit /b 1 +) + REM The extensions (Apache Arrow / DuckDB) need reflective access on Java 9+. REM JAVA_MAJOR is initialised to 0 above so this comparison always has a left REM operand (an empty one is a cmd syntax error that aborts the whole script). @@ -954,6 +1071,9 @@ try { if (`$javaVersion -gt 0 -and `$javaVersion -lt `$RequiredJava) { Write-Error "Java `$RequiredJava+ is required. Found: Java `$javaVersion" + if (`$env:JAVA_HOME) { + Write-Error "JAVA_HOME takes precedence over the PATH here - point it at a Java `$RequiredJava+ JDK, or clear it." + } exit 1 } } @@ -1144,15 +1264,31 @@ Write-Host " SoftClient4ES Installer" -ForegroundColor Cyan Write-Host "==================================================================" -ForegroundColor Cyan Write-Host "" -Check-Prerequisites -Create-Directories -Download-Jar -Extract-BundleLicenses -Show-LicenseNotice -Download-Docs -Create-Config -Create-LogbackConfig # <-- Création du fichier logback.xml -Create-Launcher -Create-Uninstaller -Create-VersionInfo -Print-Summary +# The finally runs on `exit` too, so it covers every failure path below - which is +# the point: whatever went wrong, the user is told about the JDK sitting in $Target +# rather than discovering ~300 MB of it later (issue #236). +$script:InstallCompleted = $false +try { + Check-Prerequisites + Create-Directories + Download-Jar + Extract-BundleLicenses + Show-LicenseNotice + Download-Docs + Create-Config + Create-LogbackConfig # <-- Création du fichier logback.xml + Create-Launcher + Create-Uninstaller + Create-VersionInfo + Print-Summary + $script:InstallCompleted = $true +} +finally { + if (-not $script:InstallCompleted -and $script:EmbeddedJdkHome) { + Write-Host "" + Write-Warn "The install did not complete. A portable Temurin $BOOTSTRAP_JAVA_VERSION JDK (~300 MB) is at:" + Write-Warn " $($script:EmbeddedJdkHome)" + Write-Warn "It is kept on purpose - a re-run reuses it instead of downloading it again." + Write-Warn "Delete that directory if you are not going to retry." + } +}