about summary refs log tree commit diff
path: root/nixpkgs/pkgs/build-support/rust/import-cargo-lock.nix
blob: fd3608741ea336717708ecb3e1b43b2255cf8f0f (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
{ fetchgit, fetchurl, lib, runCommand, cargo, jq }:

{
  # Cargo lock file
  lockFile ? null

  # Cargo lock file contents as string
, lockFileContents ? null

  # Hashes for git dependencies.
, outputHashes ? {}
} @ args:

assert (lockFile == null) != (lockFileContents == null);

let
  # Parse a git source into different components.
  parseGit = src:
    let
      parts = builtins.match ''git\+([^?]+)(\?(rev|tag|branch)=(.*))?#(.*)'' src;
      type = builtins.elemAt parts 2; # rev, tag or branch
      value = builtins.elemAt parts 3;
    in
      if parts == null then null
      else {
        url = builtins.elemAt parts 0;
        sha = builtins.elemAt parts 4;
      } // lib.optionalAttrs (type != null) { inherit type value; };

  # shadows args.lockFileContents
  lockFileContents =
    if lockFile != null
    then builtins.readFile lockFile
    else args.lockFileContents;

  packages = (builtins.fromTOML lockFileContents).package;

  # There is no source attribute for the source package itself. But
  # since we do not want to vendor the source package anyway, we can
  # safely skip it.
  depPackages = (builtins.filter (p: p ? "source") packages);

  # Create dependent crates from packages.
  #
  # Force evaluation of the git SHA -> hash mapping, so that an error is
  # thrown if there are stale hashes. We cannot rely on gitShaOutputHash
  # being evaluated otherwise, since there could be no git dependencies.
  depCrates = builtins.deepSeq (gitShaOutputHash) (builtins.map mkCrate depPackages);

  # Map package name + version to git commit SHA for packages with a git source.
  namesGitShas = builtins.listToAttrs (
    builtins.map nameGitSha (builtins.filter (pkg: lib.hasPrefix "git+" pkg.source) depPackages)
  );

  nameGitSha = pkg: let gitParts = parseGit pkg.source; in {
    name = "${pkg.name}-${pkg.version}";
    value = gitParts.sha;
  };

  # Convert the attrset provided through the `outputHashes` argument to a
  # a mapping from git commit SHA -> output hash.
  #
  # There may be multiple different packages with different names
  # originating from the same git repository (typically a Cargo
  # workspace). By using the git commit SHA as a universal identifier,
  # the user does not have to specify the output hash for every package
  # individually.
  gitShaOutputHash = lib.mapAttrs' (nameVer: hash:
    let
      unusedHash = throw "A hash was specified for ${nameVer}, but there is no corresponding git dependency.";
      rev = namesGitShas.${nameVer} or unusedHash; in {
      name = rev;
      value = hash;
    }) outputHashes;

  # We can't use the existing fetchCrate function, since it uses a
  # recursive hash of the unpacked crate.
  fetchCrate = pkg:
    assert lib.assertMsg (pkg ? checksum) ''
      Package ${pkg.name} does not have a checksum.
      Please note that the Cargo.lock format where checksums used to be listed
      under [metadata] is not supported.
      If that is the case, running `cargo update` with a recent toolchain will
      automatically update the format along with the crate's depenendencies.
    '';
    fetchurl {
      name = "crate-${pkg.name}-${pkg.version}.tar.gz";
      url = "https://crates.io/api/v1/crates/${pkg.name}/${pkg.version}/download";
      sha256 = pkg.checksum;
    };

  # Fetch and unpack a crate.
  mkCrate = pkg:
    let
      gitParts = parseGit pkg.source;
    in
      if pkg.source == "registry+https://github.com/rust-lang/crates.io-index" then
      let
        crateTarball = fetchCrate pkg;
      in runCommand "${pkg.name}-${pkg.version}" {} ''
        mkdir $out
        tar xf "${crateTarball}" -C $out --strip-components=1

        # Cargo is happy with largely empty metadata.
        printf '{"files":{},"package":"${pkg.checksum}"}' > "$out/.cargo-checksum.json"
      ''
      else if gitParts != null then
      let
        missingHash = throw ''
          No hash was found while vendoring the git dependency ${pkg.name}-${pkg.version}. You can add
          a hash through the `outputHashes` argument of `importCargoLock`:

          outputHashes = {
            "${pkg.name}-${pkg.version}" = "<hash>";
          };

          If you use `buildRustPackage`, you can add this attribute to the `cargoLock`
          attribute set.
        '';
        sha256 = gitShaOutputHash.${gitParts.sha} or missingHash;
        tree = fetchgit {
          inherit sha256;
          inherit (gitParts) url;
          rev = gitParts.sha; # The commit SHA is always available.
        };
      in runCommand "${pkg.name}-${pkg.version}" {} ''
        tree=${tree}

        # If the target package is in a workspace, or if it's the top-level
        # crate, we should find the crate path using `cargo metadata`.
        crateCargoTOML=$(${cargo}/bin/cargo metadata --format-version 1 --no-deps --manifest-path $tree/Cargo.toml | \
          ${jq}/bin/jq -r '.packages[] | select(.name == "${pkg.name}") | .manifest_path')

        # If the repository is not a workspace the package might be in a subdirectory.
        if [[ -z $crateCargoTOML ]]; then
          for manifest in $(find $tree -name "Cargo.toml"); do
            echo Looking at $manifest
            crateCargoTOML=$(${cargo}/bin/cargo metadata --format-version 1 --no-deps --manifest-path "$manifest" | ${jq}/bin/jq -r '.packages[] | select(.name == "${pkg.name}") | .manifest_path' || :)
            if [[ ! -z $crateCargoTOML ]]; then
              break
            fi
          done

          if [[ -z $crateCargoTOML ]]; then
            >&2 echo "Cannot find path for crate '${pkg.name}-${pkg.version}' in the tree in: $tree"
            exit 1
          fi
        fi

        echo Found crate ${pkg.name} at $crateCargoTOML
        tree=$(dirname $crateCargoTOML)

        cp -prvd "$tree/" $out
        chmod u+w $out

        # Cargo is happy with empty metadata.
        printf '{"files":{},"package":null}' > "$out/.cargo-checksum.json"

        # Set up configuration for the vendor directory.
        cat > $out/.cargo-config <<EOF
        [source."${gitParts.url}"]
        git = "${gitParts.url}"
        ${lib.optionalString (gitParts ? type) "${gitParts.type} = \"${gitParts.value}\""}
        replace-with = "vendored-sources"
        EOF
      ''
      else throw "Cannot handle crate source: ${pkg.source}";

  vendorDir = runCommand "cargo-vendor-dir" (lib.optionalAttrs (lockFile == null) {
    inherit lockFileContents;
    passAsFile = [ "lockFileContents" ];
  }) ''
    mkdir -p $out/.cargo

    ${
      if lockFile != null
      then "ln -s ${lockFile} $out/Cargo.lock"
      else "cp $lockFileContentsPath $out/Cargo.lock"
    }

    cat > $out/.cargo/config <<EOF
    [source.crates-io]
    replace-with = "vendored-sources"

    [source.vendored-sources]
    directory = "cargo-vendor-dir"
    EOF

    declare -A keysSeen

    for crate in ${toString depCrates}; do
      # Link the crate directory, removing the output path hash from the destination.
      ln -s "$crate" $out/$(basename "$crate" | cut -c 34-)

      if [ -e "$crate/.cargo-config" ]; then
        key=$(sed 's/\[source\."\(.*\)"\]/\1/; t; d' < "$crate/.cargo-config")
        if [[ -z ''${keysSeen[$key]} ]]; then
          keysSeen[$key]=1
          cat "$crate/.cargo-config" >> $out/.cargo/config
        fi
      fi
    done
  '';
in
  vendorDir