Skip to content

feat: release standalone binaries for macOS and Linux, with an install script - #320

Merged
mindsers merged 2 commits into
developfrom
feature/standalone-binaries
Oct 14, 2026
Merged

mindsers merged 2 commits into
developfrom
feature/standalone-binaries

Conversation

@mindsers

Copy link
Copy Markdown
Owner

Closes #301.

Context

configfile is a CLI that keeps dotfiles in a git repository and deploys them on each machine. Until now it was installed with npm or Homebrew, which both need Node.js 24.11 or later. That's awkward exactly where configfile is most useful: on a new machine, where nothing is set up yet and installing Node.js is one of the things your dotfiles would do.

This PR makes each release ship a standalone binary per system, with a one-line install:

curl -fsSL https://docs.configfile.sh/install.sh | sh

npm and Homebrew keep working as before.

What changes

The binaries (tools/build-binary.ts, pnpm build:binary)

  • configfile is bundled with esbuild into one CommonJS file and embedded in a copy of the running Node.js as a single executable application (postject, and an ad-hoc signature on macOS).
  • One archive per system: configfile-{macos,linux}-{arm64,x64}.tar.gz, holding configfile, LICENSE and THIRD-PARTY-LICENSES (configfile's bundled dependencies and Node.js itself).
  • The build checks that the binary reports the package's version, and fails on any missing license.
  • NODE_OPTIONS is ignored by the binary (execArgvExtension: "none"): it is meant for the user's Node.js, and a value such as --require ./.pnp.cjs would otherwise stop configfile before it starts.

Source changes needed to run as a binary

  • src/cli.ts: no top-level await, since the binary runs CommonJS. A run whose promise never settles still exits with 13, as a pending top-level await does.
  • src/program.ts: the version comes from a constant defined at build time (CONFIGFILE_VERSION), as there is no package.json next to the binary.
  • src/process.ts: a .js script without a shebang line used to run with configfile's own Node.js. In the binary, that Node.js runs configfile only, so such scripts run with the node in PATH. When there is none, or it can't be executed, the error says so. Nothing changes for npm and Homebrew installs.

The install script (docs/public/install.sh, served by the docs site)

  • POSIX sh, checked by shellcheck in CI.
  • It detects the system (including a shell under Rosetta on Apple silicon, and refuses musl/Alpine and unsupported systems with a pointer to npm).
  • It downloads the archive and checks it against the release's SHA256SUMS.
  • It runs the new binary before replacing anything, so an installed configfile is kept when the new one can't run on the system. Then it moves the binary into place in one step.
  • Default location ~/.local/bin, with CONFIGFILE_INSTALL_DIR, CONFIGFILE_VERSION and CONFIGFILE_DOWNLOAD_URL (for tests and mirrors) as settings. It says when the folder isn't in PATH, when another configfile comes first, and when git is missing.
  • Ctrl+C or a TERM signal stop it (exit 130/143), and it cleans up after itself.

Release workflow (.github/workflows/publish.yml)

  • A new binaries job builds the binary on each of the four systems and runs the end-to-end tests with it.
  • The GitHub release now holds the package tarball, the four archives, SHA256SUMS, and a build provenance attestation for each archive.
  • Re-running the workflow for a tag (to publish to npm later) keeps the files a release already has: a rebuilt archive isn't byte for byte the same, and replacing it would break the checksums users have. Only missing files are added.

CI (.github/workflows/ci.yml)

A new binary job, on the same four systems, on every PR:

  1. builds the binary and checks the archive's contents;
  2. installs it with the real install script, from a local release laid out as on GitHub;
  3. runs the end-to-end tests against the installed binary;
  4. answers configfile init's question on a real terminal (script), with NODE_OPTIONS set to a value that would crash a normal Node.js.

Docs

The installation page, the new-machine guide, the scripts reference, the upgrading-from-0.3 guide, the README and the CHANGELOG. They state the binaries' minimums (macOS 13.5, GNU C library 2.28, inherited from Node.js 24), how to update and uninstall, and the macOS Gatekeeper note for browser downloads.

Compatibility

  • No change for npm and Homebrew users.
  • The curl … | sh command works once a release with binaries is out; 1.0.0 has none (the docs say so). The README on develop shows it already: this assumes the next release follows the merge.
  • The binaries aren't notarized: the install script is not affected, but a binary downloaded with a web browser is quarantined by macOS (documented).

Tests

  • tests/install.test.ts (23 tests): the install script against fake releases and fake uname/sysctl/ldd. They cover:
    • the archive name for every system, checked against the build script's names;
    • Rosetta, Alpine and unsupported systems;
    • checksum mismatches, missing lines and *name lines;
    • a broken new binary keeping the old one;
    • wget instead of curl, and missing tools;
    • a configfile earlier in PATH, and a folder that can't be created;
    • the latest release.
  • tests/process-sea.test.ts: the binary-only .js path: node from PATH, missing, not executable.
  • tests/e2e.test.ts: --version and a .js script end to end. The suite runs with the npm build, and with the binary when CONFIGFILE_BINARY is set (as CI does).
  • pnpm check passes locally (516 tests); shellcheck, actionlint and zizmor are clean.

Manual test

On macOS or Linux:

pnpm install && pnpm build:binary
# Install it as users will, from a local release:
mkdir -p /tmp/rel/download/test && cp release/configfile-*.tar.gz /tmp/rel/download/test/
(cd /tmp/rel/download/test && shasum -a 256 configfile-*.tar.gz > SHA256SUMS)
CONFIGFILE_DOWNLOAD_URL=file:///tmp/rel CONFIGFILE_VERSION=test \
  CONFIGFILE_INSTALL_DIR=/tmp/cf-bin sh docs/public/install.sh
/tmp/cf-bin/configfile --version
NODE_OPTIONS="--require /nonexistent.js" /tmp/cf-bin/configfile --help

The binary is about 120 MB uncompressed (38 MB as an archive), mostly Node.js itself.

…l script

Each release now ships a single-file configfile binary per system (macOS and
Linux, x64 and arm64), built as a Node.js single executable application, so
configfile can be installed on a new machine without Node.js or a package
manager: curl -fsSL https://docs.configfile.sh/install.sh | sh.

Closes #301
On a re-run, the files a release already has are kept, and were attested with the new ones: a file uploaded by hand would then get a build provenance attestation it never earned.
@mindsers
mindsers merged commit 0d04374 into develop Oct 14, 2026
11 checks passed
@mindsers
mindsers deleted the feature/standalone-binaries branch October 14, 2026 20:11
@mindsers mindsers mentioned this pull request Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Standalone binaries and a one-line installer for new machines

1 participant