Bundle native inference runtimes in releases.
Some checks failed
Tagged release / prepare-release (push) Successful in 2m53s
Tagged release / build-linux-x64 (push) Failing after 6s
Tagged release / build-macos (push) Has been skipped
Tagged release / build-linux-arm64 (push) Has been skipped
Tagged release / build-windows (push) Has been skipped
Tagged release / publish-release (push) Has been skipped

This commit is contained in:
Hermes Agent
2026-08-14 14:20:18 +00:00
parent f924830176
commit 0e697750e7
10 changed files with 210 additions and 10 deletions

View File

@@ -43,7 +43,7 @@ jobs:
run: cargo +${RUST_TOOLCHAIN} run --locked --package bds-release -- prepare
build-macos:
needs: prepare-release
needs: [prepare-release, build-linux-x64]
runs-on: linux-arm64
container: ghcr.io/rust-cross/cargo-zigbuild@sha256:82af75c41958c2af2787e8bedd912da7678a9438937e223e9d83d006d747b38b
env:
@@ -96,7 +96,7 @@ jobs:
done
build-linux-arm64:
needs: prepare-release
needs: [prepare-release, build-linux-x64]
runs-on: linux-arm64
env:
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS: -C link-arg=-Wl,-rpath,$ORIGIN
@@ -174,11 +174,11 @@ jobs:
"$GITEA_REF_NAME" "target/$target/release" "$rust_lib_dir" linux-x64 dist --upload
build-windows:
needs: prepare-release
needs: [prepare-release, build-linux-x64]
runs-on: linux-arm64
container: messense/cargo-xwin@sha256:4696dd4e79edf8569fa99c4b06bd99273e0501c7adc983aa61d57945f795bef0
env:
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS: -C target-feature=+crt-static
CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_RUSTFLAGS: -Lnative=target/x86_64-pc-windows-msvc/release
steps:
- name: Check out the tag
env:
@@ -211,6 +211,10 @@ jobs:
run: |
set -eu
target=x86_64-pc-windows-msvc
cargo +${RUST_TOOLCHAIN} xwin cache xwin
cargo +${RUST_TOOLCHAIN} run --locked --package bds-release -- \
windows-runtime "target/$target/release" \
/root/.cache/cargo-xwin/xwin/sdk/lib/um/x86_64
cargo +${RUST_TOOLCHAIN} xwin build --release --locked --target "$target" \
--package bds-ui --package bds-cli --package bds-mcp
rust_lib_dir=$(rustc +${RUST_TOOLCHAIN} --target "$target" --print target-libdir)

View File

@@ -24,7 +24,7 @@ Windows packaging requires the MSVC Rust toolchain, Visual Studio Build Tools wi
Pushing a tag runs `.gitea/workflows/release.yml` on Linux arm64. The workflow builds Linux arm64 natively, cross-links Linux x64 with Ubuntu's amd64 development libraries and GNU cross-toolchain, uses Cargo Zigbuild for macOS, Cargo Xwin for Windows x64, and `bds-release` for every packaging and Gitea API operation. It does not require host installations of Python, `curl`, `tar`, `hdiutil`, or NSIS.
The workflow publishes CLI/MCP and desktop artifacts for Linux and macOS on arm64 and x64, plus Windows x64. Linux desktop binaries dynamically use the target system's GTK 3, WebKitGTK 4.1, and libxdo runtime packages. macOS desktop artifacts are ad-hoc-signed `.dmg` files, Unix artifacts are `.tar.gz`, and Windows artifacts are `.zip` files containing the native `.exe` application and its runtime DLLs. The Gitea release remains a draft unless every build and upload succeeds.
The workflow publishes CLI/MCP and desktop artifacts for Linux and macOS on arm64 and x64, plus Windows x64. Linux desktop binaries dynamically use the target system's GTK 3, WebKitGTK 4.1, and libxdo runtime packages. Every archive includes its downloaded ONNX Runtime shared library; Windows archives also include the checksum-pinned Microsoft DirectML redistributable and license. macOS desktop artifacts are ad-hoc-signed `.dmg` files, Unix artifacts are `.tar.gz`, and Windows artifacts are `.zip` files containing the native `.exe` application and its runtime DLLs. The Gitea release remains a draft unless every build and upload succeeds.
## macOS system requirements

1
Cargo.lock generated
View File

@@ -1213,6 +1213,7 @@ dependencies = [
"reqwest",
"serde",
"serde_json",
"sha2 0.10.9",
"simple-file-manifest",
"tar",
"tempfile",

View File

@@ -62,7 +62,7 @@ mlua = { version = "0.12", features = ["lua54", "vendored", "serialize"] }
url = "2.5"
fastembed = { version = "5.17.3", default-features = false, features = ["hf-hub-rustls-tls", "ort-download-binaries-rustls-tls"] }
usearch = { version = "2.26", default-features = false }
ort = { version = "=2.0.0-rc.12", default-features = false, features = ["std", "api-24", "coreml", "directml"] }
ort = { version = "=2.0.0-rc.12", default-features = false, features = ["std", "api-24", "copy-dylibs"] }
russh = "0.62.5"
ratatui = "0.30.2"
ratatui-image = { version = "11.0.6", default-features = false, features = ["crossterm", "image-defaults"] }

View File

@@ -35,7 +35,7 @@ RuDS uses no JavaScript application runtime and loads no CSS or JavaScript from
The packaged Apple Silicon application requires macOS 26 or newer.
Packaged executables share native `bds-core` and `bds-server` dynamic libraries. ONNX Runtime is statically contained in `bds-core`; the package does not ship or download a separate ONNX library. The **Install CLI** action replaces `~/.local/bin/bds-cli` with a symbolic link to the packaged CLI, so the command executes beside the same runtime libraries and always follows the package selected by the latest installation.
Packaged executables share native `bds-core` and `bds-server` dynamic libraries. Release archives also carry the matching ONNX Runtime library; Windows archives include Microsoft's DirectML redistributable and its license for GPU-accelerated local embeddings. The **Install CLI** action replaces `~/.local/bin/bds-cli` with a symbolic link to the packaged CLI, so the command executes beside the same runtime libraries and always follows the package selected by the latest installation.
Local macOS packages are ad-hoc signed without hardened runtime. A Developer ID and notarization remain optional release-channel steps for downloads that should pass Gatekeeper without a user override.

View File

@@ -48,12 +48,14 @@ fastembed = { workspace = true }
ort = { workspace = true }
[target.'cfg(target_os = "macos")'.dependencies]
ort = { workspace = true, features = ["coreml"] }
usearch = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies]
# libwebp's MSVC autodetection enables SSE 4.1 sources, so clang-cl must
# compile them with the matching target feature during cargo-xwin builds.
libwebp-sys = { workspace = true, features = ["sse41"] }
ort = { workspace = true, features = ["directml"] }
[target.'cfg(not(target_os = "macos"))'.dependencies]
usearch = { workspace = true, features = ["numkong"] }

View File

@@ -16,6 +16,7 @@ plist.workspace = true
reqwest.workspace = true
serde.workspace = true
serde_json.workspace = true
sha2.workspace = true
simple-file-manifest.workspace = true
tar.workspace = true
zip.workspace = true

View File

@@ -11,6 +11,9 @@ fn main() -> Result<(), Box<dyn Error>> {
[command, directory] if command == "macos-linker-alias" => {
packaging::prepare_macos_linker_alias(Path::new(directory))
}
[command, directory, sdk_libraries] if command == "windows-runtime" => {
packaging::prepare_windows_runtime(Path::new(directory), Path::new(sdk_libraries))
}
[command, paths @ ..] if command == "upload" && !paths.is_empty() => {
gitea::upload(paths.iter().map(Path::new))
}
@@ -39,7 +42,7 @@ fn main() -> Result<(), Box<dyn Error>> {
gitea::upload(assets.iter().map(|path| path.as_path()))
}
_ => Err(
"usage: bds-release prepare|publish|macos-linker-alias <directory>|upload <asset>...|package <tag> <release-dir> <rust-lib-dir> <platform> <dist-dir> [--upload]"
"usage: bds-release prepare|publish|macos-linker-alias <directory>|windows-runtime <directory> <sdk-libraries>|upload <asset>...|package <tag> <release-dir> <rust-lib-dir> <platform> <dist-dir> [--upload]"
.into(),
),
}

View File

@@ -3,12 +3,13 @@ use apple_codesign::{SigningSettings, UnifiedSigner};
use flate2::{Compression, write::GzEncoder};
use icns::{IconFamily, Image};
use plist::{Dictionary, Value};
use sha2::{Digest, Sha256};
use simple_file_manifest::FileEntry;
use std::{
error::Error,
ffi::OsStr,
fs::{self, File},
io::{self, BufReader},
io::{self, BufReader, Read, Seek},
path::{Path, PathBuf},
};
use zip::{CompressionMethod, ZipWriter, write::SimpleFileOptions};
@@ -23,6 +24,8 @@ const LC_LOAD_WEAK_DYLIB: u32 = 0x8000_0018;
const LC_REEXPORT_DYLIB: u32 = 0x8000_001f;
const LC_LAZY_LOAD_DYLIB: u32 = 0x20;
const LC_LOAD_UPWARD_DYLIB: u32 = 0x8000_0023;
const DIRECTML_VERSION: &str = "1.15.4";
const DIRECTML_SHA256: &str = "4e7cb7ddce8cf837a7a75dc029209b520ca0101470fcdf275c1f49736a3615b9";
pub(crate) fn prepare_macos_linker_alias(directory: &Path) -> Result<(), Box<dyn Error>> {
fs::create_dir_all(directory)?;
@@ -30,6 +33,84 @@ pub(crate) fn prepare_macos_linker_alias(directory: &Path) -> Result<(), Box<dyn
Ok(())
}
pub(crate) fn prepare_windows_runtime(
directory: &Path,
sdk_libraries: &Path,
) -> Result<(), Box<dyn Error>> {
fs::create_dir_all(directory)?;
let package_path = directory.join(format!("microsoft.ai.directml.{DIRECTML_VERSION}.nupkg"));
let result = (|| {
let url = format!(
"https://api.nuget.org/v3-flatcontainer/microsoft.ai.directml/{DIRECTML_VERSION}/microsoft.ai.directml.{DIRECTML_VERSION}.nupkg"
);
let mut response = reqwest::blocking::get(url)?.error_for_status()?;
let mut package = File::create(&package_path)?;
io::copy(&mut response, &mut package)?;
drop(package);
let mut package = BufReader::new(File::open(&package_path)?);
let mut digest = Sha256::new();
let mut buffer = [0_u8; 64 * 1024];
loop {
let read = package.read(&mut buffer)?;
if read == 0 {
break;
}
digest.update(&buffer[..read]);
}
let digest = digest.finalize();
if format!("{digest:x}") != DIRECTML_SHA256 {
return Err("DirectML package checksum mismatch".into());
}
prepare_windows_runtime_from_archive(File::open(&package_path)?, directory)?;
prepare_windows_sdk_linker_aliases(sdk_libraries, directory)
})();
if package_path.try_exists()? {
fs::remove_file(package_path)?;
}
result
}
fn prepare_windows_sdk_linker_aliases(
sdk_libraries: &Path,
directory: &Path,
) -> Result<(), Box<dyn Error>> {
let source = fs::read_dir(sdk_libraries)?
.filter_map(Result::ok)
.find(|entry| {
entry
.file_name()
.to_str()
.is_some_and(|name| name.eq_ignore_ascii_case("pathcch.lib"))
})
.ok_or_else(|| {
format!(
"PathCch.lib is missing from Windows SDK directory: {}",
sdk_libraries.display()
)
})?
.path();
fs::copy(source, directory.join("PathCch.lib"))?;
Ok(())
}
fn prepare_windows_runtime_from_archive(
archive: impl Read + Seek,
directory: &Path,
) -> Result<(), Box<dyn Error>> {
fs::create_dir_all(directory)?;
let mut archive = zip::ZipArchive::new(archive)?;
for (source, destination) in [
("bin/x64-win/DirectML.lib", "DirectML.lib"),
("bin/x64-win/DirectML.dll", "DirectML.dll"),
("LICENSE.txt", "DirectML-LICENSE.txt"),
] {
let mut source = archive.by_name(source)?;
io::copy(&mut source, &mut File::create(directory.join(destination))?)?;
}
Ok(())
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum Platform {
MacOs,
@@ -177,6 +258,37 @@ fn runtime_files(
.to_owned();
files.push((path, name));
}
let mut external_names = match platform {
Platform::MacOs => fs::read_dir(release_dir)?
.filter_map(Result::ok)
.map(|entry| entry.file_name())
.filter_map(|name| name.into_string().ok())
.filter(|name| name.starts_with("libonnxruntime") && name.contains(".dylib"))
.collect::<Vec<_>>(),
Platform::Linux => fs::read_dir(release_dir)?
.filter_map(Result::ok)
.map(|entry| entry.file_name())
.filter_map(|name| name.into_string().ok())
.filter(|name| name.starts_with("libonnxruntime.so"))
.collect::<Vec<_>>(),
Platform::Windows => ["onnxruntime.dll", "DirectML.dll", "DirectML-LICENSE.txt"]
.into_iter()
.map(str::to_owned)
.collect(),
};
external_names.sort();
if external_names.is_empty() {
return Err(format!(
"ONNX Runtime is missing from release directory: {}",
release_dir.display()
)
.into());
}
for name in external_names {
let path = release_dir.join(&name);
require_file(&path)?;
files.push((path, name));
}
Ok(files)
}
@@ -443,7 +555,7 @@ fn dmg_sectors(bundle_bytes: u64) -> Result<u32, Box<dyn Error>> {
#[cfg(test)]
mod tests {
use super::*;
use std::io::Read;
use std::io::{Read, Write};
use tempfile::tempdir;
#[test]
@@ -477,6 +589,50 @@ mod tests {
);
}
#[test]
fn windows_runtime_extracts_only_redistributable_files() {
let temp = tempdir().unwrap();
let sdk = temp.path().join("sdk");
let runtime = temp.path().join("runtime");
fs::create_dir(&sdk).unwrap();
fs::create_dir(&runtime).unwrap();
fs::write(sdk.join("pathcch.lib"), b"sdk-import-library").unwrap();
let archive = std::io::Cursor::new(Vec::new());
let mut archive = ZipWriter::new(archive);
let options = SimpleFileOptions::default();
for (name, contents) in [
("bin/x64-win/DirectML.lib", b"import-library".as_slice()),
("bin/x64-win/DirectML.dll", b"runtime".as_slice()),
("LICENSE.txt", b"license".as_slice()),
(
"bin/arm64-win/DirectML.dll",
b"wrong-architecture".as_slice(),
),
] {
archive.start_file(name, options).unwrap();
archive.write_all(contents).unwrap();
}
let archive = archive.finish().unwrap();
prepare_windows_runtime_from_archive(archive, &runtime).unwrap();
prepare_windows_sdk_linker_aliases(&sdk, &runtime).unwrap();
assert_eq!(
fs::read(runtime.join("DirectML.lib")).unwrap(),
b"import-library"
);
assert_eq!(fs::read(runtime.join("DirectML.dll")).unwrap(), b"runtime");
assert_eq!(
fs::read(runtime.join("DirectML-LICENSE.txt")).unwrap(),
b"license"
);
assert_eq!(
fs::read(runtime.join("PathCch.lib")).unwrap(),
b"sdk-import-library"
);
assert_eq!(fs::read_dir(runtime).unwrap().count(), 4);
}
#[test]
fn windows_package_contains_the_app_without_dynamic_std() {
let temp = tempdir().unwrap();
@@ -491,6 +647,9 @@ mod tests {
"bds-mcp.exe",
"bds_core.dll",
"bds_server.dll",
"onnxruntime.dll",
"DirectML.dll",
"DirectML-LICENSE.txt",
] {
fs::write(release.join(file), format!("MZ-{file}")).unwrap();
}
@@ -511,6 +670,14 @@ mod tests {
.by_name("bds-desktop-v1.2.3-windows-x64/bds_core.dll")
.is_ok()
);
for runtime in ["onnxruntime.dll", "DirectML.dll", "DirectML-LICENSE.txt"] {
assert!(
archive
.by_name(&format!("bds-desktop-v1.2.3-windows-x64/{runtime}"))
.is_ok(),
"missing {runtime}"
);
}
}
#[test]
@@ -527,6 +694,7 @@ mod tests {
"bds-mcp",
"libbds_core.so",
"libbds_server.so",
"libonnxruntime.so.1",
] {
fs::write(release.join(file), file).unwrap();
}
@@ -549,6 +717,11 @@ mod tests {
.unwrap()
.map(|entry| entry.unwrap().path().unwrap().into_owned())
.collect::<Vec<_>>();
assert!(
names
.iter()
.any(|name| name.ends_with("libonnxruntime.so.1"))
);
assert!(names.iter().any(|name| name.ends_with("bds-ui")));
assert!(names.iter().any(|name| name.ends_with("libbds_core.so")));
}

View File

@@ -60,6 +60,7 @@ fn desktop_packages_have_native_icons_and_cargo_commands() {
assert!(workspace_manifest.contains("Georg Bauer <gb@rfc1437.de>"));
assert!(workspace_manifest.contains("strip = \"symbols\""));
assert!(workspace_manifest.contains("ort-download-binaries-rustls-tls"));
assert!(workspace_manifest.contains("\"copy-dylibs\""));
assert!(workspace_manifest.contains("\"sync-secret-service\", \"vendored\""));
assert!(
workspace_manifest
@@ -134,6 +135,10 @@ fn tagged_releases_are_built_and_packaged_with_rust_tools() {
"aarch64-unknown-linux-gnu",
"x86_64-unknown-linux-gnu",
"cargo-xwin",
"windows-runtime",
"cargo +${RUST_TOOLCHAIN} xwin cache xwin",
"/root/.cache/cargo-xwin/xwin/sdk/lib/um/x86_64",
"-Lnative=target/x86_64-pc-windows-msvc/release",
"apt-get install --no-install-recommends --yes nasm",
"x86_64-pc-windows-msvc",
"bds-release",
@@ -155,6 +160,10 @@ fn tagged_releases_are_built_and_packaged_with_rust_tools() {
"release workflow must not use host tool {forbidden}"
);
}
assert!(
!workflow.contains("target-feature=+crt-static"),
"Windows and downloaded ONNX Runtime must use the same dynamic CRT"
);
assert!(
!workflow.contains("run --release --locked --package bds-release"),
"the release helper itself must stay unoptimized so cold runner jobs remain practical"
@@ -166,6 +175,13 @@ fn tagged_releases_are_built_and_packaged_with_rust_tools() {
4,
"every release job using the Ubuntu runner image must bootstrap Rust"
);
assert_eq!(
workflow
.matches("needs: [prepare-release, build-linux-x64]")
.count(),
3,
"the expensive platform builds must wait for the Linux x64 cross-build smoke test"
);
let linux_dependencies =
fs::read_to_string(workspace.join(".gitea/scripts/install-linux-build-dependencies.sh"))