Merge pull request #309460 from r-ryantm/auto-update/home-manager
[NixPkgs.git] / pkgs / build-support / rust / import-cargo-lock.nix
blobe3fe57ef06daa17d11eb7eacd92ab0c4dbeda1d2
1 { fetchgit, fetchurl, lib, writers, python3Packages, runCommand, cargo, jq }:
4   # Cargo lock file
5   lockFile ? null
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   # 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 ? {}
22   # Hashes for git dependencies.
23 , outputHashes ? {}
24 } @ args:
26 assert (lockFile == null) != (lockFileContents == null);
28 let
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; };
42   # shadows args.lockFileContents
43   lockFileContents =
44     if lockFile != null
45     then builtins.readFile lockFile
46     else args.lockFileContents;
48   parsedLockFile = builtins.fromTOML lockFileContents;
50   packages = parsedLockFile.package;
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;
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);
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   );
69   nameGitSha = pkg: let gitParts = parseGit pkg.source; in {
70     name = "${pkg.name}-${pkg.version}";
71     value = gitParts.sha;
72   };
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;
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     };
105   registries = {
106     "https://github.com/rust-lang/crates.io-index" = "https://crates.io/api/v1/crates";
107   } // extraRegistries;
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);
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
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`:
136           outputHashes = {
137             "${pkg.name}-${pkg.version}" = "<hash>";
138           };
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}
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
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
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
188         echo Found crate ${pkg.name} at $crateCargoTOML
189         tree=$(dirname $crateCargoTOML)
191         cp -prvL "$tree/" $out
192         chmod u+w $out
194         if grep -q workspace "$out/Cargo.toml"; then
195           chmod u+w "$out/Cargo.toml"
196           ${replaceWorkspaceValues} "$out/Cargo.toml" "$(${cargo}/bin/cargo metadata --format-version 1 --no-deps --manifest-path $crateCargoTOML | ${jq}/bin/jq -r .workspace_root)/Cargo.toml"
197         fi
199         # Cargo is happy with empty metadata.
200         printf '{"files":{},"package":null}' > "$out/.cargo-checksum.json"
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}";
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
223     ${
224       if lockFile != null
225       then "ln -s ${lockFile} $out/Cargo.lock"
226       else "cp $lockFileContentsPath $out/Cargo.lock"
227     }
229     cat > $out/.cargo/config <<EOF
230 [source.crates-io]
231 replace-with = "vendored-sources"
233 [source.vendored-sources]
234 directory = "cargo-vendor-dir"
237     declare -A keysSeen
239     for registry in ${toString (builtins.attrNames extraRegistries)}; do
240       cat >> $out/.cargo/config <<EOF
242 [source."$registry"]
243 registry = "$registry"
244 replace-with = "vendored-sources"
246     done
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-)
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   '';
262   vendorDir