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
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ PROF_GEN ?= 500
AUDIT_IGNORES := --ignore RUSTSEC-2024-0436 --ignore RUSTSEC-2025-0119

.PHONY: help build check test fmt fmt-check lint audit deny precommit hooks \
ci ci-metrics tag release-package release-sha release-sign tap-sync \
ci ci-metrics tag release-package release-sha release-sign bottle tap-sync \
clean serve chat info logs-tail metrics-summary \
metrics-init metrics-doctor metrics-doctor-fix metrics-export \
metrics-backup metrics-replay-pending metrics-prompts-sync \
Expand Down Expand Up @@ -181,6 +181,9 @@ release-sha: ## print sha256 of the v<ver> GitHub source tarball (append --w
release-sign: ## keyless cosign-sign dist/rmlx-v<ver>-...tar.gz -> .cosign.bundle (needs cosign + browser OIDC)
bash scripts/release/sign_artifact.sh

bottle: ## build a Homebrew bottle from the installed rmlx keg (run after brew install --build-bottle)
bash scripts/release/build_bottle.sh

tap-sync: ## copy packaging/homebrew/rmlx.rb into the homebrew-rmlx tap and push
bash scripts/release/sync_tap.sh

Expand Down
83 changes: 80 additions & 3 deletions docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,67 @@ crates are `publish = false`). There is no separate `VERSION` file.
your authenticated identity + the public Rekor log — real provenance for
the prebuilt binary. Consumer-side verification is in "Verify both install
paths".
8. **Formula url + sha256:** run `make release-sha` (or
8. **Build + publish the Homebrew bottle** (binary install channel — no Rust
toolchain required for users):

a. **Install the keg from source** on the release machine (the same
Apple-Silicon Mac used for the binary artifact above):
```sh
brew install --build-bottle packaging/homebrew/rmlx.rb
```
This is a full source build. It may take a few minutes. `brew install
--build-bottle` is required because a normal `brew install` may silently
reuse a cached bottle from a previous release, producing a wrong keg.

b. **Build the bottle and upload it:**
```sh
make bottle # runs scripts/release/build_bottle.sh
```
The script:
- Runs `brew bottle --json --root-url=https://github.com/Pushkinist/rMLX/releases/download/v<ver>`.
- Renames the local `rmlx--<ver>.<tag>.bottle.tar.gz` to the remote
single-dash name `rmlx-<ver>.<tag>.bottle.tar.gz` (Homebrew's
intentional local/remote naming split — the remote asset must use
single-dash or `brew install` gets a 404).
- Prints the `bottle do … end` block to paste into the formula.
- Prints the exact `gh release upload` command.

Run the printed upload command:
```sh
gh release upload v<version> dist/rmlx-<ver>.<tag>.bottle.tar.gz
```

c. **Paste the `bottle do` block into `packaging/homebrew/rmlx.rb`.**
The script prints the block with the correct `root_url`, `cellar`, OS
tag, and sha256. Insert it immediately after the `head` line, before
the `depends_on` lines:
```ruby
bottle do
root_url "https://github.com/Pushkinist/rMLX/releases/download/v<ver>"
sha256 cellar: :any_skip_relocation, arm64_tahoe: "<sha256>"
end
```
The `depends_on "rust" => :build` and `depends_on "mlx-c"` lines remain
unchanged. When a `bottle do` block is present, `brew install` fetches
the binary directly; if the bottle is unavailable for the user's macOS
version Homebrew falls back to the source build automatically.

d. **Commit the formula update** via a PR (main is ruleset-protected):
```sh
git add packaging/homebrew/rmlx.rb
git commit -m "chore(release): add bottle for v<version>"
```
Open the PR and merge it; then continue to step 9 (formula url+sha256).

> **Clean-machine verification (optional but recommended):**
> On a machine without a local rmlx keg:
> - `brew tap Pushkinist/rmlx && brew trust Pushkinist/rmlx && brew install rmlx` —
> should download the prebuilt bottle, not compile from source (confirm with
> `brew install --verbose rmlx` — the word "Bottled" appears in output).
> - `brew uninstall mlx-c && brew install rmlx` — should fail cleanly with a
> dependency error before attempting any download or compile.

9. **Formula url + sha256:** run `make release-sha` (or
`bash scripts/release/source_sha256.sh --write`) — it patches **both** the
`url` line in `packaging/homebrew/rmlx.rb` to the new `v<version>` tag
tarball **and** the `sha256`.
Expand All @@ -60,8 +120,8 @@ crates are `publish = false`). There is no separate `VERSION` file.
> re-fetch the archive 2-3× (`curl -fsSL .../archive/refs/tags/v<version>.tar.gz
> | shasum -a 256`) and confirm the digest is stable before trusting it.
Commit the formula bump via a PR (`main` is ruleset-protected; see below).
9. **Publish the tap:** `make tap-sync` (copies the formula into
`Pushkinist/homebrew-rmlx` as `Formula/rmlx.rb` and pushes).
10. **Publish the tap:** `make tap-sync` (copies the formula into
`Pushkinist/homebrew-rmlx` as `Formula/rmlx.rb` and pushes).

## Dependency-bump PRs (Dependabot)

Expand All @@ -88,6 +148,22 @@ fix (the migration commit is otherwise only reachable via reflog).

## Verify both install paths

**Homebrew bottle (binary — no compile):**
```sh
brew tap Pushkinist/rmlx
brew trust Pushkinist/rmlx
brew install rmlx # fetches bottle, does NOT invoke cargo
brew test rmlx
rmlx --version
```
Confirm no source build occurred: `brew install --verbose rmlx` shows "Bottled"
in the output. To confirm the clean mlx-c dependency failure:
```sh
brew uninstall mlx-c
brew install rmlx # fails with dependency error, not a dyld crash
brew install mlx-c # reinstate
```

**Prebuilt binary (from the Release):**
```sh
brew install mlx-c
Expand Down Expand Up @@ -133,6 +209,7 @@ brew audit --strict --new rmlx
| `CHANGELOG.md` | Durable release notes (Keep a Changelog); body source |
| `packaging/homebrew/rmlx.rb` | Canonical formula (source of truth) |
| `scripts/release/package_binary.sh` | Build + bundle the binary tarball |
| `scripts/release/build_bottle.sh` | Build the Homebrew bottle from an installed keg, rename, and print upload + formula instructions |
| `scripts/release/source_sha256.sh` | Compute / patch the formula source sha256 |
| `scripts/release/sync_tap.sh` | Push the formula to the tap repo |
| `scripts/release/changelog_section.sh` | Print one version's CHANGELOG section |
136 changes: 136 additions & 0 deletions scripts/release/build_bottle.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#!/usr/bin/env bash
# build_bottle.sh — build a Homebrew bottle from the installed rmlx keg and
# prepare it for upload to the GitHub Release.
#
# What this script does:
# 1. Verifies rmlx is installed in Homebrew (brew install --build-bottle
# must have been run first — see docs/RELEASING.md).
# 2. Runs `brew bottle --json --root-url=<github-release-root>` to produce:
# dist/<local-filename>.bottle.tar.gz
# dist/rmlx--<ver>.<tag>.bottle.json
# 3. Renames the local file to the remote filename (single-dash naming —
# Homebrew intentionally uses double-dash locally; the remote asset must
# use single-dash so brew install resolves it correctly).
# 4. Prints the `bottle do ... end` DSL block to paste into
# packaging/homebrew/rmlx.rb, with root_url already set.
# 5. Prints the exact `gh release upload` command to attach the bottle.
#
# Prerequisites:
# - rmlx installed via `brew install --build-bottle packaging/homebrew/rmlx.rb`
# on the same macOS major version you want to bottle.
# - A published GitHub Release for the current version tag must already
# exist (step 6 in docs/RELEASING.md) so the root_url is valid at
# install time.
# - `brew` and `jq` available in PATH.
#
# Usage:
# bash scripts/release/build_bottle.sh
# bash scripts/release/build_bottle.sh --root-url https://github.com/Pushkinist/rMLX/releases/download/v1.2.3
#
# The --root-url flag overrides the default (derived from Cargo.toml version).
# Use it when you need to bottle against a future or patched release URL.

set -euo pipefail
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$REPO_ROOT"

# ── prerequisites ──────────────────────────────────────────────────────────────
command -v brew >/dev/null 2>&1 || { echo "error: brew not found" >&2; exit 1; }
command -v jq >/dev/null 2>&1 || { echo "error: jq not found (brew install jq)" >&2; exit 1; }

# Apple Silicon only — the binary links Metal MLX.
[ "$(uname -m)" = "arm64" ] || { echo "error: must run on Apple Silicon (arm64)" >&2; exit 1; }

# ── version ────────────────────────────────────────────────────────────────────
VER=$(awk -F'"' '/^version = /{print $2; exit}' Cargo.toml)
[ -n "$VER" ] || { echo "error: could not read version from Cargo.toml" >&2; exit 1; }

# ── root-url (GitHub Release download root) ────────────────────────────────────
OWNER_REPO="Pushkinist/rMLX"
DEFAULT_ROOT_URL="https://github.com/${OWNER_REPO}/releases/download/v${VER}"

ROOT_URL="${DEFAULT_ROOT_URL}"
if [ "${1:-}" = "--root-url" ]; then
ROOT_URL="${2:?error: --root-url requires a URL argument}"
fi

echo "==> building bottle for rmlx v${VER}"
echo " root_url: ${ROOT_URL}"

# ── check keg is installed ─────────────────────────────────────────────────────
brew list rmlx >/dev/null 2>&1 || {
echo "" >&2
echo "error: rmlx is not installed in Homebrew." >&2
echo "" >&2
echo "Install it first with:" >&2
echo " brew install --build-bottle packaging/homebrew/rmlx.rb" >&2
echo "" >&2
echo "Then re-run this script." >&2
exit 1
}

# ── output directory ───────────────────────────────────────────────────────────
mkdir -p dist

# ── brew bottle ────────────────────────────────────────────────────────────────
# --json writes metadata to a JSON file (not into the formula).
# --root-url sets the root_url field in the JSON / bottle do block.
# --no-rebuild suppresses the rebuild counter (starts clean for every release).
# --force-core-tap allows bottling a non-core-tap formula.
# Output files land in the current directory; we move them into dist/ below.
echo "==> running brew bottle"
brew bottle \
--json \
--root-url="${ROOT_URL}" \
--no-rebuild \
--force-core-tap \
rmlx

# ── locate generated files ─────────────────────────────────────────────────────
# brew bottle emits files to $PWD. Local filename uses double-dash (intentional
# Homebrew quirk); the remote asset must use single-dash.
LOCAL_TAR=$(find . -maxdepth 1 -name 'rmlx--*.bottle.tar.gz' -print 2>/dev/null | sort | tail -1 | sed 's|^\./||')
JSON_FILE=$(find . -maxdepth 1 -name 'rmlx--*.bottle.json' -print 2>/dev/null | sort | tail -1 | sed 's|^\./||')

[ -n "$LOCAL_TAR" ] || { echo "error: brew bottle did not produce a .bottle.tar.gz file" >&2; exit 1; }
[ -n "$JSON_FILE" ] || { echo "error: brew bottle did not produce a .bottle.json file" >&2; exit 1; }

# ── rename local → remote filename (double-dash → single-dash) ─────────────────
# The JSON tab contains both fields:
# local_filename → rmlx--0.2.3.arm64_tahoe.bottle.tar.gz
# filename → rmlx-0.2.3.arm64_tahoe.bottle.tar.gz
REMOTE_TAR=$(jq -r '.rmlx.bottle.tags | to_entries[0].value.filename' "$JSON_FILE" 2>/dev/null || echo "")
if [ -z "$REMOTE_TAR" ]; then
# Fallback: replace first occurrence of '--' with '-'
REMOTE_TAR="${LOCAL_TAR/--/-}"
fi

mv "$LOCAL_TAR" "dist/${REMOTE_TAR}"
mv "$JSON_FILE" "dist/${JSON_FILE}"

echo "==> bottle: dist/${REMOTE_TAR}"
echo "==> json: dist/${JSON_FILE}"

# ── print bottle do block ──────────────────────────────────────────────────────
OS_TAG=$(jq -r '.rmlx.bottle.tags | keys[0]' "dist/${JSON_FILE}" 2>/dev/null || echo "arm64_tahoe")
SHA256=$(jq -r ".rmlx.bottle.tags[\"${OS_TAG}\"].sha256" "dist/${JSON_FILE}" 2>/dev/null || echo "")
CELLAR=$(jq -r ".rmlx.bottle.tags[\"${OS_TAG}\"].cellar" "dist/${JSON_FILE}" 2>/dev/null || echo ":any_skip_relocation")

echo ""
echo "==> paste this bottle do block into packaging/homebrew/rmlx.rb"
echo " (replace any existing bottle do ... end block)"
echo ""
echo " bottle do"
echo " root_url \"${ROOT_URL}\""
echo " sha256 cellar: ${CELLAR}, ${OS_TAG}: \"${SHA256}\""
echo " end"
echo ""

# ── upload instructions ────────────────────────────────────────────────────────
echo "==> upload the bottle to the GitHub Release:"
echo " gh release upload v${VER} dist/${REMOTE_TAR}"
echo ""
echo "==> then commit the updated formula and sync the tap:"
echo " make tap-sync"
echo ""
echo "See docs/RELEASING.md for the full bottle release flow."
Loading