1 { fetchgit, fetchurl, lib, writers, python3Packages, runCommand, cargo, jq }:
7 # Cargo lock file contents as string
8 , lockFileContents ? null
10 # Allow `builtins.fetchGit` to be used to not require hashes for git dependencies
11 , allowBuiltinFetchGit ? false
13 # Additional registries to pull sources from
14 # { "https://<registry index URL>" = "https://<registry download URL>"; }
15 # or if the registry is using the new sparse protocol
16 # { "sparse+https://<registry download URL>" = "https://<registry download URL>"; }
18 # - "index URL" is the "index" value of the configuration entry for that registry
19 # https://doc.rust-lang.org/cargo/reference/registries.html#using-an-alternate-registry
20 # - "download URL" is the "dl" value of its associated index configuration
21 # https://doc.rust-lang.org/cargo/reference/registry-index.html#index-configuration
22 , extraRegistries ? {}
24 # Hashes for git dependencies.
28 assert (lockFile == null) != (lockFileContents == null);
31 # Parse a git source into different components.
34 parts = builtins.match ''git\+([^?]+)(\?(rev|tag|branch)=(.*))?#(.*)'' src;
35 type = builtins.elemAt parts 2; # rev, tag or branch
36 value = builtins.elemAt parts 3;
38 if parts == null then null
40 url = builtins.elemAt parts 0;
41 sha = builtins.elemAt parts 4;
42 } // lib.optionalAttrs (type != null) { inherit type value; };
44 # shadows args.lockFileContents
47 then builtins.readFile lockFile
48 else args.lockFileContents;
50 parsedLockFile = builtins.fromTOML lockFileContents;
52 packages = parsedLockFile.package;
54 # There is no source attribute for the source package itself. But
55 # since we do not want to vendor the source package anyway, we can
57 depPackages = builtins.filter (p: p ? "source") packages;
59 # Create dependent crates from packages.
61 # Force evaluation of the git SHA -> hash mapping, so that an error is
62 # thrown if there are stale hashes. We cannot rely on gitShaOutputHash
63 # being evaluated otherwise, since there could be no git dependencies.
64 depCrates = builtins.deepSeq gitShaOutputHash (builtins.map mkCrate depPackages);
66 # Map package name + version to git commit SHA for packages with a git source.
67 namesGitShas = builtins.listToAttrs (
68 builtins.map nameGitSha (builtins.filter (pkg: lib.hasPrefix "git+" pkg.source) depPackages)
71 nameGitSha = pkg: let gitParts = parseGit pkg.source; in {
72 name = "${pkg.name}-${pkg.version}";
76 # Convert the attrset provided through the `outputHashes` argument to a
77 # a mapping from git commit SHA -> output hash.
79 # There may be multiple different packages with different names
80 # originating from the same git repository (typically a Cargo
81 # workspace). By using the git commit SHA as a universal identifier,
82 # the user does not have to specify the output hash for every package
84 gitShaOutputHash = lib.mapAttrs' (nameVer: hash:
86 unusedHash = throw "A hash was specified for ${nameVer}, but there is no corresponding git dependency.";
87 rev = namesGitShas.${nameVer} or unusedHash; in {
92 # We can't use the existing fetchCrate function, since it uses a
93 # recursive hash of the unpacked crate.
94 fetchCrate = pkg: downloadUrl:
96 checksum = pkg.checksum or parsedLockFile.metadata."checksum ${pkg.name} ${pkg.version} (${pkg.source})";
98 assert lib.assertMsg (checksum != null) ''
99 Package ${pkg.name} does not have a checksum.
102 name = "crate-${pkg.name}-${pkg.version}.tar.gz";
103 url = "${downloadUrl}/${pkg.name}/${pkg.version}/download";
108 "https://github.com/rust-lang/crates.io-index" = "https://crates.io/api/v1/crates";
109 } // extraRegistries;
111 # Replaces values inherited by workspace members.
112 replaceWorkspaceValues = writers.writePython3 "replace-workspace-values"
113 { libraries = with python3Packages; [ tomli tomli-w ]; flakeIgnore = [ "E501" "W503" ]; }
114 (builtins.readFile ./replace-workspace-values.py);
116 # Fetch and unpack a crate.
119 gitParts = parseGit pkg.source;
120 registryIndexUrl = lib.removePrefix "registry+" pkg.source;
122 if (lib.hasPrefix "registry+" pkg.source || lib.hasPrefix "sparse+" pkg.source)
123 && builtins.hasAttr registryIndexUrl registries then
125 crateTarball = fetchCrate pkg registries.${registryIndexUrl};
126 in runCommand "${pkg.name}-${pkg.version}" {} ''
128 tar xf "${crateTarball}" -C $out --strip-components=1
130 # Cargo is happy with largely empty metadata.
131 printf '{"files":{},"package":"${crateTarball.outputHash}"}' > "$out/.cargo-checksum.json"
133 else if gitParts != null then
135 missingHash = throw ''
136 No hash was found while vendoring the git dependency ${pkg.name}-${pkg.version}. You can add
137 a hash through the `outputHashes` argument of `importCargoLock`:
140 "${pkg.name}-${pkg.version}" = "<hash>";
143 If you use `buildRustPackage`, you can add this attribute to the `cargoLock`
147 if gitShaOutputHash ? ${gitParts.sha} then
149 inherit (gitParts) url;
150 rev = gitParts.sha; # The commit SHA is always available.
151 sha256 = gitShaOutputHash.${gitParts.sha};
153 else if allowBuiltinFetchGit then
155 inherit (gitParts) url;
162 in runCommand "${pkg.name}-${pkg.version}" {} ''
165 # If the target package is in a workspace, or if it's the top-level
166 # crate, we should find the crate path using `cargo metadata`.
167 # Some packages do not have a Cargo.toml at the top-level,
168 # but only in nested directories.
169 # Only check the top-level Cargo.toml, if it actually exists
170 if [[ -f $tree/Cargo.toml ]]; then
171 crateCargoTOML=$(${cargo}/bin/cargo metadata --format-version 1 --no-deps --manifest-path $tree/Cargo.toml | \
172 ${jq}/bin/jq -r '.packages[] | select(.name == "${pkg.name}") | .manifest_path')
175 # If the repository is not a workspace the package might be in a subdirectory.
176 if [[ -z $crateCargoTOML ]]; then
177 for manifest in $(find $tree -name "Cargo.toml"); do
178 echo Looking at $manifest
179 crateCargoTOML=$(${cargo}/bin/cargo metadata --format-version 1 --no-deps --manifest-path "$manifest" | ${jq}/bin/jq -r '.packages[] | select(.name == "${pkg.name}") | .manifest_path' || :)
180 if [[ ! -z $crateCargoTOML ]]; then
185 if [[ -z $crateCargoTOML ]]; then
186 >&2 echo "Cannot find path for crate '${pkg.name}-${pkg.version}' in the tree in: $tree"
191 echo Found crate ${pkg.name} at $crateCargoTOML
192 tree=$(dirname $crateCargoTOML)
194 cp -prvL "$tree/" $out
197 if grep -q workspace "$out/Cargo.toml"; then
198 chmod u+w "$out/Cargo.toml"
199 ${replaceWorkspaceValues} "$out/Cargo.toml" "$(${cargo}/bin/cargo metadata --format-version 1 --no-deps --manifest-path $crateCargoTOML | ${jq}/bin/jq -r .workspace_root)/Cargo.toml"
202 # Cargo is happy with empty metadata.
203 printf '{"files":{},"package":null}' > "$out/.cargo-checksum.json"
205 # Set up configuration for the vendor directory.
206 cat > $out/.cargo-config <<EOF
207 [source."${gitParts.url}${lib.optionalString (gitParts ? type) "?${gitParts.type}=${gitParts.value}"}"]
208 git = "${gitParts.url}"
209 ${lib.optionalString (gitParts ? type) "${gitParts.type} = \"${gitParts.value}\""}
210 replace-with = "vendored-sources"
213 else throw "Cannot handle crate source: ${pkg.source}";
215 vendorDir = runCommand "cargo-vendor-dir"
216 (if lockFile == null then {
217 inherit lockFileContents;
218 passAsFile = [ "lockFileContents" ];
228 then "ln -s ${lockFile} $out/Cargo.lock"
229 else "cp $lockFileContentsPath $out/Cargo.lock"
232 cat > $out/.cargo/config.toml <<EOF
234 replace-with = "vendored-sources"
236 [source.vendored-sources]
237 directory = "cargo-vendor-dir"
242 for registry in ${toString (builtins.attrNames extraRegistries)}; do
243 cat >> $out/.cargo/config.toml <<EOF
246 registry = "$registry"
247 replace-with = "vendored-sources"
251 for crate in ${toString depCrates}; do
252 # Link the crate directory, removing the output path hash from the destination.
253 ln -s "$crate" $out/$(basename "$crate" | cut -c 34-)
255 if [ -e "$crate/.cargo-config" ]; then
256 key=$(sed 's/\[source\."\(.*\)"\]/\1/; t; d' < "$crate/.cargo-config")
257 if [[ -z ''${keysSeen[$key]} ]]; then
259 cat "$crate/.cargo-config" >> $out/.cargo/config.toml