15 mkCrate = buildRustCrate: args: let
17 crateName = "nixtestcrate";
19 authors = [ "Test <test@example.com>" ];
22 mkHostCrate = mkCrate buildRustCrate;
25 { name, crateVersion ? "0.1.0", path ? "Cargo.toml" }:
28 name = ${builtins.toJSON name}
29 version = ${builtins.toJSON crateVersion}
32 mkFile = destination: text: writeTextFile {
34 destination = "/${destination}";
38 mkBin = name: mkFile name ''
41 let name: String = env::args().nth(0).unwrap();
42 println!("executed {}", name);
46 mkBinExtern = name: extern: mkFile name ''
47 extern crate ${extern};
49 assert_eq!(${extern}::test(), 23);
53 mkTestFile = name: functionName: mkFile name ''
56 fn ${functionName}() {
60 mkTestFileWithMain = name: functionName: mkFile name ''
63 fn ${functionName}() {
71 mkLib = name: mkFile name "pub fn test() -> i32 { return 23; }";
73 mkTest = crateArgs: let
74 crate = mkHostCrate (builtins.removeAttrs crateArgs ["expectedTestOutput"]);
75 hasTests = crateArgs.buildTests or false;
76 expectedTestOutputs = crateArgs.expectedTestOutputs or null;
77 binaries = map (v: lib.escapeShellArg v.name) (crateArgs.crateBin or []);
78 isLib = crateArgs ? libName || crateArgs ? libPath;
79 crateName = crateArgs.crateName or "nixtestcrate";
80 libName = crateArgs.libName or crateName;
82 libTestBinary = if !isLib then null else mkHostCrate {
83 crateName = "run-test-${crateName}";
84 dependencies = [ crate ];
85 src = mkBinExtern "src/main.rs" libName;
89 assert expectedTestOutputs != null -> hasTests;
90 assert hasTests -> expectedTestOutputs != null;
92 runCommand "run-buildRustCrate-${crateName}-test" {
93 nativeBuildInputs = [ crate ];
94 } (if !hasTests then ''
95 ${lib.concatMapStringsSep "\n" (binary:
96 # Can't actually run the binary when cross-compiling
97 (lib.optionalString (stdenv.hostPlatform != stdenv.buildPlatform) "type ") + binary
99 ${lib.optionalString isLib ''
100 test -e ${crate}/lib/*.rlib || exit 1
101 ${lib.optionalString (stdenv.hostPlatform != stdenv.buildPlatform) "test -x "} \
102 ${libTestBinary}/bin/run-test-${crateName}
105 '' else if stdenv.hostPlatform == stdenv.buildPlatform then ''
106 for file in ${crate}/tests/*; do
110 ${lib.concatMapStringsSep "\n" (o: "grep '${o}' $out || { echo 'output \"${o}\" not found in:'; cat $out; exit 23; }") expectedTestOutputs}
112 for file in ${crate}/tests/*; do
119 /* Returns a derivation that asserts that the crate specified by `crateArgs`
120 has the specified files as output.
122 `name` is used as part of the derivation name that performs the checking.
124 `mkCrate` can be used to override the `mkCrate` call/implementation to use to
125 override the `buildRustCrate`, useful for cross compilation. Uses `mkHostCrate` by default.
127 `crateArgs` is passed to `mkCrate` to build the crate with `buildRustCrate`
129 `expectedFiles` contains a list of expected file paths in the output. E.g.
130 `[ "./bin/my_binary" ]`.
132 `output` specifies the name of the output to use. By default, the default
133 output is used but e.g. `output = "lib";` will cause the lib output
134 to be checked instead. You do not need to specify any directories.
136 assertOutputs = { name, mkCrate ? mkHostCrate, crateArgs, expectedFiles, output? null, }:
137 assert (builtins.isString name);
138 assert (builtins.isAttrs crateArgs);
139 assert (builtins.isList expectedFiles);
142 crate = mkCrate (builtins.removeAttrs crateArgs ["expectedTestOutput"]);
143 crateOutput = if output == null then crate else crate."${output}";
144 expectedFilesFile = writeTextFile {
145 name = "expected-files-${name}";
147 let sorted = builtins.sort (a: b: a<b) expectedFiles;
148 concatenated = builtins.concatStringsSep "\n" sorted;
149 in "${concatenated}\n";
152 runCommand "assert-outputs-${name}" {
154 local actualFiles=$(mktemp)
160 # sed out the hash because it differs per platform
162 | sed 's/-${crate.metadata}//g' \
164 diff -q ${expectedFilesFile} "$actualFiles" > /dev/null || {
165 echo -e "\033[0;1;31mERROR: Difference in expected output files in ${crateOutput} \033[0m" >&2
167 sed -e 's/^/ /' $actualFiles
169 sed -e 's/^/ /' ${expectedFilesFile}
171 diff -u ${expectedFilesFile} $actualFiles |\
184 libPath = { libPath = "src/my_lib.rs"; src = mkLib "src/my_lib.rs"; };
185 srcLib = { src = mkLib "src/lib.rs"; };
187 # This used to be supported by cargo but as of 1.40.0 I can't make it work like that with just cargo anymore.
188 # This might be a regression or deprecated thing they finally removed…
189 # customLibName = { libName = "test_lib"; src = mkLib "src/test_lib.rs"; };
190 # rustLibTestsCustomLibName = {
191 # libName = "test_lib";
192 # src = mkTestFile "src/test_lib.rs" "foo";
194 # expectedTestOutputs = [ "test foo ... ok" ];
197 customLibNameAndLibPath = { libName = "test_lib"; libPath = "src/best-lib.rs"; src = mkLib "src/best-lib.rs"; };
198 crateBinWithPath = { crateBin = [{ name = "test_binary1"; path = "src/foobar.rs"; }]; src = mkBin "src/foobar.rs"; };
199 crateBinNoPath1 = { crateBin = [{ name = "my-binary2"; }]; src = mkBin "src/my_binary2.rs"; };
201 crateBin = [{ name = "my-binary3"; } { name = "my-binary4"; }];
203 name = "buildRustCrateMultipleBinariesCase";
204 paths = [ (mkBin "src/bin/my_binary3.rs") (mkBin "src/bin/my_binary4.rs") ];
207 crateBinNoPath3 = { crateBin = [{ name = "my-binary5"; }]; src = mkBin "src/bin/main.rs"; };
208 crateBinNoPath4 = { crateBin = [{ name = "my-binary6"; }]; src = mkBin "src/main.rs";};
210 crateBin = [{ name = "my-binary-rename1"; }];
211 src = mkBinExtern "src/main.rs" "foo_renamed";
212 dependencies = [ (mkHostCrate { crateName = "foo"; src = mkLib "src/lib.rs"; }) ];
213 crateRenames = { "foo" = "foo_renamed"; };
216 crateBin = [{ name = "my-binary-rename2"; }];
217 src = mkBinExtern "src/main.rs" "foo_renamed";
218 dependencies = [ (mkHostCrate { crateName = "foo"; libName = "foolib"; src = mkLib "src/lib.rs"; }) ];
219 crateRenames = { "foo" = "foo_renamed"; };
221 crateBinRenameMultiVersion = let
222 crateWithVersion = version: mkHostCrate {
223 crateName = "my_lib";
225 src = mkFile "src/lib.rs" ''
226 pub const version: &str = "${version}";
229 depCrate01 = crateWithVersion "0.1.2";
230 depCrate02 = crateWithVersion "0.2.1";
232 crateName = "my_bin";
236 (mkFile "src/main.rs" ''
238 fn my_lib_01() { assert_eq!(lib01::version, "0.1.2"); }
241 fn my_lib_02() { assert_eq!(lib02::version, "0.2.1"); }
247 dependencies = [ depCrate01 depCrate02 ];
261 expectedTestOutputs = [
262 "test my_lib_01 ... ok"
263 "test my_lib_02 ... ok"
266 rustLibTestsDefault = {
267 src = mkTestFile "src/lib.rs" "baz";
269 expectedTestOutputs = [ "test baz ... ok" ];
271 rustLibTestsCustomLibPath = {
272 libPath = "src/test_path.rs";
273 src = mkTestFile "src/test_path.rs" "bar";
275 expectedTestOutputs = [ "test bar ... ok" ];
277 rustLibTestsCustomLibPathWithTests = {
278 libPath = "src/test_path.rs";
280 name = "rust-lib-tests-custom-lib-path-with-tests-dir";
282 (mkTestFile "src/test_path.rs" "bar")
283 (mkTestFile "tests/something.rs" "something")
287 expectedTestOutputs = [
289 "test something ... ok"
292 rustBinTestsCombined = {
294 name = "rust-bin-tests-combined";
296 (mkTestFileWithMain "src/main.rs" "src_main")
297 (mkTestFile "tests/foo.rs" "tests_foo")
298 (mkTestFile "tests/bar.rs" "tests_bar")
302 expectedTestOutputs = [
303 "test src_main ... ok"
304 "test tests_foo ... ok"
305 "test tests_bar ... ok"
308 rustBinTestsSubdirCombined = {
310 name = "rust-bin-tests-subdir-combined";
312 (mkTestFileWithMain "src/main.rs" "src_main")
313 (mkTestFile "tests/foo/main.rs" "tests_foo")
314 (mkTestFile "tests/bar/main.rs" "tests_bar")
318 expectedTestOutputs = [
319 "test src_main ... ok"
320 "test tests_foo ... ok"
321 "test tests_bar ... ok"
324 linkAgainstRlibCrate = {
326 src = mkFile "src/main.rs" ''
327 extern crate somerlib;
332 crateName = "somerlib";
334 src = mkLib "src/lib.rs";
338 buildScriptDeps = let
339 depCrate = buildRustCrate: boolVal: mkCrate buildRustCrate {
341 src = mkFile "src/lib.rs" ''
342 pub const baz: bool = ${boolVal};
348 name = "build-script-and-main";
350 (mkFile "src/main.rs" ''
354 fn baz_false() { assert!(!bar::baz); }
357 (mkFile "build.rs" ''
359 fn main() { assert!(bar::baz); }
363 buildDependencies = [ (depCrate buildPackages.buildRustCrate "true") ];
364 dependencies = [ (depCrate buildRustCrate "false") ];
366 expectedTestOutputs = [ "test baz_false ... ok" ];
368 buildScriptFeatureEnv = {
369 crateName = "build-script-feature-env";
370 features = [ "some-feature" "crate/another_feature" ];
372 name = "build-script-feature-env";
374 (mkFile "src/main.rs" ''
377 fn feature_not_visible() {
378 assert!(std::env::var("CARGO_FEATURE_SOME_FEATURE").is_err());
379 assert!(option_env!("CARGO_FEATURE_SOME_FEATURE").is_none());
383 (mkFile "build.rs" ''
385 assert!(std::env::var("CARGO_FEATURE_SOME_FEATURE").is_ok());
386 assert!(option_env!("CARGO_FEATURE_SOME_FEATURE").is_none());
392 expectedTestOutputs = [ "test feature_not_visible ... ok" ];
394 # Regression test for https://github.com/NixOS/nixpkgs/pull/88054
395 # Build script output should be rewritten as valid env vars.
396 buildScriptIncludeDirDeps = let
397 depCrate = mkHostCrate {
400 name = "build-script-and-include-dir-bar";
402 (mkFile "src/lib.rs" ''
405 (mkFile "build.rs" ''
406 use std::path::PathBuf;
407 fn main() { println!("cargo:include-dir={}/src", std::env::current_dir().unwrap_or(PathBuf::from(".")).to_str().unwrap()); }
415 name = "build-script-and-include-dir-foo";
417 (mkFile "src/main.rs" ''
420 (mkFile "build.rs" ''
421 fn main() { assert!(std::env::var_os("DEP_BAR_INCLUDE_DIR").is_some()); }
425 buildDependencies = [ depCrate ];
426 dependencies = [ depCrate ];
428 # Support new invocation prefix for build scripts `cargo::`
429 # https://doc.rust-lang.org/cargo/reference/build-scripts.html#outputs-of-the-build-script
430 buildScriptInvocationPrefix = let
431 depCrate = buildRustCrate: mkCrate buildRustCrate {
433 src = mkFile "build.rs" ''
435 // Old invocation prefix
436 // We likely won't see be mixing these syntaxes in the same build script in the wild.
437 println!("cargo:key_old=value_old");
439 // New invocation prefix
440 println!("cargo::metadata=key=value");
441 println!("cargo::metadata=key_complex=complex(value)");
448 name = "build-script-and-main-invocation-prefix";
450 (mkFile "src/main.rs" ''
451 const BUILDFOO: &'static str = env!("BUILDFOO");
454 fn build_foo_check() { assert!(BUILDFOO == "yes(check)"); }
458 (mkFile "build.rs" ''
461 assert!(env::var_os("DEP_BAR_KEY_OLD").expect("metadata key 'key_old' not set in dependency") == "value_old");
462 assert!(env::var_os("DEP_BAR_KEY").expect("metadata key 'key' not set in dependency") == "value");
463 assert!(env::var_os("DEP_BAR_KEY_COMPLEX").expect("metadata key 'key_complex' not set in dependency") == "complex(value)");
465 println!("cargo::rustc-env=BUILDFOO=yes(check)");
470 buildDependencies = [ (depCrate buildPackages.buildRustCrate) ];
471 dependencies = [ (depCrate buildRustCrate) ];
473 expectedTestOutputs = [ "test build_foo_check ... ok" ];
475 # Regression test for https://github.com/NixOS/nixpkgs/issues/74071
476 # Whenevever a build.rs file is generating files those should not be overlayed onto the actual source dir
477 buildRsOutDirOverlay = {
479 name = "buildrs-out-dir-overlay";
482 (mkFile "build.rs" ''
484 use std::ffi::OsString;
488 let out_dir = env::var_os("OUT_DIR").expect("OUT_DIR not set");
489 let out_file = Path::new(&out_dir).join("lib.rs");
490 fs::write(out_file, "invalid rust code!").expect("failed to write lib.rs");
496 # Regression test for https://github.com/NixOS/nixpkgs/pull/83379
497 # link flag order should be preserved
500 name = "buildrs-out-dir-overlay";
502 (mkFile "build.rs" ''
504 // in the other order, linkage will fail
505 println!("cargo:rustc-link-lib=b");
506 println!("cargo:rustc-link-lib=a");
509 (mkFile "src/main.rs" ''
522 compile = name: text: let
523 src = writeTextFile {
524 name = "${name}-src.c";
527 in runCommandCC name {} ''
529 # Note: On darwin (which defaults to clang) we have to add
530 # `-undefined dynamic_lookup` as otherwise the compilation fails.
532 ${lib.optionalString stdenv.hostPlatform.isDarwin "-undefined dynamic_lookup"} \
533 -o $out/lib/${name}${stdenv.hostPlatform.extensions.library} ${src}
535 b = compile "libb" ''
545 a = compile "liba" ''
554 rustCargoTomlInSubDir = {
555 # The "workspace_member" can be set to the sub directory with the crate to build.
556 # By default ".", meaning the top level directory is assumed.
557 # Using null will trigger a search.
558 workspace_member = null;
559 src = symlinkJoin rec {
560 name = "find-cargo-toml";
562 (mkCargoToml { name = "ignoreMe"; })
563 (mkTestFileWithMain "src/main.rs" "ignore_main")
565 (mkCargoToml { name = "rustCargoTomlInSubDir"; path = "subdir/Cargo.toml"; })
566 (mkTestFileWithMain "subdir/src/main.rs" "src_main")
567 (mkTestFile "subdir/tests/foo/main.rs" "tests_foo")
568 (mkTestFile "subdir/tests/bar/main.rs" "tests_bar")
572 expectedTestOutputs = [
573 "test src_main ... ok"
574 "test tests_foo ... ok"
575 "test tests_bar ... ok"
579 rustCargoTomlInTopDir =
581 withoutCargoTomlSearch = builtins.removeAttrs rustCargoTomlInSubDir [ "workspace_member" ];
583 withoutCargoTomlSearch // {
584 expectedTestOutputs = [
585 "test ignore_main ... ok"
588 procMacroInPrelude = {
592 name = "proc-macro-in-prelude";
594 (mkFile "src/lib.rs" ''
595 use proc_macro::TokenTree;
601 brotliCrates = (callPackage ./brotli-crates.nix {});
602 rcgenCrates = callPackage ./rcgen-crates.nix {
603 # Suppress deprecation warning
604 buildRustCrate = null;
606 tests = lib.mapAttrs (key: value: mkTest (value // lib.optionalAttrs (!value?crateName) { crateName = key; })) cases;
609 crateBinWithPathOutputs = assertOutputs {
610 name="crateBinWithPath";
612 crateBin = [{ name = "test_binary1"; path = "src/foobar.rs"; }];
613 src = mkBin "src/foobar.rs";
620 crateBinWithPathOutputsDebug = assertOutputs {
621 name="crateBinWithPath";
624 crateBin = [{ name = "test_binary1"; path = "src/foobar.rs"; }];
625 src = mkBin "src/foobar.rs";
629 ] ++ lib.optionals stdenv.hostPlatform.isDarwin [
630 # On Darwin, the debug symbols are in a separate directory.
631 "./bin/test_binary1.dSYM/Contents/Info.plist"
632 "./bin/test_binary1.dSYM/Contents/Resources/DWARF/test_binary1"
636 crateBinNoPath1Outputs = assertOutputs {
637 name="crateBinNoPath1";
639 crateBin = [{ name = "my-binary2"; }];
640 src = mkBin "src/my_binary2.rs";
647 crateLibOutputs = assertOutputs {
651 libName = "test_lib";
653 libPath = "src/lib.rs";
654 src = mkLib "src/lib.rs";
657 "./nix-support/propagated-build-inputs"
658 "./lib/libtest_lib.rlib"
663 crateLibOutputsDebug = assertOutputs {
668 libName = "test_lib";
670 libPath = "src/lib.rs";
671 src = mkLib "src/lib.rs";
674 "./nix-support/propagated-build-inputs"
675 "./lib/libtest_lib.rlib"
680 crateLibOutputsWasm32 = assertOutputs {
681 name = "wasm32-crate-lib";
683 mkCrate = mkCrate pkgsCross.wasm32-unknown-none.buildRustCrate;
685 libName = "test_lib";
687 libPath = "src/lib.rs";
688 src = mkLib "src/lib.rs";
691 "./nix-support/propagated-build-inputs"
692 "./lib/test_lib.wasm"
698 pkg = brotliCrates.brotli_2_5_0 {};
699 in runCommand "run-brotli-test-cmd" {
700 nativeBuildInputs = [ pkg ];
701 } (if stdenv.hostPlatform == stdenv.buildPlatform then ''
702 ${pkg}/bin/brotli -c ${pkg}/bin/brotli > /dev/null && touch $out
704 test -x '${pkg}/bin/brotli' && touch $out
706 allocNoStdLibTest = let
707 pkg = brotliCrates.alloc_no_stdlib_1_3_0 {};
708 in runCommand "run-alloc-no-stdlib-test-cmd" {
709 nativeBuildInputs = [ pkg ];
711 test -e ${pkg}/bin/example && touch $out
713 brotliDecompressorTest = let
714 pkg = brotliCrates.brotli_decompressor_1_3_1 {};
715 in runCommand "run-brotli-decompressor-test-cmd" {
716 nativeBuildInputs = [ pkg ];
718 test -e ${pkg}/bin/brotli-decompressor && touch $out
722 pkg = rcgenCrates.rootCrate.build;
723 in runCommand "run-rcgen-test-cmd" {
724 nativeBuildInputs = [ pkg ];
725 } (if stdenv.hostPlatform == stdenv.buildPlatform then ''
726 ${pkg}/bin/rcgen && touch $out
728 test -x '${pkg}/bin/rcgen' && touch $out
731 test = releaseTools.aggregate {
732 name = "buildRustCrate-tests";
734 description = "Test cases for buildRustCrate";
737 constituents = builtins.attrValues tests;