Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions .github/workflows/release-tauri.yml
Original file line number Diff line number Diff line change
Expand Up @@ -270,27 +270,37 @@ jobs:
# Pass 2:MSI 独立跑——允许失败,Repair 步骤兜底 light.exe 重链。
# Beta 版跳过 MSI:Windows Installer / WiX 无法表示 `1.2.3-Beta.1`
# 这类非纯数字 prerelease 标识,稳定版仍然照常产出 MSI。
# workflow_dispatch 走分支 ref 时 OPENLESS_RELEASE_CHANNEL 仍是 stable,
# 但 tauri.conf.json 里如果是 Beta 版本号,也必须跳过 MSI。
app_version=$(node -p "require('./src-tauri/tauri.conf.json').version")
skip_msi=0
if [ "${OPENLESS_RELEASE_CHANNEL:-stable}" = "beta" ]; then
skip_msi=1
echo "[info] Skipping MSI bundle for beta release channel."
elif echo "$app_version" | grep -qiE -- '-(beta|alpha|rc)\.'; then
skip_msi=1
echo "[info] Skipping MSI bundle: app version $app_version has non-numeric prerelease (WiX incompatible)."
fi
if [ "$skip_msi" -eq 1 ]; then
echo "OPENLESS_SKIP_WINDOWS_MSI=1" >> "$GITHUB_ENV"
else
echo "OPENLESS_SKIP_WINDOWS_MSI=0" >> "$GITHUB_ENV"
fi
if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ]; then
npm run tauri -- build --bundles nsis --config '{"bundle":{"createUpdaterArtifacts":true}}'
nsis_exit=$?
if [ "${OPENLESS_RELEASE_CHANNEL:-stable}" = "beta" ]; then
echo "OPENLESS_SKIP_WINDOWS_MSI=1" >> "$GITHUB_ENV"
echo "[info] Skipping MSI bundle for beta release channel."
if [ "$skip_msi" -eq 1 ]; then
msi_exit=0
else
echo "OPENLESS_SKIP_WINDOWS_MSI=0" >> "$GITHUB_ENV"
npm run tauri -- build --bundles msi --config '{"bundle":{"createUpdaterArtifacts":true}}'
msi_exit=$?
fi
else
npm run tauri -- build --bundles nsis
nsis_exit=$?
if [ "${OPENLESS_RELEASE_CHANNEL:-stable}" = "beta" ]; then
echo "OPENLESS_SKIP_WINDOWS_MSI=1" >> "$GITHUB_ENV"
echo "[info] Skipping MSI bundle for beta release channel."
if [ "$skip_msi" -eq 1 ]; then
msi_exit=0
else
echo "OPENLESS_SKIP_WINDOWS_MSI=0" >> "$GITHUB_ENV"
npm run tauri -- build --bundles msi
msi_exit=$?
fi
Expand Down
22 changes: 16 additions & 6 deletions openless-all/app/scripts/build-mac.sh
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,15 @@ codesign --verify --deep --strict --verbose=2 "$APP" 2>&1 | tail -2

if [ "$MAC_BUNDLE_ARCH" = "aarch64" ]; then
echo "▶ 校验 MLX metallib 已进入 app / DMG / updater"
APP_METALLIB="$APP/Contents/MacOS/mlx.metallib"
APP_METALLIB="$APP/Contents/Resources/mlx.metallib"
if [ ! -s "$APP_METALLIB" ]; then
echo "✗ Apple Silicon app 缺少 Contents/MacOS/mlx.metallib"
echo "✗ Apple Silicon app 缺少 Contents/Resources/mlx.metallib"
exit 1
fi
# Contents/MacOS 里的非 Mach-O 会被 codesign 当成 nested code。
# 克隆仓 ad-hoc 签名(identity "-")会因此直接 bundle 失败。
if [ -e "$APP/Contents/MacOS/mlx.metallib" ]; then
echo "✗ mlx.metallib 不能放在 Contents/MacOS(ad-hoc codesign 会失败)"
exit 1
fi
APP_METALLIB_SHA="$(shasum -a 256 "$APP_METALLIB" | awk '{print $1}')"
Expand All @@ -84,9 +90,13 @@ if [ "$MAC_BUNDLE_ARCH" = "aarch64" ]; then
}
trap cleanup_dmg_mount EXIT
hdiutil attach "$DMG_PATH" -readonly -nobrowse -mountpoint "$DMG_MOUNT" >/dev/null
DMG_METALLIB="$DMG_MOUNT/OpenLess.app/Contents/MacOS/mlx.metallib"
DMG_METALLIB="$DMG_MOUNT/OpenLess.app/Contents/Resources/mlx.metallib"
if [ ! -s "$DMG_METALLIB" ]; then
echo "✗ DMG 中缺少 OpenLess.app/Contents/MacOS/mlx.metallib"
echo "✗ DMG 中缺少 OpenLess.app/Contents/Resources/mlx.metallib"
exit 1
fi
if [ -e "$DMG_MOUNT/OpenLess.app/Contents/MacOS/mlx.metallib" ]; then
echo "✗ DMG 中的 mlx.metallib 不能放在 Contents/MacOS"
exit 1
fi
DMG_METALLIB_SHA="$(shasum -a 256 "$DMG_METALLIB" | awk '{print $1}')"
Expand All @@ -104,14 +114,14 @@ if [ "$MAC_BUNDLE_ARCH" = "aarch64" ]; then
exit 1
fi
UPDATER_METALLIB_SHA="$(tar -xOf "$UPDATER_ARCHIVE" \
OpenLess.app/Contents/MacOS/mlx.metallib | shasum -a 256 | awk '{print $1}')"
OpenLess.app/Contents/Resources/mlx.metallib | shasum -a 256 | awk '{print $1}')"
if [ "$UPDATER_METALLIB_SHA" != "$APP_METALLIB_SHA" ]; then
echo "✗ app 与 updater 中的 mlx.metallib SHA-256 不一致"
exit 1
fi
fi
echo "✓ MLX metallib sha256=$APP_METALLIB_SHA"
elif [ -e "$APP/Contents/MacOS/mlx.metallib" ]; then
elif [ -e "$APP/Contents/MacOS/mlx.metallib" ] || [ -e "$APP/Contents/Resources/mlx.metallib" ]; then
echo "✗ Intel app 不应包含 Apple Silicon MLX metallib"
exit 1
fi
Expand Down
5 changes: 4 additions & 1 deletion openless-all/app/scripts/macos-mlx-bundle-contract.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@ assert.equal(
"node scripts/stage-macos-mlx-metallib.mjs",
)
assert.equal(
overlay.bundle.macOS.files["MacOS/mlx.metallib"],
overlay.bundle.macOS.files["Resources/mlx.metallib"],
"target/release/openless-mlx/mlx.metallib",
)
assert.equal(overlay.bundle.macOS.files["MacOS/mlx.metallib"], undefined)
assert.match(buildScript, /arm64\)[\s\S]*tauri\.macos-mlx\.conf\.json/)
assert.doesNotMatch(buildScript, /--bundles app/)
assert.doesNotMatch(buildScript, /codesign --force/)
assert.doesNotMatch(buildScript, /hdiutil create/)
assert.match(buildScript, /OpenLess\.app\.tar\.gz/)
assert.match(buildScript, /stapler validate/)
assert.match(buildScript, /Contents\/Resources\/mlx\.metallib/)
assert.match(buildScript, /Contents\/MacOS\/mlx\.metallib/)

console.log("macOS MLX bundle contract tests passed")
16 changes: 16 additions & 0 deletions openless-all/app/src-tauri/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ fn main() {

if target_os == "macos" {
link_macos_compiler_runtime();
let target_arch = std::env::var("CARGO_CFG_TARGET_ARCH").unwrap_or_default();
if target_arch == "aarch64" {
compile_mlx_metallib_path_shim();
}
}

if target_os == "android" {
Expand Down Expand Up @@ -61,6 +65,18 @@ fn link_macos_compiler_runtime() {
println!("cargo:rustc-link-lib=static=clang_rt.osx");
}

/// Apple Silicon 发布包把 mlx.metallib 放在 Contents/Resources。
/// mlx-c 默认只在可执行文件旁边找,这里补一个 C 入口去调用 set_metallib_path。
fn compile_mlx_metallib_path_shim() {
const SOURCE: &str = "src/asr/local/mlx_set_metallib_path.cpp";
println!("cargo:rerun-if-changed={SOURCE}");
cc::Build::new()
.cpp(true)
.std("c++17")
.file(SOURCE)
.compile("openless_mlx_set_metallib_path");
}

/// cpal → oboe → oboe-sys 会编译 C++;最终 cdylib 需显式链接 NDK libc++。
fn link_android_cpp_runtime() {
// oboe-ext 已部分静态链入 libc++;补链 c++abi 提供 __cxa_pure_virtual 等 ABI 符号。
Expand Down
20 changes: 20 additions & 0 deletions openless-all/app/src-tauri/src/asr/local/mlx_set_metallib_path.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// MLX worker 启动时把 bundled metallib 路径交给 libmlx。
// mlx-c 没暴露 set_metallib_path,这里直接调 C++ API。

#include <string>

namespace mlx {
namespace core {
namespace metal {
void set_metallib_path(const std::string& path);
}
}
}

extern "C" int openless_mlx_set_metallib_path(const char* path) {
if (path == nullptr || path[0] == '\0') {
return 1;
}
mlx::core::metal::set_metallib_path(std::string(path));
return 0;
}
52 changes: 52 additions & 0 deletions openless-all/app/src-tauri/src/asr/local/mlx_worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1031,7 +1031,44 @@ pub(crate) fn run_if_requested() {
}
}

fn bundled_mlx_metallib_from_exe(exe: &Path) -> Option<PathBuf> {
let macos_dir = exe.parent()?;
if macos_dir.file_name().is_none_or(|name| name != "MacOS") {
return None;
}
Some(macos_dir.parent()?.join("Resources").join("mlx.metallib"))
}

fn apply_bundled_mlx_metallib_path() {
let Ok(exe) = std::env::current_exe() else {
return;
};
let Some(metallib) = bundled_mlx_metallib_from_exe(&exe) else {
return;
};
if !metallib.is_file() {
return;
}
let Some(path) = metallib.to_str() else {
return;
};
let Ok(c_path) = std::ffi::CString::new(path) else {
return;
};
extern "C" {
fn openless_mlx_set_metallib_path(path: *const std::os::raw::c_char) -> i32;
}
let status = unsafe { openless_mlx_set_metallib_path(c_path.as_ptr()) };
if status != 0 {
eprintln!(
"failed to set MLX metallib path {}: status={status}",
metallib.display()
);
}
}

fn run_worker(socket_path: &Path) -> Result<()> {
apply_bundled_mlx_metallib_path();
let stream = UnixStream::connect(socket_path)
.with_context(|| format!("connect MLX worker socket: {}", socket_path.display()))?;
let session_dir = socket_path
Expand Down Expand Up @@ -1242,6 +1279,21 @@ mod tests {
.unwrap()
}

#[test]
fn bundled_metallib_uses_contents_resources_not_macos() {
let exe = Path::new("/Applications/OpenLess.app/Contents/MacOS/openless");
assert_eq!(
bundled_mlx_metallib_from_exe(exe).as_deref(),
Some(Path::new(
"/Applications/OpenLess.app/Contents/Resources/mlx.metallib"
))
);
assert_eq!(
bundled_mlx_metallib_from_exe(Path::new("/target/release/openless")),
None
);
}

#[test]
fn frame_round_trip_preserves_request() {
let (mut sender, mut receiver) = UnixStream::pair().unwrap();
Expand Down
Loading
Loading