Skip to content

Commit dc2e9f5

Browse files
committed
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 `pg_graphql` 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. Two compatibility issues between crane and `rustPlatform` are handled. First, crane requires `Cargo.lock` at the source root while `rustPlatform` accepts external `lockFile` paths. The builder copies external lock files into the source directory when using crane. Second, crane's `vendorCargoDeps` uses `builtins.fetchGit` which only fetches the default branch, failing when git dependencies reference non-default branches. The builder uses `rustPlatform.importCargoLock` with `allowBuiltinFetchGit` instead, which searches all refs. Extensions opt into crane builds by passing `useCrane = true` to `mkPgrxExtension`. All existing build parameters remain compatible with both backends.
1 parent 7473753 commit dc2e9f5

File tree

7 files changed

+242
-94
lines changed

7 files changed

+242
-94
lines changed

flake.lock

Lines changed: 32 additions & 16 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

flake.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
nixpkgs.url = "https://channels.nixos.org/nixos-unstable/nixexprs.tar.xz";
2626
rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
2727
rust-overlay.url = "github:oxalica/rust-overlay";
28+
crane.url = "github:ipetkov/crane";
2829
treefmt-nix.inputs.nixpkgs.follows = "nixpkgs";
2930
treefmt-nix.url = "github:numtide/treefmt-nix";
3031
};

nix/cargo-pgrx/buildPgrxExtension.nix

Lines changed: 180 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -29,17 +29,26 @@
2929
{
3030
lib,
3131
cargo-pgrx,
32+
craneLib ? null,
3233
pkg-config,
3334
rustPlatform,
3435
stdenv,
3536
writeShellScriptBin,
3637
defaultBindgenHook,
3738
}:
3839

39-
# The idea behind: Use it mostly like rustPlatform.buildRustPackage and so
40-
# we hand most of the arguments down.
40+
# Unified pgrx extension builder supporting both rustPlatform and crane.
41+
# When craneLib is provided, uses crane for better incremental builds and caching.
42+
# Otherwise falls back to rustPlatform.buildRustPackage.
4143
#
42-
# Additional arguments are:
44+
# Crane separates dependency builds from main crate builds, enabling better caching.
45+
# Both approaches accept the same arguments and produce compatible outputs.
46+
#
47+
# IMPORTANT: External Cargo.lock files are handled by extensions' postPatch phases,
48+
# not by copying during evaluation. This avoids IFD (Import From Derivation) issues
49+
# that caused cross-compilation failures when evaluating aarch64 packages on x86_64.
50+
#
51+
# Additional arguments:
4352
# - `postgresql` postgresql package of the version of postgresql this extension should be build for.
4453
# Needs to be the build platform variant.
4554
# - `useFakeRustfmt` Whether to use a noop fake command as rustfmt. cargo-pgrx tries to call rustfmt.
@@ -139,84 +148,184 @@ let
139148
pg_ctl stop
140149
'';
141150

142-
argsForBuildRustPackage = builtins.removeAttrs args [
143-
"postgresql"
144-
"useFakeRustfmt"
145-
"usePgTestCheckFeature"
146-
];
147-
148-
# so we don't accidentally `(rustPlatform.buildRustPackage argsForBuildRustPackage) // { ... }` because
149-
# we forgot parentheses
150-
finalArgs = argsForBuildRustPackage // {
151-
buildInputs = (args.buildInputs or [ ]);
152-
153-
nativeBuildInputs =
154-
(args.nativeBuildInputs or [ ])
155-
++ [
156-
cargo-pgrx
157-
postgresql
158-
pkg-config
159-
bindgenHook
160-
]
161-
++ lib.optionals useFakeRustfmt [ fakeRustfmt ];
162-
163-
buildPhase = ''
164-
runHook preBuild
165-
166-
echo "Executing cargo-pgrx buildPhase"
167-
${preBuildAndTest}
168-
${maybeEnterBuildAndTestSubdir}
169-
170-
export PGRX_BUILD_FLAGS="--frozen -j $NIX_BUILD_CORES ${builtins.concatStringsSep " " cargoBuildFlags}"
171-
export PGX_BUILD_FLAGS="$PGRX_BUILD_FLAGS"
172-
173-
${lib.optionalString needsRustcWrapper ''
174-
export ORIGINAL_RUSTC="$(command -v ${stdenv.cc.targetPrefix}rustc || command -v rustc)"
175-
export PATH="${rustcWrapper}/bin:$PATH"
176-
export RUSTC="${rustcWrapper}/bin/rustc"
177-
''}
178-
179-
${lib.optionalString stdenv.hostPlatform.isDarwin ''RUSTFLAGS="''${RUSTFLAGS:+''${RUSTFLAGS} }-Clink-args=-Wl,-undefined,dynamic_lookup"''} \
180-
cargo ${pgrxBinaryName} package \
181-
--pg-config ${lib.getDev postgresql}/bin/pg_config \
182-
${maybeDebugFlag} \
183-
--features "${builtins.concatStringsSep " " buildFeatures}" \
184-
--out-dir "$out"
185-
186-
${maybeLeaveBuildAndTestSubdir}
187-
188-
runHook postBuild
189-
'';
151+
# Crane-specific: Determine if we're using crane and handle cargo lock info
152+
# Note: External lockfiles are handled by extensions' postPatch, not here, to avoid
153+
# creating platform-specific derivations during evaluation (prevents IFD issues)
154+
useCrane = craneLib != null;
155+
cargoLockInfo = args.cargoLock or null;
156+
157+
# External Cargo.lock files are handled by the extension's postPatch phase
158+
# which creates symlinks. Crane finds them during build, not evaluation.
159+
# This approach prevents IFD cross-compilation issues.
160+
161+
# Handle git dependencies based on build system
162+
cargoVendorDir =
163+
if useCrane && cargoLockInfo != null then
164+
# For crane, use vendorCargoDeps with external Cargo.lock file
165+
craneLib.vendorCargoDeps {
166+
src = args.src;
167+
cargoLock = cargoLockInfo.lockFile;
168+
}
169+
else if cargoLockInfo != null && cargoLockInfo ? outputHashes then
170+
# For rustPlatform, use importCargoLock for git dependencies
171+
rustPlatform.importCargoLock {
172+
lockFile = cargoLockInfo.lockFile;
173+
outputHashes = cargoLockInfo.outputHashes;
174+
allowBuiltinFetchGit = true;
175+
}
176+
else
177+
null;
178+
179+
# Remove rustPlatform-specific args and pgrx-specific args.
180+
# For crane, also remove build/install phases (added back later).
181+
argsForBuilder = builtins.removeAttrs args (
182+
[
183+
"postgresql"
184+
"useFakeRustfmt"
185+
"usePgTestCheckFeature"
186+
]
187+
++ lib.optionals useCrane [
188+
"cargoHash" # rustPlatform uses this, crane uses Cargo.lock directly
189+
"cargoLock" # handled separately via modifiedSrc and cargoVendorDir
190+
"installPhase" # we provide our own pgrx-specific install phase
191+
"buildPhase" # we provide our own pgrx-specific build phase
192+
]
193+
);
194+
195+
# Common arguments for both rustPlatform and crane
196+
commonArgs =
197+
argsForBuilder
198+
// {
199+
src = args.src; # Use original source - extensions handle external lockfiles via postPatch
200+
strictDeps = true;
201+
202+
buildInputs = (args.buildInputs or [ ]);
203+
204+
nativeBuildInputs =
205+
(args.nativeBuildInputs or [ ])
206+
++ [
207+
cargo-pgrx
208+
postgresql
209+
pkg-config
210+
bindgenHook
211+
]
212+
++ lib.optionals useFakeRustfmt [ fakeRustfmt ];
213+
214+
PGRX_PG_SYS_SKIP_BINDING_REWRITE = "1";
215+
CARGO_BUILD_INCREMENTAL = "false";
216+
RUST_BACKTRACE = "full";
217+
218+
checkNoDefaultFeatures = true;
219+
checkFeatures =
220+
(args.checkFeatures or [ ])
221+
++ (lib.optionals usePgTestCheckFeature [ "pg_test" ])
222+
++ [ "pg${pgrxPostgresMajor}" ];
223+
}
224+
// lib.optionalAttrs (cargoVendorDir != null) {
225+
inherit cargoVendorDir;
226+
};
227+
228+
# Shared build and install phases for both rustPlatform and crane
229+
sharedBuildPhase = ''
230+
runHook preBuild
231+
232+
${preBuildAndTest}
233+
${maybeEnterBuildAndTestSubdir}
234+
235+
export PGRX_BUILD_FLAGS="--frozen -j $NIX_BUILD_CORES ${builtins.concatStringsSep " " cargoBuildFlags}"
236+
export PGX_BUILD_FLAGS="$PGRX_BUILD_FLAGS"
237+
238+
${lib.optionalString needsRustcWrapper ''
239+
export ORIGINAL_RUSTC="$(command -v ${stdenv.cc.targetPrefix}rustc || command -v rustc)"
240+
export PATH="${rustcWrapper}/bin:$PATH"
241+
export RUSTC="${rustcWrapper}/bin/rustc"
242+
''}
243+
244+
${lib.optionalString stdenv.hostPlatform.isDarwin ''RUSTFLAGS="''${RUSTFLAGS:+''${RUSTFLAGS} }-Clink-args=-Wl,-undefined,dynamic_lookup"''} \
245+
cargo ${pgrxBinaryName} package \
246+
--pg-config ${lib.getDev postgresql}/bin/pg_config \
247+
${maybeDebugFlag} \
248+
--features "${builtins.concatStringsSep " " buildFeatures}" \
249+
--out-dir "$out"
250+
251+
${maybeLeaveBuildAndTestSubdir}
252+
253+
runHook postBuild
254+
'';
255+
256+
sharedInstallPhase = ''
257+
runHook preInstall
258+
259+
${maybeEnterBuildAndTestSubdir}
190260
261+
cargo-${pgrxBinaryName} ${pgrxBinaryName} stop all
262+
263+
mv $out/${postgresql}/* $out
264+
mv $out/${postgresql.lib}/* $out
265+
rm -rf $out/nix
266+
267+
${maybeLeaveBuildAndTestSubdir}
268+
269+
runHook postInstall
270+
'';
271+
272+
# Arguments for rustPlatform.buildRustPackage
273+
rustPlatformArgs = commonArgs // {
274+
buildPhase = sharedBuildPhase;
275+
installPhase = sharedInstallPhase;
191276
preCheck = preBuildAndTest + args.preCheck or "";
277+
};
192278

193-
installPhase = ''
194-
runHook preInstall
279+
# Crane's two-phase build: first build dependencies, then build the extension.
280+
# buildDepsOnly creates a derivation containing only Cargo dependency artifacts.
281+
# This is cached separately, so changing extension code doesn't rebuild dependencies.
282+
cargoArtifacts =
283+
if useCrane then
284+
craneLib.buildDepsOnly (
285+
commonArgs
286+
// {
287+
pname = "${args.pname or "pgrx-extension"}-deps";
195288

196-
echo "Executing buildPgrxExtension install"
289+
# pgrx-pg-sys needs PGRX_HOME during dependency build
290+
preBuild = ''
291+
${preBuildAndTest}
292+
${maybeEnterBuildAndTestSubdir}
293+
''
294+
+ (args.preBuild or "");
197295

198-
${maybeEnterBuildAndTestSubdir}
296+
postBuild = ''
297+
${maybeLeaveBuildAndTestSubdir}
298+
''
299+
+ (args.postBuild or "");
199300

200-
cargo-${pgrxBinaryName} ${pgrxBinaryName} stop all
301+
# Dependencies don't have a postInstall phase
302+
postInstall = "";
201303

202-
mv $out/${postgresql}/* $out
203-
mv $out/${postgresql.lib}/* $out
204-
rm -rf $out/nix
304+
# Need to specify PostgreSQL version feature for pgrx dependencies
305+
# and disable default features to avoid multiple pg version conflicts
306+
cargoExtraArgs = "--no-default-features --features ${
307+
builtins.concatStringsSep "," ([ "pg${pgrxPostgresMajor}" ] ++ buildFeatures)
308+
}";
309+
}
310+
)
311+
else
312+
null;
205313

206-
${maybeLeaveBuildAndTestSubdir}
314+
# Arguments for crane.buildPackage
315+
craneArgs = commonArgs // {
316+
inherit cargoArtifacts;
317+
pname = args.pname or "pgrx-extension";
207318

208-
runHook postInstall
209-
'';
319+
# Explicitly preserve postInstall from args (needed for version-specific file renaming)
320+
postInstall = args.postInstall or "";
210321

211-
PGRX_PG_SYS_SKIP_BINDING_REWRITE = "1";
212-
CARGO_BUILD_INCREMENTAL = "false";
213-
RUST_BACKTRACE = "full";
322+
# We handle installation ourselves via pgrx, don't let crane try to install binaries
323+
doNotInstallCargoBinaries = true;
324+
doNotPostBuildInstallCargoBinaries = true;
214325

215-
checkNoDefaultFeatures = true;
216-
checkFeatures =
217-
(args.checkFeatures or [ ])
218-
++ (lib.optionals usePgTestCheckFeature [ "pg_test" ])
219-
++ [ "pg${pgrxPostgresMajor}" ];
326+
buildPhase = sharedBuildPhase;
327+
installPhase = sharedInstallPhase;
328+
preCheck = preBuildAndTest + args.preCheck or "";
220329
};
221330
in
222-
rustPlatform.buildRustPackage finalArgs
331+
if useCrane then craneLib.buildPackage craneArgs else rustPlatform.buildRustPackage rustPlatformArgs

0 commit comments

Comments
 (0)