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 473488a88e Issue 7476 : Spark / Beam validation (#7482)
473488a88e is described below

commit 473488a88ed2dca9126078df218e917a09655598
Author: Matt Casters <[email protected]>
AuthorDate: Wed Jul 15 12:55:53 2026 +0200

    Issue 7476 : Spark / Beam validation (#7482)
    
    * issue #7476 : spark pinned on 3.5.7, investigation continues
    
    * issue #7476 : spark pinned on 3.5.7, MATRIX-REPORT.md
    
    * issue #7476 : fat-jar building for specific Spark version
    
    * issue #7476 : confirmed 3.5.7 baselines because of Java 21 / Spark
    
    * issue #7476 : apache-rat markdown exclusion for the support matrices
    
    * issue #7476 : hop script should include the spark-client as well
    
    * rebase and bump to spark 3.5.8
    
    ---------
    
    Co-authored-by: Hans Van Akelyen <[email protected]>
---
 assemblies/static/src/main/resources/hop           |  15 +-
 assemblies/static/src/main/resources/hop-conf.bat  |  10 +-
 assemblies/static/src/main/resources/hop-conf.sh   |  17 +-
 .../static/src/main/resources/hop-encrypt.bat      |  10 +-
 .../static/src/main/resources/hop-encrypt.sh       |  17 +-
 assemblies/static/src/main/resources/hop-gui.bat   |  10 +-
 assemblies/static/src/main/resources/hop-gui.sh    |  11 +-
 .../static/src/main/resources/hop-import.bat       |  10 +-
 assemblies/static/src/main/resources/hop-import.sh |  17 +-
 assemblies/static/src/main/resources/hop-run.bat   |  10 +-
 assemblies/static/src/main/resources/hop-run.sh    |  17 +-
 .../static/src/main/resources/hop-search.bat       |  10 +-
 assemblies/static/src/main/resources/hop-search.sh |  17 +-
 .../static/src/main/resources/hop-server.bat       |  10 +-
 assemblies/static/src/main/resources/hop-server.sh |  17 +-
 assemblies/static/src/main/resources/hop.bat       | 200 ++++++++--------
 .../integration-tests-beam-base.yaml               |   4 +
 .../integration-tests/integration-tests-spark.yaml | 127 +++-------
 docker/integration-tests/spark/Dockerfile.master   |  60 +++--
 docker/integration-tests/spark/Dockerfile.worker   |  58 +++--
 .../integration-tests/unit-tests-beam.Dockerfile   |  13 +-
 .../pipeline/beam/getting-started-with-beam.adoc   |  12 +
 .../beam-spark-pipeline-engine.adoc                |  26 ++-
 .../scripts/run-spark-matched-matrix.sh            | 204 +++++++++++++++++
 integration-tests/scripts/run-spark-matrix.sh      | 255 +++++++++++++++++++++
 integration-tests/scripts/run-tests-docker.sh      |  85 ++++++-
 .../spark/0001-test-spark-cluster.hpl              |   2 +-
 integration-tests/spark/MATRIX-REPORT-MATCHED.md   |  45 ++++
 integration-tests/spark/MATRIX-REPORT.md           |  61 +++++
 integration-tests/spark/disabled.txt               |  18 --
 .../{spark.json => spark-local.json}               |  10 +-
 .../metadata/pipeline-run-configuration/spark.json |   6 +-
 ....hwf => optional-0002-test-hive-connection.hwf} |   0
 ....hwf => optional-0003-test-hive-write-read.hwf} |   0
 plugins/engines/beam/pom.xml                       | 171 ++++++++------
 plugins/engines/beam/src/assembly/assembly.xml     |  26 +++
 .../beam/config/GenerateFatJarConfigPlugin.java    |  42 +++-
 .../org/apache/hop/beam/gui/HopBeamGuiPlugin.java  | 130 ++++++++++-
 .../engines/spark/BeamSparkPipelineEngineTest.java |  17 +-
 pom.xml                                            |   1 +
 tools/spark-client-pack/materialize-pack.sh        | 117 ++++++++++
 tools/spark-client-pack/pom.xml                    | 125 ++++++++++
 42 files changed, 1631 insertions(+), 382 deletions(-)

diff --git a/assemblies/static/src/main/resources/hop 
b/assemblies/static/src/main/resources/hop
index ace42c87dc..567efb609f 100755
--- a/assemblies/static/src/main/resources/hop
+++ b/assemblies/static/src/main/resources/hop
@@ -86,21 +86,28 @@ Linux)
     export GDK_BACKEND=x11
   fi
   if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch 
= aarch64"; then
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/linux/arm64/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/arm64/*"
   else
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/linux/$(uname -m)/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/$(uname 
-m)/*"
   fi
   ;;
 Darwin)
   if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch 
= aarch64"; then
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/osx/arm64/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/arm64/*"
   else
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/osx/x86_64/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/x86_64/*"
   fi
   HOP_OPTIONS="${HOP_OPTIONS} -XstartOnFirstThread"
   ;;
 esac
 
+# Optional versioned Spark client pack replaces default lib/spark-client on 
the classpath.
+# Used by hop run (and other subcommands) the same as hop-run.sh.
+if [ -n "${HOP_SPARK_CLIENT_VERSION:-}" ] && [ -d 
"lib/spark-clients/${HOP_SPARK_CLIENT_VERSION}" ]; then
+  CLASSPATH=$(echo "${CLASSPATH}" | sed 's|lib/spark-client/\*||g')
+  CLASSPATH="${CLASSPATH}:lib/spark-clients/${HOP_SPARK_CLIENT_VERSION}/*"
+fi
+
 "${_HOP_JAVA}" ${HOP_OPTIONS} -Djava.library.path="${LIBPATH}" -classpath 
"${CLASSPATH}" org.apache.hop.hop.Hop "$@"
 EXITCODE=$?
 
diff --git a/assemblies/static/src/main/resources/hop-conf.bat 
b/assemblies/static/src/main/resources/hop-conf.bat
index b9268d7103..e33f891e3e 100644
--- a/assemblies/static/src/main/resources/hop-conf.bat
+++ b/assemblies/static/src/main/resources/hop-conf.bat
@@ -22,7 +22,15 @@ REM switch to script directory
 cd /D %~dp0
 
 set LIBSPATH=lib\core;lib\beam
-set CLASSPATH=lib\core\*;lib\beam\*;lib\swt\win64\*
+set CLASSPATH=lib\core\*;lib\beam\*;lib\spark-client\*;lib\swt\win64\*
+
+REM Optional versioned Spark client pack
+if defined HOP_SPARK_CLIENT_VERSION if exist 
"lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\" (
+  set CLASSPATH=%CLASSPATH%;lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\*
+)
+
+
+
 
 :NormalStart
 REM set java primary is HOP_JAVA_HOME fallback to JAVA_HOME or default java
diff --git a/assemblies/static/src/main/resources/hop-conf.sh 
b/assemblies/static/src/main/resources/hop-conf.sh
index d6e9cd0f72..b279b2ad93 100755
--- a/assemblies/static/src/main/resources/hop-conf.sh
+++ b/assemblies/static/src/main/resources/hop-conf.sh
@@ -78,22 +78,31 @@ Linux)
   if [ "${XDG_SESSION_TYPE}" = "wayland" ]; then
     export GDK_BACKEND=x11
   fi
+
+
   if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch 
= aarch64"; then
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/linux/arm64/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/arm64/*"
   else
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/linux/$(uname -m)/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/$(uname 
-m)/*"
   fi
   ;;
 Darwin)
   if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch 
= aarch64"; then
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/osx/arm64/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/arm64/*"
   else
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/osx/x86_64/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/x86_64/*"
   fi
   HOP_OPTIONS="${HOP_OPTIONS} -XstartOnFirstThread"
   ;;
 esac
 
+
+
+# Optional versioned Spark client pack replaces default lib/spark-client on 
the classpath.
+if [ -n "${HOP_SPARK_CLIENT_VERSION:-}" ] && [ -d 
"lib/spark-clients/${HOP_SPARK_CLIENT_VERSION}" ]; then
+  CLASSPATH=$(echo "${CLASSPATH}" | sed 's|lib/spark-client/\*||g')
+  CLASSPATH="${CLASSPATH}:lib/spark-clients/${HOP_SPARK_CLIENT_VERSION}/*"
+fi
 "${_HOP_JAVA}" ${HOP_OPTIONS} -Djava.library.path="${LIBPATH}" -classpath 
"${CLASSPATH}" org.apache.hop.config.HopConfig "$@"
 EXITCODE=$?
 
diff --git a/assemblies/static/src/main/resources/hop-encrypt.bat 
b/assemblies/static/src/main/resources/hop-encrypt.bat
index d57cfc86fe..a5877064c0 100644
--- a/assemblies/static/src/main/resources/hop-encrypt.bat
+++ b/assemblies/static/src/main/resources/hop-encrypt.bat
@@ -23,7 +23,15 @@ REM switch to script directory
 cd /D %~dp0
 
 set LIBSPATH=lib\core;lib\beam
-set CLASSPATH=lib\core\*;lib\beam\*;lib\swt\win64\*
+set CLASSPATH=lib\core\*;lib\beam\*;lib\spark-client\*;lib\swt\win64\*
+
+REM Optional versioned Spark client pack
+if defined HOP_SPARK_CLIENT_VERSION if exist 
"lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\" (
+  set CLASSPATH=%CLASSPATH%;lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\*
+)
+
+
+
 
 :NormalStart
 REM set java primary is HOP_JAVA_HOME fallback to JAVA_HOME or default java
diff --git a/assemblies/static/src/main/resources/hop-encrypt.sh 
b/assemblies/static/src/main/resources/hop-encrypt.sh
index c97b43da30..b89a53aded 100755
--- a/assemblies/static/src/main/resources/hop-encrypt.sh
+++ b/assemblies/static/src/main/resources/hop-encrypt.sh
@@ -78,22 +78,31 @@ Linux)
   if [ "${XDG_SESSION_TYPE}" = "wayland" ]; then
     export GDK_BACKEND=x11
   fi
+
+
   if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch 
= aarch64"; then
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/linux/arm64/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/arm64/*"
   else
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/linux/$(uname -m)/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/$(uname 
-m)/*"
   fi
   ;;
 Darwin)
   if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch 
= aarch64"; then
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/osx/arm64/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/arm64/*"
   else
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/osx/x86_64/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/x86_64/*"
   fi
   HOP_OPTIONS="${HOP_OPTIONS} -XstartOnFirstThread"
   ;;
 esac
 
+
+
+# Optional versioned Spark client pack replaces default lib/spark-client on 
the classpath.
+if [ -n "${HOP_SPARK_CLIENT_VERSION:-}" ] && [ -d 
"lib/spark-clients/${HOP_SPARK_CLIENT_VERSION}" ]; then
+  CLASSPATH=$(echo "${CLASSPATH}" | sed 's|lib/spark-client/\*||g')
+  CLASSPATH="${CLASSPATH}:lib/spark-clients/${HOP_SPARK_CLIENT_VERSION}/*"
+fi
 "${_HOP_JAVA}" ${HOP_OPTIONS} -classpath "${CLASSPATH}" 
org.apache.hop.encryption.HopEncrypt "$@"
 EXITCODE=$?
 
diff --git a/assemblies/static/src/main/resources/hop-gui.bat 
b/assemblies/static/src/main/resources/hop-gui.bat
index bdd789d498..fa830f4630 100755
--- a/assemblies/static/src/main/resources/hop-gui.bat
+++ b/assemblies/static/src/main/resources/hop-gui.bat
@@ -25,7 +25,15 @@ REM Option to change the Characterset of the Windows Shell 
to show foreign carac
 if not "%HOP_WINDOWS_SHELL_ENCODING%"=="" chcp %HOP_WINDOWS_SHELL_ENCODING%
 
 set LIBSPATH=lib\core;lib\beam
-set CLASSPATH=lib\core\*;lib\beam\*;lib\swt\win64\*
+set CLASSPATH=lib\core\*;lib\beam\*;lib\spark-client\*;lib\swt\win64\*
+
+REM Optional versioned Spark client pack
+if defined HOP_SPARK_CLIENT_VERSION if exist 
"lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\" (
+  set CLASSPATH=%CLASSPATH%;lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\*
+)
+
+
+
 
 set _temphelp=0
 if [%1]==[help] set _temphelp=1
diff --git a/assemblies/static/src/main/resources/hop-gui.sh 
b/assemblies/static/src/main/resources/hop-gui.sh
index ef3d194ae0..69f3c6a4b8 100755
--- a/assemblies/static/src/main/resources/hop-gui.sh
+++ b/assemblies/static/src/main/resources/hop-gui.sh
@@ -110,8 +110,17 @@ Darwin)
   HOP_OPTIONS="${HOP_OPTIONS} -XstartOnFirstThread"
   ;;
 esac
-CLASSPATH="lib/core/*:lib/beam/*:lib/swt/$os_path/$arch_path/*"
+CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/$os_path/$arch_path/*"
 
+
+
+
+
+# Optional versioned Spark client pack replaces default lib/spark-client on 
the classpath.
+if [ -n "${HOP_SPARK_CLIENT_VERSION:-}" ] && [ -d 
"lib/spark-clients/${HOP_SPARK_CLIENT_VERSION}" ]; then
+  CLASSPATH=$(echo "${CLASSPATH}" | sed 's|lib/spark-client/\*||g')
+  CLASSPATH="${CLASSPATH}:lib/spark-clients/${HOP_SPARK_CLIENT_VERSION}/*"
+fi
 "${_HOP_JAVA}" ${HOP_OPTIONS} -Djava.library.path="${LIBPATH}" -classpath 
"${CLASSPATH}" org.apache.hop.ui.hopgui.HopGui "$@"
 EXITCODE=$?
 
diff --git a/assemblies/static/src/main/resources/hop-import.bat 
b/assemblies/static/src/main/resources/hop-import.bat
index 55c5917e22..582dcdb396 100644
--- a/assemblies/static/src/main/resources/hop-import.bat
+++ b/assemblies/static/src/main/resources/hop-import.bat
@@ -23,7 +23,15 @@ REM switch to script directory
 cd /D %~dp0
 
 set LIBSPATH=lib\core;lib\beam
-set CLASSPATH=lib\core\*;lib\beam\*;lib\swt\win64\*
+set CLASSPATH=lib\core\*;lib\beam\*;lib\spark-client\*;lib\swt\win64\*
+
+REM Optional versioned Spark client pack
+if defined HOP_SPARK_CLIENT_VERSION if exist 
"lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\" (
+  set CLASSPATH=%CLASSPATH%;lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\*
+)
+
+
+
 
 :NormalStart
 REM set java primary is HOP_JAVA_HOME fallback to JAVA_HOME or default java
diff --git a/assemblies/static/src/main/resources/hop-import.sh 
b/assemblies/static/src/main/resources/hop-import.sh
index dd47db757f..5669e164e5 100755
--- a/assemblies/static/src/main/resources/hop-import.sh
+++ b/assemblies/static/src/main/resources/hop-import.sh
@@ -78,22 +78,31 @@ Linux)
   if [ "${XDG_SESSION_TYPE}" = "wayland" ]; then
     export GDK_BACKEND=x11
   fi
+
+
   if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch 
= aarch64"; then
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/linux/arm64/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/arm64/*"
   else
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/linux/$(uname -m)/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/$(uname 
-m)/*"
   fi
   ;;
 Darwin)
   if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch 
= aarch64"; then
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/osx/arm64/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/arm64/*"
   else
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/osx/x86_64/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/x86_64/*"
   fi
   HOP_OPTIONS="${HOP_OPTIONS} -XstartOnFirstThread"
   ;;
 esac
 
+
+
+# Optional versioned Spark client pack replaces default lib/spark-client on 
the classpath.
+if [ -n "${HOP_SPARK_CLIENT_VERSION:-}" ] && [ -d 
"lib/spark-clients/${HOP_SPARK_CLIENT_VERSION}" ]; then
+  CLASSPATH=$(echo "${CLASSPATH}" | sed 's|lib/spark-client/\*||g')
+  CLASSPATH="${CLASSPATH}:lib/spark-clients/${HOP_SPARK_CLIENT_VERSION}/*"
+fi
 "${_HOP_JAVA}" ${HOP_OPTIONS} -Djava.library.path="${LIBPATH}" -classpath 
"${CLASSPATH}" org.apache.hop.imp.HopImport "$@"
 EXITCODE=$?
 
diff --git a/assemblies/static/src/main/resources/hop-run.bat 
b/assemblies/static/src/main/resources/hop-run.bat
index 1dd10edc50..424a2737da 100755
--- a/assemblies/static/src/main/resources/hop-run.bat
+++ b/assemblies/static/src/main/resources/hop-run.bat
@@ -25,7 +25,15 @@ REM Option to change the Characterset of the Windows Shell 
to show foreign carac
 if not "%HOP_WINDOWS_SHELL_ENCODING%"=="" chcp %HOP_WINDOWS_SHELL_ENCODING%
 
 set LIBSPATH=lib\core;lib\beam
-set CLASSPATH=lib\core\*;lib\beam\*;lib\swt\win64\*
+set CLASSPATH=lib\core\*;lib\beam\*;lib\spark-client\*;lib\swt\win64\*
+
+REM Optional versioned Spark client pack
+if defined HOP_SPARK_CLIENT_VERSION if exist 
"lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\" (
+  set CLASSPATH=%CLASSPATH%;lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\*
+)
+
+
+
 
 set _temphelp=0
 if [%1]==[help] set _temphelp=1
diff --git a/assemblies/static/src/main/resources/hop-run.sh 
b/assemblies/static/src/main/resources/hop-run.sh
index 8959684f67..f25c19bac8 100755
--- a/assemblies/static/src/main/resources/hop-run.sh
+++ b/assemblies/static/src/main/resources/hop-run.sh
@@ -78,22 +78,31 @@ Linux)
   if [ "${XDG_SESSION_TYPE}" = "wayland" ]; then
     export GDK_BACKEND=x11
   fi
+
+
   if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch 
= aarch64"; then
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/linux/arm64/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/arm64/*"
   else
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/linux/$(uname -m)/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/$(uname 
-m)/*"
   fi
   ;;
 Darwin)
   if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch 
= aarch64"; then
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/osx/arm64/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/arm64/*"
   else
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/osx/x86_64/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/x86_64/*"
   fi
   HOP_OPTIONS="${HOP_OPTIONS} -XstartOnFirstThread"
   ;;
 esac
 
+
+
+# Optional versioned Spark client pack replaces default lib/spark-client on 
the classpath.
+if [ -n "${HOP_SPARK_CLIENT_VERSION:-}" ] && [ -d 
"lib/spark-clients/${HOP_SPARK_CLIENT_VERSION}" ]; then
+  CLASSPATH=$(echo "${CLASSPATH}" | sed 's|lib/spark-client/\*||g')
+  CLASSPATH="${CLASSPATH}:lib/spark-clients/${HOP_SPARK_CLIENT_VERSION}/*"
+fi
 "${_HOP_JAVA}" ${HOP_OPTIONS} -Djava.library.path="${LIBPATH}" -classpath 
"${CLASSPATH}" org.apache.hop.run.HopRun "$@"
 EXITCODE=$?
 
diff --git a/assemblies/static/src/main/resources/hop-search.bat 
b/assemblies/static/src/main/resources/hop-search.bat
index 360a3df0ee..be6d8e37a5 100644
--- a/assemblies/static/src/main/resources/hop-search.bat
+++ b/assemblies/static/src/main/resources/hop-search.bat
@@ -22,7 +22,15 @@ REM switch to script directory
 cd /D %~dp0
 
 set LIBSPATH=lib\core;lib\beam
-set CLASSPATH=lib\core\*;lib\beam\*;lib\swt\win64\*
+set CLASSPATH=lib\core\*;lib\beam\*;lib\spark-client\*;lib\swt\win64\*
+
+REM Optional versioned Spark client pack
+if defined HOP_SPARK_CLIENT_VERSION if exist 
"lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\" (
+  set CLASSPATH=%CLASSPATH%;lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\*
+)
+
+
+
 
 :NormalStart
 REM set java primary is HOP_JAVA_HOME fallback to JAVA_HOME or default java
diff --git a/assemblies/static/src/main/resources/hop-search.sh 
b/assemblies/static/src/main/resources/hop-search.sh
index 00282a0a70..6bba102dea 100755
--- a/assemblies/static/src/main/resources/hop-search.sh
+++ b/assemblies/static/src/main/resources/hop-search.sh
@@ -78,22 +78,31 @@ Linux)
   if [ "${XDG_SESSION_TYPE}" = "wayland" ]; then
     export GDK_BACKEND=x11
   fi
+
+
   if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch 
= aarch64"; then
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/linux/arm64/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/arm64/*"
   else
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/linux/$(uname -m)/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/$(uname 
-m)/*"
   fi
   ;;
 Darwin)
   if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch 
= aarch64"; then
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/osx/arm64/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/arm64/*"
   else
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/osx/x86_64/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/x86_64/*"
   fi
   HOP_OPTIONS="${HOP_OPTIONS} -XstartOnFirstThread"
   ;;
 esac
 
+
+
+# Optional versioned Spark client pack replaces default lib/spark-client on 
the classpath.
+if [ -n "${HOP_SPARK_CLIENT_VERSION:-}" ] && [ -d 
"lib/spark-clients/${HOP_SPARK_CLIENT_VERSION}" ]; then
+  CLASSPATH=$(echo "${CLASSPATH}" | sed 's|lib/spark-client/\*||g')
+  CLASSPATH="${CLASSPATH}:lib/spark-clients/${HOP_SPARK_CLIENT_VERSION}/*"
+fi
 "${_HOP_JAVA}" ${HOP_OPTIONS} -Djava.library.path="${LIBPATH}" -classpath 
"${CLASSPATH}" org.apache.hop.search.HopSearch "$@"
 EXITCODE=$?
 
diff --git a/assemblies/static/src/main/resources/hop-server.bat 
b/assemblies/static/src/main/resources/hop-server.bat
index 1d39aa1527..40227e5c65 100644
--- a/assemblies/static/src/main/resources/hop-server.bat
+++ b/assemblies/static/src/main/resources/hop-server.bat
@@ -25,7 +25,15 @@ REM Option to change the Characterset of the Windows Shell 
to show foreign carac
 if not "%HOP_WINDOWS_SHELL_ENCODING%"=="" chcp %HOP_WINDOWS_SHELL_ENCODING%
 
 set LIBSPATH=lib\core;lib\beam
-set CLASSPATH=lib\core\*;lib\beam\*;lib\swt\win64\*
+set CLASSPATH=lib\core\*;lib\beam\*;lib\spark-client\*;lib\swt\win64\*
+
+REM Optional versioned Spark client pack
+if defined HOP_SPARK_CLIENT_VERSION if exist 
"lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\" (
+  set CLASSPATH=%CLASSPATH%;lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\*
+)
+
+
+
 
 :NormalStart
 REM set java primary is HOP_JAVA_HOME fallback to JAVA_HOME or default java
diff --git a/assemblies/static/src/main/resources/hop-server.sh 
b/assemblies/static/src/main/resources/hop-server.sh
index 02604432c2..06cedd3d4a 100755
--- a/assemblies/static/src/main/resources/hop-server.sh
+++ b/assemblies/static/src/main/resources/hop-server.sh
@@ -78,17 +78,19 @@ Linux)
   if [ "${XDG_SESSION_TYPE}" = "wayland" ]; then
     export GDK_BACKEND=x11
   fi
+
+
   if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch 
= aarch64"; then
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/linux/arm64/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/arm64/*"
   else
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/linux/$(uname -m)/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/linux/$(uname 
-m)/*"
   fi
   ;;
 Darwin)
   if "${_HOP_JAVA}" -XshowSettings:properties -version 2>&1 | grep -q "os.arch 
= aarch64"; then
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/osx/arm64/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/arm64/*"
   else
-    CLASSPATH="lib/core/*:lib/beam/*:lib/swt/osx/x86_64/*"
+    CLASSPATH="lib/core/*:lib/beam/*:lib/spark-client/*:lib/swt/osx/x86_64/*"
   fi
   HOP_OPTIONS="${HOP_OPTIONS} -XstartOnFirstThread"
   ;;
@@ -99,6 +101,13 @@ if [ "${JAAS_LOGIN_MODULE_CONFIG}" != "" ] && [ 
"${JAAS_LOGIN_MODULE_NAME}" != "
   HOP_OPTIONS="${HOP_OPTIONS} -Dloginmodulename=${JAAS_LOGIN_MODULE_NAME}"
 fi
 
+
+
+# Optional versioned Spark client pack replaces default lib/spark-client on 
the classpath.
+if [ -n "${HOP_SPARK_CLIENT_VERSION:-}" ] && [ -d 
"lib/spark-clients/${HOP_SPARK_CLIENT_VERSION}" ]; then
+  CLASSPATH=$(echo "${CLASSPATH}" | sed 's|lib/spark-client/\*||g')
+  CLASSPATH="${CLASSPATH}:lib/spark-clients/${HOP_SPARK_CLIENT_VERSION}/*"
+fi
 "${_HOP_JAVA}" ${HOP_OPTIONS} -Djava.library.path="${LIBPATH}" -classpath 
"${CLASSPATH}" org.apache.hop.www.HopServer "$@"
 EXITCODE=$?
 
diff --git a/assemblies/static/src/main/resources/hop.bat 
b/assemblies/static/src/main/resources/hop.bat
index af3dd79b3a..635a87f575 100755
--- a/assemblies/static/src/main/resources/hop.bat
+++ b/assemblies/static/src/main/resources/hop.bat
@@ -1,96 +1,104 @@
-@echo off
-
-REM
-REM Licensed to the Apache Software Foundation (ASF) under one or more
-REM contributor license agreements.  See the NOTICE file distributed with
-REM this work for additional information regarding copyright ownership.
-REM The ASF licenses this file to You under the Apache License, Version 2.0
-REM (the "License"); you may not use this file except in compliance with
-REM the License.  You may obtain a copy of the License at
-REM
-REM       http://www.apache.org/licenses/LICENSE-2.0
-REM
-REM Unless required by applicable law or agreed to in writing, software
-REM distributed under the License is distributed on an "AS IS" BASIS,
-REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-REM See the License for the specific language governing permissions and
-REM limitations under the License.
-REM
-
-setlocal EnableDelayedExpansion
-
-REM switch to script directory
-cd /D %~dp0
-
-REM Option to change the character set of the Windows Shell to show foreign 
characters
-if not "%HOP_WINDOWS_SHELL_ENCODING%"=="" chcp %HOP_WINDOWS_SHELL_ENCODING%
-
-set LIBSPATH=lib\core;lib\beam
-set CLASSPATH=lib\core\*;lib\beam\*;lib\swt\win64\*
-
-:NormalStart
-REM set java primary is HOP_JAVA_HOME fallback to JAVA_HOME or default java
-REM
-if not "%HOP_JAVA_HOME%"=="" (
-    set _HOP_JAVA="%HOP_JAVA_HOME%\bin\java"
-) else if not "%JAVA_HOME%"=="" (
-    set _HOP_JAVA="%JAVA_HOME%\bin\java"
-) else (
-    set _HOP_JAVA="java"
-)
-
-REM # Settings for all OSses
-REM
-
-if "%HOP_OPTIONS%"=="" set HOP_OPTIONS="-Xmx2048m"
-
-REM See if we need to enable some remote debugging options for our developers.
-REM
-FOR %%a in (%*) DO (
-  if "%%~a" == "--dev-debug" (
-    set HOP_OPTIONS=%HOP_OPTIONS% -Xdebug -Xnoagent 
-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5010
-  )
-  if "%%~a" == "--dev-debug-wait" (
-    set HOP_OPTIONS=%HOP_OPTIONS% -Xdebug -Xnoagent 
-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5010
-  )
-)
-
-REM Pass HOP variables if they're set.
-REM
-if not "%HOP_AUDIT_FOLDER%"=="" (
-  set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER="%HOP_AUDIT_FOLDER%"
-) else (
-   set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER=.\audit
-)
-if not "%HOP_CONFIG_FOLDER%"=="" (
-  set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_CONFIG_FOLDER="%HOP_CONFIG_FOLDER%"
-)
-if not "%HOP_SHARED_JDBC_FOLDERS%"=="" (
-  set HOP_OPTIONS=%HOP_OPTIONS% 
-DHOP_SHARED_JDBC_FOLDERS="%HOP_SHARED_JDBC_FOLDERS%"
-)
-if not "%HOP_PLUGIN_BASE_FOLDERS%"=="" (
-  set HOP_OPTIONS=%HOP_OPTIONS% 
-DHOP_PLUGIN_BASE_FOLDERS="%HOP_PLUGIN_BASE_FOLDERS%"
-)
-if not "%HOP_PASSWORD_ENCODER_PLUGIN%"=="" (
-  set HOP_OPTIONS=%HOP_OPTIONS% 
-DHOP_PASSWORD_ENCODER_PLUGIN=%HOP_PASSWORD_ENCODER_PLUGIN%
-)
-if not "%HOP_AES_ENCODER_KEY%"=="" (
-  set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY=%HOP_AES_ENCODER_KEY%
-)
-if not "%HOP_AES_ENCODER_KEY_FILE%"=="" (
-  set HOP_OPTIONS=%HOP_OPTIONS% 
-DHOP_AES_ENCODER_KEY_FILE=%HOP_AES_ENCODER_KEY_FILE%
-)
-
-set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_OS=Windows
-set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_RUNTIME=Run
-set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUTO_CREATE_CONFIG=Y
-set HOP_OPTIONS=%HOP_OPTIONS% --add-opens 
java.xml/jdk.xml.internal=ALL-UNNAMED --add-opens 
java.base/java.lang=ALL-UNNAMED --add-opens 
java.base/java.lang.invoke=ALL-UNNAMED --add-opens 
java.base/java.lang.reflect=ALL-UNNAMED --add-opens 
java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED 
--add-opens java.base/java.nio=ALL-UNNAMED --add-opens 
java.base/java.util=ALL-UNNAMED --add-opens 
java.base/java.util.concurrent=ALL-UNNAMED --add-opens 
java.base/java.util.concur [...]
-
-REM ===[Collect command line 
arguments...]======================================
-REM
-set _cmdline=%*
-
-:Run
-
-%_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% 
%HOP_OPTIONS% org.apache.hop.hop.Hop %_cmdline%%
-
+@echo off
+
+REM
+REM Licensed to the Apache Software Foundation (ASF) under one or more
+REM contributor license agreements.  See the NOTICE file distributed with
+REM this work for additional information regarding copyright ownership.
+REM The ASF licenses this file to You under the Apache License, Version 2.0
+REM (the "License"); you may not use this file except in compliance with
+REM the License.  You may obtain a copy of the License at
+REM
+REM       http://www.apache.org/licenses/LICENSE-2.0
+REM
+REM Unless required by applicable law or agreed to in writing, software
+REM distributed under the License is distributed on an "AS IS" BASIS,
+REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+REM See the License for the specific language governing permissions and
+REM limitations under the License.
+REM
+
+setlocal EnableDelayedExpansion
+
+REM switch to script directory
+cd /D %~dp0
+
+REM Option to change the character set of the Windows Shell to show foreign 
characters
+if not "%HOP_WINDOWS_SHELL_ENCODING%"=="" chcp %HOP_WINDOWS_SHELL_ENCODING%
+
+set LIBSPATH=lib\core;lib\beam
+set CLASSPATH=lib\core\*;lib\beam\*;lib\spark-client\*;lib\swt\win64\*
+
+REM Optional versioned Spark client pack
+if defined HOP_SPARK_CLIENT_VERSION if exist 
"lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\" (
+  set CLASSPATH=%CLASSPATH%;lib\spark-clients\%HOP_SPARK_CLIENT_VERSION%\*
+)
+
+
+
+
+:NormalStart
+REM set java primary is HOP_JAVA_HOME fallback to JAVA_HOME or default java
+REM
+if not "%HOP_JAVA_HOME%"=="" (
+    set _HOP_JAVA="%HOP_JAVA_HOME%\bin\java"
+) else if not "%JAVA_HOME%"=="" (
+    set _HOP_JAVA="%JAVA_HOME%\bin\java"
+) else (
+    set _HOP_JAVA="java"
+)
+
+REM # Settings for all OSses
+REM
+
+if "%HOP_OPTIONS%"=="" set HOP_OPTIONS="-Xmx2048m"
+
+REM See if we need to enable some remote debugging options for our developers.
+REM
+FOR %%a in (%*) DO (
+  if "%%~a" == "--dev-debug" (
+    set HOP_OPTIONS=%HOP_OPTIONS% -Xdebug -Xnoagent 
-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5010
+  )
+  if "%%~a" == "--dev-debug-wait" (
+    set HOP_OPTIONS=%HOP_OPTIONS% -Xdebug -Xnoagent 
-Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5010
+  )
+)
+
+REM Pass HOP variables if they're set.
+REM
+if not "%HOP_AUDIT_FOLDER%"=="" (
+  set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER="%HOP_AUDIT_FOLDER%"
+) else (
+   set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUDIT_FOLDER=.\audit
+)
+if not "%HOP_CONFIG_FOLDER%"=="" (
+  set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_CONFIG_FOLDER="%HOP_CONFIG_FOLDER%"
+)
+if not "%HOP_SHARED_JDBC_FOLDERS%"=="" (
+  set HOP_OPTIONS=%HOP_OPTIONS% 
-DHOP_SHARED_JDBC_FOLDERS="%HOP_SHARED_JDBC_FOLDERS%"
+)
+if not "%HOP_PLUGIN_BASE_FOLDERS%"=="" (
+  set HOP_OPTIONS=%HOP_OPTIONS% 
-DHOP_PLUGIN_BASE_FOLDERS="%HOP_PLUGIN_BASE_FOLDERS%"
+)
+if not "%HOP_PASSWORD_ENCODER_PLUGIN%"=="" (
+  set HOP_OPTIONS=%HOP_OPTIONS% 
-DHOP_PASSWORD_ENCODER_PLUGIN=%HOP_PASSWORD_ENCODER_PLUGIN%
+)
+if not "%HOP_AES_ENCODER_KEY%"=="" (
+  set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AES_ENCODER_KEY=%HOP_AES_ENCODER_KEY%
+)
+if not "%HOP_AES_ENCODER_KEY_FILE%"=="" (
+  set HOP_OPTIONS=%HOP_OPTIONS% 
-DHOP_AES_ENCODER_KEY_FILE=%HOP_AES_ENCODER_KEY_FILE%
+)
+
+set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_OS=Windows
+set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_PLATFORM_RUNTIME=Run
+set HOP_OPTIONS=%HOP_OPTIONS% -DHOP_AUTO_CREATE_CONFIG=Y
+set HOP_OPTIONS=%HOP_OPTIONS% --add-opens 
java.xml/jdk.xml.internal=ALL-UNNAMED --add-opens 
java.base/java.lang=ALL-UNNAMED --add-opens 
java.base/java.lang.invoke=ALL-UNNAMED --add-opens 
java.base/java.lang.reflect=ALL-UNNAMED --add-opens 
java.base/java.io=ALL-UNNAMED --add-opens java.base/java.net=ALL-UNNAMED 
--add-opens java.base/java.nio=ALL-UNNAMED --add-opens 
java.base/java.util=ALL-UNNAMED --add-opens 
java.base/java.util.concurrent=ALL-UNNAMED --add-opens 
java.base/java.util.concur [...]
+
+REM ===[Collect command line 
arguments...]======================================
+REM
+set _cmdline=%*
+
+:Run
+
+%_HOP_JAVA% -classpath %CLASSPATH% -Djava.library.path=%LIBSPATH% 
%HOP_OPTIONS% org.apache.hop.hop.Hop %_cmdline%%
+
diff --git a/docker/integration-tests/integration-tests-beam-base.yaml 
b/docker/integration-tests/integration-tests-beam-base.yaml
index fb62f84eca..eb2537f4a0 100644
--- a/docker/integration-tests/integration-tests-beam-base.yaml
+++ b/docker/integration-tests/integration-tests-beam-base.yaml
@@ -25,8 +25,12 @@ services:
     build:
       context: ../../.
       dockerfile: docker/integration-tests/unit-tests-beam.Dockerfile
+      args:
+        HOP_SPARK_CLIENT_VERSION: ${HOP_SPARK_CLIENT_VERSION:-}
     volumes:
       - ../../integration-tests/:/files
     environment:
       - FLASK_ENV=docker
+      # Keep driver Spark client pack in sync with the fat jar (empty = 
default pack)
+      - HOP_SPARK_CLIENT_VERSION=${HOP_SPARK_CLIENT_VERSION:-}
     command: [ "bash", "-c", "/files/scripts/run-tests.sh ${PROJECT_NAME}" ]
diff --git a/docker/integration-tests/integration-tests-spark.yaml 
b/docker/integration-tests/integration-tests-spark.yaml
index 871732ff0c..8b689d9a2d 100644
--- a/docker/integration-tests/integration-tests-spark.yaml
+++ b/docker/integration-tests/integration-tests-spark.yaml
@@ -15,39 +15,50 @@
 # specific language governing permissions and limitations
 # under the License.
 
+# Beam Spark smoke tests: Hop (Java 21) + standalone Spark master/worker.
+# SPARK_VERSION is overridable for the multi-version matrix (see 
run-spark-matrix.sh).
+# Hive/HDFS services are intentionally omitted from the default smoke path; 
re-add via a
+# separate compose file when the Hive catalog tests are revived.
+
 services:
-  integration_test_spark:
+  # Service name must be a valid Spark RPC hostname (no underscores).
+  hop-spark-test:
     extends:
       file: integration-tests-beam-base.yaml
       service: integration_test
     depends_on:
-      - spark
-      - spark-worker
-      - namenode
-      - datanode
-      - datanode2
-      - datanode3
-      - resourcemanager
-      - nodemanager
-      - hiveserver
-      - hive-metastore
+      spark:
+        condition: service_healthy
+      spark-worker:
+        condition: service_started
     links:
       - spark
       - spark-worker
-      - namenode
-      - datanode
-      - datanode2
-      - datanode3
-      - resourcemanager
-      - nodemanager
-      - hiveserver
-      - hive-metastore
+    # Hop is always Java 21. Spark workers also run Java 21 so they can 
execute Hop
+    # bytecode from the fat jar. Advertise this container as the Spark driver 
so
+    # workers on the compose network can open reverse connections.
+    environment:
+      - FLASK_ENV=docker
+      - SPARK_LOCAL_IP=0.0.0.0
+      # Match driver Spark client pack to cluster (and to fat jar); empty = 
default pack
+      - HOP_SPARK_CLIENT_VERSION=${HOP_SPARK_CLIENT_VERSION:-}
+      # spark.ui.enabled=false: Hop ships jakarta servlet APIs that clash with 
Spark's
+      # javax-based UI stack (jersey ServletContainer). UI is not needed for 
IT smoke.
+      - HOP_OPTIONS=-Dspark.ui.enabled=false 
-Dspark.driver.host=hop-spark-test -Dspark.driver.bindAddress=0.0.0.0
   spark:
     build:
       context: ../../docker/integration-tests/spark/.
       dockerfile: Dockerfile.master
+      args:
+        SPARK_VERSION: ${SPARK_VERSION:-3.5.8}
+        HADOOP_VERSION: ${HADOOP_VERSION:-3}
+        # Fast CDN by default (current patch); the Dockerfile falls back to 
archive.apache.org
+        # for historical matrix versions that the CDN no longer carries.
+        BASE_URL: ${SPARK_BASE_URL:-https://dlcdn.apache.org/spark}
     environment:
       - INIT_DAEMON_STEP=setup_spark
+      # Advertise a stable compose-network hostname (not the random container 
id)
+      - SPARK_MASTER_HOST=spark
     ports:
       - 8080
       - 7077
@@ -55,80 +66,14 @@ services:
     build:
       context: ../../docker/integration-tests/spark/.
       dockerfile: Dockerfile.worker
+      args:
+        SPARK_VERSION: ${SPARK_VERSION:-3.5.8}
+        HADOOP_VERSION: ${HADOOP_VERSION:-3}
+        BASE_URL: ${SPARK_BASE_URL:-https://dlcdn.apache.org/spark}
     depends_on:
-      - spark
+      spark:
+        condition: service_healthy
     environment:
       - "SPARK_MASTER=spark://spark:7077"
     ports:
       - 8081
-  namenode:
-    build: 
-      dockerfile: Dockerfile.hadoop
-      context: ../../docker/integration-tests/spark/.
-    hostname: namenode
-    command: ["hdfs", "namenode"]
-    ports:
-      - 9870
-      - 8020
-    env_file:
-      - ./spark/config
-    environment:
-      ENSURE_NAMENODE_DIR: "/tmp/hadoop-root/dfs/name"
-  datanode:
-    build: 
-      dockerfile: Dockerfile.hadoop
-      context: ../../docker/integration-tests/spark/.
-    command: ["hdfs", "datanode"]
-    env_file:
-      - ./spark/config
-  datanode2:
-    build: 
-      dockerfile: Dockerfile.hadoop
-      context: ../../docker/integration-tests/spark/.
-    command: ["hdfs", "datanode"]
-    env_file:
-      - ./spark/config
-  datanode3:
-    build: 
-      dockerfile: Dockerfile.hadoop
-      context: ../../docker/integration-tests/spark/.
-    command: ["hdfs", "datanode"]
-    env_file:
-      - ./spark/config
-  resourcemanager:
-    build: 
-      dockerfile: Dockerfile.hadoop
-      context: ../../docker/integration-tests/spark/.
-    hostname: resourcemanager
-    command: ["yarn", "resourcemanager"]
-    ports:
-       - 8088
-    env_file:
-      - ./spark/config
-    volumes:
-      - ./test.sh:/opt/test.sh
-  nodemanager:
-    build: 
-      dockerfile: Dockerfile.hadoop
-      context: ../../docker/integration-tests/spark/.
-    command: ["yarn", "nodemanager"]
-    env_file:
-      - ./spark/config
-  hiveserver:
-    image: apache/hive:4.0.0-beta-1
-    ports:
-      - 10000
-      - 10002
-    depends_on:
-      - hive-metastore
-    environment:
-      - SERVICE_NAME=hiveserver2
-      - SERVICE_OPTS=-Dhive.metastore.uris=thrift://hive-metastore:9083
-    volumes:
-      - ./spark/hive-site.xml:/opt/hive/conf/hive-site.xml
-  hive-metastore:
-    image: apache/hive:4.0.0-beta-1
-    ports:
-      - 9083
-    environment:
-      - SERVICE_NAME=metastore
diff --git a/docker/integration-tests/spark/Dockerfile.master 
b/docker/integration-tests/spark/Dockerfile.master
index 20f4149885..2caae28106 100644
--- a/docker/integration-tests/spark/Dockerfile.master
+++ b/docker/integration-tests/spark/Dockerfile.master
@@ -14,36 +14,54 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 #
-FROM alpine:3.10
 
-ENV ENABLE_INIT_DAEMON false
-ENV INIT_DAEMON_BASE_URI http://identifier/init-daemon
-ENV INIT_DAEMON_STEP spark_master_init
+# Spark standalone master for Hop Beam integration tests.
+# SPARK_VERSION is parameterized so the same image definition can matrix-test 
Spark 3.x lines.
+# Java 21 matches Hop's target bytecode (workers execute Hop transforms from 
the fat jar).
+FROM eclipse-temurin:21-jre-jammy
 
-ENV BASE_URL=https://dlcdn.apache.org/spark/
-ENV SPARK_VERSION=3.4.1
-ENV HADOOP_VERSION=3
-ENV SPARK_MASTER_PORT 7077
-ENV SPARK_MASTER_WEBUI_PORT 8080
-ENV SPARK_MASTER_LOG /spark/logs
+ARG SPARK_VERSION=3.5.8
+ARG HADOOP_VERSION=3
+# dlcdn.apache.org serves the current patch of each active line at CDN speed; 
archive.apache.org
+# keeps historical releases (needed for the multi-version matrix) but 
throttles hard (~100 KB/s).
+# We try BASE_URL first and fall back to ARCHIVE_URL, so the default pin stays 
fast while older
+# matrix versions (not on the CDN) still resolve.
+ARG BASE_URL=https://dlcdn.apache.org/spark
+ARG ARCHIVE_URL=https://archive.apache.org/dist/spark
+
+ENV ENABLE_INIT_DAEMON=false
+ENV INIT_DAEMON_BASE_URI=http://identifier/init-daemon
+ENV INIT_DAEMON_STEP=spark_master_init
+ENV SPARK_VERSION=${SPARK_VERSION}
+ENV HADOOP_VERSION=${HADOOP_VERSION}
+ENV SPARK_HOME=/spark
+ENV SPARK_MASTER_PORT=7077
+ENV SPARK_MASTER_WEBUI_PORT=8080
+ENV SPARK_MASTER_LOG=/spark/logs
+ENV PATH="${SPARK_HOME}/bin:${PATH}"
 
 COPY ./scripts/wait-for-step.sh /
 COPY ./scripts/execute-step.sh /
 COPY ./scripts/finish-step.sh /
 COPY ./scripts/master.sh /
 
-RUN apk add --no-cache curl bash openjdk11-jre nss libc6-compat coreutils 
procps \
-      && ln -s /lib64/ld-linux-x86-64.so.2 /lib/ld-linux-x86-64.so.2 \
-      && chmod +x *.sh \
-      && wget 
${BASE_URL}/spark-${SPARK_VERSION}/spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION}.tgz
 \
-      && tar -xvzf spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION}.tgz \
-      && mv spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION} spark \
-      && rm spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION}.tgz \
-      && cd /
-
-#Give permission to execute scripts
-RUN chmod +x /wait-for-step.sh && chmod +x /execute-step.sh && chmod +x 
/finish-step.sh && chmod +x /master.sh
+RUN apt-get update \
+      && apt-get install -y --no-install-recommends curl bash procps \
+      && rm -rf /var/lib/apt/lists/* \
+      && chmod +x /wait-for-step.sh /execute-step.sh /finish-step.sh 
/master.sh \
+      && SPARK_TGZ="spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION}.tgz" \
+      && ( curl -fSL --connect-timeout 30 --retry 3 --retry-delay 5 
--retry-all-errors \
+             --speed-limit 10240 --speed-time 60 \
+             "${BASE_URL}/spark-${SPARK_VERSION}/${SPARK_TGZ}" -o 
/tmp/spark.tgz \
+           || curl -fSL --connect-timeout 30 --retry 5 --retry-delay 5 
--retry-all-errors \
+             "${ARCHIVE_URL}/spark-${SPARK_VERSION}/${SPARK_TGZ}" -o 
/tmp/spark.tgz ) \
+      && tar -xzf /tmp/spark.tgz -C / \
+      && mv /spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION} /spark \
+      && rm /tmp/spark.tgz
 
 EXPOSE 8080 7077 6066
 
+HEALTHCHECK --interval=5s --timeout=5s --start-period=30s --retries=24 \
+  CMD curl -fsS "http://127.0.0.1:8080/"; >/dev/null || exit 1
+
 CMD ["/bin/bash", "/master.sh"]
diff --git a/docker/integration-tests/spark/Dockerfile.worker 
b/docker/integration-tests/spark/Dockerfile.worker
index 56d5fffc18..eb6ec13116 100644
--- a/docker/integration-tests/spark/Dockerfile.worker
+++ b/docker/integration-tests/spark/Dockerfile.worker
@@ -15,35 +15,47 @@
 # limitations under the License.
 #
 
-FROM alpine:3.10
-
-ENV ENABLE_INIT_DAEMON false
-ENV INIT_DAEMON_BASE_URI http://identifier/init-daemon
-ENV INIT_DAEMON_STEP spark_master_init
-
-ENV BASE_URL=https://dlcdn.apache.org/spark/
-ENV SPARK_VERSION=3.4.1
-ENV HADOOP_VERSION=3
-ENV SPARK_WORKER_WEBUI_PORT 8081
-ENV SPARK_WORKER_LOG /spark/logs
-ENV SPARK_MASTER "spark://spark:7077"
+# Spark standalone worker for Hop Beam integration tests.
+# Keep SPARK_VERSION in sync with Dockerfile.master (matrix-tested via compose 
build args).
+# Java 21 matches Hop's target bytecode (workers execute Hop transforms from 
the fat jar).
+FROM eclipse-temurin:21-jre-jammy
+
+ARG SPARK_VERSION=3.5.8
+ARG HADOOP_VERSION=3
+# Fast CDN first (current patch only), archive fallback for historical matrix 
versions.
+# Keep in sync with Dockerfile.master.
+ARG BASE_URL=https://dlcdn.apache.org/spark
+ARG ARCHIVE_URL=https://archive.apache.org/dist/spark
+
+ENV ENABLE_INIT_DAEMON=false
+ENV INIT_DAEMON_BASE_URI=http://identifier/init-daemon
+ENV INIT_DAEMON_STEP=spark_master_init
+ENV SPARK_VERSION=${SPARK_VERSION}
+ENV HADOOP_VERSION=${HADOOP_VERSION}
+ENV SPARK_HOME=/spark
+ENV SPARK_WORKER_WEBUI_PORT=8081
+ENV SPARK_WORKER_LOG=/spark/logs
+ENV SPARK_MASTER=spark://spark:7077
+ENV PATH="${SPARK_HOME}/bin:${PATH}"
 
 COPY ./scripts/wait-for-step.sh /
 COPY ./scripts/execute-step.sh /
 COPY ./scripts/finish-step.sh /
 COPY ./scripts/worker.sh /
 
-RUN apk add --no-cache curl bash openjdk11-jre nss libc6-compat coreutils 
procps \
-      && ln -s /lib64/ld-linux-x86-64.so.2 /lib/ld-linux-x86-64.so.2 \
-      && chmod +x *.sh \
-      && wget 
${BASE_URL}/spark-${SPARK_VERSION}/spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION}.tgz
 \
-      && tar -xvzf spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION}.tgz \
-      && mv spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION} spark \
-      && rm spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION}.tgz \
-      && cd /
-
-#Give permission to execute scripts
-RUN chmod +x /wait-for-step.sh && chmod +x /execute-step.sh && chmod +x 
/finish-step.sh && chmod +x /worker.sh
+RUN apt-get update \
+      && apt-get install -y --no-install-recommends curl bash procps \
+      && rm -rf /var/lib/apt/lists/* \
+      && chmod +x /wait-for-step.sh /execute-step.sh /finish-step.sh 
/worker.sh \
+      && SPARK_TGZ="spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION}.tgz" \
+      && ( curl -fSL --connect-timeout 30 --retry 3 --retry-delay 5 
--retry-all-errors \
+             --speed-limit 10240 --speed-time 60 \
+             "${BASE_URL}/spark-${SPARK_VERSION}/${SPARK_TGZ}" -o 
/tmp/spark.tgz \
+           || curl -fSL --connect-timeout 30 --retry 5 --retry-delay 5 
--retry-all-errors \
+             "${ARCHIVE_URL}/spark-${SPARK_VERSION}/${SPARK_TGZ}" -o 
/tmp/spark.tgz ) \
+      && tar -xzf /tmp/spark.tgz -C / \
+      && mv /spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION} /spark \
+      && rm /tmp/spark.tgz
 
 EXPOSE 8081
 
diff --git a/docker/integration-tests/unit-tests-beam.Dockerfile 
b/docker/integration-tests/unit-tests-beam.Dockerfile
index e807016997..b1b97c14b0 100644
--- a/docker/integration-tests/unit-tests-beam.Dockerfile
+++ b/docker/integration-tests/unit-tests-beam.Dockerfile
@@ -22,10 +22,21 @@
 # ~700MB file, so it is deliberately kept out of the shared base image 
(unit-tests.Dockerfile) and
 # built here instead. This image is only built/used when a project that 
references the fat jar
 # actually runs (see integration-tests-beam-base.yaml and 
scripts/run-tests-docker.sh).
+#
+# HOP_SPARK_CLIENT_VERSION: when set, the fat jar embeds that Spark client pack
+# (lib/spark-clients/<version>/), matching hop-run's driver classpath 
selection.
 
 FROM hop-base-image
 
+ARG HOP_SPARK_CLIENT_VERSION=
+ENV HOP_SPARK_CLIENT_VERSION=${HOP_SPARK_CLIENT_VERSION}
+
 # Runs as the (already configured) hop/jenkins user inherited from the base 
image, which owns the
 # Hop installation under ${DEPLOYMENT_PATH}/hop.
 RUN cd ${DEPLOYMENT_PATH}/hop \
-  && ./hop-conf.sh --generate-fat-jar=/tmp/hop-fatjar.jar
+  && if [ -n "${HOP_SPARK_CLIENT_VERSION}" ]; then \
+       ./hop-conf.sh --generate-fat-jar=/tmp/hop-fatjar.jar \
+         --spark-client-version="${HOP_SPARK_CLIENT_VERSION}"; \
+     else \
+       ./hop-conf.sh --generate-fat-jar=/tmp/hop-fatjar.jar; \
+     fi
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/beam/getting-started-with-beam.adoc
 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/beam/getting-started-with-beam.adoc
index 0b61f1b47f..ec471eb3ed 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/beam/getting-started-with-beam.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/beam/getting-started-with-beam.adoc
@@ -49,9 +49,21 @@ Here is the documentation for the relevant plugins:
 [[supportedversions]]
 == What software versions are supported
 
+[NOTE]
+====
+*Java:* Hop requires *Java 21* for the Hop process (GUI, hop-run, hop-server) 
and for the fat jar executed on Beam workers. Spark/Flink clusters must run a 
JVM that can load that bytecode (Java 21).
+
+*Spark (measured):* With Hop 2.19 / Beam 2.74 on *Java 21*, only *Spark 3.5.x* 
is supported for client-mode submit (`spark://…`). Spark 3.4 and earlier fail 
on Java 21 with `NoSuchMethodException: 
java.nio.DirectByteBuffer.<init>(long,int)` 
([SPARK-42369](https://issues.apache.org/jira/browse/SPARK-42369), fixed in 
Spark 3.5.0). Matching client packs to the cluster does not overcome that JDK 
limit. See `integration-tests/spark/MATRIX-REPORT.md`. Beam upstream docs may 
still mention 3.2.x [...]
+====
+
 |===
 |Hop version |Beam version |Spark version |Flink version
 
+|2.19.0
+|2.74.0
+|3.5.x (scala 2.12; pinned 3.5.8, matrix: 3.5.7 PASS)
+|1.19.x
+
 |2.18.0
 |2.71.0
 |3.5.X (scala 2.12)
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/pipeline-run-configurations/beam-spark-pipeline-engine.adoc
 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/pipeline-run-configurations/beam-spark-pipeline-engine.adoc
index d296bd56b9..44784704cc 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/pipeline/pipeline-run-configurations/beam-spark-pipeline-engine.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/pipeline/pipeline-run-configurations/beam-spark-pipeline-engine.adoc
@@ -24,7 +24,31 @@ under the License.
 
 == Beam Spark
 
-The Apache Spark Runner can be used to execute Beam pipelines using Apache 
Spark version 3.1.
+The Apache Spark Runner can be used to execute Beam pipelines using Apache 
Spark.
+
+IMPORTANT: Hop itself always runs on *Java 21* (driver and fat-jar bytecode). 
Spark standalone clusters used with Hop must also provide a Java 21 runtime so 
workers can execute Hop transforms.
+
+*Measured support (Hop 2.19 / Beam 2.74, client submit):* *Spark 3.5.x* only 
(validated *3.5.8*). Hop requires *Java 21*; Spark did not handle the JDK 21 
`DirectByteBuffer` constructor change until 
[SPARK-42369](https://issues.apache.org/jira/browse/SPARK-42369) (fixed in 
*Spark 3.5.0*). Therefore *Spark 3.4.x and earlier cannot run* with Hop on Java 
21 — even with a matching client pack and fat jar.
+
+For `spark://master` client mode on 3.5.x, the **Spark client libraries on the 
Hop driver and in the fat jar should still match the cluster patch/minor** 
(default pack under `lib/spark-client/`, currently 3.5.8).
+
+=== Matching Spark client libraries (3.5.x)
+
+Hop installs a default Spark client pack under `lib/spark-client/` (beam 
engine `spark.version`). Optional packs under `lib/spark-clients/<version>/` 
are for aligning with a *3.5.x* cluster build, not for downgrading below 3.5.
+
+.Materialise / select a 3.5.x pack
+[source,bash]
+----
+./tools/spark-client-pack/materialize-pack.sh 3.5.8 /path/to/hop
+export HOP_SPARK_CLIENT_VERSION=3.5.8
+./hop-conf.sh --generate-fat-jar=/path/to/hop-spark-3.5.8.jar \
+  --spark-client-version=3.5.8
+./hop-run.sh -r spark -f your-pipeline.hpl
+----
+
+Point the pipeline run configuration *Fat jar* field at the version-matched 
jar. Integration evidence: `integration-tests/spark/MATRIX-REPORT.md` and 
`MATRIX-REPORT-MATCHED.md`.
+
+Beam's upstream documentation may still pin the Spark runner to an older line 
(historically 3.2.x). Prefer the 
xref:pipeline/beam/getting-started-with-beam.adoc#supportedversions[supported 
versions table] and the matrix reports over assumptions.
 
 The Spark Runner executes Beam pipelines on top of Apache Spark, providing:
 
diff --git a/integration-tests/scripts/run-spark-matched-matrix.sh 
b/integration-tests/scripts/run-spark-matched-matrix.sh
new file mode 100755
index 0000000000..5912e7ffa5
--- /dev/null
+++ b/integration-tests/scripts/run-spark-matched-matrix.sh
@@ -0,0 +1,204 @@
+#!/bin/bash
+
+#
+# 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.
+#
+
+# Matched-pair Spark matrix: for each version V, use Spark client pack V + 
cluster V.
+#
+# Unlike run-spark-matrix.sh (fixed Hop client 3.5.8 vs varying cluster), this 
script:
+#   1. Materialises lib/spark-clients/<V>/ into the Hop install
+#   2. Rebuilds hop-base + hop-beam images so the fat jar embeds pack V
+#   3. Runs the smoke suite with HOP_SPARK_CLIENT_VERSION=V and SPARK_VERSION=V
+#
+# Prerequisites: assemblies/client package built (hop-client zip present).
+#
+# Usage:
+#   ./integration-tests/scripts/run-spark-matched-matrix.sh KEEP_IMAGES=true
+#   SPARK_VERSIONS="3.5.8 3.4.4" 
./integration-tests/scripts/run-spark-matched-matrix.sh
+
+set -u
+
+CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
+REPO_ROOT="$(cd "${CURRENT_DIR}/../.." >/dev/null 2>&1 && pwd)"
+REPORT_DIR="${CURRENT_DIR}/../surefire-reports"
+COMMITTED_REPORT="${CURRENT_DIR}/../spark/MATRIX-REPORT-MATCHED.md"
+MATRIX_REPORT="${REPORT_DIR}/spark-matched-matrix-report.md"
+RUN_TESTS="${CURRENT_DIR}/run-tests-docker.sh"
+MATERIALIZE="${REPO_ROOT}/tools/spark-client-pack/materialize-pack.sh"
+DOCKER_FILES_DIR="$(cd "${CURRENT_DIR}/../../docker/integration-tests" 
>/dev/null 2>&1 && pwd)"
+HOP_HOME="${REPO_ROOT}/assemblies/client/target/hop"
+
+if [ -z "${SPARK_VERSIONS:-}" ]; then
+  # Default: current 3.5.x pin. Pre-3.5 fails on Java 21 (SPARK-42369) even 
when matched.
+  # Override to re-document the ceiling: SPARK_VERSIONS="3.5.8 3.4.4"
+  SPARK_VERSIONS="3.5.8"
+fi
+
+FORWARD_ARGS=("$@")
+mkdir -p "${REPORT_DIR}"
+
+GENERATED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
+BEAM_VERSION=$(
+  grep -m1 'apache-beam.version' "${REPO_ROOT}/pom.xml" |
+    sed -E 's/.*<apache-beam.version>([^<]+)<\/apache-beam.version>.*/\1/' || 
true
+)
+
+hadoop_version_for_spark() {
+  case "$1" in
+  3.0.* | 3.1.* | 3.2.*) echo "3.2" ;;
+  *) echo "3" ;;
+  esac
+}
+
+evaluate_result() {
+  local report_file="$1"
+  RESULT="FAIL"
+  NOTES="—"
+  if [ ! -f "${report_file}" ]; then
+    NOTES="no surefire report"
+    return
+  fi
+  if grep -q '<failure' "${report_file}"; then
+    if grep -q 'environment_setup' "${report_file}"; then
+      NOTES="docker/environment failed"
+    elif grep -q 'InvalidClassException\|serialVersionUID' "${report_file}"; 
then
+      NOTES="serialVersionUID skew (client/cluster still mismatched?)"
+    elif grep -q 'DirectByteBuffer' "${report_file}"; then
+      NOTES="Java 21 DirectByteBuffer incompatibility"
+    elif grep -q 'stopped SparkContext' "${report_file}"; then
+      NOTES="stopped SparkContext"
+    elif grep -q 'ERROR:' "${report_file}"; then
+      NOTES="pipeline ERROR"
+    else
+      NOTES="test failed"
+    fi
+    return
+  fi
+  if ! grep -q '<testcase ' "${report_file}"; then
+    NOTES="empty report"
+    return
+  fi
+  RESULT="PASS"
+  if grep -q 'Beam pipeline execution has finished' "${report_file}"; then
+    NOTES="matched pack+cluster smoke finished"
+  else
+    NOTES="hop-run exit 0"
+  fi
+}
+
+# Ensure hop tree exists (same unzip as run-tests-docker)
+if [ ! -d "${HOP_HOME}" ]; then
+  echo "Extracting hop client zip into ${HOP_HOME} ..."
+  unzip -o -q "${REPO_ROOT}/assemblies/client/target/"*.zip -d 
"${REPO_ROOT}/assemblies/client/target/"
+fi
+
+{
+  echo "# Hop Beam Spark *matched* client/cluster matrix"
+  echo
+  echo "Generated: ${GENERATED_AT}"
+  echo
+  echo "- Hop: \`2.19.0-SNAPSHOT\` · Beam: \`${BEAM_VERSION}\`"
+  echo "- Each cell: **Spark client pack V** = **cluster V** (Java 21 driver + 
cluster)"
+  echo "- Suite: \`main-0001-test-spark-cluster\`"
+  echo
+  echo "| Client pack | Cluster | Result | Notes |"
+  echo "| --- | --- | --- | --- |"
+} >"${MATRIX_REPORT}"
+
+OVERALL_RC=0
+VERSION_TIMEOUT_SEC="${VERSION_TIMEOUT_SEC:-720}"
+
+for SPARK_VERSION in ${SPARK_VERSIONS}; do
+  echo "==========================================="
+  echo "Matched matrix: client ${SPARK_VERSION} + cluster ${SPARK_VERSION}"
+  echo "==========================================="
+
+  HADOOP_VERSION="$(hadoop_version_for_spark "${SPARK_VERSION}")"
+  VERSION_SAFE=${SPARK_VERSION//./_}
+  VERSION_REPORT="${REPORT_DIR}/surefile_spark_matched_${VERSION_SAFE}.xml"
+
+  # 1) Materialise pack into the Hop install that docker base image will COPY
+  bash "${MATERIALIZE}" "${SPARK_VERSION}" "${HOP_HOME}"
+
+  # 2) Force rebuild of base + beam images so fat jar embeds this pack
+  #    (base image must pick up newly added lib/spark-clients/)
+  export HOP_SPARK_CLIENT_VERSION="${SPARK_VERSION}"
+  export SPARK_VERSION
+  export HADOOP_VERSION
+  export SKIP_SUREFIRE_CLEAN=true
+  export KEEP_IMAGES="${KEEP_IMAGES:-true}"
+
+  rm -f "${REPORT_DIR}/surefile_spark.xml"
+  rm -f "${REPORT_DIR}/failed_tests" "${REPORT_DIR}/passed_tests"
+
+  # run-tests-docker.sh will: unzip client zip, re-materialise pack, copy pack 
→ lib/spark-client,
+  # invalidate/rebuild hop-base + hop-beam images, then run smoke. Do not 
pre-build images here
+  # (a pre-build is wiped by unzip inside run-tests-docker).
+  set +e
+  timeout --signal=TERM --kill-after=60 "${VERSION_TIMEOUT_SEC}" \
+    bash "${RUN_TESTS}" \
+      PROJECT_NAME=spark \
+      SPARK_VERSION="${SPARK_VERSION}" \
+      HADOOP_VERSION="${HADOOP_VERSION}" \
+      HOP_SPARK_CLIENT_VERSION="${SPARK_VERSION}" \
+      KEEP_IMAGES="${KEEP_IMAGES:-true}" \
+      "${FORWARD_ARGS[@]}"
+  RC=$?
+  set -e
+
+  PROJECT_NAME=spark SPARK_VERSION="${SPARK_VERSION}" 
HADOOP_VERSION="${HADOOP_VERSION}" \
+    HOP_SPARK_CLIENT_VERSION="${SPARK_VERSION}" \
+    docker compose -f "${DOCKER_FILES_DIR}/integration-tests-spark.yaml" down 
--remove-orphans >/dev/null 2>&1 || true
+
+  if [ -f "${REPORT_DIR}/surefile_spark.xml" ]; then
+    cp -f "${REPORT_DIR}/surefile_spark.xml" "${VERSION_REPORT}"
+  fi
+
+  evaluate_result "${VERSION_REPORT}"
+  if [ ${RC} -eq 124 ] || [ ${RC} -eq 137 ]; then
+    RESULT="FAIL"
+    NOTES="timed out (${NOTES})"
+  fi
+  if [ "${RESULT}" != "PASS" ]; then
+    OVERALL_RC=1
+  fi
+
+  echo "| ${SPARK_VERSION} | ${SPARK_VERSION} | ${RESULT} | ${NOTES} |" 
>>"${MATRIX_REPORT}"
+  echo "Matched result ${SPARK_VERSION}: ${RESULT} (${NOTES})"
+done
+
+{
+  echo
+  echo "## How to re-run"
+  echo
+  echo '```bash'
+  echo "./integration-tests/scripts/run-spark-matched-matrix.sh 
KEEP_IMAGES=true"
+  echo "SPARK_VERSIONS=\"3.5.8 3.4.4\" 
./integration-tests/scripts/run-spark-matched-matrix.sh"
+  echo '```'
+  echo
+  echo "## Notes"
+  echo
+  echo "- Client pack materialised by 
\`tools/spark-client-pack/materialize-pack.sh\`."
+  echo "- Fat jar built with \`hop-conf --spark-client-version=V\` inside 
hop-beam-image."
+  echo "- Driver uses \`HOP_SPARK_CLIENT_VERSION=V\` so classpath matches the 
fat jar."
+  echo "- Compare with fixed-client results in \`MATRIX-REPORT.md\`."
+} >>"${MATRIX_REPORT}"
+
+cp -f "${MATRIX_REPORT}" "${COMMITTED_REPORT}"
+echo
+cat "${MATRIX_REPORT}"
+exit ${OVERALL_RC}
diff --git a/integration-tests/scripts/run-spark-matrix.sh 
b/integration-tests/scripts/run-spark-matrix.sh
new file mode 100755
index 0000000000..a404d21295
--- /dev/null
+++ b/integration-tests/scripts/run-spark-matrix.sh
@@ -0,0 +1,255 @@
+#!/bin/bash
+
+#
+# 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.
+#
+
+# Run the Hop Beam Spark smoke suite against multiple Spark 3.x standalone 
versions.
+# Hop and Beam stay fixed; only the cluster image SPARK_VERSION changes.
+#
+# Usage:
+#   ./integration-tests/scripts/run-spark-matrix.sh
+#   SPARK_VERSIONS="3.5.8 3.4.4" 
./integration-tests/scripts/run-spark-matrix.sh
+#   ./integration-tests/scripts/run-spark-matrix.sh KEEP_IMAGES=true
+#
+# Prerequisites: assemblies/client must already be built (same as 
run-tests-docker.sh).
+
+set -u
+
+CURRENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
+REPO_ROOT="$(cd "${CURRENT_DIR}/../.." >/dev/null 2>&1 && pwd)"
+REPORT_DIR="${CURRENT_DIR}/../surefire-reports"
+# Committed snapshot of the last measured matrix (refreshed by this script)
+COMMITTED_REPORT="${CURRENT_DIR}/../spark/MATRIX-REPORT.md"
+MATRIX_REPORT="${REPORT_DIR}/spark-matrix-report.md"
+RUN_TESTS="${CURRENT_DIR}/run-tests-docker.sh"
+DOCKER_COMPOSE_FILE="$(cd "${CURRENT_DIR}/../../docker/integration-tests" 
>/dev/null 2>&1 && pwd)/integration-tests-spark.yaml"
+
+# Representative patch releases for each Spark 3.x line.
+# Override with SPARK_VERSIONS="3.5.8 3.2.4" etc.
+# Note: Hop client mode bundles spark-core at plugins/engines/beam 
spark.version.
+# Clusters on a different Spark minor typically fail with InvalidClassException
+# (ApplicationDescription serialVersionUID mismatch). That is still a measured 
result.
+if [ -z "${SPARK_VERSIONS:-}" ]; then
+  SPARK_VERSIONS="3.2.4 3.3.4 3.4.4 3.5.8"
+fi
+
+# Map Spark line → Hadoop binary suffix used on archive.apache.org
+# 3.2.x ships as bin-hadoop3.2; 3.3+ use bin-hadoop3.
+hadoop_version_for_spark() {
+  case "$1" in
+  3.0.* | 3.1.* | 3.2.*) echo "3.2" ;;
+  *) echo "3" ;;
+  esac
+}
+
+FORWARD_ARGS=("$@")
+
+mkdir -p "${REPORT_DIR}"
+
+HOP_VERSION="2.19.0-SNAPSHOT"
+BEAM_VERSION="unknown"
+if [ -f "${REPO_ROOT}/pom.xml" ]; then
+  BEAM_VERSION=$(
+    grep -m1 'apache-beam.version' "${REPO_ROOT}/pom.xml" |
+      sed -E 's/.*<apache-beam.version>([^<]+)<\/apache-beam.version>.*/\1/' 
|| true
+  )
+fi
+if [ -f "${REPO_ROOT}/plugins/engines/beam/pom.xml" ]; then
+  SPARK_CLIENT_VERSION=$(
+    grep -m1 'spark.version' "${REPO_ROOT}/plugins/engines/beam/pom.xml" |
+      sed -E 's/.*<spark.version>([^<]+)<\/spark.version>.*/\1/' || true
+  )
+else
+  SPARK_CLIENT_VERSION="unknown"
+fi
+
+GENERATED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
+
+# Evaluate a surefire report. Sets RESULT and NOTES.
+# Note: run-tests.sh increments failure counters inside a pipeline subshell, so
+# suite-level failures=/errors= attributes are often wrong. Trust <failure> 
tags.
+evaluate_result() {
+  local report_file="$1"
+  RESULT="FAIL"
+  NOTES="—"
+
+  if [ ! -f "${report_file}" ]; then
+    NOTES="no surefire report produced"
+    return
+  fi
+
+  if grep -q '<failure' "${report_file}"; then
+    if grep -q 'environment_setup' "${report_file}"; then
+      NOTES="docker/environment failed to start"
+    elif grep -q 'InvalidClassException\|serialVersionUID' "${report_file}"; 
then
+      NOTES="Spark client/cluster version skew (serialVersionUID)"
+    elif grep -q 'NoClassDefFoundError' "${report_file}"; then
+      NOTES="NoClassDefFoundError (classpath)"
+    elif grep -q 'UnsupportedClassVersionError' "${report_file}"; then
+      NOTES="UnsupportedClassVersionError (Java mismatch)"
+    elif grep -q 'Invalid Spark URL' "${report_file}"; then
+      NOTES="Invalid Spark URL (driver hostname)"
+    elif grep -q 'Servlet class org.glassfish.jersey' "${report_file}"; then
+      NOTES="Spark UI servlet clash"
+    elif grep -q 'stopped SparkContext' "${report_file}"; then
+      NOTES="stopped SparkContext (often version skew)"
+    elif grep -q 'ERROR:' "${report_file}"; then
+      NOTES="pipeline ERROR (see surefire log)"
+    else
+      NOTES="test failed (see surefire log)"
+    fi
+    return
+  fi
+
+  if ! grep -q '<testcase ' "${report_file}"; then
+    NOTES="empty surefire report"
+    return
+  fi
+
+  RESULT="PASS"
+  if grep -q 'Beam pipeline execution has finished' "${report_file}"; then
+    NOTES="smoke pipeline finished"
+  else
+    NOTES="hop-run exit 0"
+  fi
+}
+
+{
+  echo "# Hop Beam Spark support matrix"
+  echo
+  echo "Generated: ${GENERATED_AT}"
+  echo
+  echo "- Hop: \`${HOP_VERSION}\`"
+  echo "- Beam: \`${BEAM_VERSION}\`"
+  echo "- Client Spark libraries (\`spark.version\`): 
\`${SPARK_CLIENT_VERSION}\` (scala 2.12)"
+  echo "- Driver JVM: Java 21 (Hop requirement)"
+  echo "- Cluster JVM: Java 21 (matches Hop bytecode in fat jar)"
+  echo "- Suite: \`integration-tests/spark\` smoke 
(\`main-0001-test-spark-cluster\`)"
+  echo "- Cluster: Spark standalone master + worker (parameterized 
\`SPARK_VERSION\`)"
+  echo
+  echo "| Spark version | Result | Surefire report | Notes |"
+  echo "| --- | --- | --- | --- |"
+} >"${MATRIX_REPORT}"
+
+OVERALL_RC=0
+
+for SPARK_VERSION in ${SPARK_VERSIONS}; do
+  echo "==========================================="
+  echo "Matrix run: Spark ${SPARK_VERSION}"
+  echo "==========================================="
+
+  VERSION_SAFE=${SPARK_VERSION//./_}
+  VERSION_REPORT="${REPORT_DIR}/surefile_spark_${VERSION_SAFE}.xml"
+  HADOOP_VERSION="$(hadoop_version_for_spark "${SPARK_VERSION}")"
+
+  echo "Cluster binary: spark-${SPARK_VERSION}-bin-hadoop${HADOOP_VERSION}"
+
+  # Drop the live surefile so a failed environment cannot inherit a prior PASS 
report
+  rm -f "${REPORT_DIR}/surefile_spark.xml"
+  rm -f "${REPORT_DIR}/failed_tests" "${REPORT_DIR}/passed_tests"
+
+  # Per-version wall clock (seconds). Version-skew / Java mismatches can thrash
+  # executors for a long time without a hard timeout.
+  VERSION_TIMEOUT_SEC="${VERSION_TIMEOUT_SEC:-600}"
+
+  set +e
+  export SKIP_SUREFIRE_CLEAN=true
+  export SPARK_VERSION
+  export HADOOP_VERSION
+  export KEEP_IMAGES="${KEEP_IMAGES:-true}"
+  # shellcheck disable=SC2086
+  timeout --signal=TERM --kill-after=60 "${VERSION_TIMEOUT_SEC}" \
+    bash "${RUN_TESTS}" \
+      PROJECT_NAME=spark \
+      SPARK_VERSION="${SPARK_VERSION}" \
+      HADOOP_VERSION="${HADOOP_VERSION}" \
+      KEEP_IMAGES="${KEEP_IMAGES:-true}" \
+      "${FORWARD_ARGS[@]}"
+  RC=$?
+  set -e
+
+  if [ ${RC} -eq 124 ] || [ ${RC} -eq 137 ]; then
+    echo "Matrix run for Spark ${SPARK_VERSION} timed out after 
${VERSION_TIMEOUT_SEC}s"
+  fi
+
+  # Always tear down this version's stack before the next rebuild
+  PROJECT_NAME=spark SPARK_VERSION="${SPARK_VERSION}" 
HADOOP_VERSION="${HADOOP_VERSION}" \
+    docker compose -f "${DOCKER_COMPOSE_FILE}" down --remove-orphans 
>/dev/null 2>&1 || true
+
+  if [ -f "${REPORT_DIR}/surefile_spark.xml" ]; then
+    cp -f "${REPORT_DIR}/surefile_spark.xml" "${VERSION_REPORT}"
+  fi
+
+  evaluate_result "${VERSION_REPORT}"
+
+  # Enrich notes from the matrix log / known environmental failures when 
surefire is empty
+  if [ "${RESULT}" = "FAIL" ] && [ "${NOTES}" = "docker/environment failed to 
start" ]; then
+    if docker logs "$(docker ps -aq --filter name=integration-tests-spark 
2>/dev/null | head -1)" 2>/dev/null |
+      grep -q 'DirectByteBuffer'; then
+      NOTES="Spark does not start on Java 21 (DirectByteBuffer)"
+    fi
+  fi
+  if [ ${RC} -eq 124 ] || [ ${RC} -eq 137 ]; then
+    RESULT="FAIL"
+    NOTES="timed out after ${VERSION_TIMEOUT_SEC}s (${NOTES})"
+  fi
+
+  if [ "${RESULT}" != "PASS" ]; then
+    OVERALL_RC=1
+  fi
+  # Script exit code from docker is informational only (container often exits 
0)
+  if [ ${RC} -ne 0 ] && [ ${RC} -ne 124 ] && [ ${RC} -ne 137 ] && [ 
"${RESULT}" = "PASS" ]; then
+    NOTES="${NOTES}; docker rc=${RC}"
+  fi
+
+  echo "| ${SPARK_VERSION} | ${RESULT} | 
\`surefile_spark_${VERSION_SAFE}.xml\` | ${NOTES} |" >>"${MATRIX_REPORT}"
+  echo "Matrix result for Spark ${SPARK_VERSION}: ${RESULT} (docker_rc=${RC}, 
notes=${NOTES})"
+done
+
+{
+  echo
+  echo "## How to re-run"
+  echo
+  echo '```bash'
+  echo "# Full matrix"
+  echo "./integration-tests/scripts/run-spark-matrix.sh KEEP_IMAGES=true"
+  echo
+  echo "# Single version"
+  echo "SPARK_VERSION=3.5.8 ./integration-tests/scripts/run-tests-docker.sh 
PROJECT_NAME=spark KEEP_IMAGES=true"
+  echo '```'
+  echo
+  echo "## Notes"
+  echo
+  echo "- Beam upstream docs still list Spark 3.2.x as the supported line; 
this matrix is **empirical** for Hop pipelines on Beam ${BEAM_VERSION}."
+  echo "- Hive catalog workflows are deferred (\`optional-0002/0003-*.hwf\`) 
and not part of this matrix."
+  echo "- **Client mode** (Hop GUI / hop-run → \`spark://master\`) uses Spark 
libraries bundled with Hop (\`${SPARK_CLIENT_VERSION}\`)."
+  echo "  Clusters on a different Spark minor almost always fail with 
\`InvalidClassException\` / serialVersionUID mismatch on 
\`ApplicationDescription\`."
+  echo "  Supported client-mode line is therefore the Hop \`spark.version\` 
minor (currently 3.5.x)."
+  echo "- Spark 3.2.x binaries use the \`bin-hadoop3.2\` artifact name; 3.3+ 
use \`bin-hadoop3\`."
+} >>"${MATRIX_REPORT}"
+
+# Refresh the committed project copy for docs / PR visibility
+cp -f "${MATRIX_REPORT}" "${COMMITTED_REPORT}"
+
+echo
+echo "Matrix report written to:"
+echo "  ${MATRIX_REPORT}"
+echo "  ${COMMITTED_REPORT}"
+echo
+cat "${MATRIX_REPORT}"
+
+exit ${OVERALL_RC}
diff --git a/integration-tests/scripts/run-tests-docker.sh 
b/integration-tests/scripts/run-tests-docker.sh
index 07787d9630..ffac427059 100755
--- a/integration-tests/scripts/run-tests-docker.sh
+++ b/integration-tests/scripts/run-tests-docker.sh
@@ -36,11 +36,31 @@ for ARGUMENT in "$@"; do
   GCP_KEY_FILE) GCP_KEY_FILE=${VALUE} ;;
   KEEP_IMAGES) KEEP_IMAGES=${VALUE} ;;
   CLIENT_UNZIP) CLIENT_UNZIP=${VALUE} ;;
+  SPARK_VERSION) SPARK_VERSION=${VALUE} ;;
+  HADOOP_VERSION) HADOOP_VERSION=${VALUE} ;;
+  SPARK_BASE_URL) SPARK_BASE_URL=${VALUE} ;;
+  HOP_SPARK_CLIENT_VERSION) HOP_SPARK_CLIENT_VERSION=${VALUE} ;;
   *) ;;
   esac
 
 done
 
+# Default Spark standalone version for integration-tests/spark (overridable 
for matrix runs)
+if [ -z "${SPARK_VERSION}" ]; then
+  SPARK_VERSION="3.5.8"
+fi
+if [ -z "${HADOOP_VERSION}" ]; then
+  HADOOP_VERSION="3"
+fi
+# Prefer the fast CDN (current patch only); the Dockerfile falls back to 
archive.apache.org
+# for historical matrix versions the CDN no longer carries.
+if [ -z "${SPARK_BASE_URL}" ]; then
+  SPARK_BASE_URL="https://dlcdn.apache.org/spark";
+fi
+# Optional: match driver + fat-jar Spark client pack to a cluster minor (see 
tools/spark-client-pack)
+export SPARK_VERSION HADOOP_VERSION SPARK_BASE_URL
+export HOP_SPARK_CLIENT_VERSION="${HOP_SPARK_CLIENT_VERSION:-}"
+
 if [ -z "${PROJECT_NAME}" ]; then
   PROJECT_NAME="*"
 fi
@@ -83,8 +103,10 @@ if [ -z "${CLIENT_UNZIP}" ]; then
   CLIENT_UNZIP="true"
 fi
 
-# Cleanup surefire reports
-rm -rf "${CURRENT_DIR}"/../surefire-reports
+# Cleanup surefire reports (matrix runs set SKIP_SUREFIRE_CLEAN=true to keep 
per-version copies)
+if [ "${SKIP_SUREFIRE_CLEAN:-false}" != "true" ]; then
+  rm -rf "${CURRENT_DIR}"/../surefire-reports
+fi
 mkdir -p "${CURRENT_DIR}"/../surefire-reports/
 chmod 777 "${CURRENT_DIR}"/../surefire-reports/
 
@@ -103,7 +125,48 @@ else
   echo "Skipping client unzip (CLIENT_UNZIP=${CLIENT_UNZIP}, using existing 
${HOP_DIR})"
 fi
 
-# Build base image only once
+# Versioned Spark client packs are not in the client zip. Re-materialise after 
unzip so
+# HOP_SPARK_CLIENT_VERSION=… finds lib/spark-clients/<ver>/ (includes 
spark-streaming, etc.).
+# Also copy the selected pack into lib/spark-client/ so the default driver 
classpath always
+# has spark-core + spark-streaming even if hop-run only loads 
lib/spark-client/*.
+HOP_HOME_FOR_PACK="${CURRENT_DIR}/../../assemblies/client/target/hop"
+if [ -n "${HOP_SPARK_CLIENT_VERSION}" ]; then
+  
MATERIALIZE_SCRIPT="${CURRENT_DIR}/../../tools/spark-client-pack/materialize-pack.sh"
+  if [ -f "${MATERIALIZE_SCRIPT}" ]; then
+    echo "Materialising Spark client pack ${HOP_SPARK_CLIENT_VERSION} into 
${HOP_HOME_FOR_PACK}"
+    bash "${MATERIALIZE_SCRIPT}" "${HOP_SPARK_CLIENT_VERSION}" 
"${HOP_HOME_FOR_PACK}"
+    
PACK_DIR="${HOP_HOME_FOR_PACK}/lib/spark-clients/${HOP_SPARK_CLIENT_VERSION}"
+    if [ -d "${PACK_DIR}" ] && [ -f 
"${PACK_DIR}/spark-core_2.12-${HOP_SPARK_CLIENT_VERSION}.jar" ]; then
+      echo "Activating pack ${HOP_SPARK_CLIENT_VERSION} as lib/spark-client 
(driver classpath)"
+      rm -rf "${HOP_HOME_FOR_PACK}/lib/spark-client"
+      mkdir -p "${HOP_HOME_FOR_PACK}/lib/spark-client"
+      cp -a "${PACK_DIR}/." "${HOP_HOME_FOR_PACK}/lib/spark-client/"
+      # Prove critical jars are present for the driver
+      ls -1 "${HOP_HOME_FOR_PACK}/lib/spark-client"/spark-core*.jar \
+            "${HOP_HOME_FOR_PACK}/lib/spark-client"/spark-streaming*.jar
+    else
+      echo "ERROR: Spark client pack incomplete at ${PACK_DIR}" >&2
+      ls -la "${PACK_DIR}" 2>/dev/null || true
+      exit 1
+    fi
+  else
+    echo "WARNING: ${MATERIALIZE_SCRIPT} not found; pack 
${HOP_SPARK_CLIENT_VERSION} may be missing"
+  fi
+fi
+
+# Bust docker cache for the hop COPY layer when packs change
+if [ -d "${HOP_HOME_FOR_PACK}" ]; then
+  date -u +%Y-%m-%dT%H:%M:%SZ > "${HOP_HOME_FOR_PACK}/.spark-client-pack-stamp"
+fi
+
+# Drop stale base/beam images when using a versioned Spark pack so COPY hop 
picks up jars
+if [ -n "${HOP_SPARK_CLIENT_VERSION}" ]; then
+  echo "Invalidating hop-base-image / hop-beam-image for Spark client pack 
${HOP_SPARK_CLIENT_VERSION}"
+  docker rmi hop-beam-image 2>/dev/null || true
+  docker rmi hop-base-image 2>/dev/null || true
+fi
+
+# Build base image only once (must run AFTER pack materialise so jars are in 
the image)
 docker compose -f ${DOCKER_FILES_DIR}/integration-tests-base.yaml build 
--build-arg JENKINS_USER=${JENKINS_USER} --build-arg JENKINS_UID=${JENKINS_UID} 
--build-arg JENKINS_GROUP=${JENKINS_GROUP} --build-arg 
JENKINS_GID=${JENKINS_GID} --build-arg GCP_KEY_FILE=${GCP_KEY_FILE}
 
 # The Hop fat jar (needed only by the Beam runners: spark/flink/gcp) is 
expensive to build, so it
@@ -129,7 +192,12 @@ for d in "${CURRENT_DIR}"/../${PROJECT_NAME}/; do
       # Built once per run, and only when such a project is actually enabled.
       if [ "${BEAM_IMAGE_BUILT}" != "true" ] && grep -rqs "hop-fatjar.jar" 
"$d" 2>/dev/null; then
         echo "Project ${PROJECT_NAME} needs the Hop fat jar; building 
hop-beam-image (once)."
-        docker compose -f ${DOCKER_FILES_DIR}/integration-tests-beam-base.yaml 
build
+        if [ -n "${HOP_SPARK_CLIENT_VERSION}" ]; then
+          echo "Spark client pack for fat jar: ${HOP_SPARK_CLIENT_VERSION}"
+        fi
+        HOP_SPARK_CLIENT_VERSION="${HOP_SPARK_CLIENT_VERSION}" \
+          docker compose -f 
${DOCKER_FILES_DIR}/integration-tests-beam-base.yaml build \
+            --build-arg HOP_SPARK_CLIENT_VERSION="${HOP_SPARK_CLIENT_VERSION}"
         EXECUTED_COMPOSE_FILES=("${EXECUTED_COMPOSE_FILES[@]}" 
"${DOCKER_FILES_DIR}/integration-tests-beam-base.yaml")
         BEAM_IMAGE_BUILT="true"
       fi
@@ -139,7 +207,14 @@ for d in "${CURRENT_DIR}"/../${PROJECT_NAME}/; do
       if [ -f "${DOCKER_FILES_DIR}/integration-tests-${PROJECT_NAME}.yaml" ]; 
then
         echo "Project compose exists."
         EXECUTED_COMPOSE_FILES=("${EXECUTED_COMPOSE_FILES[@]}" 
"${DOCKER_FILES_DIR}/integration-tests-${PROJECT_NAME}.yaml")
-        PROJECT_NAME=${PROJECT_NAME} docker compose -f 
${DOCKER_FILES_DIR}/integration-tests-${PROJECT_NAME}.yaml up 
--abort-on-container-exit
+        # Rebuild project images so SPARK_VERSION (and similar) build args 
take effect
+        if [ "${PROJECT_NAME}" = "spark" ]; then
+          echo "Spark IT cluster version: ${SPARK_VERSION} (hadoop 
${HADOOP_VERSION})"
+          PROJECT_NAME=${PROJECT_NAME} SPARK_VERSION=${SPARK_VERSION} 
HADOOP_VERSION=${HADOOP_VERSION} SPARK_BASE_URL=${SPARK_BASE_URL} \
+            docker compose -f 
${DOCKER_FILES_DIR}/integration-tests-${PROJECT_NAME}.yaml up --build 
--abort-on-container-exit
+        else
+          PROJECT_NAME=${PROJECT_NAME} docker compose -f 
${DOCKER_FILES_DIR}/integration-tests-${PROJECT_NAME}.yaml up 
--abort-on-container-exit
+        fi
       else
         echo "Project compose does not exists."
         PROJECT_NAME=${PROJECT_NAME} docker compose -f 
${DOCKER_FILES_DIR}/integration-tests-base.yaml up --abort-on-container-exit
diff --git a/integration-tests/spark/0001-test-spark-cluster.hpl 
b/integration-tests/spark/0001-test-spark-cluster.hpl
index 1e86f3d71b..734b1222b5 100644
--- a/integration-tests/spark/0001-test-spark-cluster.hpl
+++ b/integration-tests/spark/0001-test-spark-cluster.hpl
@@ -19,7 +19,7 @@ limitations under the License.
 -->
 <pipeline>
   <info>
-    <name>0001-test-flink-cluster</name>
+    <name>0001-test-spark-cluster</name>
     <name_sync_with_filename>Y</name_sync_with_filename>
     <description/>
     <extended_description/>
diff --git a/integration-tests/spark/MATRIX-REPORT-MATCHED.md 
b/integration-tests/spark/MATRIX-REPORT-MATCHED.md
new file mode 100644
index 0000000000..249ef56a9d
--- /dev/null
+++ b/integration-tests/spark/MATRIX-REPORT-MATCHED.md
@@ -0,0 +1,45 @@
+# Hop Beam Spark *matched* client/cluster matrix
+
+Generated: 2026-07-10 (issue #7476)
+
+> **Pin moved 3.5.7 → 3.5.8** (current patch on `dlcdn.apache.org`; 3.5.7 is 
now archive-only
+> and throttled). Same 3.5.x line, same conclusion. The measured row still 
shows the last
+> **3.5.7** run — re-run `run-spark-matched-matrix.sh` to refresh it for 3.5.8.
+
+- Hop: `2.19.0-SNAPSHOT` · Beam: `2.74.0`
+- Each cell: **Spark client pack V** = **cluster V** (Java 21 driver + cluster)
+- Suite: `main-0001-test-spark-cluster`
+
+## Results
+
+| Client pack | Cluster | Result | Notes |
+| --- | --- | --- | --- |
+| 3.5.7 | 3.5.7 | **PASS** | Pack activated as `lib/spark-client`; smoke 
finished |
+| 3.4.4 | 3.4.4 | **FAIL** | `NoSuchMethodException: 
java.nio.DirectByteBuffer.<init>(long,int)` — 
[SPARK-42369](https://issues.apache.org/jira/browse/SPARK-42369) fixed only 
from **Spark 3.5.0** |
+
+## Interpretation
+
+Matching the client pack to the cluster **eliminates RPC serialVersionUID 
skew**, but **cannot** make Spark 3.4 run on **Java 21**. Hop requires Java 21 
for driver and workers (fat-jar bytecode), so **Spark ≥ 3.5.0 is the platform 
floor**.
+
+Versioned packs remain valuable for:
+
+- Aligning Hop’s client jars with a **3.5.x** cluster patch level
+- Future matrix cells within 3.5.x (e.g. 3.5.7 vs 3.5.8)
+
+They are **not** a path back to Spark 3.4 / 3.3 / 3.2 while Hop stays on Java 
21.
+
+## How to run
+
+```bash
+./integration-tests/scripts/run-spark-matched-matrix.sh KEEP_IMAGES=true
+```
+
+## Tooling
+
+| Piece | Path |
+| --- | --- |
+| Default pack (install) | `lib/spark-client/` |
+| Versioned packs | `lib/spark-clients/<version>/` |
+| Materialise pack | `tools/spark-client-pack/materialize-pack.sh` |
+| Fat jar flag | `hop-conf.sh --spark-client-version=V` |
+| Driver env | `HOP_SPARK_CLIENT_VERSION=V` |
diff --git a/integration-tests/spark/MATRIX-REPORT.md 
b/integration-tests/spark/MATRIX-REPORT.md
new file mode 100644
index 0000000000..0ed515a60e
--- /dev/null
+++ b/integration-tests/spark/MATRIX-REPORT.md
@@ -0,0 +1,61 @@
+# Hop Beam Spark support matrix
+
+Generated: 2026-07-10 (issue #7476)
+
+> **Pin moved 3.5.7 → 3.5.8.** `dlcdn.apache.org` only serves the current 
patch of each
+> active line, and once 3.5.8 shipped, 3.5.7 became archive-only (downloads at 
~100 KB/s,
+> which is what made the docker build appear to hang). 3.5.8 is the same 3.5.x 
line as 3.5.7
+> — both are 
post-[SPARK-42369](https://issues.apache.org/jira/browse/SPARK-42369) — so the
+> conclusions below stand. The measured cells still show the last **3.5.7** 
run; re-run
+> `run-spark-matrix.sh` to refresh them for 3.5.8.
+
+- Hop: `2.19.0-SNAPSHOT`
+- Beam: `2.74.0`
+- Default client Spark libraries (`spark.version`): `3.5.8` (scala 2.12)
+- Driver JVM: **Java 21** (Hop requirement)
+- Cluster JVM: **Java 21** (workers must load Hop fat-jar bytecode)
+- Suite: `integration-tests/spark` smoke (`main-0001-test-spark-cluster`)
+- Mode: Hop client submit (`BeamSparkPipelineEngine` → `spark://spark:7077` + 
fat jar)
+
+## Results (fixed client 3.5.7 vs varying cluster)
+
+| Spark version | Cluster binary | Result | Notes |
+| --- | --- | --- | --- |
+| 3.2.4 | `bin-hadoop3.2` | **FAIL** | Master does not start on Java 21 
(`DirectByteBuffer.<init>(long,int)`) |
+| 3.3.4 | `bin-hadoop3` | **FAIL** | Client/cluster skew 
(`InvalidClassException` / serialVersionUID) |
+| 3.4.4 | `bin-hadoop3` | **FAIL** | Executor does not start on Java 21 
(`DirectByteBuffer.<init>(long,int)`) |
+| 3.5.7 | `bin-hadoop3` | **PASS** | Smoke pipeline finished |
+
+## Results (matched client pack V + cluster V)
+
+| Client pack | Cluster | Result | Notes |
+| --- | --- | --- | --- |
+| 3.5.7 | 3.5.7 | **PASS** | After driver classpath / pack wiring fix |
+| 3.4.4 | 3.4.4 | **FAIL** | Same Java 21 `DirectByteBuffer` failure — 
matching client pack does **not** help |
+| 3.3.4 | 3.3.4 | *(optional)* | Not required once 3.4 matched FAIL is 
explained by Java 21 |
+
+## Conclusion
+
+**Hop on Java 21 is effectively limited to Spark 3.5.x** for Beam client-mode 
submit.
+
+1. **Java 21 floor (dominant):** Spark’s unsafe `DirectByteBuffer` reflection 
broke on JDK 21 until 
[SPARK-42369](https://issues.apache.org/jira/browse/SPARK-42369) (*Fix 
constructor for java.nio.DirectByteBuffer for Java 21+*), fixed in **Spark 
3.5.0**. Hop requires Java 21 for the driver and for fat-jar workers, so the 
cluster must also run Java 21. Spark **3.4.x and earlier cannot be used** on 
that stack, even with a matching client pack / fat jar.
+2. **Client/cluster match:** When both sides are on 3.5.x, client libraries 
and cluster minor must still align (default pack `3.5.8` with a matching 
`3.5.8` cluster; `3.5.7`/`3.5.7` was validated previously). Versioned packs 
(`tools/spark-client-pack`, `HOP_SPARK_CLIENT_VERSION`) remain useful for 
**3.5.x patch** alignment, not for going back to 3.4.
+3. **Supported line:** **Spark 3.5.x** (pinned **3.5.8**; **3.5.7** measured) 
+ Beam **2.74.0** + Hop **2.19** + **Java 21**.
+
+## How to re-run
+
+```bash
+# Default pin (recommended CI smoke)
+SPARK_VERSION=3.5.8 ./integration-tests/scripts/run-tests-docker.sh 
PROJECT_NAME=spark KEEP_IMAGES=true
+
+# Fixed client vs varying cluster
+./integration-tests/scripts/run-spark-matrix.sh KEEP_IMAGES=true
+
+# Matched packs (documents Java 21 ceiling on pre-3.5)
+./integration-tests/scripts/run-spark-matched-matrix.sh KEEP_IMAGES=true
+```
+
+## Notes
+
+- Beam upstream docs may still mention Spark 3.2.x; that is not Hop’s measured 
support on Java 21.
+- Hive catalog workflows remain deferred (`optional-0002/0003-*.hwf`).
diff --git a/integration-tests/spark/disabled.txt 
b/integration-tests/spark/disabled.txt
deleted file mode 100644
index 7bf85a795a..0000000000
--- a/integration-tests/spark/disabled.txt
+++ /dev/null
@@ -1,18 +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.
-
--->
\ No newline at end of file
diff --git 
a/integration-tests/spark/metadata/pipeline-run-configuration/spark.json 
b/integration-tests/spark/metadata/pipeline-run-configuration/spark-local.json
similarity index 79%
copy from integration-tests/spark/metadata/pipeline-run-configuration/spark.json
copy to 
integration-tests/spark/metadata/pipeline-run-configuration/spark-local.json
index d15ae3dcfb..af7fefefc6 100644
--- a/integration-tests/spark/metadata/pipeline-run-configuration/spark.json
+++ 
b/integration-tests/spark/metadata/pipeline-run-configuration/spark-local.json
@@ -2,7 +2,7 @@
   "engineRunConfiguration": {
     "BeamSparkPipelineEngine": {
       "sparkMinReadTimeMillis": "",
-      "sparkMaster": "spark://spark:7077",
+      "sparkMaster": "local[2]",
       "sparkEnableSparkMetricSinks": false,
       "streamingHopTransformsBufferSize": "",
       "sparkCheckpointDir": "",
@@ -10,7 +10,7 @@
       "userAgent": "Hop",
       "sparkBatchIntervalMillis": "",
       "pluginsToStage": "",
-      "tempLocation": "/var/folders/v0/hjm6kpgd0076ncdkpf8rg3dw0000gn/T/",
+      "tempLocation": "/tmp",
       "sparkCheckpointDurationMillis": "",
       "fatJar": "/tmp/hop-fatjar.jar",
       "sparkBundleSize": "",
@@ -21,6 +21,6 @@
     }
   },
   "configurationVariables": [],
-  "name": "spark",
-  "description": ""
-}
\ No newline at end of file
+  "name": "spark-local",
+  "description": "Embedded Spark local[2] for driver-only smoke (no cluster)"
+}
diff --git 
a/integration-tests/spark/metadata/pipeline-run-configuration/spark.json 
b/integration-tests/spark/metadata/pipeline-run-configuration/spark.json
index d15ae3dcfb..b4ffb0475c 100644
--- a/integration-tests/spark/metadata/pipeline-run-configuration/spark.json
+++ b/integration-tests/spark/metadata/pipeline-run-configuration/spark.json
@@ -10,7 +10,7 @@
       "userAgent": "Hop",
       "sparkBatchIntervalMillis": "",
       "pluginsToStage": "",
-      "tempLocation": "/var/folders/v0/hjm6kpgd0076ncdkpf8rg3dw0000gn/T/",
+      "tempLocation": "/tmp",
       "sparkCheckpointDurationMillis": "",
       "fatJar": "/tmp/hop-fatjar.jar",
       "sparkBundleSize": "",
@@ -22,5 +22,5 @@
   },
   "configurationVariables": [],
   "name": "spark",
-  "description": ""
-}
\ No newline at end of file
+  "description": "Beam Spark runner against the integration-test standalone 
cluster"
+}
diff --git a/integration-tests/spark/main-0002-test-hive-connection.hwf 
b/integration-tests/spark/optional-0002-test-hive-connection.hwf
similarity index 100%
rename from integration-tests/spark/main-0002-test-hive-connection.hwf
rename to integration-tests/spark/optional-0002-test-hive-connection.hwf
diff --git a/integration-tests/spark/main-0003-test-hive-write-read.hwf 
b/integration-tests/spark/optional-0003-test-hive-write-read.hwf
similarity index 100%
rename from integration-tests/spark/main-0003-test-hive-write-read.hwf
rename to integration-tests/spark/optional-0003-test-hive-write-read.hwf
diff --git a/plugins/engines/beam/pom.xml b/plugins/engines/beam/pom.xml
index 912adbdb4b..6e7b8483fc 100644
--- a/plugins/engines/beam/pom.xml
+++ b/plugins/engines/beam/pom.xml
@@ -29,7 +29,7 @@
     <name>Hop Plugins Engines Beam</name>
 
     <properties>
-        <spark.version>3.5.7</spark.version>
+        <spark.version>3.5.8</spark.version>
     </properties>
 
     <dependencyManagement>
@@ -1474,6 +1474,105 @@
                 </exclusion>
             </exclusions>
         </dependency>
+        <!--
+          Spark core/streaming are marked provided by beam-runners-spark-3. 
Hop client-mode
+          submit (BeamSparkPipelineEngine → spark://master) needs them on the 
driver classpath
+          and in the fat jar. Keep log4j/jackson-scala exclusions to avoid 
clashing with Hop.
+        -->
+        <dependency>
+            <groupId>org.apache.spark</groupId>
+            <artifactId>spark-core_2.12</artifactId>
+            <version>${spark.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>com.fasterxml.jackson.module</groupId>
+                    <artifactId>jackson-module-scala_2.11</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.fasterxml.jackson.module</groupId>
+                    <artifactId>jackson-module-scala_2.12</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.fasterxml.jackson.module</groupId>
+                    <artifactId>jackson-module-scala_2.13</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.google.code.gson</groupId>
+                    <artifactId>gson</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.google.guava</groupId>
+                    <artifactId>guava</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.google.protobuf</groupId>
+                    <artifactId>protobuf-java</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>commons-codec</groupId>
+                    <artifactId>commons-codec</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>joda-time</groupId>
+                    <artifactId>joda-time</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.apache.logging.log4j</groupId>
+                    <artifactId>log4j-1.2-api</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.apache.logging.log4j</groupId>
+                    <artifactId>log4j-api</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.apache.logging.log4j</groupId>
+                    <artifactId>log4j-core</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.apache.logging.log4j</groupId>
+                    <artifactId>log4j-slf4j2-impl</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.lz4</groupId>
+                    <artifactId>lz4-java</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.xerial.snappy</groupId>
+                    <artifactId>snappy-java</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.spark</groupId>
+            <artifactId>spark-streaming_2.12</artifactId>
+            <version>${spark.version}</version>
+            <exclusions>
+                <exclusion>
+                    <groupId>com.fasterxml.jackson.module</groupId>
+                    <artifactId>jackson-module-scala_2.11</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.fasterxml.jackson.module</groupId>
+                    <artifactId>jackson-module-scala_2.12</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>com.fasterxml.jackson.module</groupId>
+                    <artifactId>jackson-module-scala_2.13</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.apache.logging.log4j</groupId>
+                    <artifactId>*</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.lz4</groupId>
+                    <artifactId>lz4-java</artifactId>
+                </exclusion>
+                <exclusion>
+                    <groupId>org.xerial.snappy</groupId>
+                    <artifactId>snappy-java</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
         <dependency>
             <groupId>org.apache.hop</groupId>
             <artifactId>hop-transform-constant</artifactId>
@@ -1549,75 +1648,5 @@
                 </exclusion>
             </exclusions>
         </dependency>
-        <dependency>
-            <groupId>org.apache.spark</groupId>
-            <artifactId>spark-core_2.12</artifactId>
-            <version>${spark.version}</version>
-            <scope>test</scope>
-            <exclusions>
-                <exclusion>
-                    <groupId>com.fasterxml.jackson.module</groupId>
-                    <artifactId>jackson-module-scala_2.11</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>com.fasterxml.jackson.module</groupId>
-                    <artifactId>jackson-module-scala_2.12</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>com.fasterxml.jackson.module</groupId>
-                    <artifactId>jackson-module-scala_2.13</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>com.google.code.gson</groupId>
-                    <artifactId>gson</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>com.google.guava</groupId>
-                    <artifactId>guava</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>com.google.protobuf</groupId>
-                    <artifactId>protobuf-java</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>commons-codec</groupId>
-                    <artifactId>commons-codec</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>joda-time</groupId>
-                    <artifactId>joda-time</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>org.apache.logging.log4j</groupId>
-                    <artifactId>log4j-1.2-api</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>org.apache.logging.log4j</groupId>
-                    <artifactId>log4j-api</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>org.apache.logging.log4j</groupId>
-                    <artifactId>log4j-core</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>org.apache.logging.log4j</groupId>
-                    <artifactId>log4j-slf4j2-impl</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>org.lz4</groupId>
-                    <artifactId>lz4-java</artifactId>
-                </exclusion>
-                <exclusion>
-                    <groupId>org.xerial.snappy</groupId>
-                    <artifactId>snappy-java</artifactId>
-                </exclusion>
-            </exclusions>
-        </dependency>
-        <dependency>
-            <groupId>org.apache.spark</groupId>
-            <artifactId>spark-streaming_2.12</artifactId>
-            <version>${spark.version}</version>
-            <scope>test</scope>
-        </dependency>
     </dependencies>
 </project>
diff --git a/plugins/engines/beam/src/assembly/assembly.xml 
b/plugins/engines/beam/src/assembly/assembly.xml
index 5fe15267ce..c012216d5c 100644
--- a/plugins/engines/beam/src/assembly/assembly.xml
+++ b/plugins/engines/beam/src/assembly/assembly.xml
@@ -169,9 +169,35 @@
                 <exclude>io.dropwizard.metrics:*:jar</exclude>
                 
<exclude>com.eclipsesource.minimal-json:minimal-json:jar</exclude>
                 <exclude>org.slf4j:slf4j-api:jar</exclude>
+                <!-- Spark client runtime lives in lib/spark-client (versioned 
packs); keep Beam's own Spark fragments in lib/beam -->
+                <exclude>org.apache.spark:spark-core_2.12:jar</exclude>
+                <exclude>org.apache.spark:spark-streaming_2.12:jar</exclude>
+                <exclude>org.apache.spark:spark-launcher_2.12:jar</exclude>
+                <exclude>org.apache.spark:spark-kvstore_2.12:jar</exclude>
+                
<exclude>org.apache.spark:spark-network-common_2.12:jar</exclude>
+                
<exclude>org.apache.spark:spark-network-shuffle_2.12:jar</exclude>
+                <exclude>org.apache.spark:spark-unsafe_2.12:jar</exclude>
+                <exclude>org.apache.spark:spark-tags_2.12:jar</exclude>
             </excludes>
             <outputDirectory>lib/beam</outputDirectory>
         </dependencySet>
+        <!-- Default Spark client pack (matches plugins/engines/beam 
spark.version). Optional
+             additional packs can be materialised under 
lib/spark-clients/<version>/ via
+             tools/spark-client-pack. Selected at runtime with 
HOP_SPARK_CLIENT_VERSION. -->
+        <dependencySet>
+            <scope>runtime</scope>
+            <includes>
+                <include>org.apache.spark:spark-core_2.12:jar</include>
+                <include>org.apache.spark:spark-streaming_2.12:jar</include>
+                <include>org.apache.spark:spark-launcher_2.12:jar</include>
+                <include>org.apache.spark:spark-kvstore_2.12:jar</include>
+                
<include>org.apache.spark:spark-network-common_2.12:jar</include>
+                
<include>org.apache.spark:spark-network-shuffle_2.12:jar</include>
+                <include>org.apache.spark:spark-unsafe_2.12:jar</include>
+                <include>org.apache.spark:spark-tags_2.12:jar</include>
+            </includes>
+            <outputDirectory>lib/spark-client</outputDirectory>
+        </dependencySet>
         <dependencySet>
             <scope>runtime</scope>
             <includes>
diff --git 
a/plugins/engines/beam/src/main/java/org/apache/hop/beam/config/GenerateFatJarConfigPlugin.java
 
b/plugins/engines/beam/src/main/java/org/apache/hop/beam/config/GenerateFatJarConfigPlugin.java
index c7c9a28dd3..52586c5c3a 100644
--- 
a/plugins/engines/beam/src/main/java/org/apache/hop/beam/config/GenerateFatJarConfigPlugin.java
+++ 
b/plugins/engines/beam/src/main/java/org/apache/hop/beam/config/GenerateFatJarConfigPlugin.java
@@ -17,6 +17,7 @@
 
 package org.apache.hop.beam.config;
 
+import java.io.File;
 import java.util.List;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.hop.beam.gui.HopBeamGuiPlugin;
@@ -40,6 +41,14 @@ public class GenerateFatJarConfigPlugin implements 
IConfigOptions {
           "Specify the filename of the fat jar to generate from your current 
software installation")
   private String fatJarFilename;
 
+  @CommandLine.Option(
+      names = {"-scv", "--spark-client-version"},
+      description =
+          "Spark client pack version to embed (directory 
lib/spark-clients/<version>). "
+              + "When omitted, uses HOP_SPARK_CLIENT_VERSION or the default 
pack at lib/spark-client. "
+              + "Client and Spark cluster minor versions must match for 
client-mode submit.")
+  private String sparkClientVersion;
+
   @Override
   public boolean handleOption(
       ILogChannel log, IHasHopMetadataProvider hasHopMetadataProvider, 
IVariables variables)
@@ -60,7 +69,30 @@ public class GenerateFatJarConfigPlugin implements 
IConfigOptions {
     String realFatJarFilename = variables.resolve(fatJarFilename);
     log.logBasic("Generating a Hop fat jar file in : " + realFatJarFilename);
 
-    List<String> installedJarFilenames = 
HopBeamGuiPlugin.findInstalledJarFilenames();
+    String resolvedSparkClientVersion =
+        HopBeamGuiPlugin.resolveSparkClientVersion(
+            StringUtils.isNotEmpty(sparkClientVersion)
+                ? variables.resolve(sparkClientVersion)
+                : null);
+    File packDir = 
HopBeamGuiPlugin.resolveSparkClientPackDir(resolvedSparkClientVersion);
+    if (StringUtils.isNotBlank(resolvedSparkClientVersion)) {
+      log.logBasic(
+          "Using Spark client pack version "
+              + resolvedSparkClientVersion
+              + " from "
+              + packDir.getPath());
+      if (!packDir.isDirectory()) {
+        throw new HopException(
+            "Spark client pack not found: "
+                + packDir.getPath()
+                + " — materialise it with 
tools/spark-client-pack/materialize-pack.sh");
+      }
+    } else {
+      log.logBasic("Using default Spark client pack from " + 
packDir.getPath());
+    }
+
+    List<String> installedJarFilenames =
+        HopBeamGuiPlugin.findInstalledJarFilenames(resolvedSparkClientVersion);
     log.logBasic(
         "Found " + installedJarFilenames.size() + " jar files to combine into 
one fat jar file.");
 
@@ -88,4 +120,12 @@ public class GenerateFatJarConfigPlugin implements 
IConfigOptions {
   public void setFatJarFilename(String fatJarFilename) {
     this.fatJarFilename = fatJarFilename;
   }
+
+  public String getSparkClientVersion() {
+    return sparkClientVersion;
+  }
+
+  public void setSparkClientVersion(String sparkClientVersion) {
+    this.sparkClientVersion = sparkClientVersion;
+  }
 }
diff --git 
a/plugins/engines/beam/src/main/java/org/apache/hop/beam/gui/HopBeamGuiPlugin.java
 
b/plugins/engines/beam/src/main/java/org/apache/hop/beam/gui/HopBeamGuiPlugin.java
index 674e7d097d..02e01b7c87 100644
--- 
a/plugins/engines/beam/src/main/java/org/apache/hop/beam/gui/HopBeamGuiPlugin.java
+++ 
b/plugins/engines/beam/src/main/java/org/apache/hop/beam/gui/HopBeamGuiPlugin.java
@@ -207,28 +207,146 @@ public class HopBeamGuiPlugin {
     }
   }
 
+  /**
+   * Collect jar files from the current Hop installation for fat-jar 
generation.
+   *
+   * <p>Uses the default Spark client pack ({@code lib/spark-client}), or the 
versioned pack
+   * selected by system property / env {@code HOP_SPARK_CLIENT_VERSION} under 
{@code
+   * lib/spark-clients/<version>}. Versioned packs under {@code 
lib/spark-clients/} are never all
+   * included at once.
+   */
   public static final List<String> findInstalledJarFilenames() {
+    return findInstalledJarFilenames(resolveSparkClientVersion(null));
+  }
+
+  /**
+   * @param sparkClientVersion Spark client pack version (e.g. {@code 3.5.8}), 
or null/blank for the
+   *     default pack at {@code lib/spark-client}
+   */
+  public static final List<String> findInstalledJarFilenames(String 
sparkClientVersion) {
     Set<File> jarFiles = new HashSet<>();
-    jarFiles.addAll(FileUtils.listFiles(new File("lib"), new String[] {"jar"}, 
true));
+    boolean versionedPack = StringUtils.isNotBlank(sparkClientVersion);
+
+    // lib/ tree except spark client pack directories (handled separately 
below)
+    collectJarsExcludingSparkClientPacks(new File("lib"), jarFiles, 
versionedPack);
 
     File libSwtFiles = new File("lib/swt/linux/x86_64");
-    if (libSwtFiles.exists())
+    if (libSwtFiles.exists()) {
       jarFiles.addAll(FileUtils.listFiles(libSwtFiles, new String[] {"jar"}, 
true));
+    }
 
-    jarFiles.addAll(FileUtils.listFiles(new File("plugins"), new String[] 
{"jar"}, true));
+    File pluginsDir = new File("plugins");
+    if (pluginsDir.isDirectory()) {
+      jarFiles.addAll(FileUtils.listFiles(pluginsDir, new String[] {"jar"}, 
true));
+    }
 
     // If we are in the hop-web Docker container, we need to import the Hop 
main JARs too for
-    // Dataflow
-    // (and for other runners probably too)
+    // Dataflow (and for other runners probably too)
     File hopWebJars = new File("webapps/ROOT/WEB-INF/lib");
-    if (hopWebJars.exists())
+    if (hopWebJars.exists()) {
       jarFiles.addAll(FileUtils.listFiles(hopWebJars, new String[] {"jar"}, 
true));
+    }
+
+    // Selected Spark client pack only (never all versioned packs at once)
+    File sparkClientDir = resolveSparkClientPackDir(sparkClientVersion);
+    if (sparkClientDir.isDirectory()) {
+      jarFiles.addAll(FileUtils.listFiles(sparkClientDir, new String[] 
{"jar"}, false));
+    }
+
+    // Final guard: when using a versioned pack, drop any spark-* jar outside 
that pack
+    // (avoids Beam fragments under lib/beam mixing serialVersionUID / 
version-info).
+    if (versionedPack) {
+      String packPath;
+      try {
+        packPath = sparkClientDir.getCanonicalPath();
+      } catch (Exception e) {
+        packPath = sparkClientDir.getAbsolutePath();
+      }
+      final String packPrefix = packPath;
+      jarFiles.removeIf(
+          f -> {
+            String n = f.getName();
+            // Spark distribution jars are named spark-*.jar; Beam's runner is 
beam-runners-spark-*
+            if (!n.startsWith("spark-") || !n.endsWith(".jar")) {
+              return false;
+            }
+            try {
+              return !f.getCanonicalPath().startsWith(packPrefix);
+            } catch (Exception e) {
+              return !f.getAbsolutePath().startsWith(packPrefix);
+            }
+          });
+    }
 
     List<String> jarFilenames = new ArrayList<>();
     jarFiles.forEach(file -> jarFilenames.add(file.toString()));
     return jarFilenames;
   }
 
+  /**
+   * Resolve which Spark client pack version to use. Explicit argument wins, 
then system property
+   * {@code HOP_SPARK_CLIENT_VERSION}, then env of the same name.
+   */
+  public static String resolveSparkClientVersion(String explicitVersion) {
+    if (StringUtils.isNotBlank(explicitVersion)) {
+      return explicitVersion.trim();
+    }
+    String prop = System.getProperty("HOP_SPARK_CLIENT_VERSION");
+    if (StringUtils.isNotBlank(prop)) {
+      return prop.trim();
+    }
+    String env = System.getenv("HOP_SPARK_CLIENT_VERSION");
+    if (StringUtils.isNotBlank(env)) {
+      return env.trim();
+    }
+    return null;
+  }
+
+  /** Directory for the default pack or {@code lib/spark-clients/<version>}. */
+  public static File resolveSparkClientPackDir(String sparkClientVersion) {
+    if (StringUtils.isBlank(sparkClientVersion)) {
+      return new File("lib/spark-client");
+    }
+    return new File("lib/spark-clients/" + sparkClientVersion.trim());
+  }
+
+  /**
+   * @param versionedPack when true, also skip {@code spark-*.jar} under 
{@code lib/beam} so Beam's
+   *     fixed Spark fragments cannot mix with an alternate client pack
+   */
+  private static void collectJarsExcludingSparkClientPacks(
+      File dir, Set<File> jarFiles, boolean versionedPack) {
+    if (dir == null || !dir.isDirectory()) {
+      return;
+    }
+    String name = dir.getName();
+    File parent = dir.getParentFile();
+    // Skip lib/spark-client and lib/spark-clients entirely
+    if (parent != null
+        && "lib".equals(parent.getName())
+        && ("spark-client".equals(name) || "spark-clients".equals(name))) {
+      return;
+    }
+    File[] children = dir.listFiles();
+    if (children == null) {
+      return;
+    }
+    for (File child : children) {
+      if (child.isDirectory()) {
+        collectJarsExcludingSparkClientPacks(child, jarFiles, versionedPack);
+      } else if (child.isFile() && child.getName().endsWith(".jar")) {
+        // Versioned packs own all spark-* jars; drop Beam's spark fragments 
from lib/beam
+        if (versionedPack
+            && child.getName().startsWith("spark-")
+            && parent != null
+            && "beam".equals(parent.getName())) {
+          continue;
+        }
+        jarFiles.add(child);
+      }
+    }
+  }
+
   /**
    * Add a toolbar item just above the pipeline graph to quickly visit the 
execution of a Dataflow
    * pipeline in the GCP console.
diff --git 
a/plugins/engines/beam/src/test/java/org/apache/hop/beam/engines/spark/BeamSparkPipelineEngineTest.java
 
b/plugins/engines/beam/src/test/java/org/apache/hop/beam/engines/spark/BeamSparkPipelineEngineTest.java
index c8917c31d0..a931909c09 100644
--- 
a/plugins/engines/beam/src/test/java/org/apache/hop/beam/engines/spark/BeamSparkPipelineEngineTest.java
+++ 
b/plugins/engines/beam/src/test/java/org/apache/hop/beam/engines/spark/BeamSparkPipelineEngineTest.java
@@ -26,17 +26,28 @@ import org.apache.hop.core.variables.DescribedVariable;
 import org.apache.hop.pipeline.PipelineMeta;
 import org.apache.hop.pipeline.config.PipelineRunConfiguration;
 import org.apache.hop.pipeline.engine.IPipelineEngine;
-import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 
-@Disabled("Temp disable until we migrate to spark 4")
+/**
+ * Embedded Spark local-mode smoke test for the Beam Spark pipeline engine.
+ *
+ * <p>Requires spark-core on the test classpath (see hop-engines-beam pom). 
Hop runs on Java 21;
+ * Spark 3.5.x is used for local mode. Cluster multi-version coverage lives in 
{@code
+ * integration-tests/spark} / {@code run-spark-matrix.sh}.
+ */
 class BeamSparkPipelineEngineTest extends BeamBasePipelineEngineTest {
 
   @Test
   void testSparkPipelineEngine() throws Exception {
+    // Spark UI pulls in a shaded Jetty + jersey stack that conflicts with 
Hop's servlet APIs
+    // on the unit-test classpath. The pipeline does not need the UI.
+    System.setProperty("spark.ui.enabled", "false");
+    System.setProperty("spark.driver.host", "127.0.0.1");
+    System.setProperty("spark.driver.bindAddress", "127.0.0.1");
 
     BeamSparkPipelineRunConfiguration configuration = new 
BeamSparkPipelineRunConfiguration();
-    configuration.setSparkMaster("local");
+    configuration.setSparkMaster("local[2]");
+    configuration.setTempLocation(System.getProperty("java.io.tmpdir"));
     configuration.setEnginePluginId("BeamSparkPipelineEngine");
     PipelineRunConfiguration pipelineRunConfiguration =
         new PipelineRunConfiguration(
diff --git a/pom.xml b/pom.xml
index fc6e04269d..eb865cf2f0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -509,6 +509,7 @@
 
                         <!-- External Integration Test Exclusion -->
                         <exclude>**/integration-tests/**/*.txt</exclude>
+                        <exclude>**/integration-tests/**/*.md</exclude>
                         <exclude>**/integration-tests/**/*.csv</exclude>
                         <exclude>**/integration-tests/**/*.avro</exclude>
                         <exclude>**/integration-tests/**/*.xls</exclude>
diff --git a/tools/spark-client-pack/materialize-pack.sh 
b/tools/spark-client-pack/materialize-pack.sh
new file mode 100755
index 0000000000..608206421d
--- /dev/null
+++ b/tools/spark-client-pack/materialize-pack.sh
@@ -0,0 +1,117 @@
+#!/bin/bash
+
+#
+# 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.
+#
+
+# Materialise a Spark client pack for Hop under lib/spark-clients/<version>/.
+#
+# Usage:
+#   ./tools/spark-client-pack/materialize-pack.sh 3.4.4
+#   ./tools/spark-client-pack/materialize-pack.sh 3.4.4 /path/to/hop
+#   SPARK_CLIENT_VERSIONS="3.3.4 3.4.4 3.5.8" 
./tools/spark-client-pack/materialize-pack.sh
+#
+# Then generate a matching fat jar:
+#   HOP_SPARK_CLIENT_VERSION=3.4.4 ./hop-conf.sh 
--generate-fat-jar=/tmp/hop-spark-3.4.4.jar \
+#       --spark-client-version=3.4.4
+#
+# And run Hop with the same pack on the driver classpath:
+#   HOP_SPARK_CLIENT_VERSION=3.4.4 ./hop-run.sh ...
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 && pwd)"
+REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." >/dev/null 2>&1 && pwd)"
+PACK_POM="${SCRIPT_DIR}/pom.xml"
+
+# Default Hop install to materialise into (client assembly extract or live 
install)
+HOP_HOME_RAW="${2:-${HOP_HOME:-${REPO_ROOT}/assemblies/client/target/hop}}"
+# Always absolute so mvn -DoutputDirectory and existence checks are 
cwd-independent
+HOP_HOME="$(cd "${HOP_HOME_RAW}" 2>/dev/null && pwd || true)"
+if [ -z "${HOP_HOME}" ]; then
+  mkdir -p "${HOP_HOME_RAW}"
+  HOP_HOME="$(cd "${HOP_HOME_RAW}" && pwd)"
+fi
+
+if [ -n "${1:-}" ] && [[ "${1}" != *"="* ]]; then
+  VERSIONS="$1"
+elif [ -n "${SPARK_CLIENT_VERSIONS:-}" ]; then
+  VERSIONS="${SPARK_CLIENT_VERSIONS}"
+else
+  VERSIONS="3.5.8"
+fi
+
+MVN="${REPO_ROOT}/mvnw"
+if [ ! -x "${MVN}" ]; then
+  MVN="mvn"
+fi
+
+# Only keep the same artifact set the beam assembly puts in lib/spark-client
+keep_pack_jars() {
+  local dir="$1"
+  local version="$2"
+  # Remove jars that are not part of the curated pack (transitive noise)
+  find "${dir}" -maxdepth 1 -type f -name '*.jar' | while read -r jar; do
+    base="$(basename "${jar}")"
+    case "${base}" in
+    spark-core_2.12-"${version}".jar | \
+      spark-streaming_2.12-"${version}".jar | \
+      spark-launcher_2.12-"${version}".jar | \
+      spark-kvstore_2.12-"${version}".jar | \
+      spark-network-common_2.12-"${version}".jar | \
+      spark-network-shuffle_2.12-"${version}".jar | \
+      spark-unsafe_2.12-"${version}".jar | \
+      spark-tags_2.12-"${version}".jar | \
+      spark-common-utils_2.12-"${version}".jar | \
+      spark-sql-api_2.12-"${version}".jar)
+      ;;
+    *)
+      rm -f "${jar}"
+      ;;
+    esac
+  done
+}
+
+for VERSION in ${VERSIONS}; do
+  OUT_DIR="${HOP_HOME}/lib/spark-clients/${VERSION}"
+  echo "==========================================="
+  echo "Materialising Spark client pack ${VERSION}"
+  echo "  pom: ${PACK_POM}"
+  echo "  out: ${OUT_DIR}"
+  echo "==========================================="
+
+  mkdir -p "${OUT_DIR}"
+  rm -f "${OUT_DIR}"/*.jar
+
+  "${MVN}" -f "${PACK_POM}" \
+    -Dspark.client.version="${VERSION}" \
+    dependency:copy-dependencies \
+    -DoutputDirectory="${OUT_DIR}" \
+    -DincludeScope=runtime \
+    -q
+
+  keep_pack_jars "${OUT_DIR}" "${VERSION}"
+
+  echo "Pack contents:"
+  ls -1 "${OUT_DIR}" | sed 's/^/  /'
+
+  if [ ! -f "${OUT_DIR}/spark-core_2.12-${VERSION}.jar" ]; then
+    echo "ERROR: spark-core_2.12-${VERSION}.jar missing from pack" >&2
+    exit 1
+  fi
+done
+
+echo "Done. Use HOP_SPARK_CLIENT_VERSION=<version> with hop-run / hop-conf 
--spark-client-version."
diff --git a/tools/spark-client-pack/pom.xml b/tools/spark-client-pack/pom.xml
new file mode 100644
index 0000000000..ec36d0e8bc
--- /dev/null
+++ b/tools/spark-client-pack/pom.xml
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>org.apache.hop.tools</groupId>
+  <artifactId>spark-client-pack</artifactId>
+  <version>1.0.0-SNAPSHOT</version>
+  <packaging>pom</packaging>
+  <name>Hop Spark client pack materializer</name>
+  <description>
+    Resolves Spark client runtime jars for a given spark.client.version into 
an output directory.
+    Used to build versioned packs under hop/lib/spark-clients/&lt;version&gt;/.
+  </description>
+
+  <properties>
+    <spark.client.version>3.5.8</spark.client.version>
+    <spark.scala.binary>2.12</spark.scala.binary>
+    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.spark</groupId>
+      <artifactId>spark-core_${spark.scala.binary}</artifactId>
+      <version>${spark.client.version}</version>
+      <exclusions>
+        <exclusion>
+          <groupId>com.fasterxml.jackson.module</groupId>
+          <artifactId>*</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>com.google.code.gson</groupId>
+          <artifactId>gson</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>com.google.guava</groupId>
+          <artifactId>guava</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>com.google.protobuf</groupId>
+          <artifactId>protobuf-java</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>commons-codec</groupId>
+          <artifactId>commons-codec</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>joda-time</groupId>
+          <artifactId>joda-time</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>org.apache.logging.log4j</groupId>
+          <artifactId>*</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>org.lz4</groupId>
+          <artifactId>lz4-java</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>org.xerial.snappy</groupId>
+          <artifactId>*</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>org.slf4j</groupId>
+          <artifactId>*</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.spark</groupId>
+      <artifactId>spark-streaming_${spark.scala.binary}</artifactId>
+      <version>${spark.client.version}</version>
+      <exclusions>
+        <exclusion>
+          <groupId>com.fasterxml.jackson.module</groupId>
+          <artifactId>*</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>org.apache.logging.log4j</groupId>
+          <artifactId>*</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>org.lz4</groupId>
+          <artifactId>lz4-java</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>org.xerial.snappy</groupId>
+          <artifactId>*</artifactId>
+        </exclusion>
+        <exclusion>
+          <groupId>org.slf4j</groupId>
+          <artifactId>*</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-dependency-plugin</artifactId>
+        <version>3.8.1</version>
+      </plugin>
+    </plugins>
+  </build>
+</project>

Reply via email to