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
20 changes: 14 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ permissions:
contents: write
jobs:
release:
# macOS 26 or later: each runtime imports MLX to check itself, and the default build's
# Metal 4 library does not load on earlier macOS.
runs-on: macos-latest
timeout-minutes: 30
timeout-minutes: 45
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6.8.0
Expand All @@ -19,10 +21,15 @@ jobs:
version=${GITHUB_REF_NAME#v}
uv python install 3.12.12
python_root="$(uv python dir)/cpython-3.12.12-macos-aarch64-none"
"$python_root/bin/python3.12" packaging/build-mlx-runtime.py --python-root "$python_root" --output .build/bundle-mlx
python3 packaging/package.py --runtime-dir .build/bundle-mlx --manifest packaging/flint-int4-mlx.json \
--version "$version" --output dist/rec-darwin-arm64
test "$(dist/rec-darwin-arm64 --version)" = "rec $version"
# install.sh picks the -macos15 build on macOS before 26.
for target in 26.0:rec-darwin-arm64 15.0:rec-darwin-arm64-macos15; do
macos=${target%%:*} artifact=${target#*:}
"$python_root/bin/python3.12" packaging/build-mlx-runtime.py --python-root "$python_root" \
--macos "$macos" --output ".build/bundle-mlx-$macos"
python3 packaging/package.py --runtime-dir ".build/bundle-mlx-$macos" --manifest packaging/flint-int4-mlx.json \
--version "$version" --output "dist/$artifact"
test "$("dist/$artifact" --version)" = "rec $version"
done
- name: Publish
env:
GH_TOKEN: ${{ github.token }}
Expand All @@ -38,4 +45,5 @@ jobs:
;;
esac
gh release create "$GITHUB_REF_NAME" "${notes[@]}" --verify-tag --generate-notes \
--title "rec $GITHUB_REF_NAME" dist/rec-darwin-arm64 dist/rec-darwin-arm64.meta
--title "rec $GITHUB_REF_NAME" dist/rec-darwin-arm64 dist/rec-darwin-arm64.meta \
dist/rec-darwin-arm64-macos15 dist/rec-darwin-arm64-macos15.meta
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ excluded.</sub></p>
## Requirements

- Apple Silicon Mac with an M4 Pro, M4 Max, M5 Pro, or M5 Max
- macOS 15 Sequoia or later
- At least 48 GB unified memory
- About 30 GB free disk space
- [Node.js 22](https://nodejs.org/en/download) and npm for pi
Expand Down
8 changes: 6 additions & 2 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ rec_install() (
esac
done
[ "$(uname -s)" = Darwin ] && [ "$(uname -m)" = arm64 ] || fail 'rec requires an Apple Silicon Mac'
artifact=rec-darwin-arm64
artifact=rec-darwin-arm64 variant=''
# The default build's MLX runtime uses Metal 4, which needs macOS 26.
macos=$(sw_vers -productVersion) || fail 'could not read the macOS version'
[[ "$macos" =~ ^[0-9]+ ]] || fail "unrecognized macOS version: $macos"
if [ "${BASH_REMATCH[0]}" -lt 26 ]; then variant=-macos15; artifact=$artifact$variant; fi
version=${REC_VERSION:-}
version=${version#v}
[ -z "$version" ] || [[ "$version" =~ ^[A-Za-z0-9][A-Za-z0-9._-]*$ ]] || fail 'invalid REC_VERSION'
Expand Down Expand Up @@ -77,7 +81,7 @@ rec_install() (
begin 'Checking this Mac'
doctor=$("$tmp/$artifact" doctor 2>&1) || { printf '%s' "$erase"; printf '%s\n' "$doctor" >&2; fail 'this Mac does not meet the requirements above'; }
done_step 'This Mac meets the requirements'
release="$data/releases/$version"
release="$data/releases/$version$variant"
if [ -e "$release" ]; then
[ -f "$release/.managed" ] && [ -f "$release/rec" ] || fail 'release directory is not managed'
installed_hash=$(shasum -a 256 "$release/rec"); installed_hash=${installed_hash%% *}
Expand Down
21 changes: 21 additions & 0 deletions packaging/build-mlx-runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

import argparse
import json
import os
import re
import shutil
import subprocess
import tempfile
Expand All @@ -26,9 +28,24 @@ def main():
)
parser.add_argument("--output", type=Path, required=True, help="new directory for the runtime")
parser.add_argument("--uv", default="uv", help="uv executable (default: %(default)s)")
parser.add_argument(
"--macos",
metavar="VERSION",
help="oldest macOS the runtime supports, such as 15.0 (default: this Mac's version)",
)
args = parser.parse_args()
if args.output.exists():
parser.error("output must be a new directory")
if args.macos and not re.fullmatch(r"\d+\.\d+", args.macos):
parser.error("--macos must look like 15.0")
# MLX publishes a wheel per macOS release; the macOS 26 metallib uses Metal 4 and fails to
# load on macOS 15. The deployment target selects those wheels and sets the native
# extension's minimum OS.
env = dict(os.environ)
platform = []
if args.macos:
env["MACOSX_DEPLOYMENT_TARGET"] = args.macos
platform = ["--python-platform", "aarch64-apple-darwin"]
source_python = args.python_root / "bin/python3.12"
information = json.loads(
subprocess.check_output(
Expand Down Expand Up @@ -57,6 +74,7 @@ def main():
str(wheels),
str(ENGINE),
],
env=env,
check=True,
)
(wheel,) = wheels.glob("rec_mlx-*.whl")
Expand All @@ -78,10 +96,12 @@ def main():
"--python",
str(python),
"--break-system-packages",
*platform,
"--only-binary",
":all:",
str(wheel),
],
env=env,
check=True,
)
launcher = stage / "rec-mlx"
Expand All @@ -96,6 +116,7 @@ def main():
"os": "darwin",
"arch": "arm64",
"backend": "metal",
**({"macos": args.macos} if args.macos else {}),
},
indent=2,
)
Expand Down
29 changes: 23 additions & 6 deletions tests/installer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
ROOT = Path(__file__).resolve().parents[1]
SCRIPT = ROOT / "install.sh"
ARTIFACT = "rec-darwin-arm64"
ARTIFACTS = [ARTIFACT, ARTIFACT + "-macos15"]


def fake_rec(version):
Expand All @@ -30,15 +31,17 @@ def fake_rec(version):

def publish(directory, version, reported=None, bad_hash=False):
directory.mkdir(parents=True, exist_ok=True)
body = fake_rec(reported or version).encode()
(directory / ARTIFACT).write_bytes(body)
digest = "0" * 64 if bad_hash else hashlib.sha256(body).hexdigest()
(directory / (ARTIFACT + ".meta")).write_text(f"{digest} {len(body)}\n")
for artifact in ARTIFACTS:
body = fake_rec(reported or version).encode() + f"# {artifact}\n".encode()
(directory / artifact).write_bytes(body)
digest = "0" * 64 if bad_hash else hashlib.sha256(body).hexdigest()
(directory / (artifact + ".meta")).write_text(f"{digest} {len(body)}\n")


FIXTURES = {
"uname": '#!/bin/sh\ncase "$1" in -s) echo "${TEST_OS:-Darwin}";; -m) echo "${TEST_ARCH:-arm64}";; esac\n',
"curl": "#!/bin/sh\nexit 22\n",
"sw_vers": '#!/bin/sh\n[ "$1" = -productVersion ] && echo "${TEST_MACOS:-26.1}"\n',
}


Expand All @@ -58,7 +61,10 @@ def main():
publish(releases / "download/v0.6.0", "0.6.0", bad_hash=True)
bins = root / "bin"
offline = root / "offline"
for directory, names in [(bins, ["uname"]), (offline, ["uname", "curl"])]:
for directory, names in [
(bins, ["uname", "sw_vers"]),
(offline, ["uname", "sw_vers", "curl"]),
]:
directory.mkdir()
for name in names:
(directory / name).write_text(FIXTURES[name])
Expand Down Expand Up @@ -134,6 +140,17 @@ def installed(home):
assert not (home / "running").exists()
assert installed(home).parent.name == "0.3.0"

home, env = new_home({"TEST_MACOS": "15.8.1"})
install(env)
sequoia = installed(home)
assert sequoia == (home / ".local/share/rec/releases/0.3.0-macos15/rec").resolve(), sequoia
assert sequoia.read_text().endswith("# rec-darwin-arm64-macos15\n")
# After upgrading to macOS 26, the same version installs the default build beside it.
install(dict(env, TEST_MACOS="26.0"))
assert installed(home).parent.name == "0.3.0"
assert installed(home).read_text().endswith("# rec-darwin-arm64\n")
assert "unrecognized macOS" in install(dict(env, TEST_MACOS="beta"), ok=False).stderr

home, env = new_home()
install(env, "--serve")
assert (home / "serve.log").read_text() == "serve\n"
Expand All @@ -157,7 +174,7 @@ def installed(home):
server.shutdown()
server.server_close()
print(
"Installer: public default URL, pins, idempotence, checksum/version/device rejection, running-server stop, --serve, platform checks and collision preservation passed"
"Installer: public default URL, pins, idempotence, checksum/version/device rejection, running-server stop, --serve, platform checks, macOS 15 build selection and collision preservation passed"
)


Expand Down