buildRustCrate: set CARGO_BIN_EXE_<name> for integration tests

Cargo sets `CARGO_BIN_EXE_<name>` when compiling integration tests so
they can locate the crate's own binaries via `env!()` and exec them
as subprocesses. buildRustCrate previously compiled bin targets as
test harnesses only (via `--test`) when buildTests=true, so the real
binary never existed and the env var was never set.

When buildTests is set, now first build the real (non-test) binaries
to `target/cargo-bin-exe/`, install them to `$out/bin/`, and expose
`CARGO_BIN_EXE_<name>` to subsequent rustc invocations via an `env`
prefix. Using `env` rather than bash `export` is necessary because
hyphenated binary names (`CARGO_BIN_EXE_my-crate`) produce env var
names that bash rejects as invalid identifiers; `env` sets them via
execve's envp array directly and has no such restriction.

The env array is populated by iterating `target/cargo-bin-exe/` at
build time, so it covers all binary-target shapes: explicit `crateBin`
entries, auto-detected `src/main.rs`, and auto-detected `src/bin/*.rs`.
`build_bin` in lib.sh gains an optional third argument for the output
directory so the real binary and the `--test` harness can coexist
without colliding in `target/bin/`. The lib-test build is reordered to
after the real-bin build so its compilation also sees the env vars.
This commit is contained in:
Bernardo Meurer Costa
2026-03-23 18:00:15 +00:00
parent 3a33006ef2
commit 9fd2b8c767
4 changed files with 150 additions and 9 deletions
@@ -70,6 +70,25 @@ let
binRustcOpts = lib.concatStringsSep " " baseRustcOpts;
build_bin = if buildTests then "build_bin_test" else "build_bin";
# Shell snippet that builds a binary target to target/cargo-bin-exe/
# so integration tests can exec it via CARGO_BIN_EXE_<name>.
buildBinForTests = bin: ''
mkdir -p target/cargo-bin-exe
BIN_NAME='${bin.name or crateName}'
${
if !bin ? path then
''
BIN_PATH=""
search_for_bin_path "$BIN_NAME"
''
else
''
BIN_PATH='${bin.path}'
''
}
build_bin "$BIN_NAME" "$BIN_PATH" target/cargo-bin-exe
'';
in
''
runHook preBuild
@@ -88,13 +107,60 @@ in
if [[ -e "$LIB_PATH" ]]; then
build_lib "$LIB_PATH"
${lib.optionalString buildTests ''build_lib_test "$LIB_PATH"''}
elif [[ -e src/lib.rs ]]; then
build_lib src/lib.rs
${lib.optionalString buildTests "build_lib_test src/lib.rs"}
fi
${
# When building tests, first build the real (non-test) binaries so
# integration tests can exec them via CARGO_BIN_EXE_<name>. They go
# to target/cargo-bin-exe/ to avoid colliding with the --test
# harnesses written to target/bin/. After building, populate the
# CARGO_BIN_EXE_ENV array so subsequent rustc invocations see the
# env vars (via `env` prefix in lib.sh, which unlike bash `export`
# accepts hyphenated names).
lib.optionalString buildTests (
lib.concatMapStringsSep "\n" (
bin:
let
haveRequiredFeature =
if bin ? requiredFeatures then
lib.intersectLists bin.requiredFeatures crateFeatures == bin.requiredFeatures
else
true;
in
lib.optionalString haveRequiredFeature (buildBinForTests bin)
) crateBin
+ lib.optionalString (lib.length crateBin == 0 && !hasCrateBin) ''
if [[ -e src/main.rs ]]; then
mkdir -p target/cargo-bin-exe
build_bin ${crateName} src/main.rs target/cargo-bin-exe
fi
for i in src/bin/*.rs; do
[ -e "$i" ] || continue
mkdir -p target/cargo-bin-exe
build_bin "$(basename $i .rs)" "$i" target/cargo-bin-exe
done
''
+ ''
if [ -d target/cargo-bin-exe ]; then
for b in target/cargo-bin-exe/*; do
[ -x "$b" ] || continue
name=$(basename "$b")
CARGO_BIN_EXE_ENV+=("CARGO_BIN_EXE_$name=$out/bin/$name")
done
fi
''
)
}
${lib.optionalString buildTests ''
if [[ -e "$LIB_PATH" ]]; then
build_lib_test "$LIB_PATH"
elif [[ -e src/lib.rs ]]; then
build_lib_test src/lib.rs
fi
''}
${lib.optionalString (lib.length crateBin > 0) (
lib.concatMapStringsSep "\n" (
@@ -48,6 +48,12 @@ else
-executable \
-print0 | xargs --no-run-if-empty --null install --target $out/tests;
fi
# Real (non-test) binaries that integration tests may exec via
# CARGO_BIN_EXE_<name>.
if [ -d target/cargo-bin-exe ]; then
mkdir -p $out/bin
cp -rP target/cargo-bin-exe/* $out/bin
fi
runHook postInstall
''
@@ -10,7 +10,7 @@ build_lib() {
lib_src=$1
echo_build_heading $lib_src ${libName}
noisily rustc \
noisily env "${CARGO_BIN_EXE_ENV[@]}" rustc \
--crate-name $CRATE_NAME \
$lib_src \
--out-dir target/lib \
@@ -36,17 +36,18 @@ build_bin() {
local crate_name=$1
local crate_name_=$(echo $crate_name | tr '-' '_')
local main_file=""
local out_dir="${3:-target/bin}"
if [[ ! -z $2 ]]; then
main_file=$2
fi
echo_build_heading $@
noisily rustc \
echo_build_heading $crate_name $main_file
noisily env "${CARGO_BIN_EXE_ENV[@]}" rustc \
--crate-name $crate_name_ \
$main_file \
--crate-type bin \
$BIN_RUSTC_OPTS \
--out-dir target/bin \
--out-dir "$out_dir" \
-L dependency=target/deps \
$LINK \
$EXTRA_LINK_ARGS \
@@ -60,10 +61,10 @@ build_bin() {
--color ${colors} \
if [ "$crate_name_" != "$crate_name" ]; then
if [ -f "target/bin/$crate_name_.wasm" ]; then
mv target/bin/$crate_name_.wasm target/bin/$crate_name.wasm
if [ -f "$out_dir/$crate_name_.wasm" ]; then
mv "$out_dir/$crate_name_.wasm" "$out_dir/$crate_name.wasm"
else
mv target/bin/$crate_name_ target/bin/$crate_name
mv "$out_dir/$crate_name_" "$out_dir/$crate_name"
fi
fi
}
@@ -457,6 +457,74 @@ rec {
"test flat_test ... ok"
];
};
rustBinTestsCargoBinExe = {
# Integration tests locate the crate's own binary via
# `env!("CARGO_BIN_EXE_<name>")`, which cargo sets automatically.
crateName = "my-crate";
src = symlinkJoin {
name = "rust-bin-tests-cargo-bin-exe";
paths = [
(mkFile "src/main.rs" ''
fn main() { println!("hello from my-crate"); }
'')
(mkFile "tests/run_bin.rs" ''
#[test]
fn runs_binary() {
let bin = env!("CARGO_BIN_EXE_my-crate");
let out = std::process::Command::new(bin)
.output()
.expect("spawn");
assert!(out.status.success());
assert_eq!(
String::from_utf8_lossy(&out.stdout).trim(),
"hello from my-crate"
);
}
'')
];
};
buildTests = true;
expectedTestOutputs = [
"test runs_binary ... ok"
];
};
rustBinTestsCargoBinExeAutoDetect = {
# Verify CARGO_BIN_EXE_<name> is also set for auto-detected
# src/bin/*.rs binaries, not just src/main.rs or explicit
# crateBin entries.
crateName = "multi-bin";
src = symlinkJoin {
name = "rust-bin-tests-cargo-bin-exe-auto";
paths = [
(mkFile "src/lib.rs" "")
(mkFile "src/bin/tool-a.rs" ''
fn main() { println!("tool-a ran"); }
'')
(mkFile "src/bin/tool-b.rs" ''
fn main() { println!("tool-b ran"); }
'')
(mkFile "tests/run_tools.rs" ''
#[test]
fn runs_both() {
for (bin, want) in [
(env!("CARGO_BIN_EXE_tool-a"), "tool-a ran"),
(env!("CARGO_BIN_EXE_tool-b"), "tool-b ran"),
] {
let out = std::process::Command::new(bin)
.output()
.expect("spawn");
assert!(out.status.success());
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), want);
}
}
'')
];
};
buildTests = true;
expectedTestOutputs = [
"test runs_both ... ok"
];
};
linkAgainstRlibCrate = {
crateName = "foo";
src = mkFile "src/main.rs" ''