lol
1{ fetchgit, fetchurl, lib, writers, python3Packages, runCommand, cargo, jq }:
2
3{
4 # Cargo lock file
5 lockFile ? null
6
7 # Cargo lock file contents as string
8, lockFileContents ? null
9
10 # Allow `builtins.fetchGit` to be used to not require hashes for git dependencies
11, allowBuiltinFetchGit ? false
12
13 # Additional registries to pull sources from
14 # { "https://<registry index URL>" = "https://<registry download URL>"; }
15 # where:
16 # - "index URL" is the "index" value of the configuration entry for that registry
17 # https://doc.rust-lang.org/cargo/reference/registries.html#using-an-alternate-registry
18 # - "download URL" is the "dl" value of its associated index configuration
19 # https://doc.rust-lang.org/cargo/reference/registry-index.html#index-configuration
20, extraRegistries ? {}
21
22 # Hashes for git dependencies.
23, outputHashes ? {}
24} @ args:
25
26assert (lockFile == null) != (lockFileContents == null);
27
28let
29 # Parse a git source into different components.
30 parseGit = src:
31 let
32 parts = builtins.match ''git\+([^?]+)(\?(rev|tag|branch)=(.*))?#(.*)'' src;
33 type = builtins.elemAt parts 2; # rev, tag or branch
34 value = builtins.elemAt parts 3;
35 in
36 if parts == null then null
37 else {
38 url = builtins.elemAt parts 0;
39 sha = builtins.elemAt parts 4;
40 } // lib.optionalAttrs (type != null) { inherit type value; };
41
42 # shadows args.lockFileContents
43 lockFileContents =
44 if lockFile != null
45 then builtins.readFile lockFile
46 else args.lockFileContents;
47
48 parsedLockFile = builtins.fromTOML lockFileContents;
49
50 packages = parsedLockFile.package;
51
52 # There is no source attribute for the source package itself. But
53 # since we do not want to vendor the source package anyway, we can
54 # safely skip it.
55 depPackages = builtins.filter (p: p ? "source") packages;
56
57 # Create dependent crates from packages.
58 #
59 # Force evaluation of the git SHA -> hash mapping, so that an error is
60 # thrown if there are stale hashes. We cannot rely on gitShaOutputHash
61 # being evaluated otherwise, since there could be no git dependencies.
62 depCrates = builtins.deepSeq gitShaOutputHash (builtins.map mkCrate depPackages);
63
64 # Map package name + version to git commit SHA for packages with a git source.
65 namesGitShas = builtins.listToAttrs (
66 builtins.map nameGitSha (builtins.filter (pkg: lib.hasPrefix "git+" pkg.source) depPackages)
67 );
68
69 nameGitSha = pkg: let gitParts = parseGit pkg.source; in {
70 name = "${pkg.name}-${pkg.version}";
71 value = gitParts.sha;
72 };
73
74 # Convert the attrset provided through the `outputHashes` argument to a
75 # a mapping from git commit SHA -> output hash.
76 #
77 # There may be multiple different packages with different names
78 # originating from the same git repository (typically a Cargo
79 # workspace). By using the git commit SHA as a universal identifier,
80 # the user does not have to specify the output hash for every package
81 # individually.
82 gitShaOutputHash = lib.mapAttrs' (nameVer: hash:
83 let
84 unusedHash = throw "A hash was specified for ${nameVer}, but there is no corresponding git dependency.";
85 rev = namesGitShas.${nameVer} or unusedHash; in {
86 name = rev;
87 value = hash;
88 }) outputHashes;
89
90 # We can't use the existing fetchCrate function, since it uses a
91 # recursive hash of the unpacked crate.
92 fetchCrate = pkg: downloadUrl:
93 let
94 checksum = pkg.checksum or parsedLockFile.metadata."checksum ${pkg.name} ${pkg.version} (${pkg.source})";
95 in
96 assert lib.assertMsg (checksum != null) ''
97 Package ${pkg.name} does not have a checksum.
98 '';
99 fetchurl {
100 name = "crate-${pkg.name}-${pkg.version}.tar.gz";
101 url = "${downloadUrl}/${pkg.name}/${pkg.version}/download";
102 sha256 = checksum;
103 };
104
105 registries = {
106 "https://github.com/rust-lang/crates.io-index" = "https://crates.io/api/v1/crates";
107 } // extraRegistries;
108
109 # Replaces values inherited by workspace members.
110 replaceWorkspaceValues = writers.writePython3 "replace-workspace-values"
111 { libraries = with python3Packages; [ tomli tomli-w ]; flakeIgnore = [ "E501" "W503" ]; }
112 (builtins.readFile ./replace-workspace-values.py);
113
114 # Fetch and unpack a crate.
115 mkCrate = pkg:
116 let
117 gitParts = parseGit pkg.source;
118 registryIndexUrl = lib.removePrefix "registry+" pkg.source;
119 in
120 if lib.hasPrefix "registry+" pkg.source && builtins.hasAttr registryIndexUrl registries then
121 let
122 crateTarball = fetchCrate pkg registries.${registryIndexUrl};
123 in runCommand "${pkg.name}-${pkg.version}" {} ''
124 mkdir $out
125 tar xf "${crateTarball}" -C $out --strip-components=1
126
127 # Cargo is happy with largely empty metadata.
128 printf '{"files":{},"package":"${crateTarball.outputHash}"}' > "$out/.cargo-checksum.json"
129 ''
130 else if gitParts != null then
131 let
132 missingHash = throw ''
133 No hash was found while vendoring the git dependency ${pkg.name}-${pkg.version}. You can add
134 a hash through the `outputHashes` argument of `importCargoLock`:
135
136 outputHashes = {
137 "${pkg.name}-${pkg.version}" = "<hash>";
138 };
139
140 If you use `buildRustPackage`, you can add this attribute to the `cargoLock`
141 attribute set.
142 '';
143 tree =
144 if gitShaOutputHash ? ${gitParts.sha} then
145 fetchgit {
146 inherit (gitParts) url;
147 rev = gitParts.sha; # The commit SHA is always available.
148 sha256 = gitShaOutputHash.${gitParts.sha};
149 }
150 else if allowBuiltinFetchGit then
151 builtins.fetchGit {
152 inherit (gitParts) url;
153 rev = gitParts.sha;
154 allRefs = true;
155 submodules = true;
156 }
157 else
158 missingHash;
159 in runCommand "${pkg.name}-${pkg.version}" {} ''
160 tree=${tree}
161
162 # If the target package is in a workspace, or if it's the top-level
163 # crate, we should find the crate path using `cargo metadata`.
164 # Some packages do not have a Cargo.toml at the top-level,
165 # but only in nested directories.
166 # Only check the top-level Cargo.toml, if it actually exists
167 if [[ -f $tree/Cargo.toml ]]; then
168 crateCargoTOML=$(${cargo}/bin/cargo metadata --format-version 1 --no-deps --manifest-path $tree/Cargo.toml | \
169 ${jq}/bin/jq -r '.packages[] | select(.name == "${pkg.name}") | .manifest_path')
170 fi
171
172 # If the repository is not a workspace the package might be in a subdirectory.
173 if [[ -z $crateCargoTOML ]]; then
174 for manifest in $(find $tree -name "Cargo.toml"); do
175 echo Looking at $manifest
176 crateCargoTOML=$(${cargo}/bin/cargo metadata --format-version 1 --no-deps --manifest-path "$manifest" | ${jq}/bin/jq -r '.packages[] | select(.name == "${pkg.name}") | .manifest_path' || :)
177 if [[ ! -z $crateCargoTOML ]]; then
178 break
179 fi
180 done
181
182 if [[ -z $crateCargoTOML ]]; then
183 >&2 echo "Cannot find path for crate '${pkg.name}-${pkg.version}' in the tree in: $tree"
184 exit 1
185 fi
186 fi
187
188 echo Found crate ${pkg.name} at $crateCargoTOML
189 tree=$(dirname $crateCargoTOML)
190
191 cp -prvL "$tree/" $out
192 chmod u+w $out
193
194 if grep -q workspace "$out/Cargo.toml"; then
195 chmod u+w "$out/Cargo.toml"
196 ${replaceWorkspaceValues} "$out/Cargo.toml" "${tree}/Cargo.toml"
197 fi
198
199 # Cargo is happy with empty metadata.
200 printf '{"files":{},"package":null}' > "$out/.cargo-checksum.json"
201
202 # Set up configuration for the vendor directory.
203 cat > $out/.cargo-config <<EOF
204 [source."${gitParts.url}${lib.optionalString (gitParts ? type) "?${gitParts.type}=${gitParts.value}"}"]
205 git = "${gitParts.url}"
206 ${lib.optionalString (gitParts ? type) "${gitParts.type} = \"${gitParts.value}\""}
207 replace-with = "vendored-sources"
208 EOF
209 ''
210 else throw "Cannot handle crate source: ${pkg.source}";
211
212 vendorDir = runCommand "cargo-vendor-dir"
213 (if lockFile == null then {
214 inherit lockFileContents;
215 passAsFile = [ "lockFileContents" ];
216 } else {
217 passthru = {
218 inherit lockFile;
219 };
220 }) ''
221 mkdir -p $out/.cargo
222
223 ${
224 if lockFile != null
225 then "ln -s ${lockFile} $out/Cargo.lock"
226 else "cp $lockFileContentsPath $out/Cargo.lock"
227 }
228
229 cat > $out/.cargo/config <<EOF
230[source.crates-io]
231replace-with = "vendored-sources"
232
233[source.vendored-sources]
234directory = "cargo-vendor-dir"
235EOF
236
237 declare -A keysSeen
238
239 for registry in ${toString (builtins.attrNames extraRegistries)}; do
240 cat >> $out/.cargo/config <<EOF
241
242[source."$registry"]
243registry = "$registry"
244replace-with = "vendored-sources"
245EOF
246 done
247
248 for crate in ${toString depCrates}; do
249 # Link the crate directory, removing the output path hash from the destination.
250 ln -s "$crate" $out/$(basename "$crate" | cut -c 34-)
251
252 if [ -e "$crate/.cargo-config" ]; then
253 key=$(sed 's/\[source\."\(.*\)"\]/\1/; t; d' < "$crate/.cargo-config")
254 if [[ -z ''${keysSeen[$key]} ]]; then
255 keysSeen[$key]=1
256 cat "$crate/.cargo-config" >> $out/.cargo/config
257 fi
258 fi
259 done
260 '';
261in
262 vendorDir