diff --git a/crate2nix/templates/build.nix.tera b/crate2nix/templates/build.nix.tera index d219ae60..a8af6840 100644 --- a/crate2nix/templates/build.nix.tera +++ b/crate2nix/templates/build.nix.tera @@ -105,6 +105,7 @@ rec { {%- elif crate.source.LocalDirectory.path %} src = (builtins.filterSource sourceFilter {{crate.source.LocalDirectory.path | safe}}); {%- elif crate.source.Git %} + workspace_member = null; src = pkgs.fetchgit { url = {{crate.source.Git.url}}; rev = {{crate.source.Git.rev}}; diff --git a/sample_projects/bin_with_git_branch_dep/Cargo.nix b/sample_projects/bin_with_git_branch_dep/Cargo.nix index 9b06035e..f19ae784 100644 --- a/sample_projects/bin_with_git_branch_dep/Cargo.nix +++ b/sample_projects/bin_with_git_branch_dep/Cargo.nix @@ -98,6 +98,7 @@ rec { crateName = "nix-base32"; version = "0.1.2-alpha.0"; edition = "2018"; + workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/kolloch/nix-base32"; rev = "42f5544e51187f0c7535d453fcffb4b524c99eb2"; diff --git a/sample_projects/bin_with_git_submodule_dep/Cargo.nix b/sample_projects/bin_with_git_submodule_dep/Cargo.nix index b16f350e..c24aeb50 100644 --- a/sample_projects/bin_with_git_submodule_dep/Cargo.nix +++ b/sample_projects/bin_with_git_submodule_dep/Cargo.nix @@ -736,6 +736,7 @@ rec { crateName = "librocksdb-sys"; version = "6.2.4"; edition = "2015"; + workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/rust-rocksdb/rust-rocksdb"; rev = "64bd09899306d23dfd1504465bd1e9adb15fb9ca"; @@ -980,6 +981,7 @@ rec { crateName = "rocksdb"; version = "0.13.0"; edition = "2015"; + workspace_member = null; src = pkgs.fetchgit { url = "https://github.com/rust-rocksdb/rust-rocksdb"; rev = "64bd09899306d23dfd1504465bd1e9adb15fb9ca"; diff --git a/sample_projects/sub_dir_crates/Cargo.lock b/sample_projects/sub_dir_crates/Cargo.lock new file mode 100644 index 00000000..acc3e3a4 --- /dev/null +++ b/sample_projects/sub_dir_crates/Cargo.lock @@ -0,0 +1,19 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "lib1" +version = "0.1.0" +source = "git+https://github.com/kolloch/with_sub_crates.git?rev=f8ad2b98ff0eb5fea4962f55e3ced5b0b5afe973#f8ad2b98ff0eb5fea4962f55e3ced5b0b5afe973" + +[[package]] +name = "lib2" +version = "0.1.0" +source = "git+https://github.com/kolloch/with_sub_crates.git?rev=f8ad2b98ff0eb5fea4962f55e3ced5b0b5afe973#f8ad2b98ff0eb5fea4962f55e3ced5b0b5afe973" + +[[package]] +name = "sub_dir_crates" +version = "0.1.0" +dependencies = [ + "lib1", + "lib2", +] diff --git a/sample_projects/sub_dir_crates/Cargo.nix b/sample_projects/sub_dir_crates/Cargo.nix new file mode 100644 index 00000000..0e75c3d1 --- /dev/null +++ b/sample_projects/sub_dir_crates/Cargo.nix @@ -0,0 +1,661 @@ + +# This file was @generated by crate2nix 0.8.0-alpha.0 with the command: +# "generate" "-f" "sample_projects/sub_dir_crates/Cargo.toml" "-o" "sample_projects/sub_dir_crates/Cargo.nix" +# See https://github.com/kolloch/crate2nix for more info. + +{ nixpkgs ? +, pkgs ? import nixpkgs { config = {}; } +, lib ? pkgs.lib +, stdenv ? pkgs.stdenv +, buildRustCrate ? pkgs.buildRustCrate + # This is used as the `crateOverrides` argument for `buildRustCrate`. +, defaultCrateOverrides ? pkgs.defaultCrateOverrides + # The features to enable for the root_crate or the workspace_members. +, rootFeatures ? [ "default" ] + # If true, throw errors instead of issueing deprecation warnings. +, strictDeprecation ? false +}: + +rec { + # + # "public" attributes that we attempt to keep stable with new versions of crate2nix. + # + + rootCrate = rec { + packageId = "sub_dir_crates"; + + # Use this attribute to refer to the derivation building your root crate package. + # You can override the features with rootCrate.build.override { features = [ "default" "feature1" ... ]; }. + build = internal.buildRustCrateWithFeatures { + inherit packageId; + }; + + # Debug support which might change between releases. + # File a bug if you depend on any for non-debug work! + debug = internal.debugCrate { inherit packageId; }; + }; + root_crate = + internal.deprecationWarning + "root_crate is deprecated since crate2nix 0.4. Please use rootCrate instead." + rootCrate.build; + # Refer your crate build derivation by name here. + # You can override the features with + # workspaceMembers."${crateName}".build.override { features = [ "default" "feature1" ... ]; }. + workspaceMembers = { + "sub_dir_crates" = rec { + packageId = "sub_dir_crates"; + build = internal.buildRustCrateWithFeatures { + packageId = "sub_dir_crates"; + }; + + # Debug support which might change between releases. + # File a bug if you depend on any for non-debug work! + debug = internal.debugCrate { inherit packageId; }; + }; + }; + workspace_members = + internal.deprecationWarning + "workspace_members is deprecated in crate2nix 0.4. Please use workspaceMembers instead." + lib.mapAttrs (n: v: v.build) workspaceMembers; + + # + # "internal" ("private") attributes that may change in every new version of crate2nix. + # + + internal = rec { + # Build and dependency information for crates. + # Many of the fields are passed one-to-one to buildRustCrate. + # + # Noteworthy: + # * `crateBin = [{name = ","; path = ",";}];`: a hack to disable building the binary. + # * `dependencies`/`buildDependencies`: similar to the corresponding fields for buildRustCrate. + # but with additional information which is used during dependency/feature resolution. + # * `resolvedDependencies`: the selected default features reported by cargo - only included for debugging. + # * `devDependencies` as of now not used by `buildRustCrate` but used to + # inject test dependencies into the build + + crates = { + "lib1" = rec { + crateName = "lib1"; + version = "0.1.0"; + edition = "2018"; + workspace_member = null; + src = pkgs.fetchgit { + url = "https://github.com/kolloch/with_sub_crates.git"; + rev = "f8ad2b98ff0eb5fea4962f55e3ced5b0b5afe973"; + sha256 = "0nlw7rg28p6bya040cbipq4jdcdp4h3q9shdjygfk2xkva9bjl8w"; + }; + authors = [ + "Peter Kolloch " + ]; + + }; + "lib2" = rec { + crateName = "lib2"; + version = "0.1.0"; + edition = "2018"; + workspace_member = null; + src = pkgs.fetchgit { + url = "https://github.com/kolloch/with_sub_crates.git"; + rev = "f8ad2b98ff0eb5fea4962f55e3ced5b0b5afe973"; + sha256 = "0nlw7rg28p6bya040cbipq4jdcdp4h3q9shdjygfk2xkva9bjl8w"; + }; + authors = [ + "Peter Kolloch " + ]; + + }; + "sub_dir_crates" = rec { + crateName = "sub_dir_crates"; + version = "0.1.0"; + edition = "2018"; + crateBin = [ + { name = "sub_dir_crates"; path = "src/main.rs"; } + ]; + src = (builtins.filterSource sourceFilter ./.); + authors = [ + "Peter Kolloch " + ]; + dependencies = [ + { + name = "lib1"; + packageId = "lib1"; + } + { + name = "lib2"; + packageId = "lib2"; + } + ]; + + }; + }; + + # +# crate2nix/default.nix (excerpt start) +# + + /* Target (platform) data for conditional dependencies. + This corresponds roughly to what buildRustCrate is setting. + */ + defaultTarget = { + unix = true; + windows = false; + fuchsia = true; + test = false; + + # This doesn't appear to be officially documented anywhere yet. + # See https://github.com/rust-lang-nursery/rust-forge/issues/101. + os = if stdenv.hostPlatform.isDarwin + then "macos" + else stdenv.hostPlatform.parsed.kernel.name; + arch = stdenv.hostPlatform.parsed.cpu.name; + family = "unix"; + env = "gnu"; + endian = + if stdenv.hostPlatform.parsed.cpu.significantByte.name == "littleEndian" + then "little" else "big"; + pointer_width = toString stdenv.hostPlatform.parsed.cpu.bits; + vendor = stdenv.hostPlatform.parsed.vendor.name; + debug_assertions = false; + }; + + /* Filters common temp files and build files. */ + # TODO(pkolloch): Substitute with gitignore filter + sourceFilter = name: type: + let + baseName = builtins.baseNameOf (builtins.toString name); + in + ! ( + # Filter out git + baseName == ".gitignore" + || (type == "directory" && baseName == ".git") + + # Filter out build results + || ( + type == "directory" && ( + baseName == "target" + || baseName == "_site" + || baseName == ".sass-cache" + || baseName == ".jekyll-metadata" + || baseName == "build-artifacts" + ) + ) + + # Filter out nix-build result symlinks + || ( + type == "symlink" && lib.hasPrefix "result" baseName + ) + + # Filter out IDE config + || ( + type == "directory" && ( + baseName == ".idea" || baseName == ".vscode" + ) + ) || lib.hasSuffix ".iml" baseName + + # Filter out nix build files + || baseName == "Cargo.nix" + + # Filter out editor backup / swap files. + || lib.hasSuffix "~" baseName + || builtins.match "^\\.sw[a-z]$$" baseName != null + || builtins.match "^\\..*\\.sw[a-z]$$" baseName != null + || lib.hasSuffix ".tmp" baseName + || lib.hasSuffix ".bak" baseName + || baseName == "tests.nix" + ); + + /* Returns a crate which depends on successful test execution + of crate given as the second argument. + + testCrateFlags: list of flags to pass to the test exectuable + testInputs: list of packages that should be available during test execution + */ + crateWithTest = { crate, testCrate, testCrateFlags, testInputs }: + assert builtins.typeOf testCrateFlags == "list"; + assert builtins.typeOf testInputs == "list"; + let + # override the `crate` so that it will build and execute tests instead of + # building the actual lib and bin targets We just have to pass `--test` + # to rustc and it will do the right thing. We execute the tests and copy + # their log and the test executables to $out for later inspection. + test = let + drv = testCrate.override ( + _: { + buildTests = true; + } + ); + in + pkgs.runCommand "run-tests-${testCrate.name}" { + inherit testCrateFlags; + buildInputs = testInputs; + } '' + set -ex + cd ${crate.src} + for file in ${drv}/tests/*; do + $file $testCrateFlags 2>&1 | tee -a $out + done + ''; + in + crate.overrideAttrs ( + old: { + checkPhase = '' + test -e ${test} + ''; + passthru = (old.passthru or {}) // { + inherit test; + }; + } + ); + + /* A restricted overridable version of builtRustCratesWithFeatures. */ + buildRustCrateWithFeatures = + { packageId + , features ? rootFeatures + , crateOverrides ? defaultCrateOverrides + , buildRustCrateFunc ? ( + if crateOverrides == pkgs.defaultCrateOverrides + then buildRustCrate + else buildRustCrate.override { + defaultCrateOverrides = crateOverrides; + } + ) + , runTests ? false + , testCrateFlags ? [] + , testInputs ? [] + }: + lib.makeOverridable + ( + { features, crateOverrides, runTests, testCrateFlags, testInputs }: + let + builtRustCrates = builtRustCratesWithFeatures { + inherit packageId features buildRustCrateFunc; + runTests = false; + }; + builtTestRustCrates = builtRustCratesWithFeatures { + inherit packageId features buildRustCrateFunc; + runTests = true; + }; + drv = builtRustCrates.${packageId}; + testDrv = builtTestRustCrates.${packageId}; + in + if runTests then + crateWithTest { + crate = drv; + testCrate = testDrv; + inherit testCrateFlags testInputs; + } + else drv + ) + { inherit features crateOverrides runTests testCrateFlags testInputs; }; + + /* Returns an attr set with packageId mapped to the result of buildRustCrateFunc + for the corresponding crate. + */ + builtRustCratesWithFeatures = + { packageId + , features + , crateConfigs ? crates + , buildRustCrateFunc + , runTests + , target ? defaultTarget + } @ args: + assert (builtins.isAttrs crateConfigs); + assert (builtins.isString packageId); + assert (builtins.isList features); + assert (builtins.isAttrs target); + assert (builtins.isBool runTests); + + let + rootPackageId = packageId; + mergedFeatures = mergePackageFeatures ( + args // { + inherit rootPackageId; + target = target // { test = runTests; }; + } + ); + + buildByPackageId = packageId: buildByPackageIdImpl packageId; + + # Memoize built packages so that reappearing packages are only built once. + builtByPackageId = + lib.mapAttrs (packageId: value: buildByPackageId packageId) crateConfigs; + + buildByPackageIdImpl = packageId: + let + features = mergedFeatures."${packageId}" or []; + crateConfig' = crateConfigs."${packageId}"; + crateConfig = + builtins.removeAttrs crateConfig' [ "resolvedDefaultFeatures" "devDependencies" ]; + devDependencies = + lib.optionals + (runTests && packageId == rootPackageId) + (crateConfig'.devDependencies or []); + dependencies = + dependencyDerivations { + inherit builtByPackageId features target; + dependencies = + (crateConfig.dependencies or []) + ++ devDependencies; + }; + buildDependencies = + dependencyDerivations { + inherit builtByPackageId features target; + dependencies = crateConfig.buildDependencies or []; + }; + + dependenciesWithRenames = + lib.filter (d: d ? "rename") ( + (crateConfig.buildDependencies or []) + ++ (crateConfig.dependencies or []) + ++ devDependencies + ); + + crateRenames = + builtins.listToAttrs + (map (d: { name = d.name; value = d.rename; }) dependenciesWithRenames); + in + buildRustCrateFunc ( + crateConfig // { + src = crateConfig.src or ( + pkgs.fetchurl { + name = "${crateConfig.crateName}-${crateConfig.version}.tar.gz"; + url = "https://crates.io/api/v1/crates/${crateConfig.crateName}/${crateConfig.version}/download"; + sha256 = crateConfig.sha256; + } + ); + inherit features dependencies buildDependencies crateRenames; + } + ); + in + builtByPackageId; + + /* Returns the actual derivations for the given dependencies. */ + dependencyDerivations = + { builtByPackageId + , features + , dependencies + , target + }: + assert (builtins.isAttrs builtByPackageId); + assert (builtins.isList features); + assert (builtins.isList dependencies); + assert (builtins.isAttrs target); + + let + enabledDependencies = filterEnabledDependencies { + inherit dependencies features target; + }; + depDerivation = dependency: builtByPackageId.${dependency.packageId}; + in + map depDerivation enabledDependencies; + + /* Returns a sanitized version of val with all values substituted that cannot + be serialized as JSON. + */ + sanitizeForJson = val: + if builtins.isAttrs val + then lib.mapAttrs (n: v: sanitizeForJson v) val + else if builtins.isList val + then builtins.map sanitizeForJson val + else if builtins.isFunction val + then "function" + else val; + + /* Returns various tools to debug a crate. */ + debugCrate = { packageId, target ? defaultTarget }: + assert (builtins.isString packageId); + + let + debug = rec { + # The built tree as passed to buildRustCrate. + buildTree = buildRustCrateWithFeatures { + buildRustCrateFunc = lib.id; + inherit packageId; + }; + sanitizedBuildTree = sanitizeForJson buildTree; + dependencyTree = sanitizeForJson ( + buildRustCrateWithFeatures { + buildRustCrateFunc = crate: { + "01_crateName" = crate.crateName or false; + "02_features" = crate.features or []; + "03_dependencies" = crate.dependencies or []; + }; + inherit packageId; + } + ); + mergedPackageFeatures = mergePackageFeatures { + features = rootFeatures; + inherit packageId target; + }; + diffedDefaultPackageFeatures = diffDefaultPackageFeatures { + inherit packageId target; + }; + }; + in + { internal = debug; }; + + /* Returns differences between cargo default features and crate2nix default + features. + + This is useful for verifying the feature resolution in crate2nix. + */ + diffDefaultPackageFeatures = + { crateConfigs ? crates + , packageId + , target + }: + assert (builtins.isAttrs crateConfigs); + + let + prefixValues = prefix: lib.mapAttrs (n: v: { "${prefix}" = v; }); + mergedFeatures = + prefixValues + "crate2nix" + (mergePackageFeatures { inherit crateConfigs packageId target; features = [ "default" ]; }); + configs = prefixValues "cargo" crateConfigs; + combined = lib.foldAttrs (a: b: a // b) {} [ mergedFeatures configs ]; + onlyInCargo = + builtins.attrNames + (lib.filterAttrs (n: v: !(v ? "crate2nix") && (v ? "cargo")) combined); + onlyInCrate2Nix = + builtins.attrNames + (lib.filterAttrs (n: v: (v ? "crate2nix") && !(v ? "cargo")) combined); + differentFeatures = lib.filterAttrs + ( + n: v: + (v ? "crate2nix") + && (v ? "cargo") + && (v.crate2nix.features or []) != (v."cargo".resolved_default_features or []) + ) + combined; + in + builtins.toJSON { + inherit onlyInCargo onlyInCrate2Nix differentFeatures; + }; + + /* Returns an attrset mapping packageId to the list of enabled features. + + If multiple paths to a dependency enable different features, the + corresponding feature sets are merged. Features in rust are additive. + */ + mergePackageFeatures = + { crateConfigs ? crates + , packageId + , rootPackageId ? packageId + , features ? rootFeatures + , dependencyPath ? [ crates.${packageId}.crateName ] + , featuresByPackageId ? {} + , target + # Adds devDependencies to the crate with rootPackageId. + , runTests ? false + , ... + } @ args: + assert (builtins.isAttrs crateConfigs); + assert (builtins.isString packageId); + assert (builtins.isString rootPackageId); + assert (builtins.isList features); + assert (builtins.isList dependencyPath); + assert (builtins.isAttrs featuresByPackageId); + assert (builtins.isAttrs target); + assert (builtins.isBool runTests); + + let + crateConfig = crateConfigs."${packageId}" or (builtins.throw "Package not found: ${packageId}"); + expandedFeatures = expandFeatures (crateConfig.features or {}) features; + + depWithResolvedFeatures = dependency: + let + packageId = dependency.packageId; + features = dependencyFeatures expandedFeatures dependency; + in + { inherit packageId features; }; + + resolveDependencies = cache: path: dependencies: + assert (builtins.isAttrs cache); + assert (builtins.isList dependencies); + + let + enabledDependencies = filterEnabledDependencies { + inherit dependencies target; + features = expandedFeatures; + }; + directDependencies = map depWithResolvedFeatures enabledDependencies; + foldOverCache = op: lib.foldl op cache directDependencies; + in + foldOverCache + ( + cache: { packageId, features }: + let + cacheFeatures = cache.${packageId} or []; + combinedFeatures = sortedUnique (cacheFeatures ++ features); + in + if cache ? ${packageId} && cache.${packageId} == combinedFeatures + then cache + else mergePackageFeatures { + features = combinedFeatures; + featuresByPackageId = cache; + inherit crateConfigs packageId target runTests rootPackageId; + } + ); + + cacheWithSelf = + let + cacheFeatures = featuresByPackageId.${packageId} or []; + combinedFeatures = sortedUnique (cacheFeatures ++ expandedFeatures); + in + featuresByPackageId // { + "${packageId}" = combinedFeatures; + }; + + cacheWithDependencies = + resolveDependencies cacheWithSelf "dep" ( + crateConfig.dependencies or [] + ++ lib.optionals + (runTests && packageId == rootPackageId) + (crateConfig.devDependencies or []) + ); + + cacheWithAll = + resolveDependencies + cacheWithDependencies "build" + (crateConfig.buildDependencies or []); + + in + cacheWithAll; + + /* Returns the enabled dependencies given the enabled features. */ + filterEnabledDependencies = { dependencies, features, target }: + assert (builtins.isList dependencies); + assert (builtins.isList features); + assert (builtins.isAttrs target); + + lib.filter + ( + dep: + let + targetFunc = dep.target or (features: true); + in + targetFunc { inherit features target; } + && ( + !(dep.optional or false) + || builtins.any (doesFeatureEnableDependency dep) features + ) + ) + dependencies; + + /* Returns whether the given feature should enable the given dependency. */ + doesFeatureEnableDependency = { name, rename ? null, ... }: feature: + let + prefix = "${name}/"; + len = builtins.stringLength prefix; + startsWithPrefix = builtins.substring 0 len feature == prefix; + in + feature == name + || (rename != null && rename == feature) + || startsWithPrefix; + + /* Returns the expanded features for the given inputFeatures by applying the + rules in featureMap. + + featureMap is an attribute set which maps feature names to lists of further + feature names to enable in case this feature is selected. + */ + expandFeatures = featureMap: inputFeatures: + assert (builtins.isAttrs featureMap); + assert (builtins.isList inputFeatures); + + let + expandFeature = feature: + assert (builtins.isString feature); + [ feature ] ++ (expandFeatures featureMap (featureMap."${feature}" or [])); + outFeatures = builtins.concatMap expandFeature inputFeatures; + in + sortedUnique outFeatures; + + /* + Returns the actual dependencies for the given dependency. + + features: The features of the crate that refers this dependency. + */ + dependencyFeatures = features: dependency: + assert (builtins.isList features); + assert (builtins.isAttrs dependency); + + let + defaultOrNil = if dependency.usesDefaultFeatures or true + then [ "default" ] + else []; + explicitFeatures = dependency.features or []; + additionalDependencyFeatures = + let + dependencyPrefix = dependency.name + "/"; + dependencyFeatures = + builtins.filter (f: lib.hasPrefix dependencyPrefix f) features; + in + builtins.map (lib.removePrefix dependencyPrefix) dependencyFeatures; + in + defaultOrNil ++ explicitFeatures ++ additionalDependencyFeatures; + + /* Sorts and removes duplicates from a list of strings. */ + sortedUnique = features: + assert (builtins.isList features); + assert (builtins.all builtins.isString features); + + let + outFeaturesSet = lib.foldl (set: feature: set // { "${feature}" = 1; }) {} features; + outFeaturesUnique = builtins.attrNames outFeaturesSet; + in + builtins.sort (a: b: a < b) outFeaturesUnique; + + deprecationWarning = message: value: + if strictDeprecation + then builtins.throw "strictDeprecation enabled, aborting: ${message}" + else builtins.trace message value; + + # + # crate2nix/default.nix (excerpt end) + # + + }; +} diff --git a/sample_projects/sub_dir_crates/Cargo.toml b/sample_projects/sub_dir_crates/Cargo.toml new file mode 100644 index 00000000..d0a39a4b --- /dev/null +++ b/sample_projects/sub_dir_crates/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "sub_dir_crates" +version = "0.1.0" +authors = ["Peter Kolloch "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +lib1 = { git = "https://github.com/kolloch/with_sub_crates.git", rev="f8ad2b98ff0eb5fea4962f55e3ced5b0b5afe973" } +lib2 = { git = "https://github.com/kolloch/with_sub_crates.git", rev="f8ad2b98ff0eb5fea4962f55e3ced5b0b5afe973" } diff --git a/sample_projects/sub_dir_crates/src/main.rs b/sample_projects/sub_dir_crates/src/main.rs new file mode 100644 index 00000000..4241f7ad --- /dev/null +++ b/sample_projects/sub_dir_crates/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("main with {} {}", lib1::test(), lib2::test()); +} \ No newline at end of file diff --git a/tests.nix b/tests.nix index 13eed6c3..311dd34a 100644 --- a/tests.nix +++ b/tests.nix @@ -89,6 +89,9 @@ let name = "${name}_buildTest"; phases = [ "buildPhase" ]; buildInputs = [ derivation ]; + inherit derivation generatedCargoNix; + + sanitizedBuildTree = debugFile "sanitizedBuildTree"; buildPhase = assert lib.length expectedTestOutputs > 0 -> derivation ? test; '' @@ -195,6 +198,13 @@ let expectedOutput = "Hello, bin_with_rerenamed_lib_dep!"; } + { + name = "sub_dir_crates"; + src = ./sample_projects/sub_dir_crates; + expectedOutput = "main with lib1 lib2"; + pregeneratedBuild = "sample_projects/sub_dir_crates/Cargo.nix"; + } + { name = "cfg_test"; src = ./sample_projects/cfg-test;