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
72 changes: 72 additions & 0 deletions .github/workflows/binaries.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
name: binaries

# 태그가 붙으면 단일 실행 바이너리를 만들어 그 릴리스에 첨부한다.
# npm 발행(publish.yml)과는 독립이다 — 한쪽이 막혀도 다른 쪽은 나간다.
#
# 재현성: 빌드는 **태그가 가리키는 커밋에서만** 돌고, 산출물마다 sha256 을 같이 올린다.
# install.sh / brew / scoop 가 전부 그 체크섬을 검증하므로, 이 파일이 조용히 다른 바이트를
# 올리면 설치 쪽에서 fail-closed 로 걸린다(그게 설계다).

on:
push:
tags:
- 'v*'
workflow_dispatch:
inputs:
tag:
description: 'Exact tag to build and attach (for example v2.16.0)'
required: true
type: string

permissions: {}

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: write # 릴리스 자산 업로드에만 필요
steps:
- uses: actions/checkout@v5
with:
ref: ${{ inputs.tag || github.ref }}
persist-credentials: false

- uses: oven-sh/setup-bun@v2
with:
bun-version: latest

- uses: actions/setup-node@v5
with:
node-version: 22

# 엔진 자체가 깨진 채로 바이너리를 내보내지 않는다.
- name: Engine tests and conformance vectors
run: npm test

- name: Build binaries
run: node scripts/build-binaries.mjs --out dist-bin

- name: Generate Homebrew formula and Scoop manifest
run: node scripts/gen-packaging.mjs --manifest dist-bin/manifest.json --out packaging

# 생성된 패키징 파일이 워크트리의 커밋본과 다르면, 릴리스마다 사람이 갱신해야 한다는 뜻이다.
# 그 사실을 로그에 남긴다(실패시키지는 않는다 — 태그 시점엔 이미 커밋돼 있어야 정상).
- name: Report packaging drift
run: git --no-pager diff --stat -- packaging || true

- name: Attach assets to the release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ inputs.tag || github.ref_name }}
run: |
set -eu
gh release upload "$TAG" \
dist-bin/fitllm-* \
dist-bin/manifest.json \
--repo "$GITHUB_REPOSITORY" --clobber

- uses: actions/upload-artifact@v4
with:
name: fitllm-binaries-${{ github.run_id }}
path: dist-bin
retention-days: 14
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# 단일 실행 바이너리 빌드 산출물(릴리스 자산으로만 배포 — 레포에 커밋하지 않는다)
dist-bin/
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,27 @@ FitLLM is an open-source, zero-dependency engine that checks whether a local LLM
>
> Zero dependencies. One readable file: [`engine.js`](engine.js). Conformance-vector tested. MIT.

## Install

```bash
# self-contained binary — no Node required
curl -fsSL https://fitllm.run/install.sh | sh

# Homebrew (tap)
brew tap click6067-ship-it/fitllm https://github.com/click6067-ship-it/fitllm-engine
brew install fitllm

# Windows (Scoop)
scoop install https://raw.githubusercontent.com/click6067-ship-it/fitllm-engine/master/packaging/fitllm.json

# already have Node? nothing to install
npx fitllm "Gemma 4 12b" --gpu "RTX 4090"
```

Binaries for macOS (arm64/x64), Linux (x64/arm64, glibc and musl) and Windows x64 are attached to every [release](https://github.com/click6067-ship-it/fitllm-engine/releases) with a `.sha256` next to each one; `install.sh` verifies the checksum and installs nothing if it does not match.

Two things stated plainly rather than buried: each binary embeds its JavaScript runtime, so it is **60–94 MB** — much larger than an equivalent Rust or Go tool, which is the price of not forking the engine into a second language. And this is a Homebrew **tap**, not homebrew-core: core's bar for a self-submitted project is 225 stars / 90 forks / 90 watchers, which this project does not meet yet.

## Quick start

```bash
Expand Down
28 changes: 28 additions & 0 deletions packaging/fitllm.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"version": "2.16.0",
"description": "Will this LLM fit on your GPU or Mac? Architecture-accurate memory estimates",
"homepage": "https://fitllm.run",
"license": "MIT",
"architecture": {
"64bit": {
"url": "https://github.com/click6067-ship-it/fitllm-engine/releases/download/v2.16.0/fitllm-v2.16.0-windows-x64.exe",
"hash": "4a0e3d09fbc80eb3ccf613dfc8dd63bd8ac8312f1e64a638e3818c742ec687e9",
"bin": [
[
"fitllm-v2.16.0-windows-x64.exe",
"fitllm"
]
]
}
},
"checkver": {
"github": "https://github.com/click6067-ship-it/fitllm-engine"
},
"autoupdate": {
"architecture": {
"64bit": {
"url": "https://github.com/click6067-ship-it/fitllm-engine/releases/download/v$version/fitllm-v$version-windows-x64.exe"
}
}
}
}
45 changes: 45 additions & 0 deletions packaging/fitllm.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Generated by scripts/gen-packaging.mjs — do not edit by hand.
# Tap install:
# brew tap click6067-ship-it/fitllm https://github.com/click6067-ship-it/fitllm-engine
# brew install fitllm
#
# This is a binary formula. homebrew-core does not accept prebuilt binaries and
# its notability bar for self-submitted projects is 225 stars / 90 forks /
# 90 watchers, which this project does not meet — so a tap is the honest home
# for it today, not a workaround we are hiding.
class Fitllm < Formula
desc "Will this LLM fit on your GPU or Mac? Architecture-accurate memory estimates"
homepage "https://fitllm.run"
version "2.16.0"
license "MIT"

on_macos do
on_arm do
url "https://github.com/click6067-ship-it/fitllm-engine/releases/download/v2.16.0/fitllm-v2.16.0-darwin-arm64"
sha256 "6d5a7b47ad1f9d3b08f0b21d36b932c683c92597191cd9de8dbf208536ea6196"
end
on_intel do
url "https://github.com/click6067-ship-it/fitllm-engine/releases/download/v2.16.0/fitllm-v2.16.0-darwin-x64"
sha256 "040f948391485d05bf794a108d05158fa2bc3993058b47678e1580f4587e89cf"
end
end

on_linux do
on_arm do
url "https://github.com/click6067-ship-it/fitllm-engine/releases/download/v2.16.0/fitllm-v2.16.0-linux-arm64"
sha256 "e3f3c56378b09d637e2defcf7f1ef6c644a64bef034b775118b23cc90a5d9461"
end
on_intel do
url "https://github.com/click6067-ship-it/fitllm-engine/releases/download/v2.16.0/fitllm-v2.16.0-linux-x64"
sha256 "2f2e76f61ceb3a8dbb2f966e8e9590a565466f22b10f75312d7459dff52551d0"
end
end

def install
bin.install Dir["*"].first => "fitllm"
end

test do
assert_match "FITS", shell_output("#{bin}/fitllm 'Gemma 4 31b' --gpu 'RTX 5090'")
end
end
112 changes: 112 additions & 0 deletions packaging/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/bin/sh
# FitLLM installer — https://fitllm.run/install.sh
#
# curl -fsSL https://fitllm.run/install.sh | sh
#
# Installs a single self-contained `fitllm` binary. No Node, no package manager.
#
# Design rules, because this script runs as `curl | sh` on other people's machines:
# 1. Fail closed. Unsupported platform, failed download, or a checksum mismatch
# all exit non-zero and install nothing. A wrong binary on PATH is worse than
# no binary on PATH.
# 2. Never write an executable into PATH before its sha256 has been verified
# against the checksum file published with the release.
# 3. No sudo. Default target is a user-writable directory.
#
# Env overrides: FITLLM_VERSION (default: latest release), FITLLM_INSTALL_DIR (default: ~/.local/bin)
set -eu

REPO="click6067-ship-it/fitllm-engine"
INSTALL_DIR="${FITLLM_INSTALL_DIR:-$HOME/.local/bin}"

die() { printf 'fitllm: %s\n' "$*" >&2; exit 1; }
info() { printf '%s\n' "$*" >&2; }

need() { command -v "$1" >/dev/null 2>&1 || die "required command not found: $1"; }
need uname
need mkdir
need mv
need chmod

if command -v curl >/dev/null 2>&1; then
fetch() { curl -fsSL "$1" -o "$2"; }
fetch_stdout() { curl -fsSL "$1"; }
elif command -v wget >/dev/null 2>&1; then
fetch() { wget -qO "$2" "$1"; }
fetch_stdout() { wget -qO- "$1"; }
else
die "need curl or wget"
fi

# --- platform ---------------------------------------------------------------
os_raw="$(uname -s)"
arch_raw="$(uname -m)"

case "$os_raw" in
Darwin) os="darwin" ;;
Linux) os="linux" ;;
*) die "unsupported operating system: $os_raw (supported: Darwin, Linux; on Windows use the .exe from the releases page or 'scoop install fitllm')" ;;
esac

case "$arch_raw" in
arm64|aarch64) arch="arm64" ;;
x86_64|amd64) arch="x64" ;;
*) die "unsupported architecture: $arch_raw (supported: arm64, x86_64)" ;;
esac

# musl vs glibc. Getting this wrong produces a binary that dies with a loader
# error at first run, so detect rather than assume.
libc=""
if [ "$os" = "linux" ]; then
if [ -f /etc/alpine-release ]; then
libc="-musl"
elif command -v ldd >/dev/null 2>&1 && ldd --version 2>&1 | grep -qi musl; then
libc="-musl"
fi
fi

# --- version ----------------------------------------------------------------
version="${FITLLM_VERSION:-}"
if [ -z "$version" ]; then
version="$(fetch_stdout "https://api.github.com/repos/$REPO/releases/latest" \
| sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"v\{0,1\}\([^"]*\)".*/\1/p' \
| head -n 1)"
fi
[ -n "$version" ] || die "could not determine the latest release version; set FITLLM_VERSION=x.y.z"

asset="fitllm-v${version}-${os}-${arch}${libc}"
base="https://github.com/$REPO/releases/download/v${version}/${asset}"

# --- download + verify ------------------------------------------------------
tmp="$(mktemp -d 2>/dev/null || mktemp -d -t fitllm)"
cleanup() { rm -rf "$tmp"; }
trap cleanup EXIT INT TERM

info "fitllm ${version} — ${os}/${arch}${libc}"
fetch "$base" "$tmp/fitllm" || die "download failed: $base"
fetch "$base.sha256" "$tmp/fitllm.sha256" || die "checksum file not found: $base.sha256 (refusing to install an unverified binary)"

expected="$(cut -d' ' -f1 < "$tmp/fitllm.sha256")"
[ -n "$expected" ] || die "checksum file is empty or malformed"

if command -v sha256sum >/dev/null 2>&1; then
actual="$(sha256sum "$tmp/fitllm" | cut -d' ' -f1)"
elif command -v shasum >/dev/null 2>&1; then
actual="$(shasum -a 256 "$tmp/fitllm" | cut -d' ' -f1)"
else
die "need sha256sum or shasum to verify the download"
fi

[ "$actual" = "$expected" ] || die "checksum mismatch — expected $expected, got $actual. Nothing was installed."

# --- install ----------------------------------------------------------------
mkdir -p "$INSTALL_DIR" || die "cannot create $INSTALL_DIR"
chmod 755 "$tmp/fitllm"
mv "$tmp/fitllm" "$INSTALL_DIR/fitllm" || die "cannot write to $INSTALL_DIR (set FITLLM_INSTALL_DIR to a writable path)"

info "installed: $INSTALL_DIR/fitllm"
case ":${PATH}:" in
*":$INSTALL_DIR:"*) info "run: fitllm \"Gemma 4 31b\" --gpu \"RTX 4090\"" ;;
*) info "note: $INSTALL_DIR is not on your PATH. Add it, or run it directly:"
info " $INSTALL_DIR/fitllm \"Gemma 4 31b\" --gpu \"RTX 4090\"" ;;
esac
Loading