summaryrefslogtreecommitdiffstats
path: root/pkgs/rust/lib/crate2nix-package.nix
blob: 9b16b15d66b64fc6e619f0e4ea63fdc22c84cdde (plain)
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
# Per-crate Rust builds with crate2nix.
#
# resolvedJson selects how the build plan is obtained:
#   null (default)  <src>/Cargo.json if present, else IFD from <src>/Cargo.lock
#   a path          that file (crate2nix generate --format json)
#   false           always IFD from <src>/Cargo.lock
#
# Overrides are attrs, or functions from the crate's buildRustCrate arguments
# to attrs, and are merged into those arguments before buildRustCrate sees
# them, so they can also set release.  For a workspace member,
# workspaceOverride applies first and crateOverrides.<name> sees its result.
# nixpkgs' defaultCrateOverrides still apply inside buildRustCrate.

{ pkgs
, lib ? pkgs.lib
, applyOverride

, name
, src

# crate name -> attrs or (args -> attrs)
, crateOverrides ? {}

# attrs or (args -> attrs), for every workspace member
, workspaceOverride ? {}

# Root features.  A JSON plan has its features baked in; cargoToml checks them.
, features ? [ "default" ]

# Cargo.lock the JSON plan must match; null: no check
, cargoLock ? null

# Cargo.toml to check the plan's root features; null: no check
, cargoToml ? null

, resolvedJson ? null
}:

let
  resolvedJsonFromSrc = src + "/Cargo.json";

  useJson =
    if resolvedJson == null then builtins.pathExists resolvedJsonFromSrc
    else resolvedJson != false;

  resolvedJson' =
    if resolvedJson != null then resolvedJson else resolvedJsonFromSrc;

  workspaceMembers = builtins.attrNames cargoNix.workspaceMembers;

  buildRustCrateForPkgs = pkgs: args:
    let
      withWorkspace =
        if builtins.elem args.crateName workspaceMembers
        then applyOverride workspaceOverride args
        else args;
    in
    pkgs.buildRustCrate
      (applyOverride (crateOverrides.${args.crateName} or {}) withWorkspace);

  fromJson = pkgs.crate2nix.build-from-json {
    inherit buildRustCrateForPkgs pkgs src;
    resolvedJson = resolvedJson';
  };

  fromLock =
    pkgs.callPackage
      (pkgs.crate2nix.tools.generatedCargoNix {
        inherit name src;
        additionalCargoNixArgs = [ "--all-features" ];
      })
      {
        rootFeatures = features;
        inherit buildRustCrateForPkgs;
      };

  plan = builtins.fromJSON (builtins.readFile resolvedJson');

  # Fails evaluation if the plan's root crate wasn't resolved with features:
  # its declared features must be exactly those reachable from features
  # through Cargo.toml's [features], and features not declared there (implicit
  # features of optional dependencies) must be present.
  checkFeatures = result:
    let
      declared = (builtins.fromTOML (builtins.readFile cargoToml)).features or { };
      resolved = plan.crates.${plan.root}.resolvedDefaultFeatures or [ ];
      # default is always valid, even when Cargo.toml doesn't declare it.
      requested = builtins.filter (f: f != "default" || declared ? default) features;
      expected = map (x: x.key) (builtins.genericClosure {
        startSet = map (key: { inherit key; }) (builtins.filter (f: declared ? ${f}) requested);
        operator = x: map (key: { inherit key; }) (builtins.filter (f: declared ? ${f}) declared.${x.key});
      });
      actual = builtins.filter (f: declared ? ${f}) resolved;
      extra = lib.subtractLists expected actual;
      missing = lib.unique (lib.subtractLists actual expected
        ++ builtins.filter (f: !(declared ? ${f}) && !(builtins.elem f resolved)) requested);
      show = l: "[ ${lib.concatStringsSep " " l} ]";
    in
    if cargoToml == null || plan.root or null == null || extra == [ ] && missing == [ ] then result
    else throw "crate2nix-package: ${toString resolvedJson'} was generated with other features than ${show features}${lib.optionalString (missing != [ ]) "; missing ${show missing}"}${lib.optionalString (extra != [ ]) "; extra ${show extra}"}";

  # Fails evaluation if the plan uses a crate version the lock doesn't have,
  # i.e. Cargo.lock changed since Cargo.json was generated. Dependencies added
  # to the lock aren't detected, since the plan may leave out crates for
  # features or targets it doesn't use; those fail the build instead.
  checkLock = result:
    let
      id = name: version: "${name} ${version}";
      lockPackages = (builtins.fromTOML (builtins.readFile cargoLock)).package;
      locked = builtins.listToAttrs (map
        (p: { name = id p.name p.version; value = null; })
        (builtins.filter (p: p ? source) lockPackages));
      planned = lib.mapAttrsToList (_: c: id c.crateName c.version)
        (lib.filterAttrs (_: c: (c.source.type or "local") != "local")
          plan.crates);
      missing = builtins.filter (i: !(locked ? ${i})) planned;
    in
    if cargoLock == null || missing == [] then result
    else throw "crate2nix-package: ${toString resolvedJson'} is stale against ${toString cargoLock}; not in the lock: ${lib.concatStringsSep ", " (lib.take 5 missing)}${lib.optionalString (builtins.length missing > 5) ", ..."}";

  cargoNix = if useJson then checkFeatures (checkLock fromJson) else fromLock;
in
cargoNix // {
  build =
    if cargoNix.rootCrate or null != null then cargoNix.rootCrate.build
    else cargoNix.allWorkspaceMembers;
}