Fangjun Kuang
Committed by GitHub

Simplify the usage of our non-Android Java API (#2533)

This PR simplifies the usage of the non-Android Java API by providing platform-specific JAR files that include native shared libraries, eliminating the need for users to manually manage native dependencies.

- Refactored LibraryUtils.java to support multiple library loading methods including extracting from JAR resources
- Added build infrastructure to create platform-specific native library JAR files
- Introduced debug capabilities and improved error handling for library loading
name: jar
on:
push:
branches:
- refactor-jar
tags:
- 'v[0-9]+.[0-9]+.[0-9]+*'
workflow_dispatch:
concurrency:
group: jar-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
jobs:
jar:
runs-on: ${{ matrix.os }}
name: ${{ matrix.os }} ${{ matrix.arch }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-24.04-arm
arch: "arm64"
- os: ubuntu-latest
arch: "x64"
- os: macos-latest
arch: "arm64"
- os: macos-13
arch: "x64"
- os: windows-latest
arch: "x64"
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: actions/setup-java@v4
with:
distribution: 'temurin' # See 'Supported distributions' for available options
java-version: '21'
- name: Show java version
shell: bash
run: |
java --version
- name: Download libs ${{ matrix.os }} ${{ matrix.arch }}
if: ${{ matrix.os == 'ubuntu-24.04-arm' && matrix.arch == 'arm64' }}
shell: bash
run: |
SHERPA_ONNX_VERSION=$(grep "SHERPA_ONNX_VERSION" ./CMakeLists.txt | cut -d " " -f 2 | cut -d '"' -f 2)
curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/v$SHERPA_ONNX_VERSION/sherpa-onnx-v$SHERPA_ONNX_VERSION-linux-aarch64-jni.tar.bz2
tar xvf ./*.tar.bz2
src=sherpa-onnx-v$SHERPA_ONNX_VERSION-linux-aarch64-jni
dst=sherpa-onnx/java-api/resources/sherpa-onnx/native/linux-aarch64
mkdir -p $dst
cp -v $src/lib/libsherpa-onnx-jni.so $dst/
cp -v $src/lib/libonnxruntime.so $dst/
ls -lh $dst
rm -rf $src*
- name: Download libs ${{ matrix.os }} ${{ matrix.arch }}
if: ${{ matrix.os == 'ubuntu-latest' && matrix.arch == 'x64' }}
shell: bash
run: |
SHERPA_ONNX_VERSION=$(grep "SHERPA_ONNX_VERSION" ./CMakeLists.txt | cut -d " " -f 2 | cut -d '"' -f 2)
curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/v$SHERPA_ONNX_VERSION/sherpa-onnx-v$SHERPA_ONNX_VERSION-linux-x64-jni.tar.bz2
tar xvf ./*.tar.bz2
src=sherpa-onnx-v$SHERPA_ONNX_VERSION-linux-x64-jni
dst=sherpa-onnx/java-api/resources/sherpa-onnx/native/linux-x64
mkdir -p $dst
cp -v $src/lib/libsherpa-onnx-jni.so $dst/
cp -v $src/lib/libonnxruntime.so $dst/
ls -lh $dst
rm -rf $src*
- name: Download libs ${{ matrix.os }} ${{ matrix.arch }}
if: ${{ matrix.os == 'macos-latest' && matrix.arch == 'arm64' }}
shell: bash
run: |
SHERPA_ONNX_VERSION=$(grep "SHERPA_ONNX_VERSION" ./CMakeLists.txt | cut -d " " -f 2 | cut -d '"' -f 2)
curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/v$SHERPA_ONNX_VERSION/sherpa-onnx-v$SHERPA_ONNX_VERSION-osx-arm64-jni.tar.bz2
tar xvf ./*.tar.bz2
src=sherpa-onnx-v$SHERPA_ONNX_VERSION-osx-arm64-jni
dst=sherpa-onnx/java-api/resources/sherpa-onnx/native/osx-aarch64
mkdir -p $dst
cp -v $src/lib/libonnxruntime.1.17.1.dylib $dst/
cp -v $src/lib/libsherpa-onnx-jni.dylib $dst/
ls -lh $dst
rm -rf $src*
- name: Download libs ${{ matrix.os }} ${{ matrix.arch }}
if: ${{ matrix.os == 'macos-13' && matrix.arch == 'x64' }}
shell: bash
run: |
SHERPA_ONNX_VERSION=$(grep "SHERPA_ONNX_VERSION" ./CMakeLists.txt | cut -d " " -f 2 | cut -d '"' -f 2)
curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/v$SHERPA_ONNX_VERSION/sherpa-onnx-v$SHERPA_ONNX_VERSION-osx-x86_64-jni.tar.bz2
tar xvf ./*.tar.bz2
src=sherpa-onnx-v$SHERPA_ONNX_VERSION-osx-x86_64-jni
dst=sherpa-onnx/java-api/resources/sherpa-onnx/native/osx-x64
mkdir -p $dst
cp -v $src/lib/libonnxruntime.1.17.1.dylib $dst/
cp -v $src/lib/libsherpa-onnx-jni.dylib $dst/
ls -lh $dst
rm -rf $src*
- name: Download libs ${{ matrix.os }} ${{ matrix.arch }}
if: ${{ matrix.os == 'windows-latest' && matrix.arch == 'x64' }}
shell: bash
run: |
SHERPA_ONNX_VERSION=$(grep "SHERPA_ONNX_VERSION" ./CMakeLists.txt | cut -d " " -f 2 | cut -d '"' -f 2)
curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/v$SHERPA_ONNX_VERSION/sherpa-onnx-v$SHERPA_ONNX_VERSION-win-x64-jni.tar.bz2
tar xvf ./*.tar.bz2
src=sherpa-onnx-v$SHERPA_ONNX_VERSION-win-x64-jni
ls -lh $src
ls -lh $src/lib
dst=sherpa-onnx/java-api/resources/sherpa-onnx/native/win-x64
mkdir -p $dst
cp -v $src/lib/onnxruntime.dll $dst/
cp -v $src/lib/sherpa-onnx-jni.dll $dst/
ls -lh $dst
rm -rf $src*
- name: Create java jar (source code)
shell: bash
run: |
cd sherpa-onnx/java-api
make
ls -lh build
- name: Create java jar (native lib)
shell: bash
run: |
SHERPA_ONNX_VERSION=v$(grep "SHERPA_ONNX_VERSION" ./CMakeLists.txt | cut -d " " -f 2 | cut -d '"' -f 2)
cd sherpa-onnx/java-api
ls -lh resources/sherpa-onnx/native
echo "--"
ls -lh resources/sherpa-onnx/native/*/
jar cfvm ./sherpa-onnx-native.jar MANIFEST.MF -C ./resources .
ls -lh *.jar
os=${{ matrix.os }}
arch=${{ matrix.arch }}
if [[ $os == "ubuntu-24.04-arm" && $arch == "arm64" ]]; then
mv -v sherpa-onnx-native.jar sherpa-onnx-native-lib-linux-aarch64-$SHERPA_ONNX_VERSION.jar
elif [[ $os == "ubuntu-latest" && $arch == "x64" ]]; then
mv -v sherpa-onnx-native.jar sherpa-onnx-native-lib-linux-x64-$SHERPA_ONNX_VERSION.jar
elif [[ $os == "macos-latest" && $arch == "arm64" ]]; then
mv -v sherpa-onnx-native.jar sherpa-onnx-native-lib-osx-aarch64-$SHERPA_ONNX_VERSION.jar
elif [[ $os == "macos-13" && $arch == "x64" ]]; then
mv -v sherpa-onnx-native.jar sherpa-onnx-native-lib-osx-x64-$SHERPA_ONNX_VERSION.jar
elif [[ $os == "windows-latest" && $arch == "x64" ]]; then
mv -v sherpa-onnx-native.jar sherpa-onnx-native-lib-win-x64-$SHERPA_ONNX_VERSION.jar
else
echo "Unknown os $os with arch $arch"
fi
- name: Show java jar (source code)
shell: bash
run: |
cd sherpa-onnx/java-api
unzip -l build/sherpa-onnx.jar
- name: Show java jar (native lib)
shell: bash
run: |
cd sherpa-onnx/java-api
unzip -l sherpa-onnx*.jar
- name: Release jar
if: github.repository_owner == 'k2-fsa' && github.event_name == 'push' && contains(github.ref, 'refs/tags/')
uses: svenstaro/upload-release-action@v2
with:
file_glob: true
overwrite: true
file: ./sherpa-onnx/java-api/sherpa-onnx-native-*.jar
- name: Release jar
if: github.repository_owner == 'csukuangfj' && github.event_name == 'push' && contains(github.ref, 'refs/tags/')
uses: svenstaro/upload-release-action@v2
with:
file_glob: true
overwrite: true
file: ./sherpa-onnx/java-api/sherpa-onnx-native-*.jar
repo_name: k2-fsa/sherpa-onnx
repo_token: ${{ secrets.UPLOAD_GH_SHERPA_ONNX_TOKEN }}
tag: v1.12.10
- name: Test KittenTTS
shell: bash
run: |
SHERPA_ONNX_VERSION=v$(grep "SHERPA_ONNX_VERSION" ./CMakeLists.txt | cut -d " " -f 2 | cut -d '"' -f 2)
os=${{ matrix.os }}
arch=${{ matrix.arch }}
if [[ $os == "ubuntu-24.04-arm" && $arch == "arm64" ]]; then
native_jar=sherpa-onnx-native-lib-linux-aarch64-$SHERPA_ONNX_VERSION.jar
elif [[ $os == "ubuntu-latest" && $arch == "x64" ]]; then
native_jar=sherpa-onnx-native-lib-linux-x64-$SHERPA_ONNX_VERSION.jar
elif [[ $os == "macos-latest" && $arch == "arm64" ]]; then
native_jar=sherpa-onnx-native-lib-osx-aarch64-$SHERPA_ONNX_VERSION.jar
elif [[ $os == "macos-13" && $arch == "x64" ]]; then
native_jar=sherpa-onnx-native-lib-osx-x64-$SHERPA_ONNX_VERSION.jar
elif [[ $os == "windows-latest" && $arch == "x64" ]]; then
native_jar=sherpa-onnx-native-lib-win-x64-$SHERPA_ONNX_VERSION.jar
else
echo "Unknown os $os with arch $arch"
fi
echo "native_jar: $native_jar"
ls -lh sherpa-onnx/java-api/$native_jar
if [[ ${{ matrix.os }} == "windows-latest" ]]; then
SEP=";"
else
SEP=":"
fi
cd java-api-examples
curl -SL -O https://github.com/k2-fsa/sherpa-onnx/releases/download/tts-models/kitten-nano-en-v0_1-fp16.tar.bz2
tar xf kitten-nano-en-v0_1-fp16.tar.bz2
rm kitten-nano-en-v0_1-fp16.tar.bz2
java \
-cp "../sherpa-onnx/java-api/build/sherpa-onnx.jar${SEP}../sherpa-onnx/java-api/$native_jar" \
NonStreamingTtsKittenEn.java
... ...
... ... @@ -20,7 +20,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest]
java-version: ['8', '11', '16', '17', '18', '19', '20', '21', '22', '23', '24']
java-version: ['24']
steps:
- uses: actions/checkout@v4
... ... @@ -46,7 +46,6 @@ jobs:
du -h -d1 .
- name: Build jar ${{ matrix.java-version }}
if: matrix.java-version == '23'
shell: bash
run: |
SHERPA_ONNX_VERSION=v$(grep "SHERPA_ONNX_VERSION" ./CMakeLists.txt | cut -d " " -f 2 | cut -d '"' -f 2)
... ... @@ -57,17 +56,6 @@ jobs:
cd ../..
ls -lh *.jar
- name: Build jar ${{ matrix.java-version }}
shell: bash
run: |
SHERPA_ONNX_VERSION=v$(grep "SHERPA_ONNX_VERSION" ./CMakeLists.txt | cut -d " " -f 2 | cut -d '"' -f 2)
cd sherpa-onnx/java-api
make
ls -lh build/
cp build/sherpa-onnx.jar ../../sherpa-onnx-$SHERPA_ONNX_VERSION-java${{ matrix.java-version }}.jar
cd ../..
ls -lh *.jar
- uses: actions/upload-artifact@v4
with:
name: release-jni-linux-jar-${{ matrix.java-version }}
... ... @@ -80,12 +68,11 @@ jobs:
file_glob: true
overwrite: true
file: ./*.jar
# repo_name: k2-fsa/sherpa-onnx
# repo_token: ${{ secrets.UPLOAD_GH_SHERPA_ONNX_TOKEN }}
# tag: v1.12.1
repo_name: k2-fsa/sherpa-onnx
repo_token: ${{ secrets.UPLOAD_GH_SHERPA_ONNX_TOKEN }}
tag: v1.12.10
- name: Build sherpa-onnx
if: matrix.java-version == '23'
uses: addnab/docker-run-action@v3
with:
image: quay.io/pypa/manylinux2014_x86_64
... ... @@ -151,7 +138,6 @@ jobs:
ls -lh install/bin
- name: Display dependencies of sherpa-onnx for linux
if: matrix.java-version == '23'
shell: bash
run: |
du -h -d1 .
... ... @@ -170,13 +156,11 @@ jobs:
readelf -d build/bin/sherpa-onnx
- uses: actions/upload-artifact@v4
if: matrix.java-version == '23'
with:
name: release-jni-linux-${{ matrix.java-version }}
path: build/install/*
- name: Copy files
if: matrix.java-version == '23'
shell: bash
run: |
du -h -d1 .
... ... @@ -194,8 +178,19 @@ jobs:
tar cjvf ${dst}.tar.bz2 $dst
du -h -d1 .
- name: Release pre-compiled binaries and libs for linux x64
if: (github.repository_owner == 'csukuangfj' || github.repository_owner == 'k2-fsa') && github.event_name == 'push' && contains(github.ref, 'refs/tags/')
uses: svenstaro/upload-release-action@v2
with:
file_glob: true
overwrite: true
file: sherpa-onnx-*.tar.bz2
# repo_name: k2-fsa/sherpa-onnx
# repo_token: ${{ secrets.UPLOAD_GH_SHERPA_ONNX_TOKEN }}
# tag: v1.12.10
- name: Publish to huggingface
if: (github.repository_owner == 'csukuangfj' || github.repository_owner == 'k2-fsa') && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && matrix.java-version == '23'
if: (github.repository_owner == 'csukuangfj' || github.repository_owner == 'k2-fsa') && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
env:
HF_TOKEN: ${{ secrets.HF_TOKEN }}
uses: nick-fields/retry@v3
... ... @@ -215,6 +210,7 @@ jobs:
cd huggingface
dst=jni/$SHERPA_ONNX_VERSION
mkdir -p $dst
git lfs track "*.jar"
cp -v ../sherpa-onnx-*.tar.bz2 $dst/
cp -v ../*.jar $dst/
... ... @@ -227,14 +223,3 @@ jobs:
git commit -m "add more files"
git push https://csukuangfj:$HF_TOKEN@huggingface.co/csukuangfj/sherpa-onnx-libs main
- name: Release pre-compiled binaries and libs for linux x64
if: (github.repository_owner == 'csukuangfj' || github.repository_owner == 'k2-fsa') && github.event_name == 'push' && contains(github.ref, 'refs/tags/') && matrix.java-version == '23'
uses: svenstaro/upload-release-action@v2
with:
file_glob: true
overwrite: true
file: sherpa-onnx-*.tar.bz2
# repo_name: k2-fsa/sherpa-onnx
# repo_token: ${{ secrets.UPLOAD_GH_SHERPA_ONNX_TOKEN }}
# tag: v1.12.0
... ...
... ... @@ -147,3 +147,4 @@ dict
voices.bin
kitten-nano-en-v0_1-fp16
*.egg-info
*.jar
... ...
... ... @@ -6,6 +6,7 @@ import com.k2fsa.sherpa.onnx.*;
public class NonStreamingTtsKittenEn {
public static void main(String[] args) {
LibraryUtils.enableDebug();
// please visit
// https://k2-fsa.github.io/sherpa/onnx/tts/pretrained_models/kitten.html
// to download model files
... ...
Manifest-Version: 1.0
... ...
... ... @@ -109,17 +109,28 @@ $(info -- java files $(java_files))
$(info --)
$(info -- class files $(class_files))
.phony: all clean
.PHONY: all clean native
all: $(out_jar)
# macos x86_x64 -> osx-x64
# macos arm64 -> osx-aarch64
# linux x86_x64 -> linux-x64
# linux arm64 -> linux-aarch64
# windows x86_x64 -> win-x64
# windows arm64 -> win-aarch64
# windows x86 -> win-x86
native:
jar cfvm ./sherpa-onnx-native.jar MANIFEST.MF -C ./resources .
$(out_jar): $(class_files)
# jar --create --verbose --file $(out_jar) -C $(out_dir) ./
jar cvf $(out_jar) -C $(out_dir) ./
# jar cvf $(out_jar) -C $(out_dir) ./
jar cfvm $@ MANIFEST.MF -C $(out_dir) .
clean:
$(RM) -rfv $(out_dir)
$(class_files): $(out_dir)/$(package_dir)/%.class: src/main/java/$(package_dir)/%.java
mkdir -p build
javac -d $(out_dir) -cp $(out_dir) $<
javac --release 8 -Xlint:-options -d $(out_dir) -cp $(out_dir) $<
... ...
... ... @@ -4,42 +4,170 @@ 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.StandardCopyOption;
import java.util.Locale;
import java.util.Objects;
/*
# We support the following loading methods
## Method 1 Specify the property sherpa_onnx.native.path
We assume the path contains the libraries sherpa-onnx-jni and onnxruntime.
java \
-Dsherpa_onnx.native.path=/Users/fangjun/sherpa-onnx/build/install/lib \
-cp /Users/fangjun/sherpa-onnx/sherpa-onnx/java-api/build/sherpa-onnx.jar
xxx.java
## Method 2 Specify the native jar library
java \
-cp /Users/fangjun/sherpa-onnx/sherpa-onnx/java-api/build/sherpa-onnx.jar:/path/to/sherpa-onnx-osx-x64.jar
xxx.java
Note that you need to replace : in -cp with ; on windows.
## Method 3 Specify the property java.library.path
We assume the path contains the libraries sherpa-onnx-jni and onnxruntime.
java \
-Djava.library.path=/Users/fangjun/sherpa-onnx/build/install/lib \
-cp /Users/fangjun/sherpa-onnx/sherpa-onnx/java-api/build/sherpa-onnx.jar
xxx.java
*/
public class LibraryUtils {
// System property to override native library path
private static final String NATIVE_PATH_PROP = "sherpa_onnx.native.path";
private static final String LIB_NAME = "sherpa-onnx-jni";
private static boolean debug = false;
private static String detectedOS;
public static void enableDebug() {
debug = true;
}
public static void disableDebug() {
debug = false;
}
public static void load() {
String libFileName = System.mapLibraryName(LIB_NAME);
// 1. Try to load from external directory specified by -Dsherpa_onnx.native.path if provided
if (loadFromSherpaOnnxNativePath()) {
return;
}
// 2. Load from resources contains in some jar file
try {
// 1. Try loading from external directory if provided
if (loadFromResourceInJar()) {
return;
}
} catch (IOException e) {
// pass
}
// 3. fallback to -Djava.library.path
// java -Djava.library.path=C:\mylibs;D:\otherlibs -cp sherpa-onnx.jar xxx.java
//
// It throws if it cannot load the lib sherpa-onnx-jni
System.loadLibrary(LIB_NAME);
}
// You specify -Dsherpa_onnx.native.path=/path/to/some/dir
// where /path/to/some/dir contains the sherpa-onnx-jni and onnxruntime libs
private static boolean loadFromSherpaOnnxNativePath() {
String libFileName = System.mapLibraryName(LIB_NAME);
String nativePath = System.getProperty(NATIVE_PATH_PROP);
if (nativePath != null) {
File nativeDir = new File(nativePath);
File libInDir = new File(nativeDir, libFileName);
if (nativeDir.isDirectory() && libInDir.exists()) {
System.out.println("Loading native lib from external directory: " + libInDir.getAbsolutePath());
if (debug) {
System.out.printf("Loading from: %s\n", libInDir.getAbsolutePath());
}
System.load(libInDir.getAbsolutePath());
return;
return true;
}
}
// 2. Fallback to extracting and loading from the JAR
File libFile = init(libFileName);
System.out.println("Loading native lib from: " + libFile.getAbsolutePath());
System.load(libFile.getAbsolutePath());
} catch (RuntimeException ex) {
System.loadLibrary(LIB_NAME);
if (debug) {
System.out.println("nativePath is null");
}
return false;
}
/* Computes and initializes OS_ARCH_STR (such as linux-x64) */
private static String initOsArch() {
String detectedOS = null;
private static boolean loadFromResourceInJar() throws IOException {
String libFileName = System.mapLibraryName(LIB_NAME);
String sherpaOnnxJniPath = "sherpa-onnx/native/" + getOsArch() + '/' + libFileName;
Path tempDirectory = null;
try {
if (!resourceExists(sherpaOnnxJniPath)) {
if (debug) {
System.out.printf("%s does not exist\n", sherpaOnnxJniPath);
}
return false;
}
tempDirectory = Files.createTempDirectory("sherpa-onnx-java");
if (Objects.equals(detectedOS, "osx")) {
// for macos, we need to first load libonnxruntime.1.17.1.dylib
String onnxruntimePath = "sherpa-onnx/native/" + getOsArch() + '/' + "libonnxruntime.1.17.1.dylib";
if (!resourceExists(onnxruntimePath)) {
if (debug) {
System.out.printf("%s does not exist\n", onnxruntimePath);
}
return false;
}
File tempFile = tempDirectory.resolve("libonnxruntime.1.17.1.dylib").toFile();
extractResource(onnxruntimePath, tempFile);
System.load(tempFile.getAbsolutePath());
} else {
String onnxLibFileName = System.mapLibraryName("onnxruntime");
String onnxruntimePath = "sherpa-onnx/native/" + getOsArch() + '/' + onnxLibFileName;
if (!resourceExists(onnxruntimePath)) {
if (debug) {
System.out.printf("%s does not exist\n", onnxruntimePath);
}
return false;
}
File tempFile = tempDirectory.resolve(onnxLibFileName).toFile();
extractResource(onnxruntimePath, tempFile);
System.load(tempFile.getAbsolutePath());
}
File tempFile = tempDirectory.resolve(libFileName).toFile();
extractResource(sherpaOnnxJniPath, tempFile);
System.load(tempFile.getAbsolutePath());
} finally {
if (tempDirectory != null) {
cleanUpTempDir(tempDirectory.toFile());
}
}
return true;
}
// this method is copied and modified from
// https://github.com/microsoft/onnxruntime/blob/main/java/src/main/java/ai/onnxruntime/OnnxRuntime.java#L118
private static String getOsArch() {
String os = System.getProperty("os.name", "generic").toLowerCase(Locale.ENGLISH);
if (os.contains("mac") || os.contains("darwin")) {
detectedOS = "osx";
... ... @@ -50,58 +178,60 @@ public class LibraryUtils {
} else {
throw new IllegalStateException("Unsupported os:" + os);
}
String detectedArch = null;
String arch = System.getProperty("os.arch", "generic").toLowerCase(Locale.ENGLISH);
String detectedArch;
String arch = System.getProperty("os.arch", "generic")
.toLowerCase(Locale.ENGLISH);
if (arch.startsWith("amd64") || arch.startsWith("x86_64")) {
detectedArch = "x64";
} else if (arch.startsWith("x86")) {
// 32-bit x86 is not supported by the Java API
detectedArch = "x86";
} else if (arch.startsWith("aarch64")) {
} else if (arch.startsWith("aarch64") || arch.startsWith("arm64")) {
detectedArch = "aarch64";
} else {
throw new IllegalStateException("Unsupported arch:" + arch);
}
return detectedOS + '-' + detectedArch;
}
private static File init(String libFileName) {
String osName = System.getProperty("os.name").toLowerCase();
String osArch = System.getProperty("os.arch").toLowerCase();
String userHome = System.getProperty("user.home");
System.out.printf("Detected OS=%s, ARCH=%s, HOME=%s%n", osName, osArch, userHome);
String archName = initOsArch();
// Prepare destination directory under ~/lib/<archName>/
String dstDir = userHome + File.separator + "lib" + File.separator + archName;
File libFile = new File(dstDir, libFileName);
File parentDir = libFile.getParentFile();
if (!parentDir.exists() && !parentDir.mkdirs()) {
throw new RuntimeException("Unable to create directory: " + parentDir);
return detectedOS + '-' + detectedArch;
}
// Extract the native library from JAR
extractResource("/native/" + archName + "/" + libFileName, libFile);
return libFile;
private static void extractResource(String resourcePath, File destination) {
if (debug) {
System.out.printf("Copying from resource path %s to %s\n", resourcePath, destination.toPath());
}
/**
* Copies a resource file from the jar to the specified destination.
*
* @param resourcePath The resource path inside the jar, e.g.:
* /native/linux_x64/libonnxruntime.so
* @param destination The destination file on disk
*/
private static void extractResource(String resourcePath, File destination) {
try (InputStream in = LibraryUtils.class.getResourceAsStream(resourcePath)) {
try (InputStream in = LibraryUtils.class.getClassLoader().getResourceAsStream(resourcePath)) {
if (in == null) {
throw new RuntimeException("Resource not found: " + resourcePath);
}
Files.copy(in, destination.toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
throw new RuntimeException("Failed to extract resource " + resourcePath + " to " + destination.getAbsolutePath(),
e);
throw new RuntimeException("Failed to extract resource " + resourcePath + " to " + destination.getAbsolutePath(), e);
}
}
// From ChatGPT:
// Class.getResourceAsStream(String path) behaves differently than ClassLoader
// - No leading slash → relative to the package of LibraryUtils
// - Leading slash → absolute path relative to classpath root
//
// ClassLoader.getResourceAsStream always uses absolute paths relative to classpath root,
// no leading slash needed
private static boolean resourceExists(String path) {
return LibraryUtils.class.getClassLoader().getResource(path) != null;
}
private static void cleanUpTempDir(File dir) {
if (!dir.exists()) return;
File[] files = dir.listFiles();
if (files != null) {
for (File f : files) {
f.deleteOnExit(); // schedule each .so for deletion
}
}
dir.deleteOnExit(); // schedule the directory itself
}
}
... ...