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
6 changes: 3 additions & 3 deletions .github/workflows/update-pin.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ jobs:
- name: Exit early if nothing changed
id: diff
run: |
if git diff --quiet flake.nix package-lock.json; then
if git diff --quiet flake.nix; then
echo "changed=false" >> "$GITHUB_OUTPUT"
else
echo "changed=true" >> "$GITHUB_OUTPUT"
Expand All @@ -83,10 +83,10 @@ jobs:
VERSION=$(sed -n 's/^ *version = "\(.*\)";/\1/p' flake.nix | head -1)
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add flake.nix package-lock.json
git add flake.nix
git commit -m "pin @machine0/cli@${VERSION}"
# main may have advanced while we were building; rebase and retry.
# Our commit only touches flake.nix + package-lock.json, so a rebase
# Our commit only touches flake.nix, so a rebase
# conflict means a competing pin update — fail loudly in that case.
# After a rebase the tree is a combination that was never built
# (e.g. a concurrent flake.lock change), so revalidate before
Expand Down
13 changes: 9 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@ A small, standalone [Nix flake](https://nixos.wiki/wiki/Flakes) that packages th
[machine0 CLI](https://machine0.io) (`@machine0/cli`) — no `npm` required on your
machine.

The flake fetches the prebuilt bundle straight from the public npm registry,
installs its two runtime dependencies (`open`, `update-notifier`) from a
vendored `package-lock.json` via `buildNpmPackage`, and runs it with a pinned
Node.js. No build step — the tarball ships a prebuilt bundle.
The flake fetches the prebuilt bundle straight from the public npm registry
and runs it with a pinned Node.js. No build step and no dependency
resolution — the published tarball is a self-contained bundle with no runtime
dependencies, so this is an unpack-and-wrap.

`./update.sh` asserts that on every bump: if a release ever reintroduces
runtime dependencies it refuses to pin, because this packaging has no
`node_modules` and those imports would fail at runtime rather than at build
time. (That regression really shipped — see `flake.nix` for the history.)

## Use it

Expand Down
76 changes: 60 additions & 16 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -12,33 +12,77 @@
pkgs = nixpkgs.legacyPackages.${system};

# --- pin (updated by ./update.sh) ---
version = "1.0.155";
hash = "sha256-KtSdPkCBFogWM1dwl+mqdeSQSxM/NCchtwxv/l1qFp0=";
npmDepsHash = "sha256-yqecaL02z2KoOF+1uD3mqldv8K+3eWiS4tM8EMBO9qw=";
version = "1.0.164";
hash = "sha256-7BLWBUZcV7l4KTwf0RvIn2+z7P1w5Q4KWAwsiA1jHdw=";
# -------------------------------------

machine0 = (pkgs.buildNpmPackage.override { nodejs = pkgs.nodejs_22; }) {
# The published tarball is a self-contained bundle: no runtime
# dependencies, no native modules, nothing to build. So this is an
# unpack-and-wrap, not an npm build.
#
# It used to be a `buildNpmPackage` with a vendored package-lock.json
# and an `npmDepsHash`, because 1.0.147-1.0.163 marked `open` and
# `update-notifier` as `--external` and declared them as runtime
# dependencies (fdmtl/machine0#626, a Windows path fix).
# fdmtl/machine0#733 re-bundled both and moved them back to
# devDependencies, keeping the `--format=esm` flag that was the part
# of #626 actually responsible for the Windows fix.
#
# From 1.0.164 the dependency tree is EMPTY, and buildNpmPackage
# cannot express that: `prefetch-npm-deps` refuses a lockfile with no
# cacheable dependencies ("No cacheable dependencies were found"),
# which is what made ./update.sh abort halfway and leave the pins
# stale at 1.0.155 while reporting the new hash. Rather than set
# `forceEmptyCache` to keep an npm builder that installs nothing,
# drop the builder.
#
# If a future release reintroduces runtime dependencies this package
# breaks LOUDLY at runtime (ERR_MODULE_NOT_FOUND on first invocation),
# not at build time — so ./update.sh asserts the dependency tree is
# empty on every bump.
machine0 = pkgs.stdenvNoCC.mkDerivation {
pname = "machine0-cli";
inherit version npmDepsHash;
inherit version;

src = pkgs.fetchurl {
url = "https://registry.npmjs.org/@machine0/cli/-/cli-${version}.tgz";
inherit hash;
};

# The published package.json carries devDependencies with bun
# `workspace:*` refs that npm cannot parse, and its runtime deps
# (open, update-notifier) are deliberately external to the JS bundle.
# Strip the devDependencies and install just the runtime tree from
# the vendored lockfile (regenerated by ./update.sh on every bump).
postPatch = ''
${pkgs.jq}/bin/jq 'del(.devDependencies)' package.json > package.json.tmp
mv package.json.tmp package.json
cp ${./package-lock.json} package-lock.json
nativeBuildInputs = [ pkgs.makeWrapper ];
dontBuild = true;

installPhase = ''
runHook preInstall
mkdir -p $out/lib/machine0-cli $out/bin
cp -r . $out/lib/machine0-cli
makeWrapper ${pkgs.nodejs_22}/bin/node $out/bin/machine0 \
--add-flags "$out/lib/machine0-cli/bin/entry.cjs"
runHook postInstall
'';

# The tarball ships a prebuilt bundle; there is nothing to build.
dontNpmBuild = true;
# Catch a reintroduced runtime dependency at BUILD time rather than
# on a user's first invocation. `npm pack` output is the same tree
# this derivation installs, so an empty dependency set here is
# exactly the property the unpack-and-wrap relies on.
#
# `optionalDependencies` counts too: npm installs those on a normal
# `npm i -g`, so they would be just as absent from this tree as a
# hard dependency. `peerDependencies` deliberately does not — it is
# the consumer's job to supply those.
doInstallCheck = true;
installCheckPhase = ''
runHook preInstallCheck
deps='((.dependencies // {}) + (.optionalDependencies // {}))'
if ${pkgs.jq}/bin/jq -e "$deps | length > 0" package.json >/dev/null 2>&1; then
echo "ERROR: @machine0/cli@${version} declares runtime dependencies:" >&2
${pkgs.jq}/bin/jq -r "$deps | keys[]" package.json >&2
echo "This derivation unpacks the tarball with no node_modules, so those" >&2
echo "imports would fail at runtime. Restore a dependency-aware builder." >&2
exit 1
fi
runHook postInstallCheck
'';

meta = with pkgs.lib; {
description = "machine0 CLI — cloud VMs from the command line";
Expand Down
Loading
Loading