diff --git a/.github/workflows/release_java.yml b/.github/workflows/release_java.yml
index 9d21368501f6..067816325367 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
@@ -76,24 +80,103 @@ jobs:
if: ${{ contains(matrix.os, 'ubuntu') }}
run: pip install cargo-zigbuild
+ - name: Build linux musl JNI library in Alpine
+ if: ${{ contains(matrix.os, 'ubuntu') && contains(matrix.classifier, 'musl') }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ HOST_UID="$(id -u)"
+ HOST_GID="$(id -g)"
+ PLATFORM="linux/amd64"
+ if [[ "${{ matrix.classifier }}" == *"aarch_64"* ]]; then
+ PLATFORM="linux/arm64"
+ fi
+
+ docker run --rm \
+ --platform "$PLATFORM" \
+ -e HOST_UID="$HOST_UID" \
+ -e HOST_GID="$HOST_GID" \
+ -v "$GITHUB_WORKSPACE":/work \
+ -w /work/bindings/java \
+ rust:1.85-alpine3.20 \
+ sh -lc '
+ set -eu
+ apk add --no-cache python3 build-base cmake pkgconfig git protobuf sqlite-dev zlib-dev
+ export PATH=/usr/local/cargo/bin:$PATH
+
+ python3 tools/build.py \
+ --classifier "${{ matrix.classifier }}" \
+ --profile release \
+ --enable-zigbuild false
+
+ chown -R "$HOST_UID:$HOST_GID" /work/bindings/java/target
+ '
+
- name: Local staging
working-directory: bindings/java
shell: bash
run: |
- ./mvnw -Papache-release package verify org.sonatype.plugins:nexus-staging-maven-plugin:deploy \
+ EXTRA_MVN_ARGS=""
+ if [[ "${{ matrix.classifier }}" == *"musl" ]]; then
+ EXTRA_MVN_ARGS="-Dexec.skip=true"
+ fi
+
+ MVN_GOALS="package verify"
+ if [[ "${{ github.event_name }}" != "pull_request" ]]; then
+ MVN_GOALS="$MVN_GOALS org.sonatype.plugins:nexus-staging-maven-plugin:deploy"
+ EXTRA_MVN_ARGS="$EXTRA_MVN_ARGS \
+ -DaltStagingDirectory=local-staging \
+ -DskipRemoteStaging=true \
+ -DserverId=apache.releases.https \
+ -DnexusUrl=https://repository.apache.org"
+ fi
+
+ ./mvnw -Papache-release $MVN_GOALS \
-DskipTests=true \
-Djni.classifier=${{ matrix.classifier }} \
-Dcargo-build.profile=release \
-Dcargo-build.enableZigbuild=${{ env.CARGO_BUILD_ENABLE_ZIGBUILD }} \
- -DaltStagingDirectory=local-staging \
- -DskipRemoteStaging=true \
- -DserverId=apache.releases.https \
- -DnexusUrl=https://repository.apache.org
+ $EXTRA_MVN_ARGS
env:
MAVEN_USERNAME: ${{ secrets.NEXUS_STAGE_DEPLOYER_USER }}
MAVEN_CENTRAL_TOKEN: ${{ secrets.NEXUS_STAGE_DEPLOYER_PW }}
MAVEN_GPG_PASSPHRASE: ""
+ - name: Smoke linux musl JNI library in Alpine
+ if: ${{ contains(matrix.os, 'ubuntu') && contains(matrix.classifier, 'musl') }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ PLATFORM="linux/amd64"
+ if [[ "${{ matrix.classifier }}" == *"aarch_64"* ]]; then
+ PLATFORM="linux/arm64"
+ fi
+
+ docker run --rm \
+ --platform "$PLATFORM" \
+ -v "$GITHUB_WORKSPACE":/work \
+ -w /work/bindings/java \
+ alpine:3.20 \
+ sh -lc '
+ set -eu
+ apk add --no-cache openjdk17-jdk libgcc
+ BASE_JAR="$(find target -maxdepth 1 -name "opendal-*.jar" ! -name "*-linux-*.jar" ! -name "*-sources.jar" ! -name "*-javadoc.jar" | head -n 1)"
+ NATIVE_JAR="$(find target -maxdepth 1 -name "opendal-*-${{ matrix.classifier }}.jar" | head -n 1)"
+ test -n "$BASE_JAR"
+ test -n "$NATIVE_JAR"
+
+ {
+ echo "import org.apache.opendal.NativeLibrary;"
+ echo "public class OpenDALSmoke {"
+ echo " public static void main(String[] args) {"
+ echo " NativeLibrary.loadLibrary();"
+ echo " }"
+ echo "}"
+ } > /tmp/OpenDALSmoke.java
+
+ java -cp "$BASE_JAR:$NATIVE_JAR" /tmp/OpenDALSmoke.java
+ '
- name: Upload local staging directory
+ if: ${{ github.event_name != 'pull_request' }}
uses: actions/upload-artifact@v6
with:
name: ${{ matrix.classifier }}-local-staging
@@ -102,6 +185,7 @@ jobs:
include-hidden-files: true
deploy-staged-snapshots:
+ if: ${{ github.event_name != 'pull_request' }}
runs-on: ubuntu-latest
needs: [stage-snapshot]
steps:
@@ -134,11 +218,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@v7
+ 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@v7
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@v7
+ with:
+ name: linux-aarch_64-musl-local-staging
+ path: ~/linux-aarch_64-musl-local-staging
- name: Download darwin staging directory
uses: actions/download-artifact@v7
with:
@@ -159,7 +253,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/Cargo.toml b/bindings/java/Cargo.toml
index b1d7a4cd5c24..67dcfd8e2f86 100644
--- a/bindings/java/Cargo.toml
+++ b/bindings/java/Cargo.toml
@@ -27,7 +27,7 @@ repository = "https://github.com/apache/opendal"
rust-version = "1.85"
[lib]
-crate-type = ["cdylib"]
+crate-type = ["cdylib", "staticlib"]
doc = false
[dependencies]
diff --git a/bindings/java/README.md b/bindings/java/README.md
index 24e892de22fc..6d4d1fe2966a 100644
--- a/bindings/java/README.md
+++ b/bindings/java/README.md
@@ -72,6 +72,16 @@ Then add the dependency to `opendal` as following:
```
+On musl-based Linux distributions such as Alpine, use the musl classified artifact instead of the default Linux classifier:
+
+```xml
+linux-x86_64-musl
+```
+
+Use `linux-aarch_64-musl` for aarch64 Linux musl environments.
+
+The musl native library is dynamically linked against musl libc and the GCC runtime. On Alpine Linux, install `libgcc` before loading the native library.
+
### Gradle
For Gradle, you can first add the `com.google.osdetector` for automatically detect the classifier based on your platform:
@@ -91,6 +101,10 @@ dependencies {
}
```
+On musl-based Linux distributions such as Alpine, use `linux-x86_64-musl` or `linux-aarch_64-musl` as the classifier.
+
+The musl native library is dynamically linked against musl libc and the GCC runtime. On Alpine Linux, install `libgcc` before loading the native library.
+
### Classified library
Note that the dependency without classifier ships all classes and resources except the "opendal_java" shared library. And those with classifier bundle only the shared library.
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 ccdaab8caab7..7af0a8e8b1ea 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,10 @@
import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.Properties;
/**
@@ -60,6 +64,9 @@ public enum Environment {
} else {
classifier.append("x86_64");
}
+ if (classifier.indexOf("linux-") == 0 && isMuslRuntime()) {
+ classifier.append("-musl");
+ }
INSTANCE.classifier = classifier.toString();
}
@@ -80,4 +87,16 @@ public static String getClassifier() {
public static String getVersion() {
return INSTANCE.projectVersion;
}
+
+ private static boolean isMuslRuntime() {
+ return hasMuslLoader(Paths.get("/lib")) || hasMuslLoader(Paths.get("/usr/lib"));
+ }
+
+ private static boolean hasMuslLoader(Path dir) {
+ try (DirectoryStream stream = Files.newDirectoryStream(dir, "ld-musl-*.so.1")) {
+ return stream.iterator().hasNext();
+ } catch (IOException | SecurityException e) {
+ return false;
+ }
+ }
}
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 bc67e3237610..4877e9b7f683 100644
--- a/bindings/java/src/main/java/org/apache/opendal/NativeLibrary.java
+++ b/bindings/java/src/main/java/org/apache/opendal/NativeLibrary.java
@@ -58,7 +58,9 @@ private enum LibraryState {
* You can use the prebuilt library:
*
* - org.apache.opendal:opendal-{version}-linux-x86_64
+ * - org.apache.opendal:opendal-{version}-linux-x86_64-musl
* - org.apache.opendal:opendal-{version}-linux-aarch_64
+ * - org.apache.opendal:opendal-{version}-linux-aarch_64-musl
* - org.apache.opendal:opendal-{version}-osx-x86_64
* - org.apache.opendal:opendal-{version}-osx-aarch_64
* - org.apache.opendal:opendal-{version}-windows-x86_64
@@ -77,6 +79,9 @@ public static void loadLibrary() {
} catch (IOException e) {
libraryLoaded.set(LibraryState.NOT_LOADED);
throw new UncheckedIOException("Unable to load the OpenDAL shared library", e);
+ } catch (UnsatisfiedLinkError e) {
+ libraryLoaded.set(LibraryState.NOT_LOADED);
+ throw e;
}
libraryLoaded.set(LibraryState.LOADED);
return;
@@ -104,12 +109,39 @@ private static void doLoadLibrary() throws IOException {
private static void doLoadBundledLibrary() throws IOException {
final String libraryPath = bundledLibraryPath();
+ UnsatisfiedLinkError linkError = null;
try (final InputStream is = NativeObject.class.getResourceAsStream(libraryPath)) {
+ if (is != null) {
+ 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);
+ try {
+ System.load(tmpFile.getAbsolutePath());
+ return;
+ } catch (UnsatisfiedLinkError e) {
+ linkError = e;
+ }
+ }
+ }
+
+ final String fallbackLibraryPath = fallbackBundledLibraryPath();
+ if (fallbackLibraryPath == null) {
+ if (linkError != null) {
+ throw linkError;
+ }
+ throw new IOException("cannot find " + libraryPath);
+ }
+ try (final InputStream is = NativeObject.class.getResourceAsStream(fallbackLibraryPath)) {
if (is == null) {
+ if (linkError != null) {
+ throw linkError;
+ }
throw new IOException("cannot find " + libraryPath);
}
- final int dot = libraryPath.indexOf('.');
- final File tmpFile = File.createTempFile(libraryPath.substring(0, dot), libraryPath.substring(dot));
+ final int dot = fallbackLibraryPath.indexOf('.');
+ final File tmpFile =
+ File.createTempFile(fallbackLibraryPath.substring(0, dot), fallbackLibraryPath.substring(dot));
tmpFile.deleteOnExit();
Files.copy(is, tmpFile.toPath(), StandardCopyOption.REPLACE_EXISTING);
System.load(tmpFile.getAbsolutePath());
@@ -121,4 +153,17 @@ private static String bundledLibraryPath() {
final String libraryName = System.mapLibraryName("opendal_java");
return "/native/" + classifier + "/" + libraryName;
}
+
+ private static String fallbackBundledLibraryPath() {
+ final String classifier = Environment.getClassifier();
+ if (!classifier.startsWith("linux-")) {
+ return null;
+ }
+ final String libraryName = System.mapLibraryName("opendal_java");
+ if (classifier.endsWith("-musl")) {
+ final String gnu = classifier.substring(0, classifier.length() - "-musl".length());
+ return "/native/" + gnu + "/" + libraryName;
+ }
+ return "/native/" + classifier + "-musl/" + libraryName;
+ }
}
diff --git a/bindings/java/tools/build.py b/bindings/java/tools/build.py
index aa626e88c5a8..2a76ffc7e0b9 100755
--- a/bindings/java/tools/build.py
+++ b/bindings/java/tools/build.py
@@ -19,6 +19,7 @@
from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser
from pathlib import Path
+import glob
import shutil
import subprocess
@@ -30,8 +31,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}")
@@ -47,6 +52,10 @@ def get_cargo_artifact_name(classifier: str) -> str:
raise Exception(f"Unsupported classifier: {classifier}")
+def is_musl_runtime() -> bool:
+ return len(glob.glob("/lib/ld-musl-*.so.1")) > 0 or len(glob.glob("/usr/lib/ld-musl-*.so.1")) > 0
+
+
if __name__ == "__main__":
basedir = Path(__file__).parent.parent
@@ -67,8 +76,11 @@ def get_cargo_artifact_name(classifier: str) -> str:
print("$ " + subprocess.list2cmdline(command))
subprocess.run(command, cwd=basedir, check=True)
- # Enable zigbuild if flag enabled and we are building linux target
- enable_zigbuild = args.enable_zigbuild == "true" and "linux" in target
+ # Enable zigbuild if flag enabled and we are building linux gnu target.
+ #
+ # For musl targets, prefer using the system musl toolchain (e.g. `musl-tools` on Ubuntu)
+ # instead of zigbuild.
+ enable_zigbuild = args.enable_zigbuild == "true" and "linux" in target and not target.endswith("-musl")
cmd = [
"cargo",
@@ -77,7 +89,7 @@ def get_cargo_artifact_name(classifier: str) -> str:
f"--profile={args.profile}",
]
- if enable_zigbuild:
+ if enable_zigbuild and target.endswith("-gnu"):
# Pin glibc to 2.17 if zigbuild has been enabled.
cmd += ["--target", f"{target}.2.17"]
else:
@@ -92,8 +104,15 @@ def get_cargo_artifact_name(classifier: str) -> str:
# History reason of cargo profiles.
profile = "debug" if args.profile in ["dev", "test", "bench"] else args.profile
+
artifact = get_cargo_artifact_name(args.classifier)
src = output / target / profile / artifact
dst = basedir / "target" / "classes" / "native" / args.classifier / artifact
dst.parent.mkdir(exist_ok=True, parents=True)
+
+ if target.endswith("-musl") and not is_musl_runtime():
+ raise Exception(
+ "Building musl artifacts requires running inside a musl environment (e.g. Alpine)."
+ )
+
shutil.copy2(src, dst)