1# This script implements the workspace inheritance mechanism described
2# here: https://doc.rust-lang.org/cargo/reference/workspaces.html#the-package-table
3#
4# Please run `mypy --strict`, `black`, and `isort --profile black` on this after editing, thanks!
5
6import sys
7from typing import Any
8
9import tomli
10import tomli_w
11
12
13def load_file(path: str) -> dict[str, Any]:
14 with open(path, "rb") as f:
15 return tomli.load(f)
16
17
18# This replicates the dependency merging logic from Cargo.
19# See `inner_dependency_inherit_with`:
20# https://github.com/rust-lang/cargo/blob/4de0094ac78743d2c8ff682489e35c8a7cafe8e4/src/cargo/util/toml/mod.rs#L982
21def replace_key(
22 workspace_manifest: dict[str, Any], table: dict[str, Any], section: str, key: str
23) -> bool:
24 if (
25 isinstance(table[key], dict)
26 and "workspace" in table[key]
27 and table[key]["workspace"] is True
28 ):
29 print("replacing " + key)
30
31 local_dep = table[key]
32 del local_dep["workspace"]
33
34 try:
35 workspace_dep = workspace_manifest[section][key]
36 except KeyError:
37 # Key is not present in workspace manifest, we can't inherit the value, so we mark it for deletion
38 table[key] = {}
39 return True
40
41 if section == "dependencies":
42 if isinstance(workspace_dep, str):
43 workspace_dep = {"version": workspace_dep}
44
45 final: dict[str, Any] = workspace_dep.copy()
46
47 merged_features = local_dep.pop("features", []) + workspace_dep.get(
48 "features", []
49 )
50 if merged_features:
51 final["features"] = merged_features
52
53 local_default_features = local_dep.pop(
54 "default-features", local_dep.pop("default_features", None)
55 )
56 workspace_default_features = workspace_dep.get(
57 "default-features", workspace_dep.get("default_features")
58 )
59
60 if not workspace_default_features and local_default_features:
61 final["default-features"] = True
62
63 optional = local_dep.pop("optional", False)
64 if optional:
65 final["optional"] = True
66
67 if "package" in local_dep:
68 final["package"] = local_dep.pop("package")
69
70 if local_dep:
71 raise Exception(
72 f"Unhandled keys in inherited dependency {key}: {local_dep}"
73 )
74
75 table[key] = final
76 elif section == "package":
77 table[key] = workspace_dep
78
79 return True
80
81 return False
82
83
84def replace_dependencies(
85 workspace_manifest: dict[str, Any], root: dict[str, Any]
86) -> bool:
87 changed = False
88
89 for key in ["dependencies", "dev-dependencies", "build-dependencies"]:
90 if key in root:
91 for k in root[key].keys():
92 changed |= replace_key(workspace_manifest, root[key], "dependencies", k)
93
94 return changed
95
96
97def main() -> None:
98 top_cargo_toml = load_file(sys.argv[2])
99
100 if "workspace" not in top_cargo_toml:
101 # If top_cargo_toml is not a workspace manifest, then this script was probably
102 # ran on something that does not actually use workspace dependencies
103 print(f"{sys.argv[2]} is not a workspace manifest, doing nothing.")
104 return
105
106 crate_manifest = load_file(sys.argv[1])
107 workspace_manifest = top_cargo_toml["workspace"]
108
109 if "workspace" in crate_manifest:
110 return
111
112 changed = False
113
114 to_remove = []
115 for key in crate_manifest["package"].keys():
116 changed_key = replace_key(
117 workspace_manifest, crate_manifest["package"], "package", key
118 )
119 if changed_key and crate_manifest["package"][key] == {}:
120 # Key is missing from workspace manifest, mark for deletion
121 to_remove.append(key)
122 changed |= changed_key
123 # Remove keys which have no value
124 for key in to_remove:
125 del crate_manifest["package"][key]
126
127 changed |= replace_dependencies(workspace_manifest, crate_manifest)
128
129 if "target" in crate_manifest:
130 for key in crate_manifest["target"].keys():
131 changed |= replace_dependencies(
132 workspace_manifest, crate_manifest["target"][key]
133 )
134
135 if (
136 "lints" in crate_manifest
137 and "workspace" in crate_manifest["lints"]
138 and crate_manifest["lints"]["workspace"] is True
139 ):
140 crate_manifest["lints"] = workspace_manifest["lints"]
141 changed = True
142
143 if not changed:
144 return
145
146 with open(sys.argv[1], "wb") as f:
147 tomli_w.dump(crate_manifest, f)
148
149
150if __name__ == "__main__":
151 main()