From e1ea185d9129214f50c5a2a504edcd830a486ccd Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Tue, 26 May 2026 01:19:17 +0000 Subject: [PATCH 1/2] chore(setup): refresh bundled manifest to SonarQube LTA 2026.1 The bundled runtime manifest was left at the pre-LTA pins after commit 976a8c4 migrated pom.xml to 2026.1: every artifact except sonar-go-plugin was on the old version, and the manifest's `version` field (the cache key for ~/.sonar//) still pointed at the old engine. `sonar setup` would land in the wrong directory and verify against stale SHAs. - manifest.json: bump engine 10.24.0.81415 -> 11.3.0.85510 plus all 10 analyzer pins to match pom.xml's LTA 2026.1 properties. All 11 SHA-256s recomputed from the Maven artifacts. - Manifest.java javadoc: drop the stale reference to daemon/plugins/CHECKSUMS.txt (the file does not exist in the single-module layout) and the "all-zeros placeholder" note (the engine SHA has been real for some time). - SetupCommandTest.stubRunner: fix a latent test seam. Production code keys the layout off Manifest.bundled().version(), but the stub paired that bundled-based layout with the test's injected manifest. Both happened to be "10.24.0.81415" so it worked by coincidence; the bump exposed the inconsistency. Build the layout from the test's manifest via RuntimeLayout.forVersion so the test is isolated from future bundled-manifest bumps. - ManifestTest: bump the two hardcoded version constants the bundled manifest is asserted against. --- .../sonarpredict/cli/setup/Manifest.java | 7 ++-- src/main/resources/manifest.json | 42 +++++++++---------- .../sonarpredict/cli/setup/ManifestTest.java | 4 +- .../cli/setup/SetupCommandTest.java | 8 +++- 4 files changed, 33 insertions(+), 28 deletions(-) diff --git a/src/main/java/io/github/randomcodespace/sonarpredict/cli/setup/Manifest.java b/src/main/java/io/github/randomcodespace/sonarpredict/cli/setup/Manifest.java index 5158321..a31f8a6 100644 --- a/src/main/java/io/github/randomcodespace/sonarpredict/cli/setup/Manifest.java +++ b/src/main/java/io/github/randomcodespace/sonarpredict/cli/setup/Manifest.java @@ -26,10 +26,9 @@ * manifest needs zero new dependencies, whereas a TOML manifest would * pull in a parser library purely for this one file. * - *

Engine checksum. {@code daemon/plugins/CHECKSUMS.txt} pins the ten - * plugin jars; the analysis engine jar is not listed there. The bundled - * manifest carries a placeholder engine SHA-256 (all zeros) until the real - * published checksum is pinned. {@code setup} verifies whatever the manifest + *

Engine checksum. The bundled manifest pins the SHA-256 of every + * artifact — engine and plugin jars alike — taken directly from the Maven + * artifacts that pom.xml resolves. {@code setup} verifies whatever the manifest * declares; tests inject a manifest whose engine checksum matches the fake * artifact they serve. */ diff --git a/src/main/resources/manifest.json b/src/main/resources/manifest.json index 5d98526..96b6326 100644 --- a/src/main/resources/manifest.json +++ b/src/main/resources/manifest.json @@ -1,41 +1,41 @@ { - "version": "10.24.0.81415", + "version": "11.3.0.85510", "engine": { "groupId": "org.sonarsource.sonarlint.core", "artifactId": "sonarlint-analysis-engine", - "version": "10.24.0.81415", - "sha256": "76d746bd5e2fafae22e772532974df20a3ea88cd49166c42e35c9178e6a72372" + "version": "11.3.0.85510", + "sha256": "93a767cdc3314f73fb2df8fe9627e7290d2a9ba9ec2a9f3509adcd4c4ea7a5ee" }, "plugins": [ { "groupId": "org.sonarsource.java", "artifactId": "sonar-java-plugin", - "version": "8.15.0.39343", - "sha256": "59272239c94da5b666448ea72d6447c99659328be6c165131f07d68c490f4354" + "version": "8.29.0.43460", + "sha256": "9074959dc428b8ac4e3600b956c2ce1c9e98d161a89337592a97f4ab92a5c782" }, { "groupId": "org.sonarsource.python", "artifactId": "sonar-python-plugin", - "version": "5.5.0.23291", - "sha256": "a6d3384e44f3bccd5ac10edfc14ace25d5a358452ef53036a1cb70d1832714de" + "version": "5.22.0.33216", + "sha256": "ffa138c6552163c2ee25265a3ef2ec98ae74d1515a390f00f0992775b07303d2" }, { "groupId": "org.sonarsource.javascript", "artifactId": "sonar-javascript-plugin", - "version": "10.24.0.33043", - "sha256": "eeffe334a3b3c615e139327fdf1b7d1db581b4376b2c7a24fe7d1f4a1546f96d" + "version": "12.5.0.41048", + "sha256": "795e529292809afa34f75f587ed1713d3fb4073c84a5402bb5d102d7a510ed0d" }, { "groupId": "org.sonarsource.php", "artifactId": "sonar-php-plugin", - "version": "3.46.0.13151", - "sha256": "a4b0f2889d04ce8ecc96b5bb0cd3bb422ed8870cf62c4ac8de9c61ab881901e3" + "version": "3.57.0.15976", + "sha256": "8fa48b8914902a41bd1b12d4bd08c641b9fec85ee92911f3a28c89957a0deb39" }, { "groupId": "org.sonarsource.kotlin", "artifactId": "sonar-kotlin-plugin", - "version": "3.2.0.7239", - "sha256": "c26c9e59d1df414675f11c7de2a0967dc34fd2724829f1ec2088e4fa9983081a" + "version": "3.6.0.9326", + "sha256": "c093cef45abdc2dc75465c0b593857e3a520b9467a13c5f54192f5a227b9d6ce" }, { "groupId": "org.sonarsource.slang", @@ -46,26 +46,26 @@ { "groupId": "org.sonarsource.slang", "artifactId": "sonar-ruby-plugin", - "version": "1.19.0.471", - "sha256": "b84009dace4770cab106c8a85946f56ff30897005d8b79756932a8549990cafd" + "version": "1.22.0.1992", + "sha256": "1d57a2425cb3a5253b1c9d8c0c174b99e844563cc94ec6a4d783e23bd3b82913" }, { "groupId": "org.sonarsource.slang", "artifactId": "sonar-scala-plugin", - "version": "1.19.0.484", - "sha256": "523368f715e8572b0557ab487e55ade37feb935ccd8927b3e45ba633b5a77d03" + "version": "1.23.0.2394", + "sha256": "b7cd042df5fe886c63856e6c39671be3fd883676d957e72627301cbc12f31419" }, { "groupId": "org.sonarsource.html", "artifactId": "sonar-html-plugin", - "version": "3.19.0.5695", - "sha256": "f34ed7594be94bee1921858a36a5c80505e6d01e9dfaf3419418ab61bd5809aa" + "version": "3.27.0.7699", + "sha256": "2595581d86a7978a1b022d0396159bdae2c09e5d4ce4c2d28b25b09898d91f71" }, { "groupId": "org.sonarsource.xml", "artifactId": "sonar-xml-plugin", - "version": "2.13.0.5938", - "sha256": "8a4ead9b9ab9b9c744cc88e8fca8b25e06f1972fa4d4871712f9863a869e7ec2" + "version": "2.17.0.7895", + "sha256": "ebfa3336bd3ede79b0f4bb94b7aa3fe44e64b8178b6690c6f4fa30e31607b152" } ] } diff --git a/src/test/java/io/github/randomcodespace/sonarpredict/cli/setup/ManifestTest.java b/src/test/java/io/github/randomcodespace/sonarpredict/cli/setup/ManifestTest.java index 4ceca49..34c36de 100644 --- a/src/test/java/io/github/randomcodespace/sonarpredict/cli/setup/ManifestTest.java +++ b/src/test/java/io/github/randomcodespace/sonarpredict/cli/setup/ManifestTest.java @@ -29,7 +29,7 @@ void carriesEveryArtifact() { Manifest manifest = Manifest.bundled(); assertEquals("sonarlint-analysis-engine", manifest.engine().artifactId()); - assertEquals("10.24.0.81415", manifest.engine().version()); + assertEquals("11.3.0.85510", manifest.engine().version()); assertEquals(10, manifest.plugins().size(), "the manifest must pin all ten analyzer plugins"); @@ -55,7 +55,7 @@ void javaPluginCoordinate() { .findFirst() .orElseThrow(); assertEquals("org.sonarsource.java", java.groupId()); - assertEquals("8.15.0.39343", java.version()); + assertEquals("8.29.0.43460", java.version()); } private static void assertArtifactComplete(Manifest.Artifact artifact) { diff --git a/src/test/java/io/github/randomcodespace/sonarpredict/cli/setup/SetupCommandTest.java b/src/test/java/io/github/randomcodespace/sonarpredict/cli/setup/SetupCommandTest.java index 867d8cf..f6fc2e8 100644 --- a/src/test/java/io/github/randomcodespace/sonarpredict/cli/setup/SetupCommandTest.java +++ b/src/test/java/io/github/randomcodespace/sonarpredict/cli/setup/SetupCommandTest.java @@ -85,7 +85,13 @@ private static String sha256(byte[] bytes) throws Exception { private SetupRunner stubRunner(SetupCommand.RunnerInputs inputs) { seenRepoBase.set(inputs.repoBase()); Downloader downloader = new Downloader(); - return new SetupRunner(manifest, inputs.repoBase(), inputs.layout(), + // Production SetupCommand keys the layout off Manifest.bundled().version(), + // but the test injects its own manifest with its own version. Build the + // layout from the test's manifest (sonar.home is set by setupCommandInto, + // so forVersion picks up the test base) so the test is isolated from + // bundled-manifest version bumps. + RuntimeLayout layout = RuntimeLayout.forVersion(manifest.version()); + return new SetupRunner(manifest, inputs.repoBase(), layout, new PluginProvisioner(downloader)); } From 544841330e1557a3d4640b7c41c91dd6394276e6 Mon Sep 17 00:00:00 2001 From: Amit Kumar Date: Tue, 26 May 2026 01:19:37 +0000 Subject: [PATCH 2/2] feat(scripts): add Nexus-bootstrap wrapper for the sonar CLI (sh + ps1) The dist zip already ships bin/sonar (POSIX) and bin/sonar.bat (Windows) that auto-discover a Java 21+ runtime, but they refuse with exit 2 if none is found and they assume the user has the bundle on disk already. For corporate / air-gapped deployments where users do not run the Maven build themselves and do not have a system JDK 21, neither assumption holds. This adds an outer bootstrap wrapper that: 1. Fetches the dist zip from a corporate Nexus raw repo into ~/.sonar-predictor/dist//sonar-predictor/ (idempotent; skipped if the bundle marker bin/sonar already exists). 2. Reuses the same Java 21+ discovery as bin/sonar (JAVA_HOME, PATH, common install paths) so users with a system JDK pay zero JDK download cost. 3. Otherwise fetches a Temurin JDK 21 archive from the same Nexus into ~/.sonar-predictor/jdk/21/-/ and exports JAVA_HOME so the bundle's own launcher picks it up unchanged. 4. exec's the bundle's bin/sonar (POSIX) or bin/sonar.bat (Windows) so signals propagate cleanly. The daemon launches as a child of bin/sonar via DaemonLauncher, so a single entrypoint covers both CLI and daemon use cases. Env contract: SONAR_NEXUS_BASE (required) Nexus raw repo base URL, no trailing slash SONAR_PREDICTOR_VERSION (default 0.3.0-SNAPSHOT) SONAR_PREDICTOR_HOME (default $HOME/.sonar-predictor) SONAR_PREDICTOR_FORCE=1 re-download even if cached Nexus path conventions (one sed if your layout differs): {base}/sonar-predictor/{version}/sonar-predictor-dist-{version}.zip {base}/temurin/21/{os}-{arch}.{tar.gz|zip} POSIX sh: tested with sh and dash syntax checks. PowerShell: tested via the language parser. Smoke tests: - missing SONAR_NEXUS_BASE -> exit 2 with a clear "required" message - unreachable Nexus -> exit 2 with the failing URL surfaced - pre-cached bundle -> hands off to bin/sonar (picocli help renders) Known gap: no checksum verification of the dist zip or JDK archive. The corporate Nexus is the trust boundary; a SHA pin file can be added later for defence-in-depth. --- scripts/sonar-cli.ps1 | 193 +++++++++++++++++++++++++++++++++++++ scripts/sonar-cli.sh | 215 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 408 insertions(+) create mode 100644 scripts/sonar-cli.ps1 create mode 100755 scripts/sonar-cli.sh diff --git a/scripts/sonar-cli.ps1 b/scripts/sonar-cli.ps1 new file mode 100644 index 0000000..73cbd7a --- /dev/null +++ b/scripts/sonar-cli.ps1 @@ -0,0 +1,193 @@ +# sonar-cli.ps1 — bootstrap wrapper for the sonar-predictor distribution (Windows). +# +# Mirror of scripts/sonar-cli.sh for PowerShell 5.1+ / 7+. +# +# Responsibilities (the bundle's own bin\sonar.bat does NOT do these): +# 1. Fetch the dist zip from a corporate Nexus raw repo and extract it into +# $env:USERPROFILE\.sonar-predictor\dist\\sonar-predictor\. +# 2. Ensure a Java 21+ runtime is available. If JAVA_HOME or PATH already +# has one, use it. Otherwise fetch a Temurin JDK 21 zip from the same +# Nexus and cache it under .sonar-predictor\jdk\21\windows-x64\. +# 3. Hand off to the bundle's bin\sonar.bat, which spawns the daemon child. +# +# Required env: +# SONAR_NEXUS_BASE Nexus raw repo base URL (no trailing slash) +# +# Optional env: +# SONAR_PREDICTOR_VERSION Default: 0.3.0-SNAPSHOT +# SONAR_PREDICTOR_HOME Default: $env:USERPROFILE\.sonar-predictor +# SONAR_PREDICTOR_FORCE "1" to re-download even if cached +# +# Nexus path conventions (rename if your layout differs): +# {base}/sonar-predictor/{version}/sonar-predictor-dist-{version}.zip +# {base}/temurin/21/windows-{arch}.zip +# +# Usage: powershell -ExecutionPolicy Bypass -File scripts\sonar-cli.ps1 -- + +[CmdletBinding()] +param( + [Parameter(ValueFromRemainingArguments = $true)] + [string[]] $Args +) + +$ErrorActionPreference = 'Stop' +$ProgressPreference = 'SilentlyContinue' + +function Die([string]$msg) { + Write-Error "sonar-cli: $msg" + exit 2 +} + +# --- Config ---------------------------------------------------------------- +$Version = if ($env:SONAR_PREDICTOR_VERSION) { $env:SONAR_PREDICTOR_VERSION } else { '0.3.0-SNAPSHOT' } +$HomeDir = if ($env:SONAR_PREDICTOR_HOME) { $env:SONAR_PREDICTOR_HOME } else { Join-Path $env:USERPROFILE '.sonar-predictor' } +$MinMajor = 21 + +if (-not $env:SONAR_NEXUS_BASE) { + Die 'SONAR_NEXUS_BASE is required (Nexus raw repo base URL, no trailing slash).' +} +$NexusBase = $env:SONAR_NEXUS_BASE.TrimEnd('/') + +# --- Arch detection -------------------------------------------------------- +# PowerShell on Windows: PROCESSOR_ARCHITECTURE = AMD64 / ARM64 / x86. +$arch = switch ($env:PROCESSOR_ARCHITECTURE) { + 'AMD64' { 'x64' } + 'ARM64' { 'aarch64' } + default { Die "unsupported PROCESSOR_ARCHITECTURE: $($env:PROCESSOR_ARCHITECTURE)." } +} +$Os = 'windows' + +# --- Download helper ------------------------------------------------------- +function Fetch([string]$Url, [string]$Target) { + $dir = Split-Path -Parent $Target + New-Item -ItemType Directory -Force -Path $dir | Out-Null + $tmp = "$Target.partial" + if (Test-Path $tmp) { Remove-Item $tmp -Force } + Write-Host " fetching $Url" + try { + Invoke-WebRequest -Uri $Url -OutFile $tmp -UseBasicParsing -ErrorAction Stop + } catch { + if (Test-Path $tmp) { Remove-Item $tmp -Force } + Die "download failed: $Url ($_)" + } + if (Test-Path $Target) { Remove-Item $Target -Force } + Move-Item $tmp $Target +} + +# --- Bundle --------------------------------------------------------------- +$DistDir = Join-Path $HomeDir "dist\$Version" +$BundleDir = Join-Path $DistDir 'sonar-predictor' +$BundleMarker = Join-Path $BundleDir 'bin\sonar.bat' + +function Ensure-Bundle { + if ($env:SONAR_PREDICTOR_FORCE -ne '1' -and (Test-Path $BundleMarker)) { return } + Write-Host "sonar-cli: provisioning sonar-predictor $Version into $DistDir" + $zip = Join-Path $HomeDir "cache\sonar-predictor-dist-$Version.zip" + $url = "$NexusBase/sonar-predictor/$Version/sonar-predictor-dist-$Version.zip" + Fetch $url $zip + if (Test-Path $DistDir) { Remove-Item $DistDir -Recurse -Force } + New-Item -ItemType Directory -Force -Path $DistDir | Out-Null + try { + Expand-Archive -Path $zip -DestinationPath $DistDir -Force -ErrorAction Stop + } catch { + Die "could not extract ${zip}: $_" + } + if (-not (Test-Path $BundleMarker)) { Die "bundle missing bin\sonar.bat after extract: $BundleDir" } +} + +# --- Java 21+ detection ---------------------------------------------------- +function Get-JavaMajor([string]$Java) { + if (-not (Test-Path $Java)) { return $null } + try { + $out = & $Java -version 2>&1 | Out-String + } catch { return $null } + $line = ($out -split "`r?`n")[0] + if ($line -match 'version "([^"]+)"') { + $v = $Matches[1] + if ($v -like '1.*') { return [int]($v.Split('.')[1]) } # legacy 1.8 -> 8 + return [int]($v.Split('.')[0]) # 17.0.10 -> 17 + } + return $null +} + +function Test-JavaMinPlus([string]$Java) { + $m = Get-JavaMajor $Java + return ($null -ne $m -and $m -ge $MinMajor) +} + +function Find-SystemJava { + if ($env:JAVA_HOME) { + $j = Join-Path $env:JAVA_HOME 'bin\java.exe' + if (Test-JavaMinPlus $j) { return $j } + } + $cmd = Get-Command java.exe -ErrorAction SilentlyContinue + if ($cmd -and (Test-JavaMinPlus $cmd.Source)) { return $cmd.Source } + + # Common Windows install roots. + $roots = @( + "$env:ProgramFiles\Eclipse Adoptium", + "$env:ProgramFiles\Java", + "${env:ProgramFiles(x86)}\Java", + "$env:LOCALAPPDATA\Programs\Eclipse Adoptium" + ) + foreach ($root in $roots) { + if (-not (Test-Path $root)) { continue } + Get-ChildItem -Directory -Path $root -ErrorAction SilentlyContinue | ForEach-Object { + $j = Join-Path $_.FullName 'bin\java.exe' + if (Test-JavaMinPlus $j) { return $j } + } + } + return $null +} + +# --- JDK provisioning (only if no system Java 21+) ------------------------- +$JdkDir = Join-Path $HomeDir "jdk\21\$Os-$arch" +$JdkMarker = Join-Path $JdkDir 'bin\java.exe' + +function Ensure-CachedJdk { + if ((Test-Path $JdkMarker) -and (Test-JavaMinPlus $JdkMarker)) { return } + Write-Host "sonar-cli: provisioning Temurin JDK 21 ($Os-$arch) into $JdkDir" + $archive = Join-Path $HomeDir "cache\temurin-21-$Os-$arch.zip" + $url = "$NexusBase/temurin/21/$Os-$arch.zip" + Fetch $url $archive + + $stage = Join-Path $HomeDir "cache\jdk-stage-$PID" + if (Test-Path $stage) { Remove-Item $stage -Recurse -Force } + New-Item -ItemType Directory -Force -Path $stage | Out-Null + try { + Expand-Archive -Path $archive -DestinationPath $stage -Force -ErrorAction Stop + } catch { + Die "could not extract ${archive}: $_" + } + + # Temurin Windows zips nest one level (jdk-21.0.5+11/). + $root = Get-ChildItem -Directory $stage | Select-Object -First 1 + if (-not $root) { Die "JDK archive appears empty: $archive" } + $jdkHome = $root.FullName + if (-not (Test-Path (Join-Path $jdkHome 'bin\java.exe'))) { + Die "JDK archive missing bin\java.exe: $archive" + } + + if (Test-Path $JdkDir) { Remove-Item $JdkDir -Recurse -Force } + New-Item -ItemType Directory -Force -Path (Split-Path -Parent $JdkDir) | Out-Null + Move-Item $jdkHome $JdkDir + Remove-Item $stage -Recurse -Force + + if (-not (Test-JavaMinPlus $JdkMarker)) { Die "cached JDK is not version $MinMajor+: $JdkMarker" } +} + +# --- Orchestrate ----------------------------------------------------------- +Ensure-Bundle + +$java = Find-SystemJava +if (-not $java) { + Ensure-CachedJdk + $env:JAVA_HOME = $JdkDir + # bundle's bin\sonar.bat discovers Java via JAVA_HOME / PATH; setting + # JAVA_HOME is the lowest-friction handoff. +} + +# --- Hand off -------------------------------------------------------------- +$cmdLine = @($BundleMarker) + ($Args | ForEach-Object { $_ }) +& cmd.exe /c $cmdLine +exit $LASTEXITCODE diff --git a/scripts/sonar-cli.sh b/scripts/sonar-cli.sh new file mode 100755 index 0000000..9705366 --- /dev/null +++ b/scripts/sonar-cli.sh @@ -0,0 +1,215 @@ +#!/bin/sh +# sonar-cli.sh — bootstrap wrapper for the sonar-predictor distribution. +# +# Responsibilities (the bundle's own bin/sonar does NOT do these): +# 1. Fetch the dist zip from a corporate Nexus raw repo and extract it +# into ~/.sonar-predictor/dist//sonar-predictor/. +# 2. Ensure a Java 21+ runtime is available. If JAVA_HOME or PATH already +# has one, use it. Otherwise fetch a Temurin JDK 21 tarball from the +# same Nexus and cache it under ~/.sonar-predictor/jdk/21/. +# 3. Hand off to the bundle's bin/sonar, which spawns the daemon child +# process. Only one entrypoint to invoke — bin/sonar — so this wrapper +# covers both "sonar CLI" and "sonar daemon" use cases. +# +# Required env: +# SONAR_NEXUS_BASE Nexus raw repo base URL (no trailing slash), e.g. +# https://nexus.example.com/repository/raw-internal +# +# Optional env: +# SONAR_PREDICTOR_VERSION Default: 0.3.0-SNAPSHOT +# SONAR_PREDICTOR_HOME Default: $HOME/.sonar-predictor +# SONAR_PREDICTOR_FORCE "1" to re-download even if cached +# +# Nexus path conventions (rename via search-replace if your layout differs): +# {base}/sonar-predictor/{version}/sonar-predictor-dist-{version}.zip +# {base}/temurin/21/{os}-{arch}.tar.gz (linux, mac) +# {base}/temurin/21/{os}-{arch}.zip (windows — handled by .ps1) +# +# POSIX sh — no bashisms. Tested with dash, bash, zsh. +set -eu + +VERSION="${SONAR_PREDICTOR_VERSION:-0.3.0-SNAPSHOT}" +HOME_DIR="${SONAR_PREDICTOR_HOME:-$HOME/.sonar-predictor}" +MIN_JAVA_MAJOR=21 + +# --- Pre-flight ------------------------------------------------------------ +die() { echo "sonar-cli: $*" >&2; exit 2; } + +[ -n "${SONAR_NEXUS_BASE:-}" ] || die "SONAR_NEXUS_BASE is required (Nexus raw repo base URL, no trailing slash)." +NEXUS_BASE="${SONAR_NEXUS_BASE%/}" + +# --- OS / arch detection --------------------------------------------------- +detect_os() { + case "$(uname -s)" in + Linux) echo linux ;; + Darwin) echo mac ;; + *) die "unsupported OS: $(uname -s). On Windows use scripts/sonar-cli.ps1." ;; + esac +} + +detect_arch() { + case "$(uname -m)" in + x86_64|amd64) echo x64 ;; + aarch64|arm64) echo aarch64 ;; + *) die "unsupported arch: $(uname -m). Expected x86_64 or aarch64/arm64." ;; + esac +} + +OS=$(detect_os) +ARCH=$(detect_arch) + +# --- Tool selection -------------------------------------------------------- +# Prefer curl; fall back to wget. Neither = bail (we can't bootstrap offline). +DOWNLOADER="" +if command -v curl >/dev/null 2>&1; then + DOWNLOADER=curl +elif command -v wget >/dev/null 2>&1; then + DOWNLOADER=wget +else + die "neither curl nor wget is on PATH — install one to bootstrap." +fi + +command -v unzip >/dev/null 2>&1 || die "unzip is required (extracts the dist zip)." +command -v tar >/dev/null 2>&1 || die "tar is required (extracts the JDK tarball)." + +# --- Download helpers ------------------------------------------------------ +# fetch URL TARGET — atomic write via .partial; fails loudly on HTTP error. +fetch() { + _url="$1" + _target="$2" + _tmp="${_target}.partial" + mkdir -p "$(dirname "$_target")" + rm -f "$_tmp" + echo " fetching $_url" + case "$DOWNLOADER" in + curl) + curl --fail --silent --show-error --location \ + --output "$_tmp" "$_url" \ + || { rm -f "$_tmp"; die "download failed: $_url"; } + ;; + wget) + wget --quiet --output-document="$_tmp" "$_url" \ + || { rm -f "$_tmp"; die "download failed: $_url"; } + ;; + esac + mv "$_tmp" "$_target" +} + +# --- Bundle: ensure ~/.sonar-predictor/dist//sonar-predictor/bin/sonar -- +DIST_DIR="$HOME_DIR/dist/$VERSION" +BUNDLE_DIR="$DIST_DIR/sonar-predictor" +BUNDLE_MARKER="$BUNDLE_DIR/bin/sonar" + +ensure_bundle() { + if [ "${SONAR_PREDICTOR_FORCE:-}" != "1" ] && [ -x "$BUNDLE_MARKER" ]; then + return 0 + fi + echo "sonar-cli: provisioning sonar-predictor $VERSION into $DIST_DIR" + _zip="$HOME_DIR/cache/sonar-predictor-dist-$VERSION.zip" + _url="$NEXUS_BASE/sonar-predictor/$VERSION/sonar-predictor-dist-$VERSION.zip" + fetch "$_url" "$_zip" + rm -rf "$DIST_DIR" + mkdir -p "$DIST_DIR" + unzip -q "$_zip" -d "$DIST_DIR" || die "could not extract $_zip" + [ -x "$BUNDLE_MARKER" ] || die "bundle missing bin/sonar after extract: $BUNDLE_DIR" +} + +# --- Java 21+ detection (matches bundle's bin/sonar logic) ----------------- +java_major() { + _j="$1" + [ -x "$_j" ] || return 1 + _v=$("$_j" -version 2>&1 | head -n 1 | sed -e 's/.*version "//' -e 's/".*//') + [ -n "$_v" ] || return 1 + case "$_v" in + 1.*) echo "$_v" | cut -d. -f2 ;; # legacy 1.8 -> 8 + *) echo "$_v" | cut -d. -f1 ;; # 17.0.10 -> 17 + esac +} + +is_java_min_plus() { + _m=$(java_major "$1" 2>/dev/null) || return 1 + [ -n "$_m" ] || return 1 + [ "$_m" -ge "$MIN_JAVA_MAJOR" ] 2>/dev/null || return 1 +} + +# Prints a Java 21+ executable path, or nothing if none found in standard +# locations (the same search the bundle's bin/sonar does — kept symmetric so +# a user with a system JDK never pays the JDK-download cost). +find_system_java() { + if [ -n "${JAVA_HOME:-}" ] && is_java_min_plus "$JAVA_HOME/bin/java"; then + echo "$JAVA_HOME/bin/java"; return 0 + fi + _path_java=$(command -v java 2>/dev/null || true) + if [ -n "$_path_java" ] && is_java_min_plus "$_path_java"; then + echo "$_path_java"; return 0 + fi + for _cand in \ + /usr/lib/jvm/*/bin/java \ + /usr/java/*/bin/java \ + /Library/Java/JavaVirtualMachines/*/Contents/Home/bin/java \ + "${HOME:-/nonexistent}"/.sdkman/candidates/java/*/bin/java \ + /opt/java/*/bin/java \ + /opt/*/bin/java + do + [ -x "$_cand" ] || continue + if is_java_min_plus "$_cand"; then + echo "$_cand"; return 0 + fi + done + return 1 +} + +# --- JDK provisioning (only if no system Java 21+) ------------------------- +JDK_DIR="$HOME_DIR/jdk/21/$OS-$ARCH" +JDK_MARKER="$JDK_DIR/bin/java" + +ensure_cached_jdk() { + if [ -x "$JDK_MARKER" ] && is_java_min_plus "$JDK_MARKER"; then + return 0 + fi + echo "sonar-cli: provisioning Temurin JDK 21 ($OS-$ARCH) into $JDK_DIR" + _archive="$HOME_DIR/cache/temurin-21-$OS-$ARCH.tar.gz" + _url="$NEXUS_BASE/temurin/21/$OS-$ARCH.tar.gz" + fetch "$_url" "$_archive" + + # Extract into a staging dir, then move the (single) top-level JDK dir + # contents into $JDK_DIR. Temurin tarballs nest one level (jdk-21.0.5+11/), + # so we strip it. + _stage="$HOME_DIR/cache/jdk-stage-$$" + rm -rf "$_stage" + mkdir -p "$_stage" + tar -xzf "$_archive" -C "$_stage" || die "could not extract $_archive" + + # macOS tarballs have a Contents/Home/ layer; Linux tarballs don't. + _root=$(find "$_stage" -mindepth 1 -maxdepth 1 -type d | head -n 1) + [ -n "$_root" ] || die "JDK tarball appears empty: $_archive" + if [ -d "$_root/Contents/Home" ]; then + _jdk_home="$_root/Contents/Home" + else + _jdk_home="$_root" + fi + [ -x "$_jdk_home/bin/java" ] || die "JDK tarball missing bin/java: $_archive" + + rm -rf "$JDK_DIR" + mkdir -p "$(dirname "$JDK_DIR")" + mv "$_jdk_home" "$JDK_DIR" + rm -rf "$_stage" + + is_java_min_plus "$JDK_MARKER" || die "cached JDK is not version $MIN_JAVA_MAJOR+: $JDK_MARKER" +} + +# --- Orchestrate ----------------------------------------------------------- +ensure_bundle + +JAVA=$(find_system_java || true) +if [ -z "$JAVA" ]; then + ensure_cached_jdk + JAVA_HOME="$JDK_DIR" + export JAVA_HOME + # The bundle's bin/sonar discovers Java via JAVA_HOME / PATH / common + # locations; setting JAVA_HOME is the lowest-friction handoff. +fi + +# --- Hand off -------------------------------------------------------------- +# Quote everything; exec replaces this shell so signals propagate cleanly. +exec "$BUNDLE_MARKER" "$@"