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

davidzollo pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/seatunnel.git


The following commit(s) were added to refs/heads/dev by this push:
     new 91e3da1180 [Fix][Core] Exclude Scala classes from Spark starter 
(#11393)
91e3da1180 is described below

commit 91e3da118089399d667056afae27d85d8c55b15f
Author: Jast <[email protected]>
AuthorDate: Sat Aug 1 15:12:26 2026 +0800

    [Fix][Core] Exclude Scala classes from Spark starter (#11393)
    
    Co-authored-by: zhangshenghang <[email protected]>
---
 seatunnel-core/seatunnel-spark-starter/pom.xml     |  14 ++
 .../spark/TestSparkStarterJarContentIT.java        | 140 ++++++++++++++++++++
 .../spark/TestSparkStarterJarContentIT.java        | 141 +++++++++++++++++++++
 3 files changed, 295 insertions(+)

diff --git a/seatunnel-core/seatunnel-spark-starter/pom.xml 
b/seatunnel-core/seatunnel-spark-starter/pom.xml
index 4502d72c97..af7a0a2944 100644
--- a/seatunnel-core/seatunnel-spark-starter/pom.xml
+++ b/seatunnel-core/seatunnel-spark-starter/pom.xml
@@ -100,8 +100,22 @@
                             <!-- spark3.x use slf4j + log4j2.x -->
                             
<exclude>org.apache.logging.log4j:log4j-to-slf4j</exclude>
                             
<exclude>org.apache.seatunnel:seatunnel-hadoop3-3.1.4-uber</exclude>
+                            <!-- Spark supplies its own Scala runtime in 
cluster mode. -->
+                            <exclude>org.scala-lang:scala-library</exclude>
                         </excludes>
                     </artifactSet>
+                    <filters>
+                        <filter>
+                            <artifact>*:*</artifact>
+                            <excludes>
+                                <!-- Some SeaTunnel dependencies embed Scala 
classes. -->
+                                <exclude>scala/**</exclude>
+                                <exclude>META-INF/*.SF</exclude>
+                                <exclude>META-INF/*.DSA</exclude>
+                                <exclude>META-INF/*.RSA</exclude>
+                            </excludes>
+                        </filter>
+                    </filters>
                 </configuration>
             </plugin>
         </plugins>
diff --git 
a/seatunnel-core/seatunnel-spark-starter/seatunnel-spark-2-starter/src/test/java/org/apache/seatunnel/core/starter/spark/TestSparkStarterJarContentIT.java
 
b/seatunnel-core/seatunnel-spark-starter/seatunnel-spark-2-starter/src/test/java/org/apache/seatunnel/core/starter/spark/TestSparkStarterJarContentIT.java
new file mode 100644
index 0000000000..4c08eb3ca4
--- /dev/null
+++ 
b/seatunnel-core/seatunnel-spark-starter/seatunnel-spark-2-starter/src/test/java/org/apache/seatunnel/core/starter/spark/TestSparkStarterJarContentIT.java
@@ -0,0 +1,140 @@
+/*
+ * 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.
+ */
+
+package org.apache.seatunnel.core.starter.spark;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.concurrent.TimeUnit;
+import java.util.jar.JarFile;
+import java.util.stream.Stream;
+
+import static 
org.apache.seatunnel.core.starter.constants.SeaTunnelStarterConstants.USAGE_EXIT_CODE;
+
+public class TestSparkStarterJarContentIT {
+
+    private static final String ARTIFACT_ID = "seatunnel-spark-2-starter";
+    private static final String SPARK_STARTER_MAIN =
+            "org.apache.seatunnel.core.starter.spark.SparkStarter";
+
+    @Test
+    public void testShadedJarDoesNotContainScalaClasses() throws IOException {
+        try (JarFile jarFile = new JarFile(findStarterJar().toFile())) {
+            Assertions.assertFalse(
+                    jarFile.stream().anyMatch(entry -> 
entry.getName().startsWith("scala/")),
+                    "The Spark starter must use Spark's Scala runtime instead 
of embedding one");
+        }
+    }
+
+    @Test
+    public void testStarterUsageRunsFromShadedJar() throws Exception {
+        Process process =
+                new ProcessBuilder(
+                                javaExecutable().toString(),
+                                "-cp",
+                                starterClasspath(),
+                                SPARK_STARTER_MAIN,
+                                "-h")
+                        .redirectErrorStream(true)
+                        .start();
+
+        boolean finished = process.waitFor(30, TimeUnit.SECONDS);
+        if (!finished) {
+            process.destroyForcibly();
+        }
+        String output = readOutput(process);
+        Assertions.assertTrue(finished, output);
+        Assertions.assertEquals(USAGE_EXIT_CODE, process.exitValue(), output);
+        Assertions.assertTrue(output.contains("Usage:"), output);
+    }
+
+    @Test
+    public void testStarterBuildsSparkSubmitCommandFromShadedJar() throws 
Exception {
+        Path configFile = 
Paths.get("src/main/resources/spark_application.conf").toAbsolutePath();
+        Process process =
+                new ProcessBuilder(
+                                javaExecutable().toString(),
+                                "-cp",
+                                starterClasspath(),
+                                SPARK_STARTER_MAIN,
+                                "--config",
+                                configFile.toString(),
+                                "--deploy-mode",
+                                "client")
+                        .redirectErrorStream(true)
+                        .start();
+
+        boolean finished = process.waitFor(30, TimeUnit.SECONDS);
+        if (!finished) {
+            process.destroyForcibly();
+        }
+        String output = readOutput(process);
+        Assertions.assertTrue(finished, output);
+        Assertions.assertEquals(0, process.exitValue(), output);
+        
Assertions.assertTrue(output.contains("${SPARK_HOME}/bin/spark-submit"), 
output);
+        Assertions.assertTrue(output.contains("SeaTunnelSpark"), output);
+    }
+
+    private static Path findStarterJar() throws IOException {
+        try (Stream<Path> files = Files.list(Paths.get("target"))) {
+            return files.filter(Files::isRegularFile)
+                    .filter(path -> 
isStarterJar(path.getFileName().toString()))
+                    .sorted()
+                    .findFirst()
+                    .orElseThrow(() -> new IllegalStateException("Spark 
starter jar is missing"));
+        }
+    }
+
+    private static boolean isStarterJar(String fileName) {
+        return (fileName.equals(ARTIFACT_ID + ".jar") || 
fileName.startsWith(ARTIFACT_ID + "-"))
+                && fileName.endsWith(".jar")
+                && !fileName.startsWith("original-")
+                && !fileName.endsWith("-sources.jar")
+                && !fileName.endsWith("-javadoc.jar")
+                && !fileName.endsWith("-tests.jar");
+    }
+
+    private static Path javaExecutable() {
+        String executable =
+                System.getProperty("os.name").startsWith("Windows") ? 
"java.exe" : "java";
+        return Paths.get(System.getProperty("java.home"), "bin", executable);
+    }
+
+    private static String starterClasspath() throws IOException {
+        return Paths.get("target/logging-e2e", "*") + File.pathSeparator + 
findStarterJar();
+    }
+
+    private static String readOutput(Process process) throws IOException {
+        try (InputStream inputStream = process.getInputStream();
+                ByteArrayOutputStream outputStream = new 
ByteArrayOutputStream()) {
+            byte[] buffer = new byte[1024];
+            int length;
+            while ((length = inputStream.read(buffer)) != -1) {
+                outputStream.write(buffer, 0, length);
+            }
+            return outputStream.toString("UTF-8");
+        }
+    }
+}
diff --git 
a/seatunnel-core/seatunnel-spark-starter/seatunnel-spark-3-starter/src/test/java/org/apache/seatunnel/core/starter/spark/TestSparkStarterJarContentIT.java
 
b/seatunnel-core/seatunnel-spark-starter/seatunnel-spark-3-starter/src/test/java/org/apache/seatunnel/core/starter/spark/TestSparkStarterJarContentIT.java
new file mode 100644
index 0000000000..2ff560a5f7
--- /dev/null
+++ 
b/seatunnel-core/seatunnel-spark-starter/seatunnel-spark-3-starter/src/test/java/org/apache/seatunnel/core/starter/spark/TestSparkStarterJarContentIT.java
@@ -0,0 +1,141 @@
+/*
+ * 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.
+ */
+
+package org.apache.seatunnel.core.starter.spark;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.concurrent.TimeUnit;
+import java.util.jar.JarFile;
+import java.util.stream.Stream;
+
+import static 
org.apache.seatunnel.core.starter.constants.SeaTunnelStarterConstants.USAGE_EXIT_CODE;
+
+public class TestSparkStarterJarContentIT {
+
+    private static final String ARTIFACT_ID = "seatunnel-spark-3-starter";
+    private static final String SPARK_STARTER_MAIN =
+            "org.apache.seatunnel.core.starter.spark.SparkStarter";
+
+    @Test
+    public void testShadedJarDoesNotContainScalaClasses() throws IOException {
+        try (JarFile jarFile = new JarFile(findStarterJar().toFile())) {
+            Assertions.assertFalse(
+                    jarFile.stream().anyMatch(entry -> 
entry.getName().startsWith("scala/")),
+                    "The Spark starter must use Spark's Scala runtime instead 
of embedding one");
+        }
+    }
+
+    @Test
+    public void testStarterUsageRunsFromShadedJar() throws Exception {
+        Process process =
+                new ProcessBuilder(
+                                javaExecutable().toString(),
+                                "-cp",
+                                starterClasspath(),
+                                SPARK_STARTER_MAIN,
+                                "-h")
+                        .redirectErrorStream(true)
+                        .start();
+
+        boolean finished = process.waitFor(30, TimeUnit.SECONDS);
+        if (!finished) {
+            process.destroyForcibly();
+        }
+        String output = readOutput(process);
+        Assertions.assertTrue(finished, output);
+        Assertions.assertEquals(USAGE_EXIT_CODE, process.exitValue(), output);
+        Assertions.assertTrue(output.contains("Usage:"), output);
+    }
+
+    @Test
+    public void testStarterBuildsSparkSubmitCommandFromShadedJar() throws 
Exception {
+        Path configFile =
+                
Paths.get("src/test/resources/config/fake_to_inmemory.json").toAbsolutePath();
+        Process process =
+                new ProcessBuilder(
+                                javaExecutable().toString(),
+                                "-cp",
+                                starterClasspath(),
+                                SPARK_STARTER_MAIN,
+                                "--config",
+                                configFile.toString(),
+                                "--deploy-mode",
+                                "client")
+                        .redirectErrorStream(true)
+                        .start();
+
+        boolean finished = process.waitFor(30, TimeUnit.SECONDS);
+        if (!finished) {
+            process.destroyForcibly();
+        }
+        String output = readOutput(process);
+        Assertions.assertTrue(finished, output);
+        Assertions.assertEquals(0, process.exitValue(), output);
+        
Assertions.assertTrue(output.contains("${SPARK_HOME}/bin/spark-submit"), 
output);
+        Assertions.assertTrue(output.contains("SeaTunnelSpark"), output);
+    }
+
+    private static Path findStarterJar() throws IOException {
+        try (Stream<Path> files = Files.list(Paths.get("target"))) {
+            return files.filter(Files::isRegularFile)
+                    .filter(path -> 
isStarterJar(path.getFileName().toString()))
+                    .sorted()
+                    .findFirst()
+                    .orElseThrow(() -> new IllegalStateException("Spark 
starter jar is missing"));
+        }
+    }
+
+    private static boolean isStarterJar(String fileName) {
+        return (fileName.equals(ARTIFACT_ID + ".jar") || 
fileName.startsWith(ARTIFACT_ID + "-"))
+                && fileName.endsWith(".jar")
+                && !fileName.startsWith("original-")
+                && !fileName.endsWith("-sources.jar")
+                && !fileName.endsWith("-javadoc.jar")
+                && !fileName.endsWith("-tests.jar");
+    }
+
+    private static Path javaExecutable() {
+        String executable =
+                System.getProperty("os.name").startsWith("Windows") ? 
"java.exe" : "java";
+        return Paths.get(System.getProperty("java.home"), "bin", executable);
+    }
+
+    private static String starterClasspath() throws IOException {
+        return Paths.get("target/logging-e2e", "*") + File.pathSeparator + 
findStarterJar();
+    }
+
+    private static String readOutput(Process process) throws IOException {
+        try (InputStream inputStream = process.getInputStream();
+                ByteArrayOutputStream outputStream = new 
ByteArrayOutputStream()) {
+            byte[] buffer = new byte[1024];
+            int length;
+            while ((length = inputStream.read(buffer)) != -1) {
+                outputStream.write(buffer, 0, length);
+            }
+            return outputStream.toString("UTF-8");
+        }
+    }
+}

Reply via email to