diff --git a/build.sbt b/build.sbt index 807c6178..22c7824b 100644 --- a/build.sbt +++ b/build.sbt @@ -20,7 +20,7 @@ ThisBuild / organization := "app.softnetwork" name := "softclient4es" -ThisBuild / version := "0.20.1" +ThisBuild / version := "0.20.2-SNAPSHOT" ThisBuild / scalaVersion := scala213 diff --git a/core/src/main/scala/app/softnetwork/elastic/client/repl/BundleInfo.scala b/core/src/main/scala/app/softnetwork/elastic/client/repl/BundleInfo.scala new file mode 100644 index 00000000..dd87bb77 --- /dev/null +++ b/core/src/main/scala/app/softnetwork/elastic/client/repl/BundleInfo.scala @@ -0,0 +1,89 @@ +/* + * Copyright 2025 SOFTNETWORK + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package app.softnetwork.elastic.client.repl + +import java.util.Properties + +import scala.util.Try + +/** Bundle provenance disclosure (REPL.4 / #163 fix 4). + * + * The self-contained `-all` assemblies built by the softclient4es-repl packaging repo carry a + * `softclient4es-bundle-info.properties` resource at the jar root, stamping the bundle version and + * the exact pinned engine / extension versions. When that resource is present on the classpath, + * the REPL banner and the `version` meta-command disclose the bundle provenance. Plain installs + * and sbt runs have no such resource — the surface stays silent. + */ +object BundleInfo { + + val ResourceName: String = "softclient4es-bundle-info.properties" + + final case class Bundle( + bundleVersion: String, + engineVersion: String, + communityExtensionsVersion: String, + arrowExtensionsVersion: String, + gitSha: Option[String], + javaFloor: Option[String] + ) { + + /** Full disclosure line (spec wording — `version` meta-command). */ + def summary: String = + s"Bundle $bundleVersion (engine $engineVersion, " + + s"community $communityExtensionsVersion, arrow-ext $arrowExtensionsVersion)" + + /** Compact form for the fixed-width welcome banner (must stay within 56 visible chars). */ + def bannerLine: String = + s"Bundle $bundleVersion (engine $engineVersion, " + + s"ext $communityExtensionsVersion + $arrowExtensionsVersion)" + } + + /** Bundle info from the runtime classpath, if any. */ + lazy val fromClasspath: Option[Bundle] = load(getClass.getClassLoader) + + /** Test seam: load the bundle-info resource from an explicit ClassLoader. */ + private[repl] def load(classLoader: ClassLoader): Option[Bundle] = + Option(classLoader.getResourceAsStream(ResourceName)).flatMap { is => + try { + val props = new Properties() + props.load(is) + parse(props) + } catch { + case _: Exception => None + } finally { + Try(is.close()) + } + } + + private[repl] def parse(props: Properties): Option[Bundle] = { + def get(key: String): Option[String] = + Option(props.getProperty(key)).map(_.trim).filter(_.nonEmpty) + for { + bundle <- get("bundle.version") + engine <- get("engine.version") + community <- get("community.extensions.version") + arrowExt <- get("arrow.extensions.version") + } yield Bundle( + bundleVersion = bundle, + engineVersion = engine, + communityExtensionsVersion = community, + arrowExtensionsVersion = arrowExt, + gitSha = get("bundle.git.sha"), + javaFloor = get("java.floor") + ) + } +} diff --git a/core/src/main/scala/app/softnetwork/elastic/client/repl/Repl.scala b/core/src/main/scala/app/softnetwork/elastic/client/repl/Repl.scala index fb5ca1fd..9b5c2eee 100644 --- a/core/src/main/scala/app/softnetwork/elastic/client/repl/Repl.scala +++ b/core/src/main/scala/app/softnetwork/elastic/client/repl/Repl.scala @@ -291,6 +291,7 @@ class Repl( private lazy val metaCommands: Set[String] = Set( "help", + "version", "quit", "exit", "tables", @@ -319,6 +320,9 @@ class Repl( case "help" => handleHelp(args) + case "version" => + printVersionInfo() + case "quit" | "exit" => running = false @@ -419,6 +423,9 @@ class Repl( case "h" | "help" => handleHelp(args) + case "version" => + printVersionInfo() + case "q" | "quit" => running = false @@ -629,19 +636,37 @@ class Repl( private def printWelcomeBanner(): Unit = { val name = s"║ ${formatLigne(bold(cyan("SoftClient4ES CLI")), 56)} ║" val ver = s"║ ${formatLigne(gray(s"Version $version"), 56)} ║" + // REPL.4 (#163 fix 4): -all bundle installs disclose their provenance right + // under the version line; plain installs have no bundle-info resource and + // the banner is unchanged. Same fixed-width box — always via formatLigne. + val bundleLine: Option[String] = + BundleInfo.fromClasspath.map(b => s"║ ${formatLigne(gray(b.bannerLine), 56)} ║") val help = s"║ ${formatLigne(s"Type ${yellow("help")} for available commands", 56)} ║" val quit = s"║ ${formatLigne(s"Type ${yellow("quit")} to exit", 56)} ║" - println( - s""" - |╔═══════════════════════════════════════════════════════════╗ - |$name - |$ver - |║ ║ - |$help - |$quit - |╚═══════════════════════════════════════════════════════════╝ - |""".stripMargin - ) + val lines = + Seq( + "╔═══════════════════════════════════════════════════════════╗", + name, + ver + ) ++ bundleLine ++ Seq( + "║ ║", + help, + quit, + "╚═══════════════════════════════════════════════════════════╝" + ) + println(lines.mkString("\n", "\n", "\n")) + } + + private def printVersionInfo(): Unit = { + println(s"${bold(cyan("SoftClient4ES CLI"))} ${gray(s"version $version")}") + BundleInfo.fromClasspath.foreach { b => + println(s"${cyan(b.summary)}") + println(s" engine: ${b.engineVersion}") + println(s" community extensions: ${b.communityExtensionsVersion}") + println(s" arrow extensions: ${b.arrowExtensionsVersion}") + b.gitSha.foreach(sha => println(s" bundle git SHA: $sha")) + b.javaFloor.foreach(floor => println(s" java floor: $floor+")) + } } private def formatLigne(value: String, size: Int): String = { @@ -671,6 +696,7 @@ class Repl( |${bold(cyan("Meta Commands:"))} | ${yellow("help")} Show this help | ${yellow("help ")} Show help for SQL command or function + | ${yellow("version")} Show engine (and bundle) version info | ${yellow("quit")} Exit the REPL | ${yellow("tables")} List all tables | ${yellow("pipelines")} List all pipelines diff --git a/core/src/test/scala/app/softnetwork/elastic/client/repl/BundleInfoSpec.scala b/core/src/test/scala/app/softnetwork/elastic/client/repl/BundleInfoSpec.scala new file mode 100644 index 00000000..d691b83a --- /dev/null +++ b/core/src/test/scala/app/softnetwork/elastic/client/repl/BundleInfoSpec.scala @@ -0,0 +1,120 @@ +package app.softnetwork.elastic.client.repl + +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import java.io.{ByteArrayInputStream, InputStream} +import java.nio.charset.StandardCharsets +import java.util.Properties + +/** Unit tests for BundleInfo (REPL.4 / #163 fix 4). + * + * The resource presence/absence is injected through a test ClassLoader — no test-scoped resource + * copy (and especially no `core/src/test/resources/application.conf` — REPL.2 finding). + */ +class BundleInfoSpec extends AnyWordSpec with Matchers { + + private def classLoaderWith(content: Option[String]): ClassLoader = + new ClassLoader(null) { + override def getResourceAsStream(name: String): InputStream = + content match { + case Some(text) if name == BundleInfo.ResourceName => + new ByteArrayInputStream(text.getBytes(StandardCharsets.UTF_8)) + case _ => null + } + } + + private val fullProperties: String = + """bundle.version=0.20.2 + |engine.version=0.20.1 + |community.extensions.version=0.2.1 + |arrow.extensions.version=0.2.2 + |bundle.git.sha=abcdef1234567890 + |java.floor=11 + |""".stripMargin + + "BundleInfo.load" should { + + "return None when the bundle-info resource is absent (plain installs, sbt runs)" in { + BundleInfo.load(classLoaderWith(None)) shouldBe None + } + + "parse a complete bundle-info resource" in { + val bundle = BundleInfo.load(classLoaderWith(Some(fullProperties))) + bundle shouldBe defined + bundle.get.bundleVersion shouldBe "0.20.2" + bundle.get.engineVersion shouldBe "0.20.1" + bundle.get.communityExtensionsVersion shouldBe "0.2.1" + bundle.get.arrowExtensionsVersion shouldBe "0.2.2" + bundle.get.gitSha shouldBe Some("abcdef1234567890") + bundle.get.javaFloor shouldBe Some("11") + } + + "tolerate missing optional keys (git SHA, java floor)" in { + val minimal = + """bundle.version=0.20.2 + |engine.version=0.20.1 + |community.extensions.version=0.2.1 + |arrow.extensions.version=0.2.2 + |""".stripMargin + val bundle = BundleInfo.load(classLoaderWith(Some(minimal))) + bundle shouldBe defined + bundle.get.gitSha shouldBe None + bundle.get.javaFloor shouldBe None + } + + "return None when a mandatory key is missing" in { + val missingEngine = + """bundle.version=0.20.2 + |community.extensions.version=0.2.1 + |arrow.extensions.version=0.2.2 + |""".stripMargin + BundleInfo.load(classLoaderWith(Some(missingEngine))) shouldBe None + } + + "return None when a mandatory key is blank" in { + val blankEngine = + """bundle.version=0.20.2 + |engine.version= + |community.extensions.version=0.2.1 + |arrow.extensions.version=0.2.2 + |""".stripMargin + BundleInfo.load(classLoaderWith(Some(blankEngine))) shouldBe None + } + } + + "BundleInfo.parse" should { + + "trim values" in { + val props = new Properties() + props.setProperty("bundle.version", " 0.20.2 ") + props.setProperty("engine.version", "0.20.1") + props.setProperty("community.extensions.version", "0.2.1") + props.setProperty("arrow.extensions.version", "0.2.2") + val bundle = BundleInfo.parse(props) + bundle shouldBe defined + bundle.get.bundleVersion shouldBe "0.20.2" + } + } + + "BundleInfo.Bundle rendering" should { + + val bundle = BundleInfo.Bundle( + bundleVersion = "0.20.2", + engineVersion = "0.20.1", + communityExtensionsVersion = "0.2.1", + arrowExtensionsVersion = "0.2.2", + gitSha = Some("abcdef1"), + javaFloor = Some("11") + ) + + "produce the spec disclosure line" in { + bundle.summary shouldBe "Bundle 0.20.2 (engine 0.20.1, community 0.2.1, arrow-ext 0.2.2)" + } + + "keep the banner line within the 56-char fixed-width box" in { + bundle.bannerLine.length should be <= 56 + bundle.bannerLine shouldBe "Bundle 0.20.2 (engine 0.20.1, ext 0.2.1 + 0.2.2)" + } + } +} diff --git a/documentation/client/repl.md b/documentation/client/repl.md index c90e2383..d21958c5 100644 --- a/documentation/client/repl.md +++ b/documentation/client/repl.md @@ -111,13 +111,13 @@ Before installing, you can list all available versions for a specific Elasticsea Available SoftClient4ES Versions for Elasticsearch 8 ═══════════════════════════════════════════════════════════════ - Artifact: softclient4es8-cli_2.13 - Java required: 8+ + Artifact: softclient4es8-cli-all_2.13 + Java required: 11+ Versions: - • 0.16.0-SNAPSHOT - • 0.16.0 + • 0.20.2 + • 0.20.3 Total: 2 version(s) @@ -197,8 +197,9 @@ softclient4es/ │ ├── application.conf # Application configuration │ └── logback.xml # Logging configuration ├── lib/ -│ ├── softclient4es8-cli_2.13-x.y.z-assembly.jar -│ └── ... # extension jars + their dependencies (see Extensions) +│ └── softclient4es8-cli-all_2.13-x.y.z-assembly.jar # self-contained -all bundle +│ # (fallback/--no-extensions installs: plain cli jar +│ # + extension jars and dependencies, see Extensions) ├── logs/ # Log files directory │ └── softclient4es.log # (created at runtime) ├── LICENSE @@ -207,16 +208,53 @@ softclient4es/ └── uninstall.sh ``` +### Default install: the self-contained `-all` bundle + +Two REPL artifacts are published per Elasticsearch version: + +| Artifact | Contents | License | +|---|---|---| +| `softclient4es{N}-cli` (plain) | the engine assembly only | pure **Apache-2.0** | +| `softclient4es{N}-cli-all` (bundle) | engine + community extensions + arrow JOIN extension + **all** their dependencies in ONE jar | combined — see below | + +**The default install downloads the ONE self-contained `-all` bundle** — a single +download, an empty `lib/`, no install-time dependency resolution. Everything — +cross-index JOINs, materialized views, quota enforcement — works out of the box. + +The `-all` bundle has its **own version line** (it can be re-released for an +extension hotfix without a new engine release): + +- `--list-versions` lists **bundle** versions by default, and **engine** versions + with `--no-extensions` (the output names the artifact listed); +- `-v ` selects a **bundle** version on the default path; an engine-only + version (one with no matching bundle) falls back to the plain artifact plus the + coursier-based extensions resolution described below. + +The REPL welcome banner and the `version` meta-command disclose the bundle +provenance (bundle version + the exact engine / extension versions), which the jar +also carries in its `MANIFEST.MF` and `softclient4es-bundle-info.properties`. + +**License** — the `-all` bundle contains the Apache-2.0 SoftClient4ES engine PLUS +SoftClient4ES extensions under the **Elastic License 2.0** and the **proprietary +cross-index JOIN engine** (free to use; see `licenses/` inside the jar and the +`NOTICE` file). The bundle as a whole is therefore **not** a pure Apache-2.0 +artifact. Quota enforcement is active out of the box. For a pure Apache-2.0 +install, use `--no-extensions`. Bundle bugs are reported on the +[SoftClient4ES issue tracker](https://github.com/SOFTNETWORK-APP/SoftClient4ES/issues). + ### Extensions (cross-index JOINs, materialized views) -Cross-index `JOIN`s and materialized views are delivered as **extensions** — separate -jars discovered on the classpath through the `ExtensionSpi` ServiceLoader mechanism: +Cross-index `JOIN`s and materialized views are delivered as **extensions** — jars +discovered on the classpath through the `ExtensionSpi` ServiceLoader mechanism: - `softclient4es-arrow-extensions` — the cross-index JOIN engine (**required for `SELECT … JOIN` across indices** — the same engine the JDBC driver embeds) - `softclient4es-community-extensions` — materialized views and quota enforcement -**The installer sets these up by default** — no manual step: +On the default path both ship **inside the `-all` bundle** (see above). The +coursier-based resolution below is the **fallback path** — used when no bundle is +published for the selected version, for engine-only `-v` selections, and for +pre-0.20 engines: - **community extensions**: always installed — any engine version (matching 0.1.x line for engines < 0.20) @@ -228,10 +266,10 @@ Each extension is resolved **with its full dependency closure** (Apache Arrow, D The launcher runs the REPL with `java -cp ":lib/*"`, so every jar in `lib/` is visible. -To skip them: +To skip extensions entirely: ```bash -./install.sh --no-extensions # minimal install — JOINs and MVs unavailable +./install.sh --no-extensions # pure Apache-2.0 install — JOINs and MVs unavailable ``` **Manual installation on an existing install** (or air-gapped preparation): diff --git a/install.sh b/install.sh index 2ba51005..fdb95f81 100755 --- a/install.sh +++ b/install.sh @@ -139,17 +139,32 @@ Usage: $0 [OPTIONS] Options: -t, --target Installation directory (default: $DEFAULT_TARGET_DIR) -e, --es-version Elasticsearch major version: 6, 7, 8, 9 (default: $DEFAULT_ES_VERSION) - -v, --version SoftClient4ES version (default: latest) + -v, --version Version to install (default: latest). On the default + path this selects a BUNDLE version (the -all artifact + has its own version line); with --no-extensions (or + when no bundle matches) it selects an ENGINE version. -s, --scala Scala version (default: $DEFAULT_SCALA_VERSION) -l, --list-versions List available versions for the specified ES version - --no-extensions Skip the extensions (cross-index JOINs, materialized views) + (bundle versions by default, engine versions with + --no-extensions) + --no-extensions Install the plain, pure Apache-2.0 engine only + (no cross-index JOINs, no materialized views) -h, --help Show this help message -Extensions (installed by default, with all their dependencies): +Default install = ONE self-contained -all bundle: + engine + community extensions + arrow JOIN extension + all dependencies in a + single jar (no install-time dependency resolution). The bundle contains + components under the Elastic License 2.0 plus the proprietary JOIN engine + (free to use) — it is NOT a pure Apache-2.0 artifact. + Fallback (no bundle published / engine-version -v / Java < 11): the plain + Apache-2.0 engine assembly + extensions resolved via coursier as before. + --no-extensions always yields a pure Apache-2.0 install. + Bundle bugs: https://github.com/SOFTNETWORK-APP/SoftClient4ES/issues + +Extensions on the FALLBACK path (resolved with all their dependencies): - community extensions (materialized views): always — any engine version, Java 8+ - arrow extensions (cross-index JOINs): engine >= 0.20; requires Java 11+ (Apache Arrow constraint) - Use --no-extensions for a minimal install. Java Requirements: ES 6, 7, 8 → Java 11 or higher (the 0.20+ CLI bundles logback 1.5.x = Java-11 bytecode) @@ -158,8 +173,8 @@ Java Requirements: Examples: $0 $0 --list-versions --es-version 8 - $0 --target /opt/softclient4es --es-version 8 --version 0.16-SNAPSHOT - $0 -t ~/tools/softclient4es -e 7 -v 0.20.0 + $0 --target /opt/softclient4es --es-version 8 --no-extensions + $0 -t ~/tools/softclient4es -e 7 -v 0.20.0 --no-extensions Detected OS: $OS_TYPE @@ -231,7 +246,15 @@ fi # Derived Variables # ============================================================================= -ARTIFACT_NAME="softclient4es${ES_VERSION}-cli_${SCALA_VERSION}" +# Plain artifact: the pure Apache-2.0 engine assembly (published by elasticsql). +PLAIN_ARTIFACT_NAME="softclient4es${ES_VERSION}-cli_${SCALA_VERSION}" +# Bundle artifact: the self-contained -all assembly (engine + community +# extensions + arrow JOIN extension + all dependencies), published by the +# softclient4es-repl packaging repo on its OWN version line. +BUNDLE_ARTIFACT_NAME="softclient4es${ES_VERSION}-cli-all_${SCALA_VERSION}" +# ARTIFACT_NAME is finalized by the bundle-selection block below; until then it +# refers to the plain artifact (fallback/legacy paths). +ARTIFACT_NAME="$PLAIN_ARTIFACT_NAME" # ============================================================================= # Get Required Java Version @@ -256,7 +279,7 @@ REQUIRED_JAVA_VERSION=$(get_required_java_version "$ES_VERSION") # ============================================================================= fetch_versions() { - local artifact="${1:-$ARTIFACT_NAME}" + local artifact="${1:-$PLAIN_ARTIFACT_NAME}" local api_url="${JFROG_API_URL}/${artifact}" local response="" @@ -272,7 +295,7 @@ fetch_versions() { if [[ -z "$response" ]]; then error "Failed to fetch versions from repository" error "URL: $api_url" - error "Artifact: $ARTIFACT_NAME" + error "Artifact: $artifact" return 1 fi @@ -287,11 +310,26 @@ fetch_versions() { list_available_versions() { info "Fetching available versions for ES$ES_VERSION..." - local versions - versions=$(fetch_versions) + # List the versions of the artifact the install would actually download: + # the -all bundle by default (its OWN version line), the plain artifact + # under --no-extensions or when no bundle is published. + local listed_artifact versions + if [[ "$WITH_EXTENSIONS" == true ]]; then + listed_artifact="$BUNDLE_ARTIFACT_NAME" + # `|| true`: an empty bundle listing must fall through (set -e is active) + versions=$(fetch_versions "$listed_artifact" 2>/dev/null || true) + if [[ -z "$versions" ]]; then + warn "No -all bundle versions found for $listed_artifact — listing the plain artifact instead" + listed_artifact="$PLAIN_ARTIFACT_NAME" + versions=$(fetch_versions "$listed_artifact") + fi + else + listed_artifact="$PLAIN_ARTIFACT_NAME" + versions=$(fetch_versions "$listed_artifact") + fi if [[ -z "$versions" ]]; then - error "No versions found for $ARTIFACT_NAME" + error "No versions found for $listed_artifact" exit 1 fi @@ -300,7 +338,7 @@ list_available_versions() { echo -e "${CYAN} Available SoftClient4ES Versions for Elasticsearch $ES_VERSION${NC}" echo -e "${CYAN}═══════════════════════════════════════════════════════════════${NC}" echo "" - echo -e " ${YELLOW}Artifact:${NC} $ARTIFACT_NAME" + echo -e " ${YELLOW}Artifact:${NC} $listed_artifact" echo -e " ${YELLOW}Java required:${NC} $REQUIRED_JAVA_VERSION+" echo "" echo -e " ${GREEN}Versions:${NC}" @@ -337,8 +375,11 @@ fi resolve_latest_version() { info "Resolving latest version..." + # Latest of the PLAIN artifact (engine version line) — used on the + # fallback/--no-extensions paths; the bundle path resolves its latest + # from the bundle listing in the bundle-selection block below. local versions - versions=$(fetch_versions) + versions=$(fetch_versions "$PLAIN_ARTIFACT_NAME") if [[ -z "$versions" ]]; then error "Could not fetch versions" @@ -365,8 +406,51 @@ resolve_latest_version() { echo "$latest" } -if [[ "$SOFT_VERSION" == "latest" ]]; then - SOFT_VERSION=$(resolve_latest_version) +# ============================================================================= +# Bundle Selection: default install = ONE self-contained -all assembly +# ============================================================================= +# (This block owns latest-resolution for BOTH paths: the bundle listing first, +# the plain listing on fallback. The version list consulted is always the list +# of the artifact actually downloaded — the bundle has its OWN version line.) + +# Remember whether the user asked for "latest" (needed if the bundle path is +# abandoned later by the existence probe — the plain latest must be re-resolved). +REQUESTED_VERSION="$SOFT_VERSION" + +USE_BUNDLE=false +if [[ "$WITH_EXTENSIONS" == true ]]; then + # NOTE: the trailing `|| true` matters — with `set -e` an empty listing + # (no bundles published yet → grep exits 1) must fall back, not abort. + bundle_versions=$(fetch_versions "$BUNDLE_ARTIFACT_NAME" 2>/dev/null | grep -v 'SNAPSHOT' || true) + if [[ -n "$bundle_versions" ]]; then + if [[ "$SOFT_VERSION" == "latest" ]]; then + SOFT_VERSION=$(echo "$bundle_versions" | tail -1) # bundle-version line (0.20.2, 0.20.3, ...) + USE_BUNDLE=true + success "Resolved latest -all bundle version: $SOFT_VERSION" + elif echo "$bundle_versions" | grep -qx "$SOFT_VERSION"; then + USE_BUNDLE=true # -v selects a BUNDLE version on the default path + else + warn "No -all bundle for version $SOFT_VERSION — falling back to the plain artifact + extensions resolution" + fi + else + warn "No -all bundles published for $BUNDLE_ARTIFACT_NAME — falling back to the plain artifact + extensions resolution" + fi + # Belt-and-braces: the bundle needs Java 11+ (Arrow/logback bytecode); + # check_prerequisites aborts below the ES-version floor anyway. + java_version=$(get_java_version) + if [[ -n "$java_version" ]] && [[ "$java_version" -lt 11 ]]; then + if [[ "$USE_BUNDLE" == true ]]; then + warn "Java $java_version found — the -all bundle requires Java 11+; falling back to the plain artifact" + USE_BUNDLE=false + SOFT_VERSION="$REQUESTED_VERSION" + fi + fi +fi + +if [[ "$USE_BUNDLE" != true ]] && [[ "$SOFT_VERSION" == "latest" ]]; then + # Plain/fallback path: resolve latest from the PLAIN artifact's listing + # (`|| true` keeps set -e from aborting before the empty-check below) + SOFT_VERSION=$(resolve_latest_version || true) if [[ -z "$SOFT_VERSION" ]]; then error "Failed to resolve latest version" error "Try specifying a version manually with --version" @@ -376,9 +460,48 @@ if [[ "$SOFT_VERSION" == "latest" ]]; then success "Resolved latest version: $SOFT_VERSION" fi +if [[ "$USE_BUNDLE" == true ]]; then + ARTIFACT_NAME="$BUNDLE_ARTIFACT_NAME" +else + ARTIFACT_NAME="$PLAIN_ARTIFACT_NAME" +fi JAR_NAME="${ARTIFACT_NAME}-${SOFT_VERSION}-assembly.jar" DOWNLOAD_URL="${JFROG_REPO_URL}/${ARTIFACT_NAME}/${SOFT_VERSION}/${JAR_NAME}" +# ── Existence probe (belt-and-braces over the listing check: covers a pruned +# scala variant or a listing/artifact race). Self-contained — never rely on +# $DOWNLOADER, which is only set later inside check_prerequisites(). +probe_url() { + local url="$1" + if command -v curl &> /dev/null; then + curl -fsI "$url" > /dev/null 2>&1 + elif command -v wget &> /dev/null; then + wget -q --spider "$url" 2>/dev/null + else + return 1 + fi +} + +if [[ "$USE_BUNDLE" == true ]]; then + if ! probe_url "$DOWNLOAD_URL"; then + warn "-all bundle not reachable at $DOWNLOAD_URL" + warn "Falling back to the plain artifact + extensions resolution" + USE_BUNDLE=false + SOFT_VERSION="$REQUESTED_VERSION" + if [[ "$SOFT_VERSION" == "latest" ]]; then + SOFT_VERSION=$(resolve_latest_version || true) + if [[ -z "$SOFT_VERSION" ]]; then + error "Failed to resolve latest version" + exit 1 + fi + success "Resolved latest version: $SOFT_VERSION" + fi + ARTIFACT_NAME="$PLAIN_ARTIFACT_NAME" + JAR_NAME="${ARTIFACT_NAME}-${SOFT_VERSION}-assembly.jar" + DOWNLOAD_URL="${JFROG_REPO_URL}/${ARTIFACT_NAME}/${SOFT_VERSION}/${JAR_NAME}" + fi +fi + # ============================================================================= # Check Prerequisites # ============================================================================= @@ -501,6 +624,36 @@ download_jar() { success "Downloaded to $dest" } +# ============================================================================= +# Extract the licence bundle (bundle installs only) +# The -all jar mixes Apache-2.0 + ELv2 + proprietary: materialize licenses/ +# and NOTICE into the install root so the visible tree is not just the +# Apache-2.0 LICENSE. Failure-tolerant (set -e): the jar keeps the canonical +# copies either way. +# ============================================================================= + +extract_bundle_licenses() { + [[ "$USE_BUNDLE" == true ]] || return 0 + + local jar="$TARGET_DIR/lib/$JAR_NAME" + info "Extracting licence bundle (licenses/ + NOTICE) from $JAR_NAME..." + + if command -v unzip >/dev/null 2>&1; then + if unzip -o -q "$jar" 'licenses/*' NOTICE -d "$TARGET_DIR" 2>/dev/null; then + success "Extracted licenses/ and NOTICE to $TARGET_DIR" + return 0 + fi + elif command -v jar >/dev/null 2>&1; then + if (cd "$TARGET_DIR" && jar -xf "$jar" licenses NOTICE) 2>/dev/null; then + success "Extracted licenses/ and NOTICE to $TARGET_DIR" + return 0 + fi + fi + + warn "Could not extract the licence bundle (unzip/jar unavailable or failed)." + warn "The canonical copies remain inside the jar: licenses/ and NOTICE." +} + # ============================================================================= # Install Extensions (cross-index JOINs, materialized views) # ============================================================================= @@ -556,6 +709,13 @@ install_one_extension() { } install_extensions() { + if [[ "$USE_BUNDLE" == true ]]; then + info "Extensions are bundled inside $JAR_NAME — no dependency resolution needed" + EXTENSIONS_INSTALLED="bundled (community + arrow JOIN)" + extract_bundle_licenses + return 0 + fi + if [[ "$WITH_EXTENSIONS" != true ]]; then info "Skipping extensions (--no-extensions)" return 0 @@ -613,6 +773,32 @@ install_extensions() { fi } +# ============================================================================= +# License Notice (AC 4b) — wording rules: the bundle is "free to use", NEVER +# "open source" / "source-available" / "Apache-2.0" as a whole. +# ============================================================================= + +print_license_notice() { + if [[ "$USE_BUNDLE" == true ]]; then + echo " License: this bundle contains the Apache-2.0 SoftClient4ES engine PLUS" + echo " SoftClient4ES extensions under the Elastic License 2.0 and the proprietary" + echo " cross-index JOIN engine (free to use; see the licenses/ directory and NOTICE" + echo " in the install root — canonical copies ship inside the jar)." + echo " Quota enforcement is active. For a pure Apache-2.0 install re-run with --no-extensions." + elif [[ "$WITH_EXTENSIONS" != true ]] || [[ "$EXTENSIONS_INSTALLED" == "none" ]]; then + echo " License: pure Apache-2.0 engine (no extensions)." + elif [[ "$EXTENSIONS_INSTALLED" == *arrow-extensions* ]]; then + echo " License: this installation contains the Apache-2.0 SoftClient4ES engine PLUS" + echo " SoftClient4ES extensions under the Elastic License 2.0 and the proprietary" + echo " cross-index JOIN engine (free to use). Quota enforcement is active." + echo " For a pure Apache-2.0 install re-run with --no-extensions." + else + echo " License: this installation contains the Apache-2.0 SoftClient4ES engine PLUS" + echo " SoftClient4ES extensions under the Elastic License 2.0 (free to use)." + echo " Quota enforcement is active. For a pure Apache-2.0 install re-run with --no-extensions." + fi +} + # ============================================================================= # Download Documentation and License # ============================================================================= @@ -905,7 +1091,27 @@ UNINSTALL_EOF # ============================================================================= create_version_info() { - cat > "$TARGET_DIR/VERSION" << EOF + if [[ "$USE_BUNDLE" == true ]]; then + # Bundle path: $SOFT_VERSION is the BUNDLE version (its own line); + # the exact engine/extension versions are disclosed by the jar + # MANIFEST and the REPL banner / 'version' command. + cat > "$TARGET_DIR/VERSION" << EOF +SoftClient4ES Installation Info +================================ +Installed: $(date -u +"%Y-%m-%d %H:%M:%S UTC") +Elasticsearch: $ES_VERSION +Version: $SOFT_VERSION (bundle version line) +Note: engine and extension versions are disclosed by the jar + MANIFEST and the REPL banner ('version' command) +Scala: $SCALA_VERSION +Java Required: $REQUIRED_JAVA_VERSION+ +Artifact: $ARTIFACT_NAME +Extensions: $EXTENSIONS_INSTALLED +OS: $OS_TYPE +$(print_license_notice) +EOF + else + cat > "$TARGET_DIR/VERSION" << EOF SoftClient4ES Installation Info ================================ Installed: $(date -u +"%Y-%m-%d %H:%M:%S UTC") @@ -916,7 +1122,9 @@ Java Required: $REQUIRED_JAVA_VERSION+ Artifact: $ARTIFACT_NAME Extensions: $EXTENSIONS_INSTALLED OS: $OS_TYPE +$(print_license_notice) EOF + fi success "Created $TARGET_DIR/VERSION" } @@ -946,18 +1154,31 @@ print_summary() { echo " │ └── logback.xml" echo " ├── lib/" echo " │ ├── $JAR_NAME" - if [[ "$EXTENSIONS_INSTALLED" != "none" ]]; then + if [[ "$USE_BUNDLE" == true ]]; then + echo " │ └── (self-contained -all bundle — no additional jars)" + elif [[ "$EXTENSIONS_INSTALLED" != "none" ]]; then echo " │ └── (+ extension jars: $EXTENSIONS_INSTALLED)" else echo " │ └── (no extensions installed)" fi echo " ├── logs/" echo " │ └── softclient4es.log (created at runtime)" - echo " ├── LICENSE" + if [[ "$USE_BUNDLE" == true ]]; then + echo " ├── licenses/" + echo " │ ├── LICENSE-Apache-2.0.txt" + echo " │ ├── LICENSE-Elastic-2.0.txt" + echo " │ └── NOTICE-arrow-extensions.txt" + echo " ├── LICENSE" + echo " ├── NOTICE" + else + echo " ├── LICENSE" + fi echo " ├── README.md" echo " ├── VERSION" echo " └── uninstall.sh" echo "" + print_license_notice + echo "" echo -e " ${CYAN}Quick Start:${NC}" echo "" echo " # Start the REPL (interactive mode)" @@ -1027,6 +1248,8 @@ main() { create_directories download_jar install_extensions + # AC 4b: state the licence terms right after the download (repeated in the summary) + print_license_notice >&2 download_docs create_config create_logback_config