This is an automated email from the ASF dual-hosted git repository.

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new 1a84051451 minor spark upgrade, fixes  #7672 (#7673)
1a84051451 is described below

commit 1a840514514d95a5702e38049536c25bd0018872
Author: Hans Van Akelyen <[email protected]>
AuthorDate: Wed Jul 29 13:06:37 2026 +0200

    minor spark upgrade, fixes  #7672 (#7673)
---
 .dockerignore                                      |   3 +
 docker/build-hop-images.sh                         |  73 +++-
 .../integration-tests-spark-native-cluster.yaml    |   4 +-
 docker/unified.Dockerfile                          |  60 ++-
 .../spark/cluster-mapping-walkthrough.adoc         |   2 +-
 .../spark/getting-started-with-native-spark.adoc   |   4 +-
 .../ROOT/pages/pipeline/spark/lakehouse.adoc       |   2 +-
 lib/pom.xml                                        |  80 +---
 plugins/databases/clickhouse/pom.xml               |   3 +-
 plugins/engines/spark/README.md                    | 455 ---------------------
 plugins/engines/spark/pom.xml                      |  31 +-
 plugins/misc/marketplace/README.md                 |   1 +
 pom.xml                                            |   2 +-
 tools/install-wave1-plugins.sh                     |   8 +
 14 files changed, 163 insertions(+), 565 deletions(-)

diff --git a/.dockerignore b/.dockerignore
index ada15024a7..d96baeafb8 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -60,3 +60,6 @@
 !lib/**
 !lib-jdbc/
 !lib-jdbc/**
+# Build helpers used inside the unified Dockerfile (marketplace-optional 
plugin install)
+!tools/
+!tools/**
diff --git a/docker/build-hop-images.sh b/docker/build-hop-images.sh
index e9e73f9f86..8f04949bc9 100755
--- a/docker/build-hop-images.sh
+++ b/docker/build-hop-images.sh
@@ -40,6 +40,9 @@
 #   --progress <mode>             Docker build output mode: auto (default), 
plain (verbose), tty (compact)
 #   --builder <type>              Builder type: full (Maven, default) or fast 
(pre-built artifacts)
 #   --skip-fat-jar                Skip building the fat jar (auto-detected 
based on images, only needed for dataflow/web-beam)
+#   --with-optional-plugins       Install the marketplace-optional (Wave 1) 
plugins into the image
+#                                 (auto-enabled for dataflow/web-beam, which 
need the Beam engine)
+#   --no-optional-plugins         Never install the marketplace-optional 
plugins
 #   --no-cache                    Build without using cache
 #   -h, --help                    Show this help message
 #
@@ -68,6 +71,12 @@
 #   # Manually skip fat jar generation (saves time if not building 
dataflow/web-beam)
 #   ./build-hop-images.sh --images web,client --skip-fat-jar
 #
+#   # Ship the marketplace-optional (Wave 1) plugins inside the server image
+#   ./build-hop-images.sh --images client --with-optional-plugins
+#
+#   # Same, reusing the plugin zips from a local build instead of rebuilding 
them
+#   ./build-hop-images.sh --images client --with-optional-plugins --builder 
fast
+#
 
################################################################################
 
 set -e
@@ -113,6 +122,7 @@ MAVEN_THREADS="1C"  # Maven thread count (1C, 2C, or 
specific number like 4)
 BUILD_PROGRESS="auto"  # Docker build progress output (auto, plain, tty)
 BUILDER_TYPE="full"  # Builder type: full (Maven build) or fast (pre-built 
artifacts)
 SKIP_FAT_JAR="auto"  # Whether to skip fat jar generation (auto, true, false)
+OPTIONAL_PLUGINS="auto"  # Install marketplace-optional (Wave 1) plugins 
(auto, true, false)
 SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
 PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
 
@@ -204,6 +214,19 @@ needs_fat_jar() {
     return 1
 }
 
+# Function to determine whether a stage needs the marketplace-optional (Wave 
1) plugins.
+# The Beam engine is one of them and it owns --generate-fat-jar, so anything 
built around the
+# fat jar can only work with the optional plugins installed.
+stage_needs_optional_plugins() {
+    local stage_name="$1"
+
+    if [[ "$stage_name" == "dataflow"* ]] || [[ "$stage_name" == "web-beam"* 
]]; then
+        return 0
+    fi
+
+    return 1
+}
+
 # Function to detect version from pom.xml
 detect_version() {
     if [ -f "$PROJECT_ROOT/pom.xml" ]; then
@@ -320,6 +343,14 @@ parse_args() {
                 SKIP_FAT_JAR="true"
                 shift
                 ;;
+            --with-optional-plugins)
+                OPTIONAL_PLUGINS="true"
+                shift
+                ;;
+            --no-optional-plugins)
+                OPTIONAL_PLUGINS="false"
+                shift
+                ;;
             --no-cache)
                 USE_CACHE="false"
                 shift
@@ -407,14 +438,41 @@ build_image() {
     if [[ "$skip_fat_jar_value" == "true" ]]; then
         print_info "Skipping fat jar generation (not needed for $stage_name)"
     fi
-    
+
+    # Determine whether the marketplace-optional plugins go into this specific 
image
+    local optional_plugins_value="false"
+    if [[ "$OPTIONAL_PLUGINS" == "true" ]]; then
+        optional_plugins_value="true"
+    elif [[ "$OPTIONAL_PLUGINS" == "auto" ]]; then
+        # Auto-detect: only images that need the Beam engine get them
+        if stage_needs_optional_plugins "$stage_name"; then
+            optional_plugins_value="true"
+        fi
+    fi
+
+    # The full builder already has the whole reactor; the fast builder needs 
the flavor that
+    # also picks up the locally built plugin zips (see builder-fast-plugins in 
the Dockerfile)
+    local effective_builder="$BUILDER_TYPE"
+    if [[ "$optional_plugins_value" == "true" ]]; then
+        print_info "Installing marketplace-optional (Wave 1) plugins into 
$stage_name"
+        if [[ "$BUILDER_TYPE" == "fast" ]]; then
+            effective_builder="fast-plugins"
+            if ! ls "$PROJECT_ROOT"/plugins/*/*/target/*.zip &> /dev/null; then
+                print_error "No plugin zips found under plugins/*/*/target/"
+                print_error "Run a local build first, or drop 
--with-optional-plugins"
+                exit 1
+            fi
+        fi
+    fi
+
     # Build arguments
     local build_args=(
         "--build-arg" "HOP_BUILD_FROM_SOURCE=$SOURCE_TYPE"
         "--build-arg" "TARGET_IMAGE=$stage_name"
         "--build-arg" "MAVEN_THREADS=$maven_threads"
-        "--build-arg" "BUILDER_TYPE=$BUILDER_TYPE"
+        "--build-arg" "BUILDER_TYPE=$effective_builder"
         "--build-arg" "SKIP_FAT_JAR=$skip_fat_jar_value"
+        "--build-arg" "INCLUDE_OPTIONAL_PLUGINS=$optional_plugins_value"
         "--progress" "$BUILD_PROGRESS"
         "--file" "$SCRIPT_DIR/unified.Dockerfile"
         "--target" "$stage_name"
@@ -528,7 +586,16 @@ build_images() {
     else
         echo "Fat jar:       enabled"
     fi
-    
+
+    # Show marketplace-optional plugin status
+    if [[ "$OPTIONAL_PLUGINS" == "auto" ]]; then
+        echo "Wave1 plugins: auto (only for dataflow/web-beam, which need the 
Beam engine)"
+    elif [[ "$OPTIONAL_PLUGINS" == "true" ]]; then
+        echo "Wave1 plugins: enabled (all marketplace-optional plugins)"
+    else
+        echo "Wave1 plugins: disabled"
+    fi
+
     echo "Push:          $PUSH"
     echo "Cache:         $USE_CACHE"
     echo ""
diff --git 
a/docker/integration-tests/integration-tests-spark-native-cluster.yaml 
b/docker/integration-tests/integration-tests-spark-native-cluster.yaml
index bb9750bfd8..d94d7eb4e8 100644
--- a/docker/integration-tests/integration-tests-spark-native-cluster.yaml
+++ b/docker/integration-tests/integration-tests-spark-native-cluster.yaml
@@ -57,7 +57,7 @@ services:
       context: ./spark
       dockerfile: Dockerfile.master
       args:
-        SPARK_VERSION: ${SPARK_VERSION:-4.1.2}
+        SPARK_VERSION: ${SPARK_VERSION:-4.1.3}
         HADOOP_VERSION: ${HADOOP_VERSION:-3}
         BASE_URL: ${SPARK_BASE_URL:-https://dlcdn.apache.org/spark}
     hostname: spark
@@ -89,7 +89,7 @@ services:
       context: ./spark
       dockerfile: Dockerfile.worker
       args:
-        SPARK_VERSION: ${SPARK_VERSION:-4.1.2}
+        SPARK_VERSION: ${SPARK_VERSION:-4.1.3}
         HADOOP_VERSION: ${HADOOP_VERSION:-3}
         BASE_URL: ${SPARK_BASE_URL:-https://dlcdn.apache.org/spark}
     depends_on:
diff --git a/docker/unified.Dockerfile b/docker/unified.Dockerfile
index bb63c4a410..037a3915e5 100644
--- a/docker/unified.Dockerfile
+++ b/docker/unified.Dockerfile
@@ -116,6 +116,23 @@ COPY ./docker/resources/ /build/docker/resources/
 #
 # These will be extracted and prepared in Stage 3
 
+################################################################################
+# Stage 2b': Fast Builder + marketplace-optional plugins
+################################################################################
+# Same as builder-fast, plus everything Stage 3 needs to install the optional 
("Wave 1")
+# plugins from the local reactor output: the helper scripts, the registry they 
read, and the
+# plugin zips themselves. Docker COPY flattens globs, so the zips land in a 
single directory
+# and Stage 3 resolves them by file name through HOP_PLUGIN_ZIP_DIR.
+#
+# This is a separate flavor on purpose: the zip glob fails the build when 
nothing matches, and
+# a plain "fast" build must not start requiring plugins/**/target/*.zip to be 
present.
+FROM builder-fast AS builder-fast-plugins
+
+COPY ./tools/ /build/tools/
+COPY 
./plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/optional-plugins.yaml
 \
+     
/build/plugins/misc/marketplace/src/main/resources/org/apache/hop/marketplace/optional-plugins.yaml
+COPY ./plugins/*/*/target/*.zip /build/optional-plugin-zips/
+
 
################################################################################
 # Stage 2c: Builder Selector
 
################################################################################
@@ -172,16 +189,49 @@ RUN echo "=== Extracting assemblies ===" && \
         echo "WARNING: Plugins assembly not found, will use built plugins 
directly"; \
     fi
 
+# Step 1b: Install the marketplace-optional ("Wave 1") plugins into the 
extracted client
+#
+# These plugins are released to Maven but deliberately kept out of 
hop-client.zip, so they are
+# absent from the images unless requested. The list is read from 
optional-plugins.yaml, the same
+# source of truth Jenkins and the integration tests use 
(tools/install-wave1-plugins.sh).
+#
+# Note: the Beam engine is one of these plugins and it owns the 
--generate-fat-jar option, so the
+# dataflow and web-beam targets require this to be enabled 
(build-hop-images.sh does that for you).
+#
+# Works with both builders: "full" resolves the zips from the reactor it just 
built, "fast" from
+# the flat directory it copied out of the local plugins/**/target/ folders 
(HOP_PLUGIN_ZIP_DIR).
+ARG INCLUDE_OPTIONAL_PLUGINS=false
+RUN if [ "${INCLUDE_OPTIONAL_PLUGINS}" = "true" ]; then \
+        echo "=== Installing marketplace-optional plugins ===" && \
+        if [ ! -f /build/tools/install-wave1-plugins.sh ]; then \
+            echo "ERROR: tools/install-wave1-plugins.sh is not present in this 
builder." && \
+            exit 1; \
+        fi && \
+        HOP_VERSION="$(ls /build/assemblies/client/target/hop-client-*.zip | 
head -1 | sed -e 's#.*/hop-client-##' -e 's#\.zip$##')" && \
+        echo "Hop version: ${HOP_VERSION}" && \
+        export HOP_VERSION && \
+        export HOP_PLUGIN_ZIP_DIR=/build/optional-plugin-zips && \
+        bash /build/tools/install-wave1-plugins.sh 
/build/assemblies/client/target/hop; \
+    else \
+        echo "=== Skipping marketplace-optional plugins 
(INCLUDE_OPTIONAL_PLUGINS=false) ==="; \
+    fi
+
 # Step 2: Generate fat jar for dataflow template (only if needed)
 ARG SKIP_FAT_JAR=false
 RUN if [ "${SKIP_FAT_JAR}" = "false" ]; then \
         echo "=== Generating fat jar ===" && \
-        if [ -f /build/assemblies/client/target/hop/hop-conf.sh ]; then \
-            /build/assemblies/client/target/hop/hop-conf.sh \
-            --generate-fat-jar=/tmp/hop-fatjar.jar; \
-        else \
+        if [ ! -f /build/assemblies/client/target/hop/hop-conf.sh ]; then \
             echo "ERROR: hop-conf.sh not found" && exit 1; \
-        fi; \
+        fi && \
+        # --generate-fat-jar is contributed by the Beam engine plugin, which 
is marketplace-optional.
+        # Without it hop-conf silently produces nothing usable, so fail loudly 
instead.
+        if [ ! -d /build/assemblies/client/target/hop/plugins/engines/beam ]; 
then \
+            echo "ERROR: the Beam engine plugin is missing, so the fat jar 
cannot be generated." && \
+            echo "       Rebuild with --with-optional-plugins 
(INCLUDE_OPTIONAL_PLUGINS=true)." && \
+            exit 1; \
+        fi && \
+        /build/assemblies/client/target/hop/hop-conf.sh \
+        --generate-fat-jar=/tmp/hop-fatjar.jar; \
     else \
         echo "=== Skipping fat jar generation (not needed) ===" && \
         touch /tmp/hop-fatjar.jar; \
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/cluster-mapping-walkthrough.adoc
 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/cluster-mapping-walkthrough.adoc
index 99a705fb43..6dc166acec 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/cluster-mapping-walkthrough.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/cluster-mapping-walkthrough.adoc
@@ -129,7 +129,7 @@ Compose mounts:
 If you used a non-default `DIST_DIR` in `prepare-dist.sh`, set both 
`HOP_DIST_DIR` and `HOP_DATA_HOST_DIR=${HOP_DIST_DIR}/hop-data` for compose.
 
 * Master UI: http://localhost:8080 (two workers should register).  
-* Images use the existing `docker/integration-tests/spark` Dockerfiles with 
**`SPARK_VERSION=4.1.2`** (override with env if needed).  
+* Images use the existing `docker/integration-tests/spark` Dockerfiles with 
**`SPARK_VERSION=4.1.3`** (override with env if needed).  
 * After a run you can inspect **on the host** without `docker exec`, for 
example:
 
 [source,bash]
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/getting-started-with-native-spark.adoc
 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/getting-started-with-native-spark.adoc
index cdea5d0ed5..f0fa23c38f 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/getting-started-with-native-spark.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/getting-started-with-native-spark.adoc
@@ -67,7 +67,7 @@ The *native* engine skips Beam entirely:
 |Beam pipelines on Spark (batch *and* streaming)
 
 |Spark line
-|https://spark.apache.org/docs/4.1.2/[4.1.x] (Scala 2.13)
+|https://spark.apache.org/docs/4.1.3/[4.1.x] (Scala 2.13)
 |3.5.x (Scala 2.12; see Beam support matrix)
 
 |Programming model
@@ -115,7 +115,7 @@ You still design pipelines *visually* in Hop. Spark’s role 
is the distributed
 ====
 *Java:* Hop requires *Java 21* for the GUI, `hop-run`, and the fat jar 
executed on the Spark driver/executors. Your Spark cluster must also run a JVM 
that can load that bytecode (Java 21).
 
-*Spark:* The native engine is built and tested against *Spark 4.1.x* 
(currently *4.1.2* in the Hop source tree, Scala *2.13*). For `local[*]` runs, 
Spark libraries are packaged under `plugins/engines/spark/lib`, so you do *not* 
need a separate Spark install on your laptop.
+*Spark:* The native engine is built and tested against *Spark 4.1.x* 
(currently *4.1.3* in the Hop source tree, Scala *2.13*). For `local[*]` runs, 
Spark libraries are packaged under `plugins/engines/spark/lib`, so you do *not* 
need a separate Spark install on your laptop.
 ====
 
 [options="header",cols="1,1,2,2"]
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/lakehouse.adoc 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/lakehouse.adoc
index 66bd1a883c..7421e8a490 100644
--- a/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/lakehouse.adoc
+++ b/docs/hop-user-manual/modules/ROOT/pages/pipeline/spark/lakehouse.adoc
@@ -55,7 +55,7 @@ It assumes you already have a working *Native Spark* pipeline 
run configuration
 | Coordinate / version
 
 | Spark (Hop native engine)
-| `spark-sql_2.13` *4.1.2* (Scala *2.13*)
+| `spark-sql_2.13` *4.1.3* (Scala *2.13*)
 
 | Delta Lake
 | `io.delta:delta-spark_4.1_2.13:*4.3.1*`
diff --git a/lib/pom.xml b/lib/pom.xml
index 32acd80150..7332b7eb4c 100644
--- a/lib/pom.xml
+++ b/lib/pom.xml
@@ -93,7 +93,7 @@
         <kaml.version>0.104.0</kaml.version>
         <kotlin.version>1.9.25</kotlin.version>
         <log4j.version>2.25.4</log4j.version>
-        <lz4-java.version>1.11.0</lz4-java.version>
+        <lz4-java.version>1.11.1</lz4-java.version>
         
<maven-resolver-provider.version>3.9.9</maven-resolver-provider.version>
         <maven-resolver.version>1.9.22</maven-resolver.version>
         <msal4j-persistence.version>1.3.0</msal4j-persistence.version>
@@ -341,7 +341,7 @@
                 <version>${juniversalchardet.version}</version>
             </dependency>
             <dependency>
-                <groupId>org.lz4</groupId>
+                <groupId>at.yawk.lz4</groupId>
                 <artifactId>lz4-java</artifactId>
                 <version>${lz4-java.version}</version>
             </dependency>
@@ -452,80 +452,10 @@
             </dependency>
             <dependency>
                 <groupId>io.netty</groupId>
-                <artifactId>netty-all</artifactId>
-                <version>${netty.version}</version>
-            </dependency>
-            <dependency>
-                <groupId>io.netty</groupId>
-                <artifactId>netty-buffer</artifactId>
-                <version>${netty.version}</version>
-            </dependency>
-            <dependency>
-                <groupId>io.netty</groupId>
-                <artifactId>netty-codec</artifactId>
-                <version>${netty.version}</version>
-            </dependency>
-            <dependency>
-                <groupId>io.netty</groupId>
-                <artifactId>netty-codec-base</artifactId>
-                <version>${netty.version}</version>
-            </dependency>
-            <dependency>
-                <groupId>io.netty</groupId>
-                <artifactId>netty-codec-http</artifactId>
-                <version>${netty.version}</version>
-            </dependency>
-            <dependency>
-                <groupId>io.netty</groupId>
-                <artifactId>netty-codec-http2</artifactId>
-                <version>${netty.version}</version>
-            </dependency>
-            <dependency>
-                <groupId>io.netty</groupId>
-                <artifactId>netty-resolver</artifactId>
-                <version>${netty.version}</version>
-            </dependency>
-            <dependency>
-                <groupId>io.netty</groupId>
-                <artifactId>netty-transport</artifactId>
-                <version>${netty.version}</version>
-            </dependency>
-            <dependency>
-                <groupId>io.netty</groupId>
-                <artifactId>netty-transport-classes-epoll</artifactId>
-                <version>${netty.version}</version>
-            </dependency>
-            <dependency>
-                <groupId>io.netty</groupId>
-                <artifactId>netty-common</artifactId>
-                <version>${netty.version}</version>
-            </dependency>
-            <dependency>
-                <groupId>io.netty</groupId>
-                <artifactId>netty-handler</artifactId>
-                <version>${netty.version}</version>
-            </dependency>
-            <dependency>
-                <groupId>io.netty</groupId>
-                <artifactId>netty-handler-proxy</artifactId>
-                <version>${netty.version}</version>
-            </dependency>
-            <dependency>
-                <groupId>io.netty</groupId>
-                <artifactId>netty-transport-native-epoll</artifactId>
-                <version>${netty.version}</version>
-                <classifier>linux-x86_64</classifier>
-            </dependency>
-            <dependency>
-                <groupId>io.netty</groupId>
-                <artifactId>netty-transport-native-kqueue</artifactId>
-                <version>${netty.version}</version>
-                <classifier>osx-x86_64</classifier>
-            </dependency>
-            <dependency>
-                <groupId>io.netty</groupId>
-                <artifactId>netty-transport-native-unix-common</artifactId>
+                <artifactId>netty-bom</artifactId>
                 <version>${netty.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
             </dependency>
             <dependency>
                 <groupId>io.projectreactor.netty</groupId>
diff --git a/plugins/databases/clickhouse/pom.xml 
b/plugins/databases/clickhouse/pom.xml
index 4b62f71b49..f8abd5009b 100644
--- a/plugins/databases/clickhouse/pom.xml
+++ b/plugins/databases/clickhouse/pom.xml
@@ -29,7 +29,7 @@
     <name>Hop Plugins Databases ClickHouse</name>
 
     <properties>
-        <hop.jdbc.driver1>com.clickhouse:clickhouse-jdbc</hop.jdbc.driver1>
+        
<hop.libcore.exclude1>com.clickhouse:clickhouse-jdbc</hop.libcore.exclude1>
     </properties>
 
     <dependencyManagement>
@@ -48,7 +48,6 @@
         <dependency>
             <groupId>com.clickhouse</groupId>
             <artifactId>clickhouse-jdbc</artifactId>
-            <!-- below is only needed when all you want is a shaded jar -->
             <classifier>all</classifier>
             <scope>provided</scope>
         </dependency>
diff --git a/plugins/engines/spark/README.md b/plugins/engines/spark/README.md
deleted file mode 100644
index 25b455d5c7..0000000000
--- a/plugins/engines/spark/README.md
+++ /dev/null
@@ -1,455 +0,0 @@
-<!--
-Licensed to the Apache Software Foundation (ASF) under one or more
-contributor license agreements.  See the NOTICE file distributed with
-this work for additional information regarding copyright ownership.
-The ASF licenses this file to You 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.
--->
-
-# Native Apache Spark pipeline engine
-
-Implements [issue #7486](https://github.com/apache/hop/issues/7486): a Hop
-`IPipelineEngine` that runs batch pipelines on **Apache Spark 4.1.x** without
-Apache Beam.
-
-## Design notes (Phase 0 decisions)
-
-| Topic | Choice |
-|-------|--------|
-| Spark version | `4.1.2` (`spark-sql_2.13`), independent of Beam’s Spark 3.x |
-| Row model | Spark `Row` + `StructType` (`HopSparkRowConverter`) |
-| Hop transforms | `mapPartitions` via `HopMapPartitionsFn` (init once per 
partition) |
-| Metadata on the wire | Transform XML + `JsonRowMeta` + 
`SerializableMetadataProvider` JSON |
-| Deployment v1 | `local[*]` and standalone `spark://`; fat-jar field for 
cluster |
-| Streaming / windows | Out of scope — use Beam Spark |
-| Stateful transforms | Native handlers: Memory Group By, Merge Join, Unique 
Rows, Sort Rows; sorted Group By remains banned |
-
-## Classpath
-
-- **Compile / local run:** Spark 4.1 is packaged under 
`plugins/engines/spark/lib`
-  so Hop can start `local[*]` without a separate Spark install. The plugin must
-  also ship **hadoop-client-api/runtime** and **log4j**: Spark 4 loads
-  `org.apache.spark.sql.classic.SparkSession` reflectively and fails with
-  *“Cannot find a SparkSession implementation on the Classpath”* when those
-  classes are missing (the real `ClassNotFoundException` is swallowed).
-- **Fat jar tokens (not real pack directories under `lib/spark-clients/`):**
-
-  | Token | Use for | Spark runtime |
-  |-------|---------|----------------|
-  | `native` | hop-run client / embed Spark | Ships Spark 4 from 
`plugins/engines/spark/lib` |
-  | **`native-provided`** | **`spark-submit` on a cluster** | **Cluster 
provides Spark** (do not embed) |
-
-  **Do not** embed Spark inside the app jar for `spark-submit`. That mixes two
-  Spark copies on the driver classpath and fails on Java 21 (e.g.
-  `DirectByteBuffer` / wrong `Running Spark version`).
-
-  ```bash
-  # From a Hop install — fat jar for spark-submit:
-  ./hop-conf.sh --generate-fat-jar=/tmp/hop-native-spark4-submit.jar \
-    --spark-client-version=native-provided
-  ```
-
-- **spark-submit (cluster):** Use driver main class
-  `org.apache.hop.spark.run.MainSpark` with a **native-provided** fat jar:
-
-  ```bash
-  # Critical: pin SPARK_HOME to this distro. If SPARK_HOME points at an older
-  # install (e.g. /opt/spark -> 3.3.0), even "./bin/spark-submit" from a 4.1.x
-  # tree re-executes ${SPARK_HOME}/bin/spark-class and loads the wrong Spark.
-  export SPARK_HOME=~/Downloads/spark-4.1.3-bin-hadoop3
-
-  # Export project metadata to JSON, then:
-  "${SPARK_HOME}/bin/spark-submit" \
-    --master spark://spark:7077 \
-    --class org.apache.hop.spark.run.MainSpark \
-    /tmp/hop-native-spark4-submit.jar \
-    /path/to/pipeline.hpl \
-    /path/to/metadata.json \
-    YourNativeSparkRunConfig
-  ```
-
-  Positional args: `pipeline.hpl`, `metadata.json`, `runConfigName`, optional
-  env config file. Named form also works: `--HopPipelinePath=...`,
-  `--HopMetadataPath=...`, `--HopRunConfigurationName=...`,
-  `--HopConfigFile=...`.
-
-  The run configuration must use the **native** Spark pipeline engine (not
-  Beam). Prefer leaving **master blank** so `spark-submit --master` wins, or 
set
-  master to `spark://spark:7077`. Leave **fatJar empty** under submit.
-  Paths must be readable on the driver; data paths must be reachable from 
workers.
-- After rebuilding the plugin, reinstall into your Hop distro (or rebuild
-  `assemblies/client`) so `plugins/engines/spark/lib` is updated.
-
-## Execution information location
-
-Attach an **execution information location** (and optionally an execution data
-profile) on the native Spark **pipeline run configuration**, same as 
Local/Beam.
-
-The driver will:
-
-1. Load and initialize the location at prepare time  
-2. `registerExecution` when the pipeline starts  
-3. Periodically `updateExecutionState` for the pipeline and each transform
-   component (metrics come from Spark accumulators / `EngineMetrics`)  
-4. Write a final state and `close` the location when the pipeline finishes,
-   fails, or is stopped  
-
-Notes:
-
-- **State updates** (`updateExecutionState`) run on the **driver** from
-  `EngineMetrics` (all transform types).  
-- **Row sampling** (`registerData`) runs on **executors** for generic
-  mapPartitions transforms when the run configuration also names an
-  **execution data profile** (First/Last/Random rows, etc.). Extra GUI
-  samplers on the engine are serialized into the closure like Beam.  
-- File locations used for sampling must be **writable from every executor**
-  (shared FS / object store). Local-only paths only receive driver state.  
-- For `spark-submit`, export location + data profile into the metadata JSON.
-
-## Module layout
-
-- `engines/` — `SparkPipelineEngine`, run configuration, capabilities
-- `pipeline/` — Hop → Spark converter and transform handlers
-- `pipeline/handler/` — native shuffle handlers + generic mapPartitions
-- `core/` — `HopMapPartitionsFn`, row conversion, executor-side Hop init
-- `run/` — `MainSpark` for spark-submit
-
-## Native handlers (Phase 3)
-
-| Plugin id | Spark API | Notes |
-|-----------|-----------|--------|
-| `MemoryGroupBy` | `groupBy` + agg | SUM, AVG, MIN, MAX, COUNTs, FIRST/LAST, 
STDDEV, percentile, concat variants |
-| `MergeJoin` | `Dataset.join` | INNER / LEFT / RIGHT / FULL OUTER |
-| `Unique` | `dropDuplicates` (+ count via groupBy) | Reject-to-error not 
supported |
-| `SortRows` | `orderBy` | Optional unique-on-keys |
-| `GroupBy` (sorted) | — | **Banned** — use Memory Group By |
-| `SparkFileInput` | `spark.read.format(...).load` | CSV/Parquet/JSON/ORC/text 
|
-| `SparkFileOutput` | `df.write.format(...).save` | Save modes, optional 
coalesce/partitionBy |
-
-## Classic Hop I/O (POC)
-
-`HopMapPartitionsFn` sets partition-scoped variables for classic writers:
-
-| Variable | Value |
-|----------|--------|
-| `${Internal.Transform.ID}` | `{transformName}-{partitionId}` |
-| `${Internal.Transform.CopyNr}` / `BundleNr` | partition id |
-
-It also enables `beamContext` so Text File Output auto-appends `_id_bundle` 
like Beam.
-`dependencies.xml` stages `textfile`, `getfilenames`, and `excel` for 
mapPartitions POCs
-(Get File Names → Text File Input, small Excel lookups, etc.). Pure sources 
still run on
-one partition so files are not listed/read N times.
-
-## File I/O transforms
-
-Dedicated **Spark File Input** / **Spark File Output** transforms (not Hop 
Text File Input)
-provide a clean Dataset API mapping:
-
-- **Input:** path, format, header/separator/quote (CSV), optional field schema 
or `inferSchema`
-- **Output:** path, format, save mode (Overwrite/Append/Ignore/ErrorIfExists), 
header, coalesce,
-  partition-by columns
-
-**Paths are Spark/Hadoop URIs** (e.g. local path, `hdfs://…`, `s3a://…`), not 
Hop VFS schemes
-(`s3://`, named MinIO `minio://`, …). Dataset `load`/`save` never call 
`HopVfs`. Classic
-mapPartitions transforms still use Hop VFS. See user manual: *Paths and file 
systems on native
-Spark*.
-
-Optional **path scheme map** on the run configuration (`from=to` lines, e.g. 
`s3=s3a`) rewrites
-URI schemes for Spark File / Lake PATH only via `SparkPathDialect.toSparkUri`. 
`SparkPathDialect`
-also appends a hint when a known Hop-only scheme still appears in I/O errors 
after mapping.
-
-## Project package (nested Simple Mapping / Pipeline Executor)
-
-**Specifically for Native Spark execution** (not File → Export current project 
to zip).
-
-```bash
-# GUI: Tools → Export project package for Native Spark…
-# CLI:
-./hop-conf.sh -j my-project --export-spark-project=/tmp/my-project-spark.zip
-
-# spark-submit
---class org.apache.hop.spark.run.MainSpark ... \
-  --HopProjectPackage=/tmp/my-project-spark.zip \
-  --HopPipelinePath=pipelines/run.hpl \
-  --HopRunConfigurationName=YourNativeSparkRunConfig
-```
-
-`SparkProjectPackage` + `SparkPipelineEngine`:
-
-1. Driver stages the zip and sets `HOP_SPARK_PROJECT_PACKAGE`
-2. Session start: `SparkContext.addFile` ships the zip to **all executors** 
(`SparkFiles`)
-3. Mini-pipeline init: `SparkFiles.get` → extract under `java.io.tmpdir` → 
`PROJECT_HOME`
-
-Do **not** use package `PROJECT_HOME` as the root for Spark Dataset data paths.
-
-## Cluster walk-through (Docker)
-
-Sample project: `src/samples/spark-demo`  
-Compose: 
`docker/integration-tests/integration-tests-spark-native-cluster.yaml` (Spark 
**4.1.2**)
-
-```bash
-./plugins/engines/spark/src/samples/spark-demo/scripts/prepare-dist.sh
-# artifacts default to /tmp/spark-demo-dist (override DIST_DIR / HOP_DIST_DIR)
-# shared data plane: /tmp/spark-demo-dist/hop-data → /data/hop-data 
(host-visible)
-docker compose -f 
docker/integration-tests/integration-tests-spark-native-cluster.yaml \
-  up -d --build --scale spark-worker=2
-docker compose -f 
docker/integration-tests/integration-tests-spark-native-cluster.yaml \
-  exec spark /opt/hop-samples/spark-demo/scripts/spark-submit-demo.sh
-# Workflow Executor demo:
-docker compose -f 
docker/integration-tests/integration-tests-spark-native-cluster.yaml \
-  exec spark /opt/hop-samples/spark-demo/scripts/spark-submit-workflows-demo.sh
-# Host inspect: ls /tmp/spark-demo-dist/hop-data/out 
/tmp/spark-demo-dist/hop-data/executions
-```
-
-See user manual: *Cluster demo walk-through*.
-
-Writes run as Spark actions during graph build; the output handler registers 
an empty leaf
-Dataset so a later engine `count()` does not re-write files.
-
-## Lakehouse connectors (Delta Lake / Apache Iceberg)
-
-**Open table format** support on the native Spark engine (ACID tables, 
catalogs, time travel,
-MERGE, maintenance). Connectors **ship in the default** `hop-engines-spark` 
plugin assembly:
-
-| Path | Contents |
-|------|----------|
-| `plugins/engines/spark/lib/delta/` | Delta Lake 4.3.1 tree |
-| `plugins/engines/spark/lib/iceberg/` | Iceberg Spark runtime 1.11.0 |
-
-**User documentation (Antora):**
-`docs/hop-user-manual/.../pipeline/spark/lakehouse.adoc` plus transform pages
-`spark-lake-table-*.adoc` and `metadata-types/spark-catalog.adoc`.
-**Integration tests:** `integration-tests/lakehouse` (Delta) and
-`integration-tests/iceberg` (Iceberg) — PATH Input/Output, overwrite/time 
travel, MERGE.
-
-| Pin | Coordinate |
-|-----|------------|
-| Spark | `spark-sql_2.13:4.1.2` (existing) |
-| Delta Lake | `io.delta:delta-spark_4.1_2.13:4.3.1` |
-| Apache Iceberg | `org.apache.iceberg:iceberg-spark-runtime-4.1_2.13:1.11.0` |
-
-The `_4.1_2.13` suffix means **Spark 4.1 / Scala 2.13**, not the product major 
version.
-
-### Classloader (why not a sibling plugin)
-
-`SparkPipelineEngine` sets TCCL to the **Spark engine plugin classloader**, 
which loads:
-
-1. JARs under `plugins/engines/spark/lib/` (recursive — includes `lib/delta`, 
`lib/iceberg`)
-2. Folders from `plugins/engines/spark/dependencies.xml`
-3. The engine plugin jar
-
-A sibling plugin under `plugins/engines/spark-delta` is a **separate** 
classloader and is
-**not** visible to `SparkSession` / DataSource resolution. Connectors must 
live **inside**
-the engine lib tree (as shipped) or on the spark-submit driver/executor 
classpath.
-
-### hop-run / GUI `local[*]`
-
-No manual overlay is required on a normal Hop install. After rebuilding the 
plugin:
-
-```bash
-./mvnw -pl plugins/engines/spark -am package
-# zip / client assembly unpacks lib/delta and lib/iceberg under 
plugins/engines/spark/
-```
-
-`target/lakehouse-connectors/` is a frozen jar list for docs verification 
(generated every build).
-
-### spark-submit
-
-Use a **native-provided** fat jar so Spark is not embedded twice. Lakehouse 
jars under
-`plugins/engines/spark/lib/{delta,iceberg}` are **included** in that fat jar 
(they are not
-filtered out like Spark/Scala/Hadoop client). Optional `--packages` only if 
you strip them:
-
-```bash
-export SPARK_HOME=/path/to/spark-4.1.x-bin-hadoop3
-
-"${SPARK_HOME}/bin/spark-submit" \
-  --master spark://spark:7077 \
-  --conf 
spark.sql.extensions=io.delta.sql.DeltaSparkSessionExtension,org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions
 \
-  --conf 
spark.sql.catalog.spark_catalog=org.apache.spark.sql.delta.catalog.DeltaCatalog 
\
-  --class org.apache.hop.spark.run.MainSpark \
-  /tmp/hop-native-provided.jar \
-  /path/to/pipeline.hpl \
-  /path/to/metadata.json \
-  YourNativeSparkRunConfig
-```
-
-When both Delta and Iceberg are used, keep **Delta** on `spark_catalog` and 
register Iceberg
-catalogs under **other** names (e.g. `lake`).
-
-### Session conf matrix (spike / future transforms)
-
-| When | Conf |
-|------|------|
-| Any Delta use | `spark.sql.extensions` includes 
`io.delta.sql.DeltaSparkSessionExtension` |
-| Any Delta use | 
`spark.sql.catalog.spark_catalog=org.apache.spark.sql.delta.catalog.DeltaCatalog`
 |
-| Any Iceberg use | `spark.sql.extensions` includes 
`org.apache.iceberg.spark.extensions.IcebergSparkSessionExtensions` |
-| Iceberg TABLE | `spark.sql.catalog.<name>=…` (+ type/warehouse/uri) |
-
-### Spike findings (PR 1 — 2026-07, Spark 4.1.2 + Delta 4.3.1 + Iceberg 1.11.0)
-
-Recorded by `SparkLakehouseConnectorTest` with `-Plakehouse`:
-
-| Check | Result |
-|-------|--------|
-| `Class.forName(DeltaSparkSessionExtension)` with connector on CL | OK |
-| PATH write/read with extension **and** DeltaCatalog | OK (required gate) |
-| PATH write/read with extension **only** (no DeltaCatalog) | **Fails** with 
`DELTA_CONFIGURE_SPARK_SESSION_WITH_EXTENSION_AND_CATALOG` |
-| Iceberg extension on CL | OK |
-| Iceberg bare `format("iceberg").save/load(path)` (no catalog conf) | 
**Fails** — defaults toward `HiveCatalog` / needs HMS jars |
-| Iceberg `writeTo(hop_iceberg.\`file:///path\`)` + SQL read (PR 3) | **OK** |
-| Iceberg Hadoop warehouse + `writeTo("local.db.t")` + `spark.table` | OK 
(named-table style) |
-| Iceberg TABLE `writeTo` vs `saveAsTable` | **writeTo for PATH**; formal 
TABLE lock still PR 5 / KD-24 |
-
-**Tiered probe default (updated by spike + PR 3):**
-
-- Always require connector classes on the classpath.
-- **Any Delta use (including PATH-only)** requires session-registered
-  `DeltaSparkSessionExtension` **and** 
`spark.sql.catalog.spark_catalog=…DeltaCatalog`.
-- **Any Iceberg PATH use** requires Iceberg extensions + Hadoop catalog 
`hop_iceberg`
-  (auto-registered on hop-run; can be set at runtime on active sessions).
-- hop-run always applies full Delta/Iceberg conf when those formats are 
present.
-
-### Frozen jar list (shipped under `lib/delta` + `lib/iceberg`)
-
-Also staged into `target/lakehouse-connectors/` on every build for docs 
verification.
-
-| Jar | Role |
-|-----|------|
-| `delta-spark_4.1_2.13-4.3.1.jar` | Delta Spark SQL / DataSource |
-| `delta-storage-4.3.1.jar` | Delta storage layer |
-| `delta-kernel-api-4.3.1.jar` | Delta Kernel API |
-| `delta-kernel-defaults-4.3.1.jar` | Delta Kernel defaults |
-| `delta-kernel-unitycatalog-4.3.1.jar` | Transitive from delta-spark 4.3.1 |
-| `unitycatalog-client-0.5.0.jar` | Transitive (UC-related) |
-| `unitycatalog-hadoop-0.5.0.jar` | Transitive (UC-related) |
-| `iceberg-spark-runtime-4.1_2.13-1.11.0.jar` | Shaded Iceberg Spark runtime |
-
-### Probe API
-
-`org.apache.hop.spark.table.SparkLakeConnectorProbe` verifies connector 
classes are loadable
-and throws `HopException` with reinstall / `--packages` guidance when missing.
-`LakeSessionPlan` scans active lake transforms before `SparkSession` build: 
probes the
-classpath and applies Delta extension + `DeltaCatalog` on new hop-run sessions.
-
-### Lake Table transforms (PR 2–3 — Delta + Iceberg PATH)
-
-| Plugin id | Role |
-|-----------|------|
-| `SparkLakeTableInput` | Read Delta or Iceberg PATH tables |
-| `SparkLakeTableOutput` | Write Delta or Iceberg PATH; **default save mode 
ErrorIfExists** |
-
-| Format | PATH read | PATH write |
-|--------|-----------|------------|
-| **Delta** | `spark.read.format("delta").load(path)` | 
`format("delta").mode(…).save(path)` |
-| **Iceberg** | SQL `SELECT * FROM hop_iceberg.\`file:///…\`` | 
`writeTo(hop_iceberg.\`uri\`).using("iceberg")` |
-
-Iceberg PATH uses a built-in Hadoop catalog named **`hop_iceberg`** (not 
`spark_catalog`, so it
-can co-exist with Delta’s `DeltaCatalog`). Bare `format("iceberg").load(path)` 
is **not** used —
-on Spark 4.1 + Iceberg 1.11 it defaults toward HiveCatalog and fails without 
HMS jars.
-
-hop-run auto-registers:
-
-| When | Session conf |
-|------|----------------|
-| Any Delta | extensions + `spark.sql.catalog.spark_catalog=…DeltaCatalog` |
-| Any Iceberg | extensions + `spark.sql.catalog.hop_iceberg=…SparkCatalog` 
(type=hadoop, warehouse under tmp) |
-
-Classic **Spark File Input/Output** are unchanged (csv/parquet/json/orc/text 
only).
-
-### Time travel (PR 4)
-
-Lake Table Input supports:
-
-| UI type | Delta option | Iceberg SQL |
-|---------|--------------|-------------|
-| `NONE` | (current) | (current) |
-| `VERSION` | `versionAsOf` | `VERSION AS OF <snapshot_id>` |
-| `TIMESTAMP` | `timestampAsOf` | `TIMESTAMP AS OF TIMESTAMP '…'` |
-
-Example: write twice with Overwrite, then set Time travel = VERSION and 
Version = `0` (Delta)
-or the first snapshot id from `{table}.snapshots` (Iceberg).
-
-### Spark Catalog metadata + TABLE mode (PR 5)
-
-**Metadata type:** `SparkCatalog` (category Connections) — Spark SQL catalog 
conf for Iceberg
-Hadoop/REST (or custom). Fields: catalog name, type, warehouse/uri, optional 
credential, extra
-`key=value` lines → `spark.sql.catalog.<name>.*`.
-
-Lake Table transforms in **TABLE** mode:
-
-| Field | Purpose |
-|-------|---------|
-| Identifier mode | `TABLE` |
-| Table identifier | e.g. `lake.db.orders` |
-| Spark Catalog metadata name | Hop metadata entry to register (optional if 
catalog already on session) |
-
-| Format | TABLE read | TABLE write |
-|--------|------------|-------------|
-| **Iceberg** (primary) | `SELECT * FROM catalog.ns.table` (+ time travel 
clauses) | `writeTo(id).using("iceberg")` |
-| **Delta** (advanced) | `format("delta").table(id)` | 
`format("delta").saveAsTable(id)` |
-
-`LakeSessionPlan` loads referenced `SparkCatalog` entries and applies them 
when building the
-session (hop-run) or registers them on an active session when possible 
(submit).
-
-For spark-submit, export project metadata JSON including `SparkCatalog` 
definitions, or pass
-equivalent `--conf spark.sql.catalog.<name>=…`.
-
-### MERGE (PR 6)
-
-**Spark Lake Table Merge** — single upstream hop supplies source rows; target 
is PATH or TABLE.
-
-| Field | Notes |
-|-------|--------|
-| Merge condition | ON clause, aliases `t` (target) and `s` (source) |
-| When matched | `UPDATE_ALL` / `DELETE` / `NONE` |
-| When not matched | `INSERT_ALL` / `NONE` |
-| Not matched by source | optional `DELETE` (Delta; Iceberg depends on 
version) |
-| Raw MERGE SQL | advanced override (empty default) |
-
-Target SQL ids: Delta PATH → `delta.\`path\``; Iceberg PATH → 
`hop_iceberg.\`uri\``; TABLE → multi-part id.
-
-Metrics are log-only (merge completion), not Dataset row counts.
-
-### Maintenance (PR 7)
-
-**Spark Lake Table Maintenance** — zero-input action (no hop required). 
Destructive ops require
-`acknowledgeDestructive`.
-
-| Operation | Delta | Iceberg |
-|-----------|-------|---------|
-| OPTIMIZE | `OPTIMIZE delta.\`path\` [ZORDER BY …]` | `CALL 
cat.system.rewrite_data_files` |
-| VACUUM | `VACUUM … RETAIN n HOURS` (**n required**) | n/a → use 
EXPIRE_SNAPSHOTS |
-| EXPIRE_SNAPSHOTS | n/a | `CALL … expire_snapshots(retain_last => N)` |
-| REWRITE_MANIFESTS | n/a | `CALL … rewrite_manifests` |
-| DELETE_WHERE | `DELETE FROM … WHERE …` (**WHERE required**) | same |
-
-Metrics are log-only (expect 0 in/out in the GUI).
-
-
-### RAT / license
-
-Delta Lake and Apache Iceberg are Apache-2.0 licensed and redistributed under
-`plugins/engines/spark/lib/{delta,iceberg}/`. Re-check transitive licenses 
when pins change.
-Unity Catalog client jars ship as Delta transitives (Apache-2.0).
-
-## Build
-
-```bash
-# Unit + lakehouse ITs (connectors are default runtime deps)
-./mvnw -pl plugins/engines/spark -am test
-
-# Package plugin zip (includes lib/delta + lib/iceberg)
-./mvnw -pl plugins/engines/spark -am package
-
-# -Plakehouse is a no-op alias retained for older scripts
-./mvnw -pl plugins/engines/spark -am test -Plakehouse
-```
diff --git a/plugins/engines/spark/pom.xml b/plugins/engines/spark/pom.xml
index 03fe995bf9..3985641885 100644
--- a/plugins/engines/spark/pom.xml
+++ b/plugins/engines/spark/pom.xml
@@ -31,26 +31,30 @@
     <description>Native Apache Spark 4.x pipeline engine for Hop (issue 
#7486)</description>
 
     <properties>
-        <!-- Lakehouse connectors shipped in default engines-spark assembly 
under lib/{delta,iceberg}.
-             Artifact suffix _4.1_2.13 = Spark 4.1 / Scala 2.13, not product 
version. -->
         <delta.version>4.3.1</delta.version>
-        <!-- Matches Spark 4.1.2 parent; needed for local[*] unit tests only 
-->
         <hadoop.client.version>3.4.2</hadoop.client.version>
-        <!-- Do not stage Jackson into plugins/engines/spark/lib (Hop parent 
CL has it).
-             Dual ObjectMapper classes under HopURLClassLoader break HopJson 
on local[*]. -->
         <hop.pluginlib.exclude1>jackson-core</hop.pluginlib.exclude1>
         <hop.pluginlib.exclude2>jackson-databind</hop.pluginlib.exclude2>
         <hop.pluginlib.exclude3>jackson-annotations</hop.pluginlib.exclude3>
         <iceberg.version>1.11.0</iceberg.version>
-        <!-- Staged jar freeze list for docs / verification (not a second 
install path). -->
         
<lakehouse.connectors.dir>${project.basedir}/target/lakehouse-connectors</lakehouse.connectors.dir>
         <scala.binary.version>2.13</scala.binary.version>
-        <!-- Pin Spark 4.1.x independently of Beam's Spark 3.x dependency -->
-        <spark.version>4.1.2</spark.version>
-        <!-- Spark major.minor used in Delta/Iceberg Maven artifact names (4.1 
from 4.1.2). -->
+        <spark.version>4.1.3</spark.version>
         <spark.version.prefix>4.1</spark.version.prefix>
     </properties>
 
+    <dependencyManagement>
+        <dependencies>
+            <dependency>
+                <groupId>org.apache.hop</groupId>
+                <artifactId>hop-libs</artifactId>
+                <version>${project.version}</version>
+                <type>pom</type>
+                <scope>import</scope>
+            </dependency>
+        </dependencies>
+    </dependencyManagement>
+
     <dependencies>
         <!-- Spark MetricsServlet / Jetty needs servlet API (SingleThreadModel 
→ use 5.0) -->
         <dependency>
@@ -69,15 +73,6 @@
             <artifactId>hadoop-client-runtime</artifactId>
             <version>${hadoop.client.version}</version>
         </dependency>
-
-        <!-- Spark 4.1 — packaged under plugins/engines/spark/lib for local[*].
-             Hadoop client + Log4j MUST stay on the plugin classpath: Spark 4 
looks up
-             org.apache.spark.sql.classic.SparkSession reflectively and fails 
with
-             "Cannot find a SparkSession implementation on the Classpath" when 
those
-             transitive classes are missing (ClassNotFoundException is 
swallowed).
-             slf4j is provided by Hop; keep it excluded to avoid dual bindings.
-             Jackson core is also provided by Hop lib/core — dual copies under 
the plugin
-             classloader cause LinkageError on HopJson.newMapper() (sampling). 
-->
         <dependency>
             <groupId>org.apache.spark</groupId>
             <artifactId>spark-sql_${scala.binary.version}</artifactId>
diff --git a/plugins/misc/marketplace/README.md 
b/plugins/misc/marketplace/README.md
index 4b09e2b425..90f016de89 100644
--- a/plugins/misc/marketplace/README.md
+++ b/plugins/misc/marketplace/README.md
@@ -235,6 +235,7 @@ Do not hardcode plugin lists elsewhere. Everything below 
reads this registry:
 | `full-client-env.yaml` | Generated from this registry during the marketplace 
build (Maven `generate-resources`) |
 | Local publish to Nexus/Artifactory | 
`docker/marketplace-nexus/publish-marketplace-plugins.sh` |
 | Unpack zips into an install (IT images) | `tools/install-wave1-plugins.sh` 
via `tools/list-marketplace-plugins.sh` |
+| Docker images | `docker/build-hop-images.sh --with-optional-plugins` runs 
`tools/install-wave1-plugins.sh` inside the builder stage |
 
 Each entry needs at least:
 
diff --git a/pom.xml b/pom.xml
index c608a24967..6c34d85254 100644
--- a/pom.xml
+++ b/pom.xml
@@ -154,7 +154,7 @@
         <maven.compiler.target>${target.jdk.version}</maven.compiler.target>
         <minimalMavenBuildVersion>3.6.3</minimalMavenBuildVersion>
         <mockito-core.version>5.22.0</mockito-core.version>
-        <netty.version>4.2.15.Final</netty.version>
+        <netty.version>4.2.16.Final</netty.version>
         <objenesis.version>3.5</objenesis.version>
         <opentelemetry.version>1.62.0</opentelemetry.version>
         <org.eclipse.platform.version>3.132.0</org.eclipse.platform.version>
diff --git a/tools/install-wave1-plugins.sh b/tools/install-wave1-plugins.sh
index 71994e756b..56869a781e 100755
--- a/tools/install-wave1-plugins.sh
+++ b/tools/install-wave1-plugins.sh
@@ -17,12 +17,17 @@
 #
 # Requires plugin modules to have been packaged (*.zip under 
plugins/**/target/).
 #
+# HOP_PLUGIN_ZIP_DIR: optional flat directory of plugin zips, searched by file 
name when the
+# reactor layout is not available. Docker COPY flattens globs, so the "fast" 
image builder
+# hands the zips over in one directory instead of 
plugins/<category>/<name>/target/.
+#
 set -euo pipefail
 
 ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
 INSTALL_DIR="${1:-${ROOT}/assemblies/client/target/hop}"
 VERSION="${HOP_VERSION:-2.19.0-SNAPSHOT}"
 LIST_SCRIPT="${ROOT}/tools/list-marketplace-plugins.sh"
+ZIP_DIR="${HOP_PLUGIN_ZIP_DIR:-}"
 
 if [[ ! -d "${INSTALL_DIR}" ]]; then
   echo "Hop install not found: ${INSTALL_DIR}" >&2
@@ -48,6 +53,9 @@ for entry in ${PLUGINS[@]+"${PLUGINS[@]}"}; do
   id="${entry%%|*}"
   rel="${entry#*|}"
   zip="${ROOT}/${rel}"
+  if [[ ! -f "${zip}" && -n "${ZIP_DIR}" && -f "${ZIP_DIR}/${rel##*/}" ]]; then
+    zip="${ZIP_DIR}/${rel##*/}"
+  fi
   if [[ ! -f "${zip}" ]]; then
     echo "SKIP (not built): ${id} — ${rel}"
     skipped=$((skipped + 1))

Reply via email to