From c13e207dc7c3a5843bcbcbabd94107fb51d07eb6 Mon Sep 17 00:00:00 2001 From: Yvan Sraka Date: Sun, 14 Dec 2025 02:14:44 +0100 Subject: [PATCH] feat: add crane support for pgrx extensions This change adds [crane](https://crane.dev/) support to the existing `pgrx` extension builder, enabling better incremental build performance and caching for `pg_jsonschema` and other pgrx extensions. Crane separates dependency builds from main crate builds, allowing dependencies to be cached independently. The unified `buildPgrxExtension.nix` now accepts an optional `craneLib` parameter. When provided, it uses crane's two-phase build (`buildDepsOnly` + `buildPackage`). Otherwise, it falls back to `rustPlatform.buildRustPackage`. Both paths share the same build and install logic, avoiding code duplication. Crane requires `Cargo.lock` at the source root while `rustPlatform` accepts external `lockFile` paths. The builder handles this by ensuring external lock files are properly accessible during the build process. Crane's `cargoVendorDir` is only used for crane builds to avoid conflicts with rustPlatform's cargo handling. Extensions opt into crane builds by passing `useCrane = true` to `mkPgrxExtension`. All existing build parameters remain compatible with both backends. --- flake.lock | 48 +++-- flake.nix | 1 + nix/cargo-pgrx/buildPgrxExtension.nix | 244 ++++++++++++++++++-------- nix/cargo-pgrx/mkPgrxExtension.nix | 20 ++- nix/ext/pg_graphql/default.nix | 9 +- nix/ext/pg_jsonschema/default.nix | 5 +- nix/overlays/default.nix | 5 +- 7 files changed, 238 insertions(+), 94 deletions(-) diff --git a/flake.lock b/flake.lock index 4924941a8..42f98a056 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,20 @@ { "nodes": { + "crane": { + "locked": { + "lastModified": 1765145449, + "narHash": "sha256-aBVHGWWRzSpfL++LubA0CwOOQ64WNLegrYHwsVuVN7A=", + "owner": "ipetkov", + "repo": "crane", + "rev": "69f538cdce5955fcd47abfed4395dc6d5194c1c5", + "type": "github" + }, + "original": { + "owner": "ipetkov", + "repo": "crane", + "type": "github" + } + }, "flake-compat": { "flake": false, "locked": { @@ -21,11 +36,11 @@ "nixpkgs-lib": "nixpkgs-lib" }, "locked": { - "lastModified": 1763759067, - "narHash": "sha256-LlLt2Jo/gMNYAwOgdRQBrsRoOz7BPRkzvNaI/fzXi2Q=", + "lastModified": 1765495779, + "narHash": "sha256-MhA7wmo/7uogLxiewwRRmIax70g6q1U/YemqTGoFHlM=", "owner": "hercules-ci", "repo": "flake-parts", - "rev": "2cccadc7357c0ba201788ae99c4dfa90728ef5e0", + "rev": "5635c32d666a59ec9a55cab87e898889869f7b71", "type": "github" }, "original": { @@ -61,11 +76,11 @@ ] }, "locked": { - "lastModified": 1765016596, - "narHash": "sha256-rhSqPNxDVow7OQKi4qS5H8Au0P4S3AYbawBSmJNUtBQ=", + "lastModified": 1765464257, + "narHash": "sha256-dixPWKiHzh80PtD0aLuxYNQ0xP+843dfXG/yM3OzaYQ=", "owner": "cachix", "repo": "git-hooks.nix", - "rev": "548fc44fca28a5e81c5d6b846e555e6b9c2a5a3c", + "rev": "09e45f2598e1a8499c3594fe11ec2943f34fe509", "type": "github" }, "original": { @@ -166,11 +181,11 @@ ] }, "locked": { - "lastModified": 1761716996, - "narHash": "sha256-vdOuy2pid2/DasUgb08lDOswdPJkN5qjXfBYItVy/R4=", + "lastModified": 1765614563, + "narHash": "sha256-YP867hf3ApYYHZ6ugNRq4HmTwtDXV1tmkkHg6FYOXDU=", "owner": "nlewo", "repo": "nix2container", - "rev": "e5496ab66e9de9e3f67dc06f692dfbc471b6316e", + "rev": "954bfece93c6f1475be983820d860b2f9a39e7ce", "type": "github" }, "original": { @@ -225,11 +240,11 @@ }, "nixpkgs_2": { "locked": { - "lastModified": 1765186076, - "narHash": "sha256-fO54fp5kvvF8znmtV8QGwOsaBcB0NcFP41nC5Bhy/TM=", - "rev": "addf7cf5f383a3101ecfba091b98d0a1263dc9b8", + "lastModified": 1765472234, + "narHash": "sha256-yMgWBfeR/K9Tdk7wSxLZHJ+UUCuPVzu/rk4Ujtp0kDA=", + "rev": "2fbfb1d73d239d2402a8fe03963e37aab15abe8b", "type": "tarball", - "url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre908783.addf7cf5f383/nixexprs.tar.xz?lastModified=1765186076&rev=addf7cf5f383a3101ecfba091b98d0a1263dc9b8" + "url": "https://releases.nixos.org/nixos/unstable/nixos-26.05pre910362.2fbfb1d73d23/nixexprs.tar.xz?lastModified=1765472234&rev=2fbfb1d73d239d2402a8fe03963e37aab15abe8b" }, "original": { "type": "tarball", @@ -238,6 +253,7 @@ }, "root": { "inputs": { + "crane": "crane", "flake-parts": "flake-parts", "flake-utils": "flake-utils", "git-hooks": "git-hooks", @@ -257,11 +273,11 @@ ] }, "locked": { - "lastModified": 1765248027, - "narHash": "sha256-ngar+yP06x3+2k2Iey29uU0DWx5ur06h3iPBQXlU+yI=", + "lastModified": 1765593578, + "narHash": "sha256-qbl874bCIy9+OLImdfBfZ9ITUDDjjTAB04Dk4PlZFV0=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "7b50ad68415ae5be7ee4cc68fa570c420741b644", + "rev": "348b94ed9ddffccdf1a65582a2dcff0a4a3eeeb4", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 595cd2d96..de1cd8233 100644 --- a/flake.nix +++ b/flake.nix @@ -25,6 +25,7 @@ nixpkgs.url = "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz"; rust-overlay.inputs.nixpkgs.follows = "nixpkgs"; rust-overlay.url = "github:oxalica/rust-overlay"; + crane.url = "github:ipetkov/crane"; treefmt-nix.inputs.nixpkgs.follows = "nixpkgs"; treefmt-nix.url = "github:numtide/treefmt-nix"; }; diff --git a/nix/cargo-pgrx/buildPgrxExtension.nix b/nix/cargo-pgrx/buildPgrxExtension.nix index a316015a5..8c31ac540 100644 --- a/nix/cargo-pgrx/buildPgrxExtension.nix +++ b/nix/cargo-pgrx/buildPgrxExtension.nix @@ -29,6 +29,7 @@ { lib, cargo-pgrx, + craneLib ? null, pkg-config, rustPlatform, stdenv, @@ -36,10 +37,18 @@ defaultBindgenHook, }: -# The idea behind: Use it mostly like rustPlatform.buildRustPackage and so -# we hand most of the arguments down. +# Unified pgrx extension builder supporting both rustPlatform and crane. +# When craneLib is provided, uses crane for better incremental builds and caching. +# Otherwise falls back to rustPlatform.buildRustPackage. # -# Additional arguments are: +# Crane separates dependency builds from main crate builds, enabling better caching. +# Both approaches accept the same arguments and produce compatible outputs. +# +# IMPORTANT: External Cargo.lock files are handled by extensions' postPatch phases, +# not by copying during evaluation. This avoids IFD (Import From Derivation) issues +# that caused cross-compilation failures when evaluating aarch64 packages on x86_64. +# +# Additional arguments: # - `postgresql` postgresql package of the version of postgresql this extension should be build for. # Needs to be the build platform variant. # - `useFakeRustfmt` Whether to use a noop fake command as rustfmt. cargo-pgrx tries to call rustfmt. @@ -139,84 +148,177 @@ let pg_ctl stop ''; - argsForBuildRustPackage = builtins.removeAttrs args [ - "postgresql" - "useFakeRustfmt" - "usePgTestCheckFeature" - ]; - - # so we don't accidentally `(rustPlatform.buildRustPackage argsForBuildRustPackage) // { ... }` because - # we forgot parentheses - finalArgs = argsForBuildRustPackage // { - buildInputs = (args.buildInputs or [ ]); - - nativeBuildInputs = - (args.nativeBuildInputs or [ ]) - ++ [ - cargo-pgrx - postgresql - pkg-config - bindgenHook - ] - ++ lib.optionals useFakeRustfmt [ fakeRustfmt ]; - - buildPhase = '' - runHook preBuild - - echo "Executing cargo-pgrx buildPhase" - ${preBuildAndTest} - ${maybeEnterBuildAndTestSubdir} - - export PGRX_BUILD_FLAGS="--frozen -j $NIX_BUILD_CORES ${builtins.concatStringsSep " " cargoBuildFlags}" - export PGX_BUILD_FLAGS="$PGRX_BUILD_FLAGS" - - ${lib.optionalString needsRustcWrapper '' - export ORIGINAL_RUSTC="$(command -v ${stdenv.cc.targetPrefix}rustc || command -v rustc)" - export PATH="${rustcWrapper}/bin:$PATH" - export RUSTC="${rustcWrapper}/bin/rustc" - ''} - - ${lib.optionalString stdenv.hostPlatform.isDarwin ''RUSTFLAGS="''${RUSTFLAGS:+''${RUSTFLAGS} }-Clink-args=-Wl,-undefined,dynamic_lookup"''} \ - cargo ${pgrxBinaryName} package \ - --pg-config ${lib.getDev postgresql}/bin/pg_config \ - ${maybeDebugFlag} \ - --features "${builtins.concatStringsSep " " buildFeatures}" \ - --out-dir "$out" - - ${maybeLeaveBuildAndTestSubdir} - - runHook postBuild - ''; + # Crane-specific: Determine if we're using crane and handle cargo lock info + # Note: External lockfiles are handled by extensions' postPatch, not here, to avoid + # creating platform-specific derivations during evaluation (prevents IFD issues) + useCrane = craneLib != null; + cargoLockInfo = args.cargoLock or null; + + # External Cargo.lock files are handled by the extension's postPatch phase + # which creates symlinks. Crane finds them during build, not evaluation. + # This approach prevents IFD cross-compilation issues. + + # Handle git dependencies based on build system + cargoVendorDir = + if useCrane && cargoLockInfo != null then + # For crane, use vendorCargoDeps with external Cargo.lock file + craneLib.vendorCargoDeps { + src = args.src; + cargoLock = cargoLockInfo.lockFile; + } + else + null; + + # Remove rustPlatform-specific args and pgrx-specific args. + # For crane, also remove build/install phases (added back later). + argsForBuilder = builtins.removeAttrs args ( + [ + "postgresql" + "useFakeRustfmt" + "usePgTestCheckFeature" + ] + ++ lib.optionals useCrane [ + "cargoHash" # rustPlatform uses this, crane uses Cargo.lock directly + "cargoLock" # handled separately via modifiedSrc and cargoVendorDir + "installPhase" # we provide our own pgrx-specific install phase + "buildPhase" # we provide our own pgrx-specific build phase + ] + ); + + # Common arguments for both rustPlatform and crane + commonArgs = + argsForBuilder + // { + src = args.src; # Use original source - extensions handle external lockfiles via postPatch + strictDeps = true; + + buildInputs = (args.buildInputs or [ ]); + + nativeBuildInputs = + (args.nativeBuildInputs or [ ]) + ++ [ + cargo-pgrx + postgresql + pkg-config + bindgenHook + ] + ++ lib.optionals useFakeRustfmt [ fakeRustfmt ]; + + PGRX_PG_SYS_SKIP_BINDING_REWRITE = "1"; + CARGO_BUILD_INCREMENTAL = "false"; + RUST_BACKTRACE = "full"; + + checkNoDefaultFeatures = true; + checkFeatures = + (args.checkFeatures or [ ]) + ++ (lib.optionals usePgTestCheckFeature [ "pg_test" ]) + ++ [ "pg${pgrxPostgresMajor}" ]; + } + // lib.optionalAttrs (cargoVendorDir != null) { + inherit cargoVendorDir; + }; + + # Shared build and install phases for both rustPlatform and crane + sharedBuildPhase = '' + runHook preBuild + + ${preBuildAndTest} + ${maybeEnterBuildAndTestSubdir} + + export PGRX_BUILD_FLAGS="--frozen -j $NIX_BUILD_CORES ${builtins.concatStringsSep " " cargoBuildFlags}" + export PGX_BUILD_FLAGS="$PGRX_BUILD_FLAGS" + + ${lib.optionalString needsRustcWrapper '' + export ORIGINAL_RUSTC="$(command -v ${stdenv.cc.targetPrefix}rustc || command -v rustc)" + export PATH="${rustcWrapper}/bin:$PATH" + export RUSTC="${rustcWrapper}/bin/rustc" + ''} + + ${lib.optionalString stdenv.hostPlatform.isDarwin ''RUSTFLAGS="''${RUSTFLAGS:+''${RUSTFLAGS} }-Clink-args=-Wl,-undefined,dynamic_lookup"''} \ + cargo ${pgrxBinaryName} package \ + --pg-config ${lib.getDev postgresql}/bin/pg_config \ + ${maybeDebugFlag} \ + --features "${builtins.concatStringsSep " " buildFeatures}" \ + --out-dir "$out" + + ${maybeLeaveBuildAndTestSubdir} + + runHook postBuild + ''; + + sharedInstallPhase = '' + runHook preInstall + + ${maybeEnterBuildAndTestSubdir} + cargo-${pgrxBinaryName} ${pgrxBinaryName} stop all + + mv $out/${postgresql}/* $out + mv $out/${postgresql.lib}/* $out + rm -rf $out/nix + + ${maybeLeaveBuildAndTestSubdir} + + runHook postInstall + ''; + + # Arguments for rustPlatform.buildRustPackage + rustPlatformArgs = commonArgs // { + buildPhase = sharedBuildPhase; + installPhase = sharedInstallPhase; preCheck = preBuildAndTest + args.preCheck or ""; + }; - installPhase = '' - runHook preInstall + # Crane's two-phase build: first build dependencies, then build the extension. + # buildDepsOnly creates a derivation containing only Cargo dependency artifacts. + # This is cached separately, so changing extension code doesn't rebuild dependencies. + cargoArtifacts = + if useCrane then + craneLib.buildDepsOnly ( + commonArgs + // { + pname = "${args.pname or "pgrx-extension"}-deps"; - echo "Executing buildPgrxExtension install" + # pgrx-pg-sys needs PGRX_HOME during dependency build + preBuild = '' + ${preBuildAndTest} + ${maybeEnterBuildAndTestSubdir} + '' + + (args.preBuild or ""); - ${maybeEnterBuildAndTestSubdir} + postBuild = '' + ${maybeLeaveBuildAndTestSubdir} + '' + + (args.postBuild or ""); - cargo-${pgrxBinaryName} ${pgrxBinaryName} stop all + # Dependencies don't have a postInstall phase + postInstall = ""; - mv $out/${postgresql}/* $out - mv $out/${postgresql.lib}/* $out - rm -rf $out/nix + # Need to specify PostgreSQL version feature for pgrx dependencies + # and disable default features to avoid multiple pg version conflicts + cargoExtraArgs = "--no-default-features --features ${ + builtins.concatStringsSep "," ([ "pg${pgrxPostgresMajor}" ] ++ buildFeatures) + }"; + } + ) + else + null; - ${maybeLeaveBuildAndTestSubdir} + # Arguments for crane.buildPackage + craneArgs = commonArgs // { + inherit cargoArtifacts; + pname = args.pname or "pgrx-extension"; - runHook postInstall - ''; + # Explicitly preserve postInstall from args (needed for version-specific file renaming) + postInstall = args.postInstall or ""; - PGRX_PG_SYS_SKIP_BINDING_REWRITE = "1"; - CARGO_BUILD_INCREMENTAL = "false"; - RUST_BACKTRACE = "full"; + # We handle installation ourselves via pgrx, don't let crane try to install binaries + doNotInstallCargoBinaries = true; + doNotPostBuildInstallCargoBinaries = true; - checkNoDefaultFeatures = true; - checkFeatures = - (args.checkFeatures or [ ]) - ++ (lib.optionals usePgTestCheckFeature [ "pg_test" ]) - ++ [ "pg${pgrxPostgresMajor}" ]; + buildPhase = sharedBuildPhase; + installPhase = sharedInstallPhase; + preCheck = preBuildAndTest + args.preCheck or ""; }; in -rustPlatform.buildRustPackage finalArgs +if useCrane then craneLib.buildPackage craneArgs else rustPlatform.buildRustPackage rustPlatformArgs diff --git a/nix/cargo-pgrx/mkPgrxExtension.nix b/nix/cargo-pgrx/mkPgrxExtension.nix index c7970451a..a0af6211d 100644 --- a/nix/cargo-pgrx/mkPgrxExtension.nix +++ b/nix/cargo-pgrx/mkPgrxExtension.nix @@ -5,6 +5,9 @@ makeRustPlatform, rust-bin, system, + crane ? null, + useCrane ? false, + pkgs, }: let inherit ((callPackage ./default.nix { inherit rustVersion; })) mkCargoPgrx; @@ -51,9 +54,22 @@ let } else rustPlatform.bindgenHook; + + # Initialize crane with the same Rust toolchain as rustPlatform to ensure consistency. + # crane.mkLib creates a library of crane functions bound to a specific package set, + # then we override the toolchain to match the pgrx-required Rust version. + craneLib = + if useCrane then + let + # Use crane parameter if provided, otherwise get it from pkgs overlay + craneInput = if crane != null then crane else pkgs.crane; + in + (craneInput.mkLib pkgs).overrideToolchain rust-bin.stable.${rustVersion}.default + else + null; in +# Use unified builder that supports both crane and rustPlatform callPackage ./buildPgrxExtension.nix { - inherit rustPlatform; - inherit cargo-pgrx; + inherit rustPlatform cargo-pgrx craneLib; defaultBindgenHook = bindgenHook; } diff --git a/nix/ext/pg_graphql/default.nix b/nix/ext/pg_graphql/default.nix index a7f6d1065..6e51982d1 100644 --- a/nix/ext/pg_graphql/default.nix +++ b/nix/ext/pg_graphql/default.nix @@ -17,6 +17,7 @@ let cargo = rust-bin.stable.${rustVersion}.default; mkPgrxExtension = callPackages ../../cargo-pgrx/mkPgrxExtension.nix { inherit rustVersion pgrxVersion; + useCrane = false; }; src = fetchFromGitHub { owner = "supabase"; @@ -103,11 +104,13 @@ let }; } // lib.optionalAttrs (builtins.compareVersions "1.2.0" version >= 0) { - # Add missing Cargo.lock - patches = [ ./0001-Add-missing-Cargo.lock-${version}.patch ]; + # For crane: Copy the external Cargo.lock into the source so vendorCargoDeps can find it + postPatch = '' + cp ${./Cargo-${version}.lock} Cargo.lock + ''; cargoLock = { - lockFile = ./Cargo-${version}.lock; + inherit lockFile; outputHashes = { "pgx-contrib-spiext-0.1.0" = if (version == "1.2.0") then diff --git a/nix/ext/pg_jsonschema/default.nix b/nix/ext/pg_jsonschema/default.nix index d3a72036f..47bae448f 100644 --- a/nix/ext/pg_jsonschema/default.nix +++ b/nix/ext/pg_jsonschema/default.nix @@ -15,6 +15,7 @@ let cargo = rust-bin.stable.${rustVersion}.default; mkPgrxExtension = callPackages ../../cargo-pgrx/mkPgrxExtension.nix { inherit rustVersion pgrxVersion; + useCrane = true; }; src = fetchFromGitHub { owner = "supabase"; @@ -42,7 +43,9 @@ let ""; nativeBuildInputs = [ cargo ]; - buildInputs = [ postgresql ]; + buildInputs = [ + postgresql + ]; # update the following array when the pg_jsonschema version is updated # required to ensure that extensions update scripts from previous versions are generated previousVersions = [ diff --git a/nix/overlays/default.nix b/nix/overlays/default.nix index a5a18d5c9..782bec9af 100644 --- a/nix/overlays/default.nix +++ b/nix/overlays/default.nix @@ -1,4 +1,4 @@ -{ self, ... }: +{ self, inputs, ... }: { flake.overlays.default = final: _prev: { # NOTE: add any needed overlays here. in theory we could @@ -17,6 +17,9 @@ xmrig = throw "The xmrig package has been explicitly disabled in this flake."; + # Make crane available as pkgs.crane for Rust builds + crane = inputs.crane; + cargo-pgrx = final.callPackage ../cargo-pgrx/default.nix { inherit (final) lib; inherit (final) fetchCrate;