Skip to content
Merged
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
47 changes: 12 additions & 35 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,48 +7,25 @@ on:
branches: [ main, master ]

jobs:
build:
build-and-test:
runs-on: macos-15

steps:
- uses: actions/checkout@v7

- name: Build
run: |
XCODEPROJ=$(find . -name "*.xcodeproj" -maxdepth 2 | head -1)
if [ -z "$XCODEPROJ" ]; then
echo "No Xcode project found"
exit 1
fi

SCHEME=$(xcodebuild -list -project "$XCODEPROJ" 2>/dev/null | awk '/Schemes:/{found=1; next} found && NF{print; exit}' | xargs)
if [ -z "$SCHEME" ]; then
SCHEME=$(basename "$XCODEPROJ" .xcodeproj)
fi
- name: Install Metal Toolchain
# MLX Code bundles mlx-swift, which compiles Metal shaders; recent Xcode no
# longer ships the Metal Toolchain by default, so the build fails without this.
run: xcodebuild -downloadComponent MetalToolchain || true

echo "Building scheme: $SCHEME"
xcodebuild build \
-project "$XCODEPROJ" \
-scheme "$SCHEME" \
-destination 'platform=macOS' \
CODE_SIGNING_ALLOWED=NO \
SWIFT_TREAT_WARNINGS_AS_ERRORS=YES \
| tail -20

- name: Test
- name: Build & Test
run: |
XCODEPROJ=$(find . -name "*.xcodeproj" -maxdepth 2 | head -1)

SCHEME=$(xcodebuild -list -project "$XCODEPROJ" 2>/dev/null | awk '/Schemes:/{found=1; next} found && NF{print; exit}' | xargs)
if [ -z "$SCHEME" ]; then
SCHEME=$(basename "$XCODEPROJ" .xcodeproj)
fi

echo "Running tests for scheme: $SCHEME"
set -o pipefail
xcodebuild test \
-project "$XCODEPROJ" \
-scheme "$SCHEME" \
-project "MLX Code.xcodeproj" \
-scheme "MLX Code" \
-destination 'platform=macOS' \
CODE_SIGNING_ALLOWED=NO \
SWIFT_TREAT_WARNINGS_AS_ERRORS=YES \
| tail -50
CODE_SIGNING_REQUIRED=NO \
SWIFT_TREAT_WARNINGS_AS_ERRORS=NO \
| tail -60
33 changes: 29 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,19 +227,44 @@ Local HTTP API on port **37422** (loopback only).

## Installation

### From DMG
### From DMG (recommended for most users)

Download from [Releases](https://github.com/kochj23/MLXCode/releases), open the DMG, drag to Applications, launch, download a model from Settings.
1. Download the latest `.dmg` from [Releases](https://github.com/kochj23/MLXCode/releases).
2. Open it and drag **MLX Code** into your **Applications** folder.
3. Launch it from Applications and download a model from Settings. That's it — no Xcode, no toolchains, nothing else to install.

> **See "MLX Code can't be opened because the developer cannot be verified"?**
> That means you have a build that isn't yet Developer-ID-signed **and** notarized. To open it anyway:
> - **macOS 14 and earlier:** Control-click (right-click) the app → **Open** → **Open**.
> - **macOS 15 (Sequoia) / 26 and later:** double-click it, dismiss the dialog, then open **System Settings → Privacy & Security**, scroll down, and click **Open Anyway**.
> - Or from Terminal: `xattr -dr com.apple.quarantine "/Applications/MLX Code.app"`
>
> **Notarized releases open with no prompt at all** — maintainers, see [RELEASE.md](RELEASE.md).

### From Source

Requires **Xcode 15 or later**. Because the app bundles **MLX** for on-device LLM inference, the build
compiles Metal GPU shaders, which needs Apple's **Metal Toolchain** — a component recent Xcode versions
no longer ship by default. Install it once:

```bash
xcodebuild -downloadComponent MetalToolchain
# (or in Xcode: Settings → Components → Metal Toolchain → Get)
```

Then build:

```bash
git clone git@github.com:kochj23/MLXCode.git
cd MLXCode
open "MLX Code.xcodeproj"
# Build: Cmd+R (Xcode 15+, macOS 14.0+ target)
open "MLX Code.xcodeproj" # Xcode resolves Swift packages on first open (mlx-swift, mlx-swift-examples, …)
# Build & run: Cmd+R (Xcode 15+, macOS 14.0+ target)
```

> Skipping the Metal Toolchain step produces a wall of `CompileMetalFile … cannot execute tool 'metal'
> due to missing Metal Toolchain` errors from the `mlx-swift` dependency. That's the missing component,
> not a problem with the project.

### Enabling the Xcode Extension

1. System Settings > Privacy & Security > Extensions > Xcode Source Editor
Expand Down
61 changes: 61 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Releasing MLX Code

The goal: ship a `.dmg` that end users can **open with a simple drag-and-drop, no Gatekeeper
warnings**. On macOS that requires the app to be **signed with a "Developer ID Application"
certificate** *and* **notarized by Apple**. A build signed only with an "Apple Development"
certificate (the default for local debug builds) is **rejected by Gatekeeper** and gives users the
dreaded *"the developer cannot be verified"* dialog.

`scripts/release.sh` automates the whole pipeline: **archive → export (Developer ID) → DMG →
notarize → staple → verify**.

## One-time setup

1. **Developer ID Application certificate** (needs a paid Apple Developer account, team `QRRCB8HB3W`).
In Xcode: *Settings → Accounts → Manage Certificates → + → Developer ID Application*. Confirm it's
installed:
```bash
security find-identity -v -p codesigning | grep "Developer ID Application"
```

2. **Notary credentials as a keychain profile** (so no secrets live in the repo). Create an
[app-specific password](https://support.apple.com/en-us/102654) for your Apple ID, then:
```bash
xcrun notarytool store-credentials MLXCODE_NOTARY \
--apple-id "you@digitalnoise.net" \
--team-id QRRCB8HB3W \
--password "abcd-efgh-ijkl-mnop" # the app-specific password
```

3. **Metal Toolchain** (once per machine, needed to compile the MLX dependency):
```bash
xcodebuild -downloadComponent MetalToolchain
```

## Cut a release

```bash
./scripts/release.sh 2.3.0 # or omit the version to use today's date
```

The script prints the finished path, e.g. `build/MLX-Code-2.3.0.dmg`. Upload that `.dmg` to a
[GitHub Release](https://github.com/kochj23/MLXCode/releases). Users drag it to Applications and
it launches clean — the stapled notarization ticket means it works even offline.

## Overrides (env vars)

| Var | Default | Purpose |
|---|---|---|
| `SCHEME` | `MLX Code` | Xcode scheme to archive |
| `CONFIG` | `Release` | Build configuration |
| `TEAM_ID` | `QRRCB8HB3W` | Apple Developer team |
| `NOTARY_PROFILE` | `MLXCODE_NOTARY` | `notarytool` keychain profile name |

## Troubleshooting

- **`error: No signing certificate "Developer ID Application" found`** → step 1 above isn't done.
- **`Error: No Keychain password item found for profile: MLXCODE_NOTARY`** → step 2 above isn't done.
- **Notarization status `Invalid`** → run `xcrun notarytool log <submission-id> --keychain-profile MLXCODE_NOTARY`
to see which binary failed (usually an un-signed nested helper — the export step signs everything,
so this is rare).
- **`missing Metal Toolchain`** during archive → step 3 above.
12 changes: 12 additions & 0 deletions scripts/ExportOptions.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>developer-id</string>
<key>teamID</key>
<string>QRRCB8HB3W</string>
<key>signingStyle</key>
<string>automatic</string>
</dict>
</plist>
83 changes: 83 additions & 0 deletions scripts/release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env bash
#
# release.sh — build, sign (Developer ID), notarize, staple, and package MLX Code
# as a Gatekeeper-clean .dmg that end users can open with a simple drag-and-drop.
#
# One-time setup is documented in RELEASE.md:
# 1. A "Developer ID Application" certificate in your login keychain.
# 2. Notary credentials stored as a keychain profile named "$NOTARY_PROFILE":
# xcrun notarytool store-credentials MLXCODE_NOTARY \
# --apple-id "you@example.com" --team-id QRRCB8HB3W --password <app-specific-password>
# 3. The Metal Toolchain: xcodebuild -downloadComponent MetalToolchain
#
# Usage: ./scripts/release.sh [version] (version defaults to today's date)
#
set -euo pipefail

cd "$(dirname "$0")/.."

PROJECT="MLX Code.xcodeproj"
SCHEME="${SCHEME:-MLX Code}"
CONFIG="${CONFIG:-Release}"
TEAM_ID="${TEAM_ID:-QRRCB8HB3W}"
NOTARY_PROFILE="${NOTARY_PROFILE:-MLXCODE_NOTARY}"
APP_NAME="MLX Code"
VERSION="${1:-$(date +%Y.%m.%d)}"

BUILD_DIR="$(pwd)/build"
ARCHIVE="$BUILD_DIR/${APP_NAME}.xcarchive"
EXPORT_DIR="$BUILD_DIR/export"
DMG="$BUILD_DIR/${APP_NAME// /-}-${VERSION}.dmg"

# ---- Preflight ---------------------------------------------------------------
echo "==> Preflight checks"
if ! security find-identity -v -p codesigning | grep -q "Developer ID Application"; then
echo "ERROR: no 'Developer ID Application' certificate found (see RELEASE.md step 1)." >&2
exit 1
fi
if ! xcrun notarytool history --keychain-profile "$NOTARY_PROFILE" >/dev/null 2>&1; then
echo "ERROR: notary profile '$NOTARY_PROFILE' not set up (see RELEASE.md step 2)." >&2
exit 1
fi

rm -rf "$BUILD_DIR"; mkdir -p "$BUILD_DIR"

# ---- Archive -----------------------------------------------------------------
echo "==> Archiving ($CONFIG)…"
xcodebuild -project "$PROJECT" -scheme "$SCHEME" -configuration "$CONFIG" \
-destination "generic/platform=macOS" \
-archivePath "$ARCHIVE" archive

# ---- Export (Developer ID signed) -------------------------------------------
echo "==> Exporting with Developer ID…"
xcodebuild -exportArchive -archivePath "$ARCHIVE" \
-exportPath "$EXPORT_DIR" \
-exportOptionsPlist "scripts/ExportOptions.plist"

APP="$EXPORT_DIR/${APP_NAME}.app"
[ -d "$APP" ] || { echo "ERROR: export failed — $APP not found." >&2; exit 1; }

# ---- Package DMG -------------------------------------------------------------
echo "==> Building DMG…"
STAGE="$(mktemp -d)"
cp -R "$APP" "$STAGE/"
ln -s /Applications "$STAGE/Applications"
hdiutil create -volname "$APP_NAME" -srcfolder "$STAGE" -ov -format UDZO "$DMG" >/dev/null
rm -rf "$STAGE"

# ---- Notarize + staple -------------------------------------------------------
echo "==> Notarizing (uploads to Apple and waits — usually a few minutes)…"
xcrun notarytool submit "$DMG" --keychain-profile "$NOTARY_PROFILE" --wait

echo "==> Stapling the notarization ticket…"
xcrun stapler staple "$DMG"

# ---- Verify ------------------------------------------------------------------
echo "==> Verifying…"
codesign --verify --deep --strict --verbose=2 "$APP"
spctl -a -t open --context context:primary-signature -vv "$DMG" || true

echo
echo "✅ Done: $DMG"
echo " Upload this .dmg to a GitHub Release. It opens with no Gatekeeper prompt,"
echo " even offline, because the notarization ticket is stapled."
Loading