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]
 

Reply via email to