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
5 changes: 4 additions & 1 deletion .github/workflows/build-wheels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ jobs:
run: |
python -m pip install --upgrade pip
python -m pip install build setuptools wheel "cmake>=3.27" "nanobind==2.13.0" "mlx==0.32.0"
# This job is not the Fusion DMG path. The signed macOS 26 DMG must
# use the guarded cp311 pair (docs/packaging-guarded-mlx.md) and
# must never package 0.32.2.
export CMAKE_ARGS="-DPython_EXECUTABLE=$(python -c 'import sys; print(sys.executable)')"
python -m build --wheel --no-isolation

Expand All @@ -61,7 +64,7 @@ jobs:
/tmp/wheel-smoke/bin/python - <<'PY'
import importlib
failed = []
for pkg in ("bonsai", "glm_moe_dsa", "minimax_m3", "qwen35_prefill"):
for pkg in ("bonsai", "decode_fast", "glm_moe_dsa", "minimax_m3", "qwen35_prefill"):
fast = importlib.import_module(f"omlx.custom_kernels.{pkg}.fast")
ok = bool(fast.is_native_available())
print(f"{pkg}: native_available={ok} import_error={fast.import_error()}")
Expand Down
49 changes: 48 additions & 1 deletion .github/workflows/release-macos-dmg.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ on:
workflow_dispatch:
inputs:
tag:
description: Existing version tag to build (for example, v0.6.3)
description: Existing version tag to build (for example, v0.6.4b1)
required: true
type: string

Expand All @@ -19,9 +19,20 @@ concurrency:
jobs:
build-unsigned-app:
name: Build unsigned arm64 app
# GitHub-hosted macos-26 only. Do not retarget to a self-hosted Mac that
# is the live Fusion cluster coordinator (Studio serving DS4).
runs-on: macos-26
timeout-minutes: 120
steps:
- name: Refuse self-hosted cluster coordinator
run: |
set -euo pipefail
if [ "${RUNNER_ENVIRONMENT:-}" != "github-hosted" ]; then
echo "this job must run on GitHub-hosted macos-26, not a self-hosted Mac" >&2
echo "the live Fusion Studio / cluster coordinator serving DS4 is off-limits" >&2
exit 1
fi

- name: Check out the release tag
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
Expand Down Expand Up @@ -64,9 +75,25 @@ jobs:
"venvstacks==0.7.0" "cmake==4.4.2" "nanobind==2.13.0" \
"setuptools==80.10.2" "wheel==0.47.0"

- name: Require guarded cp311 MLX wheels
run: |
set -euo pipefail
python -c "
import importlib.util
spec = importlib.util.spec_from_file_location(
'omlx_packaging_build', 'packaging/build.py'
)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
module.require_guarded_mlx_wheels()
print('guarded mlx wheels present')
"

- name: Build staged app with native kernels
env:
OMLX_RELEASE_REPOSITORY: ${{ github.repository }}
MACOSX_DEPLOYMENT_TARGET: "26.0"
OMLX_CUSTOM_KERNEL_DEPLOYMENT_TARGET: "26.0"
run: apps/omlx-mac/Scripts/build.sh release --with-custom-kernel --rebuild-donor

- name: Confirm arm64 application output
Expand All @@ -77,6 +104,16 @@ jobs:
test "$(lipo -archs "$app/Contents/MacOS/oMLX")" = arm64
actual_repo=$(/usr/libexec/PlistBuddy -c 'Print :OMLXReleaseRepository' "$app/Contents/Info.plist")
test "$actual_repo" = "${{ github.repository }}"
min_os=$(/usr/libexec/PlistBuddy -c 'Print :LSMinimumSystemVersion' "$app/Contents/Info.plist")
[[ "$min_os" == 26* ]] || {
echo "expected macOS 26-only LSMinimumSystemVersion, got $min_os" >&2
exit 1
}
py="$app/Contents/Resources/Python/cpython-3.11/bin/python3"
site="$app/Contents/Resources/Python/framework-mlx-base/lib/python3.11/site-packages"
mlx_ver=$(PYTHONPATH="$site" "$py" -c 'import mlx; print(mlx.__version__)')
test "$mlx_ver" = "0.32.1.dev20260825+26421e953"
case "$mlx_ver" in *0.32.2*) echo "refusing mlx 0.32.2" >&2; exit 1 ;; esac

- name: Archive app without losing symlinks or extended attributes
run: |
Expand All @@ -95,12 +132,22 @@ jobs:
sign-notarize-release:
name: Sign, notarize, and draft release
needs: build-unsigned-app
# GitHub-hosted macos-26 only. Do not retarget to the live Fusion coordinator.
runs-on: macos-26
timeout-minutes: 180
environment: macos-release
permissions:
contents: write
steps:
- name: Refuse self-hosted cluster coordinator
run: |
set -euo pipefail
if [ "${RUNNER_ENVIRONMENT:-}" != "github-hosted" ]; then
echo "this job must run on GitHub-hosted macos-26, not a self-hosted Mac" >&2
echo "the live Fusion Studio / cluster coordinator serving DS4 is off-limits" >&2
exit 1
fi

# Release tooling and entitlements come from the protected default
# branch, while the untrusted/tagged app payload comes from the prior
# secretless job.
Expand Down
8 changes: 6 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,12 @@ temp/
# MLX cache
.mlx_cache/

# Build artifacts
packaging/_wheels/
# Build artifacts. Guarded mlx wheel *bytes* stay untracked; the README
# that documents the cp311 pin is tracked.
packaging/_wheels/*
!packaging/_wheels/guarded/
packaging/_wheels/guarded/*
!packaging/_wheels/guarded/README.md
packaging/_export/
packaging/_build/
*.dmg
Expand Down
33 changes: 29 additions & 4 deletions apps/omlx-mac/Scripts/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,9 @@
# PYTHON_BIN=/path/to/python3 # python used for venvstacks driver
# (default: PATH lookup of python3)
# OMLX_WITH_CUSTOM_KERNEL=1 # same as --with-custom-kernel
# OMLX_CUSTOM_KERNEL_DEPLOYMENT_TARGET=15.0
# OMLX_CUSTOM_KERNEL_DEPLOYMENT_TARGET=26.0
# # macOS min version for custom kernels
# # (Path B Fusion beta is macOS 26-only)
# OMLX_RELEASE_REPOSITORY=owner/repo # GitHub Releases source embedded in
# # Info.plist (default: jonathan308/omlx)
# OMLX_BUILD_ARCHS=arm64 # application architectures; the
Expand Down Expand Up @@ -117,6 +118,8 @@ CUSTOM_KERNEL_DIRS=(
"$REPO_ROOT/omlx/custom_kernels/glm_moe_dsa"
"$REPO_ROOT/omlx/custom_kernels/minimax_m3"
"$REPO_ROOT/omlx/custom_kernels/qwen35_prefill"
"$REPO_ROOT/omlx/custom_kernels/decode_fast"
"$REPO_ROOT/omlx/custom_kernels/bonsai"
)
# OMLX_EXPORT_DIR overrides the venvstacks export tree we copy Python
# layers from. Release builds use this to point at a per-target export
Expand Down Expand Up @@ -222,7 +225,7 @@ _rebuild_venvstacks_export() {
}

_custom_kernel_deployment_target() {
printf "%s\n" "${OMLX_CUSTOM_KERNEL_DEPLOYMENT_TARGET:-${MACOSX_DEPLOYMENT_TARGET:-15.0}}"
printf "%s\n" "${OMLX_CUSTOM_KERNEL_DEPLOYMENT_TARGET:-${MACOSX_DEPLOYMENT_TARGET:-26.0}}"
}

_custom_kernel_pythonpath() {
Expand Down Expand Up @@ -251,7 +254,9 @@ _clean_custom_kernel_build_artifacts() {
for ext_name in \
"omlx.custom_kernels.glm_moe_dsa._ext" \
"omlx.custom_kernels.minimax_m3._ext" \
"omlx.custom_kernels.qwen35_prefill._ext"; do
"omlx.custom_kernels.qwen35_prefill._ext" \
"omlx.custom_kernels.decode_fast._ext" \
"omlx.custom_kernels.bonsai._ext"; do
find "$REPO_ROOT/build" \
-type d \
-name "$ext_name" \
Expand Down Expand Up @@ -328,7 +333,7 @@ import sys
import mlx.core as mx

failures = []
for name in ("glm_moe_dsa", "minimax_m3", "qwen35_prefill"):
for name in ("glm_moe_dsa", "minimax_m3", "qwen35_prefill", "decode_fast", "bonsai"):
ext_dir = pathlib.Path("omlx/custom_kernels") / name
so = next(ext_dir.glob("_ext.*.so"), None)
if so is None:
Expand Down Expand Up @@ -387,6 +392,10 @@ _build_custom_kernels() {
|| die "custom kernel build finished but MiniMax M3 metallib is missing."
[ -f "$REPO_ROOT/omlx/custom_kernels/qwen35_prefill/omlx_qwen35_prefill_kernels.metallib" ] \
|| die "custom kernel build finished but Qwen3.5 prefill metallib is missing."
[ -f "$REPO_ROOT/omlx/custom_kernels/decode_fast/omlx_decode_fast_kernels.metallib" ] \
|| die "custom kernel build finished but decode_fast metallib is missing."
[ -f "$REPO_ROOT/omlx/custom_kernels/bonsai/omlx_bonsai_kernels.metallib" ] \
|| die "custom kernel build finished but bonsai metallib is missing."
# The NAX (M5 tensor unit) metallib is SDK-gated in cmake, not
# deployment-gated: any build on SDK 26.2+ must produce it. A silent
# omission would ship DMGs whose M5 qmm quietly falls back to the
Expand Down Expand Up @@ -510,6 +519,7 @@ xcodebuild \
MARKETING_VERSION="$APP_VERSION" \
CURRENT_PROJECT_VERSION="$BUILD_NUMBER" \
OMLX_RELEASE_REPOSITORY="$OMLX_RELEASE_REPOSITORY" \
MACOSX_DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-26.0}" \
ARCHS="$OMLX_BUILD_ARCHS" \
ONLY_ACTIVE_ARCH=YES \
build >"$BUILD_DIR/xcodebuild.log" 2>&1 \
Expand Down Expand Up @@ -558,6 +568,21 @@ if [ -d "$DONOR_LAYERS/__venvstacks__" ]; then
ok " + __venvstacks__ metadata"
fi

log "Verifying bundled MLX is the guarded cp311 pair…"
BUNDLE_PYTHON="$PYTHON_DIR/cpython-3.11/bin/python3"
MLX_SITE="$PYTHON_DIR/framework-mlx-base/lib/python3.11/site-packages"
[ -x "$BUNDLE_PYTHON" ] || die "bundled CPython 3.11 interpreter missing: $BUNDLE_PYTHON"
[ -d "$MLX_SITE" ] || die "bundled MLX site-packages missing: $MLX_SITE"
GUARDED_MLX_VERSION="0.32.1.dev20260825+26421e953"
actual_mlx="$(PYTHONPATH="$MLX_SITE" "$BUNDLE_PYTHON" -c 'import mlx; print(mlx.__version__)')" \
|| die "bundled environment cannot import mlx; stock PyPI resolve is forbidden."
case "$actual_mlx" in
*0.32.2*) die "bundled mlx $actual_mlx is 0.32.2; TP2 rejected that ABI. Pin $GUARDED_MLX_VERSION." ;;
esac
[ "$actual_mlx" = "$GUARDED_MLX_VERSION" ] \
|| die "bundled mlx $actual_mlx does not match guarded pin $GUARDED_MLX_VERSION."
ok " + mlx $actual_mlx"

# --- Embed omlx package ---------------------------------------------------

if [ "$WITH_CUSTOM_KERNEL" = "1" ]; then
Expand Down
29 changes: 23 additions & 6 deletions apps/omlx-mac/Sources/Updater/ReleasesChecker.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
// single source of truth for distribution. No appcast XML, no EdDSA key
// management on the maintainer side. Pulls a page of releases, picks the
// latest stable PEP 440 tag, and selects the DMG asset whose filename
// embeds the current macOS major version (e.g. `-macos15-` or `-macos26-`).
// embeds the current macOS major version. This Fusion Path B beta ships
// `-macos26-` only; a `macos15-26` range asset must not be chosen on 26.
//
// Channel handling: Stable accepts final tags only, Release Candidate also
// accepts rc tags, and Dev accepts dev/pre-release tags. GitHub's
Expand Down Expand Up @@ -310,9 +311,9 @@ enum ReleasesChecker {
}

/// Pick the DMG asset whose filename embeds the current macOS major
/// version (e.g. `-macos15-` / `-macos26-` / `-macos15_`). Version
/// ranges such as `-macos26-27.` are accepted after exact matches.
/// Falls back to the single DMG when there's only one.
/// version (e.g. `-macos26-` / `-macos15-`). Exact majors win.
/// Forward ranges such as `-macos26-27.` are accepted after exact
/// matches. A `macos15-26` span is not a macOS 26 asset for this beta.
static func findMatchingDMG(assets: [GitHubRelease.Asset]) -> GitHubRelease.Asset? {
findMatchingDMG(assets: assets, macOSMajor: currentMacOSMajor())
}
Expand All @@ -335,11 +336,27 @@ enum ReleasesChecker {
return exact
}
if let ranged = candidates.first(where: { candidate in
candidate.ranges.contains { $0.contains(macOSMajor) }
candidate.ranges.contains { range in
guard range.contains(macOSMajor) else { return false }
// Path B: never treat macos15-26 as macOS 26.
if macOSMajor == 26 && range.lowerBound < 26 {
return false
}
return true
}
})?.asset {
return ranged
}
return dmgs.count == 1 ? dmgs[0] : nil
if dmgs.count == 1 {
let only = dmgs[0]
let ranges = macOSMajorRanges(in: only.name)
if macOSMajor == 26,
ranges.contains(where: { $0.lowerBound < 26 && $0.contains(26) }) {
return nil
}
return only
}
return nil
}

private static func macOSMajorRanges(in assetName: String) -> [ClosedRange<Int>] {
Expand Down
36 changes: 36 additions & 0 deletions apps/omlx-mac/Tests/oMLXTests/ReleasesCheckerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,42 @@ final class ReleasesCheckerTests: XCTestCase {
)
}

func testFindMatchingDMGSelectsMacOS26OnlyBetaAsset() {
let tahoeOnly = "oMLX-0.6.4b1-macos26-arm64.dmg"
let mislabeledRange = "oMLX-0.6.4b1-macos15-26-arm64.dmg"
let assets = [
asset(tahoeOnly),
asset(mislabeledRange),
]

XCTAssertEqual(
ReleasesChecker.findMatchingDMG(
assets: assets,
macOSMajor: 26
)?.name,
tahoeOnly
)
XCTAssertEqual(
ReleasesChecker.findMatchingDMG(
assets: assets,
macOSMajor: 15
)?.name,
mislabeledRange
)
XCTAssertNil(
ReleasesChecker.findMatchingDMG(
assets: [asset(tahoeOnly)],
macOSMajor: 15
)
)
XCTAssertNil(
ReleasesChecker.findMatchingDMG(
assets: [asset(mislabeledRange)],
macOSMajor: 26
)
)
}

func testFindMatchingDMGPrefersExactAssetOverRangeAsset() {
let range = "oMLX-0.4.4-macos26-27.dmg"
let exact = "oMLX-0.4.4-macos27-beta.dmg"
Expand Down
12 changes: 6 additions & 6 deletions apps/omlx-mac/oMLX.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -883,7 +883,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 15.0;
MACOSX_DEPLOYMENT_TARGET = 26.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
ONLY_ACTIVE_ARCH = YES;
Expand Down Expand Up @@ -940,7 +940,7 @@
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MACOSX_DEPLOYMENT_TARGET = 15.0;
MACOSX_DEPLOYMENT_TARGET = 26.0;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
SDKROOT = macosx;
Expand All @@ -965,7 +965,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.0;
MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 0.0.1;
OMLX_RELEASE_REPOSITORY = jonathan308/omlx;
PRODUCT_BUNDLE_IDENTIFIER = app.omlx;
Expand All @@ -992,7 +992,7 @@
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 15.0;
MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 0.0.1;
OMLX_RELEASE_REPOSITORY = jonathan308/omlx;
PRODUCT_BUNDLE_IDENTIFIER = app.omlx;
Expand All @@ -1009,7 +1009,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 15.0;
MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = app.omlx.tests;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -1026,7 +1026,7 @@
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 15.0;
MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = app.omlx.tests;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand Down
Loading