Chapter 16: nix, packages, a module and an image

Building a Postgres extension is easy on your own machine and surprisingly hard to do reproducibly: it compiles against one Postgres major's headers, it needs a C toolchain and libclang for the bindings, and it needs a few hundred crates from crates.io. Nix is how this repository pins every one of those, and this folder is everything nix builds from it: the extension for each supported major, the sidecar CLI, a NixOS module that runs a sidecar, a container image for everyone without NixOS, and two virtual machines that check the last two actually work.

The trick in package.nix is that it does not use the usual pgrx builder. The crates here exist only as buck targets, so building with cargo-pgrx would need a second, hand-kept Cargo graph beside them. Instead, the package runs buck2 inside the nix sandbox and builds the very target the tests build, //crates/postjevsql:ext.

flowchart TD
  B["third-party/BUCK: every crate's url and sha256"] -->|"read at eval"| F["nix fetches each crate (fetchurl)"]
  F --> S["the sandbox: no network"]
  O["offline_archive.bzl, loaded over http_archive"] --> S
  S --> X["buck2 build //crates/postjevsql:ext --target-platforms //platforms:pgNN"]
  X --> P["lib/postjevsql.so and share/postgresql/extension/, in nixpkgs' layout"]
  P --> PK["postgresqlNNPackages.postjevsql"]
  PK --> M["nixosModules.sidecar"]
  PK --> I["postjevsql-sidecar-image"]

Aside: three small lies to the sandbox. buck2 was not written to run inside a nix build, and three things had to be smoothed over. Its downloader refuses file:// URLs, so a tiny rule extracts the crates nix already fetched. It loads a trust store at startup even though nothing is downloaded, so SSL_CERT_FILE points at one. And the prelude's scripts start #!/usr/bin/env bash, which the sandbox lacks, so buck runs under bubblewrap with that one path added.

Try it. nix build -L .#checks.x86_64-linux.sidecar boots a VM with the sidecar module and a stand-in managed database (TLS and SCRAM only) and checks that CREATE EXTENSION postjevsql works, the target's tables are imported and readable, a jev call plans as the scan over a ForeignScan, a restart picks up a new target column, and the password never reaches the nix store.

The flake's outputs, all for x86_64-linux:

OutputWhat
legacyPackages.postgresql17Packages.postjevsql, …postgresql18Packages.postjevsqlThe extension per major, named as nixpkgs names its sets.
packages.defaultThe extension for the newest major.
packages.postjevsql-sidecarThe sidecar CLI.
packages.postjevsql-sidecar-imageThe sidecar as a docker load-able image.
nixosModules.sidecarservices.postjevsql-sidecar: a local Postgres with the extension and postgres_fdw, and a oneshot that runs the CLI.
checks.sidecar, checks.sidecar-imageThe module, and the image in docker, each booted in a VM against the same target.
devShells.defaultbuck2, reindeer, rustc, clippy, clang and bindgen's hook, the newest Postgres on PATH, and a shellHook that writes the tools' store paths, and every supported major's server and pg_config, into .buckconfig.local.

For the people who maintain it

FileWhat
package.nixThe extension for one major: the crates fetched from third-party/BUCK's records, src as a fileset of only what the build reads (docs excluded), buck2 under bwrap, and nixpkgs' layout on install. passthru.buck builds another target the same way.
majors.nixThe supported majors, read from PG_MAJORS in build/defs.bzl, the one place they are written.
offline_archive.bzlThe sandbox's stand-in for buck's http_archive: extracts a crate nix fetched. Unused outside the package build.
sidecar-cli.nixThe postjevsql-sidecar binary, built by package.nix's derivation with another target, so the two cannot drift.
sidecar.nixThe NixOS module: services.postgresql with the extension and postgres_fdw, and postjevsql-sidecar.service, which writes one TOML per server and runs converge then sync --apply. Passwords reach it as systemd credentials.
sidecar-image.nixThe OCI image: PostgreSQL 18, the extension, postgres_fdw and the CLI, whose serve is the entrypoint; config and secrets are mounted, never built in.
sidecar-test.nixThe flake check sidecar: the module in a VM.
sidecar-image-test.nixThe flake check sidecar-image: the image in docker in a VM, including a secret given twice stopping the container with the CLI's refusal.
test-target.nixThe stand-in managed database both checks use: Postgres 18 on loopback port 5433, TLS and SCRAM only, with build-time test certificates.

← Previous: Chapter 15, fixtures/ · Up: postjevsql · Next: Chapter 17, tools/ →