#!/bin/sh # MLX Node for Mac: curl -fsSL mlx.app/i | sh # Downloads the notarized build from https://mlx.app, checks its signature and copies it to Applications. # Read it first: https://mlx.app/install?raw=1 set -eu INVITE="" BASE="https://mlx.app" TEAM="CCKXS9W488" BUNDLE="app.mlx.mac" say() { printf '%s\n' "$*"; } fail() { printf 'MLX Node: %s\n' "$*" >&2; exit 1; } [ "$(uname -s)" = "Darwin" ] || fail "MLX Node runs on a Mac with Apple silicon (M1 or later)." [ "$(sysctl -n hw.optional.arm64 2>/dev/null || echo 0)" = "1" ] || fail "MLX Node needs a Mac with Apple silicon (M1 or later)." major=$(sw_vers -productVersion | cut -d. -f1) [ "$major" -ge 14 ] 2>/dev/null || fail "MLX Node needs macOS 14 Sonoma or later. This Mac has macOS $(sw_vers -productVersion)." tmp=$(mktemp -d "${TMPDIR:-/tmp}/mlx-node.XXXXXX") mnt="" cleanup() { [ -n "$mnt" ] && hdiutil detach "$mnt" -quiet -force >/dev/null 2>&1 || true rm -rf "$tmp" } trap cleanup EXIT INT TERM say "Downloading MLX Node…" curl -fL --progress-bar "$BASE/api/download/mac?via=cli" -o "$tmp/MLX-Node.dmg" || fail "The download didn't finish. Check your connection and try again." mnt="$tmp/mnt" mkdir -p "$mnt" hdiutil attach -nobrowse -readonly -noautoopen -quiet -mountpoint "$mnt" "$tmp/MLX-Node.dmg" || { mnt=""; fail "The disk image couldn't be opened."; } app="$mnt/MLX Node.app" [ -d "$app" ] || fail "The disk image doesn't hold MLX Node." say "Checking the signature…" codesign --verify --deep --strict "$app" 2>/dev/null || fail "The app's signature doesn't check out. Nothing was installed." team=$(codesign -dv "$app" 2>&1 | sed -n 's/^TeamIdentifier=//p') [ "$team" = "$TEAM" ] || fail "The app isn't signed by MLX (team $TEAM). Nothing was installed." id=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$app/Contents/Info.plist" 2>/dev/null || true) [ "$id" = "$BUNDLE" ] || fail "Unexpected app ($id). Nothing was installed." spctl --assess --type execute "$app" 2>/dev/null || fail "Gatekeeper didn't accept the app. Nothing was installed." new=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$app/Contents/Info.plist") # version a.b.c compared numerically: is $1 at least $2? atleast() { awk -v a="$1" -v b="$2" 'BEGIN { n = split(a, x, "."); m = split(b, y, "."); k = n > m ? n : m for (i = 1; i <= k; i++) { p = x[i] + 0; q = y[i] + 0; if (p > q) exit 0; if (p < q) exit 1 } exit 0 }' } dest="/Applications" [ -w "$dest" ] || { dest="$HOME/Applications"; mkdir -p "$dest"; } target="$dest/MLX Node.app" installed="" for d in "/Applications" "$HOME/Applications"; do for n in "MLX Node.app" "MLX.app"; do p="$d/$n" if [ -d "$p" ] && [ "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$p/Contents/Info.plist" 2>/dev/null || true)" = "$BUNDLE" ]; then installed="$p"; break 2 fi done done if [ -n "$installed" ] && atleast "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$installed/Contents/Info.plist" 2>/dev/null || echo 0)" "$new"; then say "MLX Node is already up to date in $(dirname "$installed")." target="$installed" else if pgrep -xq MLX; then say "Closing the running MLX Node…" osascript -e 'tell application id "app.mlx.mac" to quit' >/dev/null 2>&1 || true i=0; while pgrep -xq MLX && [ $i -lt 20 ]; do sleep 0.5; i=$((i + 1)); done fi say "Installing MLX Node $new in $dest…" rm -rf "$target.installing" ditto "$app" "$target.installing" || fail "Couldn't copy MLX Node to $dest." if [ -n "$installed" ] && [ "$installed" != "$target" ]; then rm -rf "$installed"; fi rm -rf "$target" mv "$target.installing" "$target" xattr -dr com.apple.quarantine "$target" 2>/dev/null || true fi hdiutil detach "$mnt" -quiet -force >/dev/null 2>&1 || true mnt="" open "$target" if [ -n "$INVITE" ]; then sleep 2 open "mlx://invite?ref=$INVITE" 2>/dev/null || true say "MLX Node $new is open. Create your wallet with the invite code $INVITE." else say "MLX Node $new is open: look for its icon in the menu bar." fi