build: package with Nix and publish a static release artifact

packages.default builds natively and runs the suite: 357 tests pass in the
sandbox, including the 60 socket integration tests, which settles the open
question of whether they can bind in a Nix build. The sandbox's private
network namespace has loopback up and the tests take port 0, so there is
nothing to collide with.

packages.static is the published artifact, fully static against musl, so a
consumer needs neither autoPatchelfHook nor a matching glibc. There is no C
dependency anywhere in the tree, not even a TLS library, so unlike bunshin's
sqlite there is nothing to cross-provide. 2.3 MB stripped.

nixpkgs is pinned to unstable and deliberately not followed from a consumer:
every crate here declares rust-version 1.97 and nixos-25.11 ships 1.91.1, so
bucur's nixpkgs could not build this. It does not need to, since it fetches
the release asset.

wml is built in. It is opt-in upstream, and the bundled itsybitsy.toml names
it, so a default build refuses to boot the config shipped beside it.

apps.release-static publishes to Forgejo tagged by short rev, following
bunshin's. Note the asymmetry that bit bunshin: assets upload to
/releases/<id>/assets but download from /releases/download/<tag>/<name>.

THIRD_PARTY_NOTICES.md covers the static link closure, 73 crates across five
license families, plus the two vendored FIGlet fonts, which carry their terms
in the FIGfont comment header and which no crate scan can see. They are behind
the non-default figlet feature and so are not in this binary, but they are
redistributed with the source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
randogoth 2026-10-06 08:46:04 +03:00
parent eebb33fde1
commit dffa42c304
3 changed files with 683 additions and 0 deletions

120
flake.nix Normal file
View file

@ -0,0 +1,120 @@
{
description = "itsybitsy - serve folders of Markdown over HTTP, Gemini, Spartan, Nex and Gopher";
inputs = {
# Pinned independently of any consumer, and deliberately not `follows`-ed.
# Every crate here declares rust-version = "1.97"; nixos-25.11 ships 1.91.1,
# so a consumer's nixpkgs cannot build this. Nothing needs to: bucur fetches
# the release asset that `apps.release-static` publishes.
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs =
{ self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
common = {
pname = "itsybitsy";
version = "0.1.0";
src = ./.;
cargoLock.lockFile = ./Cargo.lock;
# The workspace root is virtual, and cargo refuses `--features` there
# without a package selector, so name the binary crate outright.
cargoBuildFlags = [ "--package" "itsybitsy" ];
# WML is opt-in upstream and the bundled itsybitsy.toml lists it, so a
# default build refuses to boot that config at all. The deployment
# wants it too: WAP handsets are the only client that asks for it, and
# they are exactly the audience a WML face exists for.
buildFeatures = [ "wml" ];
meta = {
description = "Serve folders of Markdown to several domains";
license = pkgs.lib.licenses.asl20;
mainProgram = "itsybitsy";
};
};
in
{
# Native build, and where the test suite runs. The integration tests
# bind 127.0.0.1 on port 0, which the Nix sandbox allows: it has its own
# network namespace with loopback up, so there is nothing to collide
# with and no fixed port to claim.
packages.default = pkgs.rustPlatform.buildRustPackage (
common // { cargoTestFlags = [ "--workspace" ]; }
);
# The published artifact. Fully static against musl, so the consumer
# needs neither autoPatchelfHook nor a matching glibc: there is no C
# dependency anywhere in the tree, not even a TLS library, so unlike
# bunshin's sqlite there is nothing to cross-provide.
packages.static = pkgs.pkgsStatic.rustPlatform.buildRustPackage (
common // { doCheck = false; }
);
apps.default = flake-utils.lib.mkApp {
drv = self.packages.${system}.default;
name = "itsybitsy";
};
# Builds packages.static and publishes it as a Forgejo release tagged by
# short commit hash, creating the release if absent and replacing the
# asset if present. Needs FORGEJO_TOKEN (write:repository) in the
# environment or in .env. Same shape as bunshin's.
#
# Note the asymmetry that bit bunshin: assets are *uploaded* to
# /releases/<id>/assets but *downloaded* from /releases/download/<tag>/
# <name>. The generic package-registry path 404s; the consumer must use
# the download form.
apps.release-static = flake-utils.lib.mkApp {
drv = pkgs.writeShellApplication {
name = "itsybitsy-release-static";
runtimeInputs = [ pkgs.nix pkgs.curl pkgs.git pkgs.jq ];
text = ''
if [ -f .env ]; then
set -a
# shellcheck disable=SC1091
. ./.env
set +a
fi
: "''${FORGEJO_TOKEN:?set FORGEJO_TOKEN (env or .env) to a Forgejo token with write:repository scope}"
rev=$(git rev-parse --short HEAD)
sha=$(git rev-parse HEAD)
out=$(nix build .#static --no-link --print-out-paths)
bin="$out/bin/itsybitsy"
api="https://code.randogoth.com/api/v1/repos/randogoth/itsybitsy"
auth=(-H "Authorization: token ''${FORGEJO_TOKEN}")
release_id=$(curl -sS "''${auth[@]}" "$api/releases/tags/$rev" | jq -r '.id // empty')
if [ -z "$release_id" ]; then
release_id=$(curl -sSf "''${auth[@]}" -H "Content-Type: application/json" \
-d "$(jq -n --arg tag "$rev" --arg sha "$sha" \
'{tag_name:$tag, target_commitish:$sha, name:$tag, body:"Static musl build.", draft:false, prerelease:false}')" \
"$api/releases" | jq -r '.id')
fi
asset_id=$(curl -sSf "''${auth[@]}" "$api/releases/$release_id/assets" | jq -r '.[] | select(.name=="itsybitsy") | .id' | head -1)
if [ -n "$asset_id" ]; then
curl -sSf -X DELETE "''${auth[@]}" "$api/releases/$release_id/assets/$asset_id" >/dev/null
fi
curl -sSf "''${auth[@]}" -F "attachment=@$bin;filename=itsybitsy" "$api/releases/$release_id/assets?name=itsybitsy" >/dev/null
echo "released: https://code.randogoth.com/randogoth/itsybitsy/releases/tag/$rev"
echo "fetch at: https://code.randogoth.com/randogoth/itsybitsy/releases/download/$rev/itsybitsy"
'';
};
};
devShells.default = pkgs.mkShell {
packages = with pkgs; [ cargo rustc rustfmt clippy ];
};
}
);
}