Skip to content

Commit c13e207

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 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.
1 parent 7473753 commit c13e207

File tree

7 files changed

+238
-94
lines changed

7 files changed

+238
-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: 173 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,177 @@ 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
170+
null;
171+
172+
# Remove rustPlatform-specific args and pgrx-specific args.
173+
# For crane, also remove build/install phases (added back later).
174+
argsForBuilder = builtins.removeAttrs args (
175+
[
176+
"postgresql"
177+
"useFakeRustfmt"
178+
"usePgTestCheckFeature"
179+
]
180+
++ lib.optionals useCrane [
181+
"cargoHash" # rustPlatform uses this, crane uses Cargo.lock directly
182+
"cargoLock" # handled separately via modifiedSrc and cargoVendorDir
183+
"installPhase" # we provide our own pgrx-specific install phase
184+
"buildPhase" # we provide our own pgrx-specific build phase
185+
]
186+
);
187+
188+
# Common arguments for both rustPlatform and crane
189+
commonArgs =
190+
argsForBuilder
191+
// {
192+
src = args.src; # Use original source - extensions handle external lockfiles via postPatch
193+
strictDeps = true;
194+
195+
buildInputs = (args.buildInputs or [ ]);
196+
197+
nativeBuildInputs =
198+
(args.nativeBuildInputs or [ ])
199+
++ [
200+
cargo-pgrx
201+
postgresql
202+
pkg-config
203+
bindgenHook
204+
]
205+
++ lib.optionals useFakeRustfmt [ fakeRustfmt ];
206+
207+
PGRX_PG_SYS_SKIP_BINDING_REWRITE = "1";
208+
CARGO_BUILD_INCREMENTAL = "false";
209+
RUST_BACKTRACE = "full";
210+
211+
checkNoDefaultFeatures = true;
212+
checkFeatures =
213+
(args.checkFeatures or [ ])
214+
++ (lib.optionals usePgTestCheckFeature [ "pg_test" ])
215+
++ [ "pg${pgrxPostgresMajor}" ];
216+
}
217+
// lib.optionalAttrs (cargoVendorDir != null) {
218+
inherit cargoVendorDir;
219+
};
220+
221+
# Shared build and install phases for both rustPlatform and crane
222+
sharedBuildPhase = ''
223+
runHook preBuild
224+
225+
${preBuildAndTest}
226+
${maybeEnterBuildAndTestSubdir}
227+
228+
export PGRX_BUILD_FLAGS="--frozen -j $NIX_BUILD_CORES ${builtins.concatStringsSep " " cargoBuildFlags}"
229+
export PGX_BUILD_FLAGS="$PGRX_BUILD_FLAGS"
230+
231+
${lib.optionalString needsRustcWrapper ''
232+
export ORIGINAL_RUSTC="$(command -v ${stdenv.cc.targetPrefix}rustc || command -v rustc)"
233+
export PATH="${rustcWrapper}/bin:$PATH"
234+
export RUSTC="${rustcWrapper}/bin/rustc"
235+
''}
236+
237+
${lib.optionalString stdenv.hostPlatform.isDarwin ''RUSTFLAGS="''${RUSTFLAGS:+''${RUSTFLAGS} }-Clink-args=-Wl,-undefined,dynamic_lookup"''} \
238+
cargo ${pgrxBinaryName} package \
239+
--pg-config ${lib.getDev postgresql}/bin/pg_config \
240+
${maybeDebugFlag} \
241+
--features "${builtins.concatStringsSep " " buildFeatures}" \
242+
--out-dir "$out"
243+
244+
${maybeLeaveBuildAndTestSubdir}
245+
246+
runHook postBuild
247+
'';
248+
249+
sharedInstallPhase = ''
250+
runHook preInstall
251+
252+
${maybeEnterBuildAndTestSubdir}
190253
254+
cargo-${pgrxBinaryName} ${pgrxBinaryName} stop all
255+
256+
mv $out/${postgresql}/* $out
257+
mv $out/${postgresql.lib}/* $out
258+
rm -rf $out/nix
259+
260+
${maybeLeaveBuildAndTestSubdir}
261+
262+
runHook postInstall
263+
'';
264+
265+
# Arguments for rustPlatform.buildRustPackage
266+
rustPlatformArgs = commonArgs // {
267+
buildPhase = sharedBuildPhase;
268+
installPhase = sharedInstallPhase;
191269
preCheck = preBuildAndTest + args.preCheck or "";
270+
};
192271

193-
installPhase = ''
194-
runHook preInstall
272+
# Crane's two-phase build: first build dependencies, then build the extension.
273+
# buildDepsOnly creates a derivation containing only Cargo dependency artifacts.
274+
# This is cached separately, so changing extension code doesn't rebuild dependencies.
275+
cargoArtifacts =
276+
if useCrane then
277+
craneLib.buildDepsOnly (
278+
commonArgs
279+
// {
280+
pname = "${args.pname or "pgrx-extension"}-deps";
195281

196-
echo "Executing buildPgrxExtension install"
282+
# pgrx-pg-sys needs PGRX_HOME during dependency build
283+
preBuild = ''
284+
${preBuildAndTest}
285+
${maybeEnterBuildAndTestSubdir}
286+
''
287+
+ (args.preBuild or "");
197288

198-
${maybeEnterBuildAndTestSubdir}
289+
postBuild = ''
290+
${maybeLeaveBuildAndTestSubdir}
291+
''
292+
+ (args.postBuild or "");
199293

200-
cargo-${pgrxBinaryName} ${pgrxBinaryName} stop all
294+
# Dependencies don't have a postInstall phase
295+
postInstall = "";
201296

202-
mv $out/${postgresql}/* $out
203-
mv $out/${postgresql.lib}/* $out
204-
rm -rf $out/nix
297+
# Need to specify PostgreSQL version feature for pgrx dependencies
298+
# and disable default features to avoid multiple pg version conflicts
299+
cargoExtraArgs = "--no-default-features --features ${
300+
builtins.concatStringsSep "," ([ "pg${pgrxPostgresMajor}" ] ++ buildFeatures)
301+
}";
302+
}
303+
)
304+
else
305+
null;
205306

206-
${maybeLeaveBuildAndTestSubdir}
307+
# Arguments for crane.buildPackage
308+
craneArgs = commonArgs // {
309+
inherit cargoArtifacts;
310+
pname = args.pname or "pgrx-extension";
207311

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

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

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

0 commit comments

Comments
 (0)