fix: package local onnx runtime for release

This commit is contained in:
coso
2026-05-27 05:35:02 +05:00
parent be2a0399dc
commit ad0b09e9ad
5 changed files with 220 additions and 20 deletions
+108
View File
@@ -277,6 +277,70 @@ jobs:
echo "tauri.conf.headless.json version is now: $(node -e "console.log(JSON.parse(require('fs').readFileSync('tauri.conf.headless.json','utf8')).version)")"
echo "createUpdaterArtifacts: $(node -e "console.log(JSON.parse(require('fs').readFileSync('tauri.conf.json','utf8')).bundle.createUpdaterArtifacts)")"
- name: Configure ONNX runtime bundling
shell: bash
env:
TARGET_TRIPLE: ${{ matrix.target }}
run: |
set -euo pipefail
node - <<'NODE'
const fs = require('fs');
const path = require('path');
const target = process.env.TARGET_TRIPLE;
const srcTauri = path.resolve('src-tauri');
const portablePath = (value) => value.replace(/\\/g, '/');
const unique = (values) => [...new Set(values.filter(Boolean))];
if (target === 'x86_64-pc-windows-msvc') {
const releaseDir = path.join(srcTauri, 'target', target, 'release');
const dlls = ['onnxruntime.dll', 'sherpa-onnx-c-api.dll'];
const hookPath = path.join(srcTauri, 'target', 'release-runtime-libs.nsh');
fs.mkdirSync(path.dirname(hookPath), { recursive: true });
fs.writeFileSync(
hookPath,
[
'!macro NSIS_HOOK_POSTINSTALL',
' SetOutPath "$INSTDIR"',
...dlls.map(
(name) =>
` File /a "/oname=${name}" "${portablePath(path.join(releaseDir, name))}"`,
),
'!macroend',
'',
].join('\n'),
);
const configPath = path.join(srcTauri, 'tauri.windows.conf.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
config.bundle = config.bundle || {};
config.bundle.windows = config.bundle.windows || {};
config.bundle.windows.nsis = config.bundle.windows.nsis || {};
config.bundle.windows.nsis.installerHooks = hookPath;
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
console.log(`Configured Windows ONNX runtime NSIS hook: ${hookPath}`);
} else if (target.endsWith('apple-darwin')) {
const configPath = path.join(srcTauri, 'tauri.conf.json');
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
const frameworks = [
'libonnxruntime.1.24.4.dylib',
'libonnxruntime.dylib',
'libsherpa-onnx-c-api.dylib',
].map((name) => portablePath(path.join(srcTauri, 'target', target, 'release', name)));
config.bundle = config.bundle || {};
config.bundle.macOS = config.bundle.macOS || {};
config.bundle.macOS.frameworks = unique([
...(config.bundle.macOS.frameworks || []),
...frameworks,
]);
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
console.log(`Configured macOS ONNX runtime frameworks for ${target}`);
} else {
console.log(`No ONNX runtime bundling override needed for ${target}`);
}
NODE
- name: Resolve updater endpoint
shell: bash
run: |
@@ -480,6 +544,27 @@ jobs:
# 默认不启用 voice feature(包含 whisper-rs,编译很慢)
args: --target ${{ matrix.target }} --config tauri.windows.conf.json
- name: Verify packaged ONNX runtime libraries (Windows)
if: matrix.platform == 'windows-2022'
shell: bash
run: |
set -euo pipefail
RELEASE_DIR="src-tauri/target/${{ matrix.target }}/release"
for dll in onnxruntime.dll sherpa-onnx-c-api.dll; do
if [ ! -f "$RELEASE_DIR/$dll" ]; then
echo "::error::Expected ONNX runtime DLL missing: $RELEASE_DIR/$dll"
exit 1
fi
done
HOOK_FILE="src-tauri/target/release-runtime-libs.nsh"
if [ ! -f "$HOOK_FILE" ]; then
echo "::error::Expected NSIS runtime hook missing: $HOOK_FILE"
exit 1
fi
grep -F 'onnxruntime.dll' "$HOOK_FILE"
grep -F 'sherpa-onnx-c-api.dll' "$HOOK_FILE"
- name: Stage Windows release assets
if: matrix.platform == 'windows-2022'
shell: bash
@@ -538,6 +623,7 @@ jobs:
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 32
CARGO_INCREMENTAL: 0
CARGO_TARGET_DIR: src-tauri/target
RUSTFLAGS: "-C link-arg=-Wl,-rpath,@executable_path/../Frameworks"
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: sccache
# Apple 签名环境变量
@@ -589,6 +675,7 @@ jobs:
CARGO_PROFILE_RELEASE_CODEGEN_UNITS: 32
CARGO_INCREMENTAL: 0
CARGO_TARGET_DIR: src-tauri/target
RUSTFLAGS: "-C link-arg=-Wl,-rpath,@executable_path/../Frameworks"
SCCACHE_GHA_ENABLED: "true"
RUSTC_WRAPPER: sccache
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
@@ -685,6 +772,27 @@ jobs:
fi
echo "Validating notarized app bundle: $app_bundle"
FRAMEWORKS_DIR="$app_bundle/Contents/Frameworks"
for dylib in libonnxruntime.1.24.4.dylib libonnxruntime.dylib libsherpa-onnx-c-api.dylib; do
if [ ! -f "$FRAMEWORKS_DIR/$dylib" ]; then
echo "::error::Expected ONNX runtime dylib missing: $FRAMEWORKS_DIR/$dylib"
exit 1
fi
done
MAIN_BINARY="$app_bundle/Contents/MacOS/Lime"
if [ ! -f "$MAIN_BINARY" ]; then
MAIN_BINARY="$(find "$app_bundle/Contents/MacOS" -maxdepth 1 -type f | sort | head -n 1)"
fi
if [ -z "$MAIN_BINARY" ] || [ ! -f "$MAIN_BINARY" ]; then
echo "::error::Unable to resolve macOS app main binary under $app_bundle/Contents/MacOS"
exit 1
fi
if ! otool -l "$MAIN_BINARY" | grep -F '@executable_path/../Frameworks' >/dev/null; then
echo "::error::macOS app binary is missing ONNX runtime Frameworks rpath: $MAIN_BINARY"
exit 1
fi
spctl -a -vv "$app_bundle"
xcrun stapler validate "$app_bundle"
+11 -18
View File
@@ -4227,12 +4227,6 @@ dependencies = [
"digest",
]
[[package]]
name = "hmac-sha256"
version = "1.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec9d92d097f4749b64e8cc33d924d9f40a2d4eb91402b458014b781f5733d60f"
[[package]]
name = "home"
version = "0.5.12"
@@ -5198,6 +5192,16 @@ dependencies = [
"windows-link 0.2.1",
]
[[package]]
name = "libloading"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "754ca22de805bb5744484a5b151a9e1a8e837d5dc232c2d7d8c2e3492edc8b60"
dependencies = [
"cfg-if",
"windows-link 0.2.1",
]
[[package]]
name = "libm"
version = "0.2.16"
@@ -5876,12 +5880,6 @@ version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
[[package]]
name = "lzma-rust2"
version = "0.15.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e20f57f9918e5bd7bc58c22cdd70a6afc7375d4dd9683af5f2b34bd3d2bba619"
[[package]]
name = "mac"
version = "0.1.1"
@@ -7160,11 +7158,11 @@ version = "2.0.0-rc.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7de3af33d24a745ffb8fab904b13478438d1cd52868e6f17735ef6e1f8bf133"
dependencies = [
"libloading 0.9.0",
"ndarray",
"ort-sys",
"smallvec",
"tracing",
"ureq 3.2.0",
]
[[package]]
@@ -7172,11 +7170,6 @@ name = "ort-sys"
version = "2.0.0-rc.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7b497d21a8b6fbb4b5a544f8fadb77e801a09ae0add9e411d31c6f89e3c1e90"
dependencies = [
"hmac-sha256",
"lzma-rust2",
"ureq 3.2.0",
]
[[package]]
name = "os_info"
+1 -1
View File
@@ -21,7 +21,7 @@ tokio = { version = "1", features = ["full"] }
# 本地 ONNX 嵌入
dirs = { workspace = true, optional = true }
fastembed = { version = "5.13.4", default-features = false, features = ["hf-hub-native-tls", "ort-download-binaries-native-tls"], optional = true }
fastembed = { version = "5.13.4", default-features = false, features = ["hf-hub-native-tls", "ort-load-dynamic"], optional = true }
# 错误处理
anyhow = "1"
+99
View File
@@ -27,6 +27,8 @@ const LOCAL_ONNX_HF_ENDPOINT_ENV: &str = "LIME_LOCAL_ONNX_HF_ENDPOINT";
const LOCAL_ONNX_MODEL_REPO: &str = "Qdrant/all-MiniLM-L6-v2-onnx";
#[cfg(feature = "local-onnx")]
const LOCAL_ONNX_MODEL_REVISION: &str = "main";
#[cfg(feature = "local-onnx")]
const ORT_DYLIB_PATH_ENV: &str = "ORT_DYLIB_PATH";
/// OpenAI Embedding API 请求
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -131,6 +133,54 @@ fn local_onnx_hf_endpoint() -> String {
.unwrap_or_else(|| "https://huggingface.co".to_string())
}
#[cfg(feature = "local-onnx")]
fn ort_dylib_name() -> &'static str {
if cfg!(target_os = "windows") {
"onnxruntime.dll"
} else if cfg!(target_os = "macos") {
"libonnxruntime.dylib"
} else {
"libonnxruntime.so"
}
}
#[cfg(feature = "local-onnx")]
fn resolve_packaged_ort_dylib_path_from_exe(exe_path: &Path) -> Option<PathBuf> {
let exe_dir = exe_path.parent()?;
let dylib_name = ort_dylib_name();
let next_to_exe = exe_dir.join(dylib_name);
if next_to_exe.is_file() {
return Some(next_to_exe);
}
if cfg!(target_os = "macos") {
let frameworks_dir = exe_dir.parent()?.join("Frameworks");
let bundled_framework = frameworks_dir.join(dylib_name);
if bundled_framework.is_file() {
return Some(bundled_framework);
}
}
None
}
#[cfg(feature = "local-onnx")]
fn configure_local_onnx_runtime_library_path() {
if std::env::var_os(ORT_DYLIB_PATH_ENV).is_some() {
return;
}
let Some(path) = std::env::current_exe()
.ok()
.and_then(|exe_path| resolve_packaged_ort_dylib_path_from_exe(&exe_path))
else {
return;
};
std::env::set_var(ORT_DYLIB_PATH_ENV, path);
}
#[cfg(feature = "local-onnx")]
fn local_onnx_manual_cache_path(cache_dir: &Path, file: &str) -> PathBuf {
let mut path = cache_dir
@@ -215,6 +265,8 @@ fn build_local_onnx_model_from_manual_cache(
#[cfg(feature = "local-onnx")]
fn build_local_onnx_model(spec: &LocalOnnxModelSpec) -> Result<TextEmbedding, String> {
configure_local_onnx_runtime_library_path();
let cache_dir = local_onnx_cache_dir();
std::fs::create_dir_all(&cache_dir)
.map_err(|e| format!("创建本地 ONNX 嵌入模型缓存目录失败: {e}"))?;
@@ -564,6 +616,53 @@ mod tests {
assert!(error.contains("不支持的本地 ONNX 嵌入模型"));
}
#[cfg(feature = "local-onnx")]
#[test]
fn test_resolve_packaged_ort_dylib_path_from_exe() {
let root = std::env::temp_dir().join(format!(
"lime-embedding-ort-path-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
let exe_dir = if cfg!(target_os = "macos") {
root.join("Contents").join("MacOS")
} else {
root.clone()
};
std::fs::create_dir_all(&exe_dir).unwrap();
let exe_path = exe_dir.join(if cfg!(target_os = "windows") {
"Lime.exe"
} else {
"lime"
});
let next_to_exe = exe_dir.join(ort_dylib_name());
std::fs::write(&next_to_exe, []).unwrap();
assert_eq!(
resolve_packaged_ort_dylib_path_from_exe(&exe_path),
Some(next_to_exe.clone())
);
std::fs::remove_file(&next_to_exe).unwrap();
if cfg!(target_os = "macos") {
let frameworks_dir = root.join("Contents").join("Frameworks");
std::fs::create_dir_all(&frameworks_dir).unwrap();
let framework_dylib = frameworks_dir.join(ort_dylib_name());
std::fs::write(&framework_dylib, []).unwrap();
assert_eq!(
resolve_packaged_ort_dylib_path_from_exe(&exe_path),
Some(framework_dylib)
);
} else {
assert_eq!(resolve_packaged_ort_dylib_path_from_exe(&exe_path), None);
}
let _ = std::fs::remove_dir_all(root);
}
#[cfg(feature = "local-onnx")]
#[tokio::test]
#[ignore = "真实模型下载和本地 ONNX 推理测试:设置 LIME_REAL_API_TEST=1 后执行"]
+1 -1
View File
@@ -21,7 +21,7 @@ cpal = "0.15"
whisper-rs = { version = "0.12", optional = true }
# SenseVoice 本地识别(可选,链接 sherpa-onnx 运行时,不内置模型文件)
sherpa-onnx = { version = "1.13", default-features = false, features = ["static"], optional = true }
sherpa-onnx = { version = "1.13", default-features = false, features = ["shared"], optional = true }
# WAV 处理
hound = "3.5"