From 86d0cbe7eb97305c56918882f8ee51541dc1a570 Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Mon, 24 Nov 2025 10:10:29 -0800 Subject: [PATCH 1/4] Add Enable-Native-Access manifest attribute for JDK 24+ MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the Enable-Native-Access: ALL-UNNAMED attribute to the JAR manifest to suppress native access warnings on JDK 24+ without requiring users to add the --enable-native-access=ALL-UNNAMED command-line flag. This manifest attribute allows the library to declare its need for native access, which helps suppress the warnings introduced by JEP 472 when snappy-java loads native libraries via JNI. Benefits: - Users no longer need to add JVM flags for basic usage - Warnings are suppressed automatically when the JAR is on the classpath - Backwards compatible with all JDK versions (attribute is ignored on JDK < 24) Related to #689 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- build.sbt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/build.sbt b/build.sbt index 54380f55..92f2cc6b 100644 --- a/build.sbt +++ b/build.sbt @@ -106,6 +106,11 @@ libraryDependencies ++= enablePlugins(SbtOsgi) +// Add Enable-Native-Access manifest attribute for JDK 24+ (JEP 472) +packageOptions := Seq( + Package.ManifestAttributes("Enable-Native-Access" -> "ALL-UNNAMED") +) + osgiSettings OsgiKeys.exportPackage := From 85056bf9c027f22c42dfb2e6fdf5d6dd2afb8a5d Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Mon, 24 Nov 2025 10:17:04 -0800 Subject: [PATCH 2/4] Add integration test for JAR manifest and native access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a comprehensive integration test that verifies snappy-java works in a separate JVM process. This test: 1. Builds the JAR with sbt 2. Compiles a simple test program that uses snappy-java 3. Runs the test in a fresh JVM WITHOUT --enable-native-access flag 4. Verifies the compression/decompression works correctly 5. Checks for JEP 472 warnings on JDK 24+ The test can be run two ways: - Via JUnit: testOnly org.xerial.snappy.JarManifestIntegrationTest - Via script: ./script/test-jar-integration.sh Findings: The Enable-Native-Access manifest attribute is present but does NOT suppress warnings when the JAR is used as a library on the classpath. The attribute only works for executable JARs (java -jar). Users still need to add --enable-native-access=ALL-UNNAMED for now. Related to #689 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- script/test-jar-integration.sh | 72 ++++++ .../snappy/JarManifestIntegrationTest.java | 212 ++++++++++++++++++ .../integration/SnappyIntegrationTest.java | 32 +++ 3 files changed, 316 insertions(+) create mode 100755 script/test-jar-integration.sh create mode 100644 src/test/java/org/xerial/snappy/JarManifestIntegrationTest.java create mode 100644 src/test/resources/integration/SnappyIntegrationTest.java diff --git a/script/test-jar-integration.sh b/script/test-jar-integration.sh new file mode 100755 index 00000000..c874adf3 --- /dev/null +++ b/script/test-jar-integration.sh @@ -0,0 +1,72 @@ +#!/bin/bash +set -e + +echo "==========================================" +echo "Snappy-Java Integration Test" +echo "==========================================" + +# Detect Java version +JAVA_VERSION=$(java -version 2>&1 | head -1 | cut -d'"' -f2 | sed 's/^1\.//' | cut -d'.' -f1) +echo "Java version: $JAVA_VERSION" + +# Build the JAR +echo "" +echo "Building JAR..." +./sbt package + +# Find the JAR +JAR_FILE=$(ls -t target/snappy-java-*.jar | grep -v sources | grep -v javadoc | head -1) +if [ -z "$JAR_FILE" ]; then + echo "ERROR: Could not find snappy-java JAR" + exit 1 +fi +echo "Using JAR: $JAR_FILE" + +# Check manifest +echo "" +echo "Checking JAR manifest..." +jar xf "$JAR_FILE" META-INF/MANIFEST.MF +if grep -q "Enable-Native-Access: ALL-UNNAMED" META-INF/MANIFEST.MF; then + echo "✓ Manifest contains Enable-Native-Access: ALL-UNNAMED" +else + echo "✗ WARNING: Manifest does NOT contain Enable-Native-Access attribute" +fi +rm -rf META-INF + +# Create temp directory +TEMP_DIR=$(mktemp -d) +echo "" +echo "Using temp directory: $TEMP_DIR" + +# Copy test source +cp src/test/resources/integration/SnappyIntegrationTest.java "$TEMP_DIR/" + +# Compile test +echo "" +echo "Compiling test program..." +javac -cp "$JAR_FILE" -d "$TEMP_DIR" "$TEMP_DIR/SnappyIntegrationTest.java" + +# Run test WITHOUT --enable-native-access flag +echo "" +echo "==========================================" +echo "Running test (WITHOUT --enable-native-access flag)..." +echo "==========================================" + +cd "$TEMP_DIR" +java -cp ".:$OLDPWD/$JAR_FILE" SnappyIntegrationTest 2>&1 +EXIT_CODE=$? +cd - > /dev/null + +echo "" +echo "==========================================" +if [ $EXIT_CODE -eq 0 ]; then + echo "✓ Test PASSED (exit code: $EXIT_CODE)" +else + echo "✗ Test FAILED (exit code: $EXIT_CODE)" +fi +echo "==========================================" + +# Cleanup +rm -rf "$TEMP_DIR" + +exit $EXIT_CODE diff --git a/src/test/java/org/xerial/snappy/JarManifestIntegrationTest.java b/src/test/java/org/xerial/snappy/JarManifestIntegrationTest.java new file mode 100644 index 00000000..f0e1a3a5 --- /dev/null +++ b/src/test/java/org/xerial/snappy/JarManifestIntegrationTest.java @@ -0,0 +1,212 @@ +package org.xerial.snappy; + +import org.junit.Test; + +import java.io.BufferedReader; +import java.io.File; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.junit.Assert.*; + +/** + * Integration test that verifies the snappy-java JAR works correctly + * when run in a separate JVM process. This tests: + * 1. The JAR manifest is correctly configured + * 2. Native library loading works + * 3. On JDK 24+, the Enable-Native-Access manifest attribute suppresses warnings + */ +public class JarManifestIntegrationTest { + + @Test + public void testJarWorksInSeparateJvm() throws Exception { + // Get Java version + int javaVersion = getJavaVersion(); + System.err.println("Running integration test on Java " + javaVersion); + + // Find the snappy-java JAR + Path jarPath = findSnappyJar(); + assertNotNull("Could not find snappy-java JAR", jarPath); + System.err.println("Using JAR: " + jarPath); + + // Get the integration test source file + Path testSource = Paths.get("src/test/resources/integration/SnappyIntegrationTest.java"); + assertTrue("Integration test source not found: " + testSource, Files.exists(testSource)); + + // Create a temporary directory for compilation + Path tempDir = Files.createTempDirectory("snappy-integration-test"); + try { + // Compile the test program + compileTestProgram(testSource, jarPath, tempDir); + + // Run the test program in a separate JVM + ProcessResult result = runTestProgram(jarPath, tempDir, javaVersion); + + // Verify the program succeeded + assertEquals("Test program failed with exit code: " + result.exitCode + + "\nStdout: " + result.stdout + + "\nStderr: " + result.stderr, + 0, result.exitCode); + + // Verify expected output + assertTrue("Expected SUCCESS message in output", + result.stdout.contains("SUCCESS")); + + // On JDK 24+, check for restricted method warnings + if (javaVersion >= 24) { + if (result.stderr.contains("restricted method") || + result.stderr.contains("enable-native-access")) { + System.err.println("WARNING: Still seeing restricted method warnings on JDK 24+"); + System.err.println("This may indicate the manifest attribute needs additional configuration"); + System.err.println("Stderr: " + result.stderr); + // Don't fail the test - this is informational for now + // assertFalse("Expected no 'restricted method' warnings on JDK 24+ with manifest attribute", + // result.stderr.contains("restricted method") || + // result.stderr.contains("enable-native-access")); + } else { + System.err.println("SUCCESS: No restricted method warnings on JDK 24+!"); + } + } + + System.err.println("Integration test passed!"); + System.err.println("Output: " + result.stdout); + + } finally { + // Cleanup temp directory + deleteDirectory(tempDir.toFile()); + } + } + + private void compileTestProgram(Path source, Path jarPath, Path outputDir) throws Exception { + List command = new ArrayList<>(); + command.add(getJavacPath()); + command.add("-cp"); + command.add(jarPath.toString()); + command.add("-d"); + command.add(outputDir.toString()); + command.add(source.toString()); + + ProcessBuilder pb = new ProcessBuilder(command); + pb.redirectErrorStream(true); + Process process = pb.start(); + + String output = readStream(process.getInputStream()); + int exitCode = process.waitFor(); + + if (exitCode != 0) { + fail("Failed to compile test program. Exit code: " + exitCode + "\nOutput: " + output); + } + } + + private ProcessResult runTestProgram(Path jarPath, Path classPath, int javaVersion) throws Exception { + List command = new ArrayList<>(); + command.add(getJavaPath()); + + // Note: We intentionally do NOT add --enable-native-access flag here + // to test that the manifest attribute alone is sufficient + + command.add("-cp"); + command.add(classPath.toString() + File.pathSeparator + jarPath.toString()); + command.add("SnappyIntegrationTest"); + + ProcessBuilder pb = new ProcessBuilder(command); + Process process = pb.start(); + + String stdout = readStream(process.getInputStream()); + String stderr = readStream(process.getErrorStream()); + int exitCode = process.waitFor(); + + return new ProcessResult(exitCode, stdout, stderr); + } + + private Path findSnappyJar() { + Path targetDir = Paths.get("target"); + if (!Files.exists(targetDir)) { + return null; + } + + try (Stream paths = Files.walk(targetDir, 1)) { + return paths + .filter(p -> p.getFileName().toString().startsWith("snappy-java-")) + .filter(p -> p.getFileName().toString().endsWith(".jar")) + .filter(p -> !p.getFileName().toString().contains("sources")) + .filter(p -> !p.getFileName().toString().contains("javadoc")) + .findFirst() + .orElse(null); + } catch (Exception e) { + return null; + } + } + + private String getJavaPath() { + String javaHome = System.getProperty("java.home"); + return Paths.get(javaHome, "bin", "java").toString(); + } + + private String getJavacPath() { + String javaHome = System.getProperty("java.home"); + Path javacPath = Paths.get(javaHome, "bin", "javac"); + if (Files.exists(javacPath)) { + return javacPath.toString(); + } + // For JDK 8 and some installations, javac might be in parent/bin + javacPath = Paths.get(javaHome).getParent().resolve("bin").resolve("javac"); + if (Files.exists(javacPath)) { + return javacPath.toString(); + } + return "javac"; // Fall back to PATH + } + + private int getJavaVersion() { + String version = System.getProperty("java.version"); + if (version.startsWith("1.")) { + version = version.substring(2, 3); + } else { + int dot = version.indexOf("."); + if (dot != -1) { + version = version.substring(0, dot); + } + } + return Integer.parseInt(version); + } + + private String readStream(java.io.InputStream stream) throws Exception { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) { + return reader.lines().collect(Collectors.joining("\n")); + } + } + + private void deleteDirectory(File dir) { + if (dir.exists()) { + File[] files = dir.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isDirectory()) { + deleteDirectory(file); + } else { + file.delete(); + } + } + } + dir.delete(); + } + } + + private static class ProcessResult { + final int exitCode; + final String stdout; + final String stderr; + + ProcessResult(int exitCode, String stdout, String stderr) { + this.exitCode = exitCode; + this.stdout = stdout; + this.stderr = stderr; + } + } +} diff --git a/src/test/resources/integration/SnappyIntegrationTest.java b/src/test/resources/integration/SnappyIntegrationTest.java new file mode 100644 index 00000000..618c0bf5 --- /dev/null +++ b/src/test/resources/integration/SnappyIntegrationTest.java @@ -0,0 +1,32 @@ +import org.xerial.snappy.Snappy; + +/** + * Simple integration test to verify snappy-java works in a separate JVM. + * This is compiled and run as a standalone program to test the JAR manifest + * and native library loading on different JDK versions. + */ +public class SnappyIntegrationTest { + public static void main(String[] args) throws Exception { + String input = "Hello snappy-java! Snappy-java is a JNI-based wrapper of " + + "Snappy, a fast compresser/decompresser."; + + // Test compression + byte[] compressed = Snappy.compress(input.getBytes("UTF-8")); + System.out.println("Compressed " + input.length() + " bytes to " + compressed.length + " bytes"); + + // Test decompression + byte[] uncompressed = Snappy.uncompress(compressed); + String result = new String(uncompressed, "UTF-8"); + + // Verify result matches input + if (!result.equals(input)) { + System.err.println("ERROR: Decompressed data does not match input!"); + System.err.println("Expected: " + input); + System.err.println("Got: " + result); + System.exit(1); + } + + System.out.println("SUCCESS: " + result); + System.exit(0); + } +} From 98f92e5f7199536402d9c1a3a1458189f366dfa7 Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Mon, 24 Nov 2025 10:38:58 -0800 Subject: [PATCH 3/4] Clean up and finalize integration test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes: - Remove Enable-Native-Access manifest attribute (doesn't work for library JARs) - Remove unused JUnit integration test (using shell script instead) - Add JAR integration test to CI workflow for all JDK versions The integration test now runs in CI after regular tests, verifying that the built JAR works correctly in a separate JVM process on each tested JDK version (8, 11, 17, 21, 25). Related to #689 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .github/workflows/test.yml | 2 + build.sbt | 5 - .../snappy/JarManifestIntegrationTest.java | 212 ------------------ 3 files changed, 2 insertions(+), 217 deletions(-) delete mode 100644 src/test/java/org/xerial/snappy/JarManifestIntegrationTest.java diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index f60b3f1e..7bae620f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -63,3 +63,5 @@ jobs: export _JAVA_OPTIONS="--enable-native-access=ALL-UNNAMED" fi ./sbt test + - name: JAR Integration Test + run: ./script/test-jar-integration.sh diff --git a/build.sbt b/build.sbt index 92f2cc6b..54380f55 100644 --- a/build.sbt +++ b/build.sbt @@ -106,11 +106,6 @@ libraryDependencies ++= enablePlugins(SbtOsgi) -// Add Enable-Native-Access manifest attribute for JDK 24+ (JEP 472) -packageOptions := Seq( - Package.ManifestAttributes("Enable-Native-Access" -> "ALL-UNNAMED") -) - osgiSettings OsgiKeys.exportPackage := diff --git a/src/test/java/org/xerial/snappy/JarManifestIntegrationTest.java b/src/test/java/org/xerial/snappy/JarManifestIntegrationTest.java deleted file mode 100644 index f0e1a3a5..00000000 --- a/src/test/java/org/xerial/snappy/JarManifestIntegrationTest.java +++ /dev/null @@ -1,212 +0,0 @@ -package org.xerial.snappy; - -import org.junit.Test; - -import java.io.BufferedReader; -import java.io.File; -import java.io.InputStreamReader; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import static org.junit.Assert.*; - -/** - * Integration test that verifies the snappy-java JAR works correctly - * when run in a separate JVM process. This tests: - * 1. The JAR manifest is correctly configured - * 2. Native library loading works - * 3. On JDK 24+, the Enable-Native-Access manifest attribute suppresses warnings - */ -public class JarManifestIntegrationTest { - - @Test - public void testJarWorksInSeparateJvm() throws Exception { - // Get Java version - int javaVersion = getJavaVersion(); - System.err.println("Running integration test on Java " + javaVersion); - - // Find the snappy-java JAR - Path jarPath = findSnappyJar(); - assertNotNull("Could not find snappy-java JAR", jarPath); - System.err.println("Using JAR: " + jarPath); - - // Get the integration test source file - Path testSource = Paths.get("src/test/resources/integration/SnappyIntegrationTest.java"); - assertTrue("Integration test source not found: " + testSource, Files.exists(testSource)); - - // Create a temporary directory for compilation - Path tempDir = Files.createTempDirectory("snappy-integration-test"); - try { - // Compile the test program - compileTestProgram(testSource, jarPath, tempDir); - - // Run the test program in a separate JVM - ProcessResult result = runTestProgram(jarPath, tempDir, javaVersion); - - // Verify the program succeeded - assertEquals("Test program failed with exit code: " + result.exitCode + - "\nStdout: " + result.stdout + - "\nStderr: " + result.stderr, - 0, result.exitCode); - - // Verify expected output - assertTrue("Expected SUCCESS message in output", - result.stdout.contains("SUCCESS")); - - // On JDK 24+, check for restricted method warnings - if (javaVersion >= 24) { - if (result.stderr.contains("restricted method") || - result.stderr.contains("enable-native-access")) { - System.err.println("WARNING: Still seeing restricted method warnings on JDK 24+"); - System.err.println("This may indicate the manifest attribute needs additional configuration"); - System.err.println("Stderr: " + result.stderr); - // Don't fail the test - this is informational for now - // assertFalse("Expected no 'restricted method' warnings on JDK 24+ with manifest attribute", - // result.stderr.contains("restricted method") || - // result.stderr.contains("enable-native-access")); - } else { - System.err.println("SUCCESS: No restricted method warnings on JDK 24+!"); - } - } - - System.err.println("Integration test passed!"); - System.err.println("Output: " + result.stdout); - - } finally { - // Cleanup temp directory - deleteDirectory(tempDir.toFile()); - } - } - - private void compileTestProgram(Path source, Path jarPath, Path outputDir) throws Exception { - List command = new ArrayList<>(); - command.add(getJavacPath()); - command.add("-cp"); - command.add(jarPath.toString()); - command.add("-d"); - command.add(outputDir.toString()); - command.add(source.toString()); - - ProcessBuilder pb = new ProcessBuilder(command); - pb.redirectErrorStream(true); - Process process = pb.start(); - - String output = readStream(process.getInputStream()); - int exitCode = process.waitFor(); - - if (exitCode != 0) { - fail("Failed to compile test program. Exit code: " + exitCode + "\nOutput: " + output); - } - } - - private ProcessResult runTestProgram(Path jarPath, Path classPath, int javaVersion) throws Exception { - List command = new ArrayList<>(); - command.add(getJavaPath()); - - // Note: We intentionally do NOT add --enable-native-access flag here - // to test that the manifest attribute alone is sufficient - - command.add("-cp"); - command.add(classPath.toString() + File.pathSeparator + jarPath.toString()); - command.add("SnappyIntegrationTest"); - - ProcessBuilder pb = new ProcessBuilder(command); - Process process = pb.start(); - - String stdout = readStream(process.getInputStream()); - String stderr = readStream(process.getErrorStream()); - int exitCode = process.waitFor(); - - return new ProcessResult(exitCode, stdout, stderr); - } - - private Path findSnappyJar() { - Path targetDir = Paths.get("target"); - if (!Files.exists(targetDir)) { - return null; - } - - try (Stream paths = Files.walk(targetDir, 1)) { - return paths - .filter(p -> p.getFileName().toString().startsWith("snappy-java-")) - .filter(p -> p.getFileName().toString().endsWith(".jar")) - .filter(p -> !p.getFileName().toString().contains("sources")) - .filter(p -> !p.getFileName().toString().contains("javadoc")) - .findFirst() - .orElse(null); - } catch (Exception e) { - return null; - } - } - - private String getJavaPath() { - String javaHome = System.getProperty("java.home"); - return Paths.get(javaHome, "bin", "java").toString(); - } - - private String getJavacPath() { - String javaHome = System.getProperty("java.home"); - Path javacPath = Paths.get(javaHome, "bin", "javac"); - if (Files.exists(javacPath)) { - return javacPath.toString(); - } - // For JDK 8 and some installations, javac might be in parent/bin - javacPath = Paths.get(javaHome).getParent().resolve("bin").resolve("javac"); - if (Files.exists(javacPath)) { - return javacPath.toString(); - } - return "javac"; // Fall back to PATH - } - - private int getJavaVersion() { - String version = System.getProperty("java.version"); - if (version.startsWith("1.")) { - version = version.substring(2, 3); - } else { - int dot = version.indexOf("."); - if (dot != -1) { - version = version.substring(0, dot); - } - } - return Integer.parseInt(version); - } - - private String readStream(java.io.InputStream stream) throws Exception { - try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) { - return reader.lines().collect(Collectors.joining("\n")); - } - } - - private void deleteDirectory(File dir) { - if (dir.exists()) { - File[] files = dir.listFiles(); - if (files != null) { - for (File file : files) { - if (file.isDirectory()) { - deleteDirectory(file); - } else { - file.delete(); - } - } - } - dir.delete(); - } - } - - private static class ProcessResult { - final int exitCode; - final String stdout; - final String stderr; - - ProcessResult(int exitCode, String stdout, String stderr) { - this.exitCode = exitCode; - this.stdout = stdout; - this.stderr = stderr; - } - } -} From 520bf514cc8d2bbe1f7d6c82309850dfc46d4c19 Mon Sep 17 00:00:00 2001 From: "Taro L. Saito" Date: Mon, 24 Nov 2025 10:44:01 -0800 Subject: [PATCH 4/4] Remove unnecessary manifest check from integration test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The manifest check is no longer needed since we decided not to include the Enable-Native-Access attribute. Related to #689 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- script/test-jar-integration.sh | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/script/test-jar-integration.sh b/script/test-jar-integration.sh index c874adf3..af345ad7 100755 --- a/script/test-jar-integration.sh +++ b/script/test-jar-integration.sh @@ -22,17 +22,6 @@ if [ -z "$JAR_FILE" ]; then fi echo "Using JAR: $JAR_FILE" -# Check manifest -echo "" -echo "Checking JAR manifest..." -jar xf "$JAR_FILE" META-INF/MANIFEST.MF -if grep -q "Enable-Native-Access: ALL-UNNAMED" META-INF/MANIFEST.MF; then - echo "✓ Manifest contains Enable-Native-Access: ALL-UNNAMED" -else - echo "✗ WARNING: Manifest does NOT contain Enable-Native-Access attribute" -fi -rm -rf META-INF - # Create temp directory TEMP_DIR=$(mktemp -d) echo ""