This is an automated email from the ASF dual-hosted git repository. xuanwo pushed a commit to branch xuanwo/java-alpine-v1 in repository https://gitbox.apache.org/repos/asf/opendal.git
commit 48a8af38882a95c0663ca0cede59f5ff4c604cd5 Author: Xuanwo <[email protected]> AuthorDate: Mon Dec 22 21:05:50 2025 +0800 feat(bindings/java): Add musl platform support --- .github/workflows/release_java.yml | 16 ++++ .../main/java/org/apache/opendal/Environment.java | 55 ++++++++++++- .../java/org/apache/opendal/NativeLibrary.java | 96 +++++++++++++++++++--- .../java/org/apache/opendal/EnvironmentTest.java | 67 +++++++++++++++ bindings/java/tools/build.py | 11 ++- 5 files changed, 228 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release_java.yml b/.github/workflows/release_java.yml index c530d0fcb..fea5d5722 100644 --- a/.github/workflows/release_java.yml +++ b/.github/workflows/release_java.yml @@ -42,8 +42,12 @@ jobs: include: - os: ubuntu-latest classifier: linux-x86_64 + - os: ubuntu-latest + classifier: linux-x86_64-musl - os: ubuntu-24.04-arm classifier: linux-aarch_64 + - os: ubuntu-24.04-arm + classifier: linux-aarch_64-musl - os: windows-latest classifier: windows-x86_64 - os: macos-latest @@ -135,11 +139,21 @@ jobs: with: name: linux-x86_64-local-staging path: ~/linux-x86_64-local-staging + - name: Download linux x86_64 (musl) staging directory + uses: actions/download-artifact@v5 + with: + name: linux-x86_64-musl-local-staging + path: ~/linux-x86_64-musl-local-staging - name: Download linux aarch_64 staging directory uses: actions/download-artifact@v5 with: name: linux-aarch_64-local-staging path: ~/linux-aarch_64-local-staging + - name: Download linux aarch_64 (musl) staging directory + uses: actions/download-artifact@v5 + with: + name: linux-aarch_64-musl-local-staging + path: ~/linux-aarch_64-musl-local-staging - name: Download darwin staging directory uses: actions/download-artifact@v5 with: @@ -160,7 +174,9 @@ jobs: python ./scripts/merge_local_staging.py $LOCAL_STAGING_DIR/staging \ ~/windows-x86_64-local-staging/staging \ ~/linux-x86_64-local-staging/staging \ + ~/linux-x86_64-musl-local-staging/staging \ ~/linux-aarch_64-local-staging/staging \ + ~/linux-aarch_64-musl-local-staging/staging \ ~/osx-x86_64-local-staging/staging \ ~/osx-aarch_64-local-staging/staging diff --git a/bindings/java/src/main/java/org/apache/opendal/Environment.java b/bindings/java/src/main/java/org/apache/opendal/Environment.java index ccdaab8ca..5f6edc65d 100644 --- a/bindings/java/src/main/java/org/apache/opendal/Environment.java +++ b/bindings/java/src/main/java/org/apache/opendal/Environment.java @@ -22,6 +22,8 @@ package org.apache.opendal; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Paths; import java.util.Properties; /** @@ -31,6 +33,7 @@ public enum Environment { INSTANCE; public static final String UNKNOWN = "<unknown>"; + private static final String LIBC_PROPERTY = "org.apache.opendal.libc"; private String classifier = UNKNOWN; private String projectVersion = UNKNOWN; @@ -44,8 +47,18 @@ public enum Environment { throw new UncheckedIOException("cannot load environment properties file", e); } + INSTANCE.classifier = detectClassifier(System.getProperty("os.name"), System.getProperty("os.arch")); + } + + static String detectClassifier(String osName, String osArch) { + final String os = osName == null ? "" : osName.toLowerCase(); + final String arch = osArch == null ? "" : osArch.toLowerCase(); + final boolean musl = !os.startsWith("windows") && !os.startsWith("mac") && isMusl(arch); + return buildClassifier(os, arch, musl); + } + + static String buildClassifier(String os, String arch, boolean musl) { final StringBuilder classifier = new StringBuilder(); - final String os = System.getProperty("os.name").toLowerCase(); if (os.startsWith("windows")) { classifier.append("windows"); } else if (os.startsWith("mac")) { @@ -54,13 +67,49 @@ public enum Environment { classifier.append("linux"); } classifier.append("-"); - final String arch = System.getProperty("os.arch").toLowerCase(); if (arch.equals("aarch64")) { classifier.append("aarch_64"); } else { classifier.append("x86_64"); } - INSTANCE.classifier = classifier.toString(); + if (classifier.toString().startsWith("linux-") && musl) { + classifier.append("-musl"); + } + return classifier.toString(); + } + + static boolean isMusl(String osArch) { + final String override = System.getProperty(LIBC_PROPERTY); + if (override != null) { + final String libc = override.trim().toLowerCase(); + if (libc.equals("musl")) { + return true; + } + if (libc.equals("gnu") || libc.equals("glibc")) { + return false; + } + } + + final String loader = muslLoaderName(osArch); + if (loader == null) { + return false; + } + return Files.exists(Paths.get("/lib", loader)) || Files.exists(Paths.get("/usr/lib", loader)); + } + + private static String muslLoaderName(String osArch) { + if (osArch == null) { + return null; + } + switch (osArch) { + case "aarch64": + return "ld-musl-aarch64.so.1"; + case "x86_64": + case "amd64": + return "ld-musl-x86_64.so.1"; + default: + return null; + } } /** diff --git a/bindings/java/src/main/java/org/apache/opendal/NativeLibrary.java b/bindings/java/src/main/java/org/apache/opendal/NativeLibrary.java index bc67e3237..4e65c59d0 100644 --- a/bindings/java/src/main/java/org/apache/opendal/NativeLibrary.java +++ b/bindings/java/src/main/java/org/apache/opendal/NativeLibrary.java @@ -59,6 +59,8 @@ public class NativeLibrary { * <ul> * <li>org.apache.opendal:opendal-{version}-linux-x86_64</li> * <li>org.apache.opendal:opendal-{version}-linux-aarch_64</li> + * <li>org.apache.opendal:opendal-{version}-linux-x86_64-musl</li> + * <li>org.apache.opendal:opendal-{version}-linux-aarch_64-musl</li> * <li>org.apache.opendal:opendal-{version}-osx-x86_64</li> * <li>org.apache.opendal:opendal-{version}-osx-aarch_64</li> * <li>org.apache.opendal:opendal-{version}-windows-x86_64</li> @@ -77,6 +79,9 @@ public class NativeLibrary { } catch (IOException e) { libraryLoaded.set(LibraryState.NOT_LOADED); throw new UncheckedIOException("Unable to load the OpenDAL shared library", e); + } catch (RuntimeException | Error e) { + libraryLoaded.set(LibraryState.NOT_LOADED); + throw e; } libraryLoaded.set(LibraryState.LOADED); return; @@ -103,22 +108,89 @@ public class NativeLibrary { } private static void doLoadBundledLibrary() throws IOException { - final String libraryPath = bundledLibraryPath(); - try (final InputStream is = NativeObject.class.getResourceAsStream(libraryPath)) { - if (is == null) { - throw new IOException("cannot find " + libraryPath); + final String libraryName = System.mapLibraryName("opendal_java"); + final String[] libraryPaths = bundledLibraryPaths(libraryName); + + final UnsatisfiedLinkError[] linkErrors = new UnsatisfiedLinkError[libraryPaths.length]; + for (int i = 0; i < libraryPaths.length; i++) { + final String libraryPath = libraryPaths[i]; + try (final InputStream is = NativeObject.class.getResourceAsStream(libraryPath)) { + if (is == null) { + continue; + } + + final File tmpFile = createTempLibraryFile(libraryName); + tmpFile.deleteOnExit(); + Files.copy(is, tmpFile.toPath(), StandardCopyOption.REPLACE_EXISTING); + try { + System.load(tmpFile.getAbsolutePath()); + return; + } catch (UnsatisfiedLinkError e) { + linkErrors[i] = e; + } + } + } + + final StringBuilder attempted = new StringBuilder(); + for (int i = 0; i < libraryPaths.length; i++) { + if (i > 0) { + attempted.append(", "); + } + attempted.append(libraryPaths[i]); + } + + final UnsatisfiedLinkError last = lastNonNull(linkErrors); + if (last != null) { + final UnsatisfiedLinkError e = new UnsatisfiedLinkError( + "Unable to load the OpenDAL shared library from classpath. Tried: " + attempted); + for (UnsatisfiedLinkError err : linkErrors) { + if (err != null) { + e.addSuppressed(err); + } } - final int dot = libraryPath.indexOf('.'); - final File tmpFile = File.createTempFile(libraryPath.substring(0, dot), libraryPath.substring(dot)); - tmpFile.deleteOnExit(); - Files.copy(is, tmpFile.toPath(), StandardCopyOption.REPLACE_EXISTING); - System.load(tmpFile.getAbsolutePath()); + throw e; } + throw new IOException("cannot find bundled OpenDAL shared library in classpath. Tried: " + attempted); } - private static String bundledLibraryPath() { + private static String[] bundledLibraryPaths(String libraryName) { final String classifier = Environment.getClassifier(); - final String libraryName = System.mapLibraryName("opendal_java"); - return "/native/" + classifier + "/" + libraryName; + if (classifier.startsWith("linux-") && classifier.endsWith("-musl")) { + final String gnu = classifier.substring(0, classifier.length() - "-musl".length()); + return new String[] { + "/native/" + classifier + "/" + libraryName, + "/native/" + gnu + "/" + libraryName, + }; + } + if (classifier.startsWith("linux-")) { + return new String[] { + "/native/" + classifier + "/" + libraryName, + "/native/" + classifier + "-musl/" + libraryName, + }; + } + return new String[] {"/native/" + classifier + "/" + libraryName}; + } + + private static File createTempLibraryFile(String libraryName) throws IOException { + final int dot = libraryName.lastIndexOf('.'); + final String prefix; + final String suffix; + if (dot >= 0) { + prefix = libraryName.substring(0, dot); + suffix = libraryName.substring(dot); + } else { + prefix = libraryName; + suffix = null; + } + return File.createTempFile(prefix + "-", suffix); + } + + private static UnsatisfiedLinkError lastNonNull(UnsatisfiedLinkError[] errors) { + for (int i = errors.length - 1; i >= 0; i--) { + if (errors[i] != null) { + return errors[i]; + } + } + return null; } } diff --git a/bindings/java/src/test/java/org/apache/opendal/EnvironmentTest.java b/bindings/java/src/test/java/org/apache/opendal/EnvironmentTest.java new file mode 100644 index 000000000..3eeb264fb --- /dev/null +++ b/bindings/java/src/test/java/org/apache/opendal/EnvironmentTest.java @@ -0,0 +1,67 @@ +/* + * 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.opendal; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +public class EnvironmentTest { + @Test + public void testBuildClassifierLinuxGnuX8664() { + assertThat(Environment.buildClassifier("linux", "x86_64", false)).isEqualTo("linux-x86_64"); + } + + @Test + public void testBuildClassifierLinuxMuslX8664() { + assertThat(Environment.buildClassifier("linux", "x86_64", true)).isEqualTo("linux-x86_64-musl"); + } + + @Test + public void testBuildClassifierLinuxMuslAarch64() { + assertThat(Environment.buildClassifier("linux", "aarch64", true)).isEqualTo("linux-aarch_64-musl"); + } + + @Test + public void testBuildClassifierNonLinuxIgnoreMusl() { + assertThat(Environment.buildClassifier("mac os x", "x86_64", true)).isEqualTo("osx-x86_64"); + assertThat(Environment.buildClassifier("windows", "x86_64", true)).isEqualTo("windows-x86_64"); + } + + @Test + public void testIsMuslOverride() { + final String key = "org.apache.opendal.libc"; + final String previous = System.getProperty(key); + try { + System.setProperty(key, "musl"); + assertThat(Environment.isMusl("x86_64")).isTrue(); + + System.setProperty(key, "gnu"); + assertThat(Environment.isMusl("x86_64")).isFalse(); + } finally { + if (previous == null) { + System.clearProperty(key); + } else { + System.setProperty(key, previous); + } + } + } +} + diff --git a/bindings/java/tools/build.py b/bindings/java/tools/build.py index 0eedaa65e..c0fa55121 100755 --- a/bindings/java/tools/build.py +++ b/bindings/java/tools/build.py @@ -30,8 +30,12 @@ def classifier_to_target(classifier: str) -> str: return "x86_64-apple-darwin" if classifier == "linux-aarch_64": return "aarch64-unknown-linux-gnu" + if classifier == "linux-aarch_64-musl": + return "aarch64-unknown-linux-musl" if classifier == "linux-x86_64": return "x86_64-unknown-linux-gnu" + if classifier == "linux-x86_64-musl": + return "x86_64-unknown-linux-musl" if classifier == "windows-x86_64": return "x86_64-pc-windows-msvc" raise Exception(f"Unsupported classifier: {classifier}") @@ -82,8 +86,11 @@ if __name__ == "__main__": cmd += ["--features", args.features] if enable_zigbuild: - # Pin glibc to 2.17 if zigbuild has been enabled. - cmd += ["--target", f"{target}.2.17"] + # Pin glibc to 2.17 for gnu builds. + # + # Note: The `.2.17` suffix is a zig target detail and is only valid for gnu. + zig_target = f"{target}.2.17" if target.endswith("-gnu") else target + cmd += ["--target", zig_target] else: cmd += ["--target", target]
