Skip to content

Commit fc3dfde

Browse files
committed
Self-contained binary
1 parent 6fba298 commit fc3dfde

7 files changed

Lines changed: 458 additions & 1 deletion

File tree

.github/workflows/release.yml

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
name: Release Binaries
2+
3+
# Builds the self-contained `db` CLI binary for each supported platform and
4+
# publishes them (with SHA256 checksums) to a GitHub Release. Triggered by
5+
# pushing a version tag (e.g. v0.2.1); also runnable manually for dry runs.
6+
on:
7+
push:
8+
tags: ["v*"]
9+
workflow_dispatch:
10+
11+
permissions:
12+
contents: write
13+
14+
jobs:
15+
build:
16+
name: Build ${{ matrix.target_os }}-${{ matrix.arch }}
17+
runs-on: ${{ matrix.runner }}
18+
strategy:
19+
fail-fast: false
20+
matrix:
21+
include:
22+
# Built on the oldest practical glibc for forward compatibility.
23+
- runner: ubuntu-22.04
24+
target_os: linux
25+
arch: x86_64
26+
# GitHub-hosted ARM Linux runner: free for public repos; requires a
27+
# Team/Enterprise plan for private repos.
28+
- runner: ubuntu-22.04-arm
29+
target_os: linux
30+
arch: aarch64
31+
- runner: macos-13 # Intel
32+
target_os: darwin
33+
arch: x86_64
34+
- runner: macos-14 # Apple Silicon
35+
target_os: darwin
36+
arch: aarch64
37+
steps:
38+
- uses: actions/checkout@v4
39+
40+
- name: Install uv
41+
uses: astral-sh/setup-uv@v5
42+
with:
43+
python-version: "3.12"
44+
45+
- name: Build binary
46+
run: ./scripts/build_binary.sh --arch ${{ matrix.arch }}
47+
48+
- name: Verify checksum
49+
working-directory: dist
50+
run: |
51+
if command -v sha256sum >/dev/null 2>&1; then
52+
sha256sum -c "db-${{ matrix.target_os }}-${{ matrix.arch }}.sha256"
53+
else
54+
shasum -a 256 -c "db-${{ matrix.target_os }}-${{ matrix.arch }}.sha256"
55+
fi
56+
57+
- name: Upload artifact
58+
uses: actions/upload-artifact@v4
59+
with:
60+
name: db-${{ matrix.target_os }}-${{ matrix.arch }}
61+
path: |
62+
dist/db-${{ matrix.target_os }}-${{ matrix.arch }}
63+
dist/db-${{ matrix.target_os }}-${{ matrix.arch }}.sha256
64+
if-no-files-found: error
65+
66+
release:
67+
name: Publish GitHub Release
68+
needs: build
69+
runs-on: ubuntu-latest
70+
# Only publish for real tag pushes; workflow_dispatch runs just build + verify.
71+
if: startsWith(github.ref, 'refs/tags/')
72+
steps:
73+
- name: Download all artifacts
74+
uses: actions/download-artifact@v4
75+
with:
76+
path: dist
77+
merge-multiple: true
78+
79+
- name: List artifacts
80+
run: ls -lR dist
81+
82+
- name: Publish release
83+
env:
84+
GH_TOKEN: ${{ github.token }}
85+
run: |
86+
set -euo pipefail
87+
tag="${GITHUB_REF_NAME}"
88+
if gh release view "$tag" >/dev/null 2>&1; then
89+
echo "Release $tag exists; uploading assets (clobbering)."
90+
gh release upload "$tag" dist/* --clobber
91+
else
92+
echo "Creating release $tag."
93+
gh release create "$tag" \
94+
--title "$tag" \
95+
--generate-notes \
96+
dist/*
97+
fi

.github/workflows/tests.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,3 +45,26 @@ jobs:
4545

4646
- name: Run tests
4747
run: uv run pytest
48+
49+
# Smoke-test the standalone binary build so scripts/build_binary.sh can't
50+
# silently break between releases. The release workflow builds the full
51+
# cross-platform matrix; here we just prove the x86_64 Linux build works.
52+
build-binary:
53+
runs-on: ubuntu-latest
54+
steps:
55+
- uses: actions/checkout@v4
56+
57+
- name: Install uv
58+
uses: astral-sh/setup-uv@v5
59+
with:
60+
python-version: "3.12"
61+
62+
- name: Build standalone db binary
63+
run: ./scripts/build_binary.sh
64+
65+
- name: Verify checksum
66+
working-directory: dist
67+
run: sha256sum -c db-linux-x86_64.sha256
68+
69+
- name: Smoke-test binary
70+
run: ./dist/db-linux-x86_64 --version

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,20 @@ uv tool install .
202202

203203
This drops a `db` executable into `~/.local/bin` (ensure it is on your `PATH`). Use `--force` to reinstall or upgrade after changes, or `--editable` to have source edits take effect immediately. Alternatively, a plain `pip install .` (or `pip install -e .`) also installs the `db` entry point into the active environment.
204204

205+
### Standalone binary (no Python required)
206+
207+
Every release also ships a self-contained `db` binary for Linux and macOS (x86_64 and aarch64/arm64) — no Python install needed. The installer detects your platform, verifies the SHA256 checksum, and installs (or upgrades) `db` into `~/.local/bin`:
208+
209+
```bash
210+
curl -fsSL https://raw.githubusercontent.com/diffbot/diffbot-python/main/install.sh | sh
211+
```
212+
213+
Pin a specific release or install location with flags (or the `DB_VERSION` / `DB_INSTALL_DIR` environment variables); re-running the installer upgrades an existing install in place:
214+
215+
```bash
216+
curl -fsSL https://raw.githubusercontent.com/diffbot/diffbot-python/main/install.sh | sh -s -- --version v0.2.1 --bin-dir ~/bin
217+
```
218+
205219
```bash
206220
export DIFFBOT_API_TOKEN=your-token-here
207221

install.sh

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
#!/bin/sh
2+
#
3+
# install.sh — Install (or update) the standalone Diffbot `db` CLI binary.
4+
#
5+
# Detects your platform, downloads the matching binary from the latest GitHub
6+
# release, verifies its SHA256 checksum, and installs it to a bin directory on
7+
# your PATH. Re-running upgrades an existing install in place.
8+
#
9+
# Quick start:
10+
# curl -fsSL https://raw.githubusercontent.com/diffbot/diffbot-python/main/install.sh | sh
11+
#
12+
# Options (also settable via env var):
13+
# --version <tag> DB_VERSION Release tag to install (default: latest).
14+
# --bin-dir <dir> DB_INSTALL_DIR Install location (default: ~/.local/bin).
15+
# --repo <owner/repo> DB_REPO Source repo (default: diffbot/diffbot-python).
16+
# -h, --help
17+
#
18+
# Supported platforms: linux/darwin on x86_64 (x64/amd64) and aarch64 (arm64).
19+
set -eu
20+
21+
REPO="${DB_REPO:-diffbot/diffbot-python}"
22+
VERSION="${DB_VERSION:-latest}"
23+
BIN_DIR="${DB_INSTALL_DIR:-${HOME}/.local/bin}"
24+
BIN_NAME="db"
25+
26+
err() { printf 'error: %s\n' "$*" >&2; exit 1; }
27+
info() { printf '%s\n' "$*" >&2; }
28+
29+
# True if $1 is a Python console-script shim (pip / uv tool / pipx / venv),
30+
# i.e. a text shebang wrapper pointing at python. Our standalone binary is an
31+
# ELF/Mach-O file and never starts with "#!", so this never matches it.
32+
is_python_console_script() {
33+
[ -f "$1" ] || return 1
34+
[ "$(dd if="$1" bs=2 count=1 2>/dev/null)" = "#!" ] || return 1
35+
head -n1 "$1" 2>/dev/null | grep -q 'python'
36+
}
37+
38+
while [ $# -gt 0 ]; do
39+
case "$1" in
40+
--version) VERSION="$2"; shift 2 ;;
41+
--version=*) VERSION="${1#*=}"; shift ;;
42+
--bin-dir) BIN_DIR="$2"; shift 2 ;;
43+
--bin-dir=*) BIN_DIR="${1#*=}"; shift ;;
44+
--repo) REPO="$2"; shift 2 ;;
45+
--repo=*) REPO="${1#*=}"; shift ;;
46+
-h | --help) sed -n '2,/^[^#]/p' "$0" | sed 's/^# \{0,1\}//;$d'; exit 0 ;;
47+
*) err "unknown argument: $1 (try --help)" ;;
48+
esac
49+
done
50+
51+
# --- Detect platform -------------------------------------------------------
52+
os="$(uname -s)"
53+
case "$os" in
54+
Linux) os="linux" ;;
55+
Darwin) os="darwin" ;;
56+
*) err "unsupported OS: $os (this installer supports Linux and macOS)" ;;
57+
esac
58+
59+
arch="$(uname -m)"
60+
case "$arch" in
61+
x86_64 | x64 | amd64) arch="x86_64" ;;
62+
aarch64 | arm64) arch="aarch64" ;;
63+
*) err "unsupported architecture: $arch" ;;
64+
esac
65+
66+
asset="${BIN_NAME}-${os}-${arch}"
67+
68+
# --- Pick a download tool --------------------------------------------------
69+
if command -v curl >/dev/null 2>&1; then
70+
download() { curl -fsSL "$1" -o "$2"; }
71+
fetch() { curl -fsSL "$1"; }
72+
elif command -v wget >/dev/null 2>&1; then
73+
download() { wget -qO "$2" "$1"; }
74+
fetch() { wget -qO - "$1"; }
75+
else
76+
err "need curl or wget to download the binary"
77+
fi
78+
79+
# --- Resolve the release tag ----------------------------------------------
80+
if [ "$VERSION" = "latest" ]; then
81+
info "Resolving latest release of ${REPO}..."
82+
api="https://api.github.com/repos/${REPO}/releases/latest"
83+
VERSION="$(fetch "$api" | grep -m1 '"tag_name"' \
84+
| sed -E 's/.*"tag_name"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/')"
85+
[ -n "$VERSION" ] || err "could not determine the latest release tag from ${api}"
86+
fi
87+
88+
base="https://github.com/${REPO}/releases/download/${VERSION}"
89+
info "Installing ${asset} from ${REPO} ${VERSION}"
90+
91+
# --- Download binary + checksum into a temp dir ----------------------------
92+
tmp="$(mktemp -d)"
93+
trap 'rm -rf "$tmp"' EXIT INT TERM
94+
95+
info "Downloading binary..."
96+
download "${base}/${asset}" "${tmp}/${asset}" \
97+
|| err "failed to download ${base}/${asset} (no build for ${os}/${arch} in ${VERSION}?)"
98+
99+
info "Downloading checksum..."
100+
download "${base}/${asset}.sha256" "${tmp}/${asset}.sha256" \
101+
|| err "failed to download checksum ${base}/${asset}.sha256"
102+
103+
# --- Verify checksum -------------------------------------------------------
104+
info "Verifying SHA256 checksum..."
105+
(
106+
cd "$tmp"
107+
if command -v sha256sum >/dev/null 2>&1; then
108+
sha256sum -c "${asset}.sha256"
109+
elif command -v shasum >/dev/null 2>&1; then
110+
shasum -a 256 -c "${asset}.sha256"
111+
else
112+
err "need sha256sum or shasum to verify the download"
113+
fi
114+
) >/dev/null 2>&1 || err "checksum verification failed — refusing to install"
115+
info "Checksum OK."
116+
117+
# --- Install (atomically), updating any existing install -------------------
118+
mkdir -p "$BIN_DIR"
119+
target="${BIN_DIR}/${BIN_NAME}"
120+
121+
if [ -e "$target" ]; then
122+
old="$("$target" --version 2>/dev/null || echo "unknown")"
123+
if is_python_console_script "$target"; then
124+
info ""
125+
info "Warning: ${target} looks like a pip/uv-managed 'db' entry point (${old}),"
126+
info "not a binary installed by this script. Overwriting it replaces that launcher,"
127+
info "but your Python package manager still treats the file as its own — a later"
128+
info "'pip install --upgrade' / 'uv tool upgrade' could clobber it again, and an"
129+
info "uninstall would delete it. To avoid the conflict, remove the managed copy first:"
130+
info " pip uninstall diffbot-python # or: uv tool uninstall diffbot-python"
131+
info ""
132+
info "Proceeding to overwrite ${target}..."
133+
else
134+
info "Updating existing install at ${target} (${old})"
135+
fi
136+
else
137+
info "Installing to ${target}"
138+
fi
139+
140+
chmod +x "${tmp}/${asset}"
141+
# mv within the same filesystem is atomic; fall back to cp for cross-device.
142+
mv -f "${tmp}/${asset}" "$target" 2>/dev/null || cp -f "${tmp}/${asset}" "$target"
143+
144+
new="$("$target" --version 2>/dev/null || echo "$VERSION")"
145+
info ""
146+
info "Installed ${new} -> ${target}"
147+
148+
# --- PATH guidance ---------------------------------------------------------
149+
case ":${PATH}:" in
150+
*":${BIN_DIR}:"*) ;;
151+
*)
152+
info ""
153+
info "Note: ${BIN_DIR} is not on your PATH. Add it, e.g.:"
154+
info " export PATH=\"${BIN_DIR}:\$PATH\""
155+
;;
156+
esac
157+
158+
# Warn if a different `db` shadows the one we just installed.
159+
existing="$(command -v "$BIN_NAME" 2>/dev/null || true)"
160+
if [ -n "$existing" ] && [ "$existing" != "$target" ]; then
161+
info ""
162+
info "Note: another '${BIN_NAME}' is first on your PATH and will take precedence:"
163+
info " ${existing}"
164+
if is_python_console_script "$existing"; then
165+
info " (it looks pip/uv-managed; remove it with 'pip uninstall diffbot-python'"
166+
info " or 'uv tool uninstall diffbot-python', or put ${BIN_DIR} earlier on PATH.)"
167+
fi
168+
fi
169+
170+
info ""
171+
info "Run '${BIN_NAME} --help' to get started."

0 commit comments

Comments
 (0)