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
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ jobs:
run: gleam build

- name: Publish to Hex
run: gleam publish -y
run: echo 'I am not using semantic versioning' | gleam publish -y
env:
HEXPM_API_KEY: ${{ secrets.HEXPM_API_KEY }}

Expand Down
43 changes: 36 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# sqlode

[![Hex](https://img.shields.io/hexpm/v/sqlode)](https://hex.pm/packages/sqlode)
[![Hex Downloads](https://img.shields.io/hexpm/dt/sqlode)](https://hex.pm/packages/sqlode)
[![CI](https://github.com/nao1215/sqlode/actions/workflows/ci.yml/badge.svg)](https://github.com/nao1215/sqlode/actions/workflows/ci.yml)
[![license](https://img.shields.io/github/license/nao1215/sqlode)](./LICENSE)

sqlode reads SQL schema and query files, then generates typed Gleam code. The workflow follows [sqlc](https://sqlc.dev/) conventions: write SQL, run the generator, get type-safe functions.
Expand All @@ -10,15 +13,32 @@ Supported engines: PostgreSQL, MySQL (parsing only), SQLite.

### Install

There are two ways to install sqlode:
sqlode ships as an Erlang escript. Every install path therefore needs an Erlang/OTP runtime on the host (`escript` on PATH). The easiest way to cover both downloading the escript and detecting a missing runtime is the one-line installer:

#### Option A: Standalone CLI (escript)
#### Option A: One-line install (recommended)

Download the pre-built escript from [GitHub Releases](https://github.com/nao1215/sqlode/releases) and place it on your PATH. Requires Erlang/OTP runtime on the host machine.
```console
curl -fsSL https://raw.githubusercontent.com/nao1215/sqlode/main/scripts/install.sh | sh
```

Prefer to inspect the script before executing it? Download it first, read it, then run it:

```console
chmod +x sqlode
./sqlode generate --config=sqlode.yaml
curl -fsSL -o install.sh https://raw.githubusercontent.com/nao1215/sqlode/main/scripts/install.sh
sh install.sh
```

The installer writes the latest release's escript to `$HOME/.local/bin/sqlode`, makes it executable, and warns if Erlang/OTP is missing (with a per-distro install hint).

Environment variables:

- `SQLODE_VERSION=v0.1.0` — pin a specific release tag instead of `latest`.
- `SQLODE_INSTALL_DIR=/path/to/bin` — install into a different directory. System paths such as `/usr/local/bin` require elevated privileges, e.g. `curl -fsSL ... | sudo SQLODE_INSTALL_DIR=/usr/local/bin sh`.

If `$HOME/.local/bin` is not on your `PATH`, add it to your shell config:

```console
export PATH="$HOME/.local/bin:$PATH"
```

You still need sqlode as a project dependency because generated code imports `sqlode/runtime`:
Expand All @@ -27,9 +47,18 @@ You still need sqlode as a project dependency because generated code imports `sq
gleam add sqlode
```

#### Option B: Run via Gleam
#### Option B: Manual escript download

Download the pre-built escript from [GitHub Releases](https://github.com/nao1215/sqlode/releases) and place it on your `PATH`:

```console
chmod +x sqlode
./sqlode generate --config=sqlode.yaml
```

#### Option C: Run via Gleam

Add sqlode as a dependency and invoke the CLI through `gleam run`:
If you already have a Gleam project, you can invoke the CLI through `gleam run` without downloading a separate binary:

```console
gleam add sqlode
Expand Down
161 changes: 161 additions & 0 deletions scripts/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
#!/bin/sh
# One-line installer for sqlode. Downloads the released escript into a
# user-writable directory and verifies it runs.
#
# Usage:
# curl -fsSL https://raw.githubusercontent.com/nao1215/sqlode/main/scripts/install.sh | sh
#
# Environment overrides:
# SQLODE_VERSION - tag to install (default: latest). Example: v0.1.0
# SQLODE_INSTALL_DIR - directory to install into (default: $HOME/.local/bin)

set -eu

REPO="nao1215/sqlode"
BIN_NAME="sqlode"
VERSION="${SQLODE_VERSION:-latest}"
INSTALL_DIR="${SQLODE_INSTALL_DIR:-$HOME/.local/bin}"

DOWNLOAD=""

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

# Clean up any half-downloaded temp file on early exit (ctrl-C, mkdir/mv
# failure, etc). install_bin clears DOWNLOAD after a successful move so the
# trap becomes a no-op.
cleanup() {
[ -n "$DOWNLOAD" ] && [ -e "$DOWNLOAD" ] && rm -f "$DOWNLOAD"
}
trap cleanup EXIT INT HUP TERM

need_cmd() {
command -v "$1" >/dev/null 2>&1 || die "missing required command: $1"
}

need_cmd curl
need_cmd uname

# sqlode ships as an escript, which is a portable Erlang archive. It needs
# `escript` (part of the Erlang/OTP runtime) on the PATH at run time.
detect_distro_id() {
if [ -r /etc/os-release ]; then
# shellcheck disable=SC1091
(. /etc/os-release; printf '%s' "${ID:-}")
fi
}

check_erlang() {
if command -v escript >/dev/null 2>&1; then
return 0
fi

warn "Erlang/OTP runtime not found on PATH."
os="$(uname -s)"
case "$os" in
Linux)
case "$(detect_distro_id)" in
ubuntu|debian) warn "install it with: sudo apt-get install -y erlang" ;;
fedora|rhel|centos) warn "install it with: sudo dnf install -y erlang" ;;
arch|manjaro) warn "install it with: sudo pacman -S --noconfirm erlang" ;;
alpine) warn "install it with: sudo apk add erlang" ;;
*) warn "install Erlang/OTP via your distribution's package manager" ;;
esac
;;
Darwin)
warn "install it with: brew install erlang"
;;
*)
warn "see https://www.erlang.org/downloads for install options"
;;
esac
warn "sqlode will still be downloaded, but you must install Erlang/OTP before running it."
}

resolve_version() {
[ "$VERSION" = "latest" ] || return 0

# Prefer the JSON API because it gives a clean tag name, but it is rate
# limited to 60 req/h for unauthenticated clients. Fall back to parsing
# the Location header from /releases/latest, which is not subject to the
# same limit.
api_url="https://api.github.com/repos/${REPO}/releases/latest"
tag="$(curl -fsSL "$api_url" | sed -n 's/.*"tag_name": *"\([^"]*\)".*/\1/p' | head -n1)"

if [ -z "$tag" ]; then
redirect_url="https://github.com/${REPO}/releases/latest"
tag="$(curl -fsSI "$redirect_url" \
| sed -n 's/^[Ll]ocation: .*\/releases\/tag\/\([^[:space:]]*\).*/\1/p' \
| tr -d '\r' \
| tail -n1)"
fi

if [ -z "$tag" ]; then
die "could not determine latest release tag (GitHub API rate limit or no releases yet). Set SQLODE_VERSION=vX.Y.Z to pin a version."
fi
VERSION="$tag"
}

download() {
url="https://github.com/${REPO}/releases/download/${VERSION}/${BIN_NAME}"
info "downloading ${BIN_NAME} ${VERSION} from ${url}"
tmp="$(mktemp -t sqlode.XXXXXX)"
if ! curl -fsSL -o "$tmp" "$url"; then
rm -f "$tmp"
die "failed to download ${url}. Check SQLODE_VERSION or visit https://github.com/${REPO}/releases."
fi
DOWNLOAD="$tmp"
}
Comment on lines +100 to +109
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Consider adding checksum/signature verification for the downloaded binary.

This script is intended to be run via curl ... | sh, and it then fetches and chmod +xes another binary from the internet without any integrity check. HTTPS from github.com is a reasonable baseline, but a compromised release asset, a proxy MITM, or a partial download (unlikely with -f, but possible) would be silently installed and executed by verify(). Publishing a sqlode.sha256 alongside each release and verifying it here (when sha256sum/shasum is available) would materially improve the security posture of the public install path, especially since this is the path promoted as "recommended" in the README.

Not a blocker for v0.1.0, but worth tracking as a follow-up before adoption grows.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/install.sh` around lines 77 - 86, The download() function currently
saves the release asset without integrity checks; update it to fetch a released
checksum (e.g., ${BIN_NAME}.sha256 or ${VERSION}.sha256) from the same GitHub
release and verify the downloaded file before returning. Modify download() to:
after successful curl of the binary, attempt to curl the checksum file, detect
available verifier (sha256sum or shasum -a 256), validate the binary against the
checksum, rm -f and die on mismatch or missing verifier, and only set
DOWNLOAD="$tmp" if verification passes; keep a clear error message mentioning
the checksum URL and verification failure. Ensure behavior degrades gracefully
(fail fast) when checksum is missing or verification tools are unavailable.


install_bin() {
# Detect non-writable targets up front so the user sees an actionable
# hint instead of a raw `mv: Permission denied` from `set -e`.
if ! mkdir -p "$INSTALL_DIR" 2>/dev/null; then
die "cannot create $INSTALL_DIR. Re-run with sudo (e.g. 'sudo SQLODE_INSTALL_DIR=$INSTALL_DIR sh') or pick a user-writable path like \$HOME/.local/bin."
fi
if [ ! -w "$INSTALL_DIR" ]; then
die "$INSTALL_DIR is not writable. Re-run with sudo (e.g. 'sudo SQLODE_INSTALL_DIR=$INSTALL_DIR sh') or pick a user-writable path like \$HOME/.local/bin."
fi

dest="$INSTALL_DIR/$BIN_NAME"
mv "$DOWNLOAD" "$dest"
chmod +x "$dest"
INSTALLED="$dest"
# Successful move: the temp file no longer exists, so clear the trap
# state to avoid a stray `rm -f` on EXIT.
DOWNLOAD=""
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

verify() {
if ! command -v escript >/dev/null 2>&1; then
info "skipping run check (Erlang/OTP not on PATH yet)"
return 0
fi
if ! "$INSTALLED" --help >/dev/null 2>&1; then
warn "${INSTALLED} was installed but --help failed. Inspect manually."
fi
}

path_hint() {
case ":$PATH:" in
*":$INSTALL_DIR:"*) ;;
*)
warn "$INSTALL_DIR is not on your PATH."
warn "add it to your shell config, e.g. for bash/zsh:"
warn " export PATH=\"$INSTALL_DIR:\$PATH\""
;;
esac
}

main() {
check_erlang
resolve_version
download
install_bin
verify
path_hint
info "installed ${BIN_NAME} ${VERSION} at ${INSTALLED}"
}

main "$@"