1#![warn(clippy::pedantic)]
2#![allow(clippy::too_many_lines)]
3
4use anyhow::anyhow;
5use serde::Deserialize;
6use std::{collections::HashMap, env, fs, path::PathBuf, process::Command};
7
8#[derive(Deserialize)]
9struct CargoLock<'a> {
10 #[serde(rename = "package", borrow)]
11 packages: Vec<Package<'a>>,
12 metadata: Option<HashMap<&'a str, &'a str>>,
13}
14
15#[derive(Deserialize)]
16struct Package<'a> {
17 name: &'a str,
18 version: &'a str,
19 source: Option<&'a str>,
20 checksum: Option<&'a str>,
21}
22
23#[derive(Deserialize)]
24struct PrefetchOutput {
25 sha256: String,
26}
27
28fn main() -> anyhow::Result<()> {
29 let mut hashes = HashMap::new();
30
31 let attr_count = env::args().len() - 1;
32
33 for (i, attr) in env::args().skip(1).enumerate() {
34 println!("converting {attr} ({}/{attr_count})", i + 1);
35
36 convert(&attr, &mut hashes)?;
37 }
38
39 Ok(())
40}
41
42fn convert(attr: &str, hashes: &mut HashMap<String, String>) -> anyhow::Result<()> {
43 let package_path = nix_eval(format!("{attr}.meta.position"))?
44 .and_then(|p| p.split_once(':').map(|(f, _)| PathBuf::from(f)));
45
46 if package_path.is_none() {
47 eprintln!("can't automatically convert {attr}: doesn't exist");
48 return Ok(());
49 }
50
51 let package_path = package_path.unwrap();
52
53 if package_path.with_file_name("Cargo.lock").exists() {
54 eprintln!("skipping {attr}: already has a vendored Cargo.lock");
55 return Ok(());
56 }
57
58 let mut src = PathBuf::from(
59 String::from_utf8(
60 Command::new("nix-build")
61 .arg("-A")
62 .arg(format!("{attr}.src"))
63 .output()?
64 .stdout,
65 )?
66 .trim(),
67 );
68
69 if !src.exists() {
70 eprintln!("can't automatically convert {attr}: src doesn't exist (bad attr?)");
71 return Ok(());
72 } else if !src.metadata()?.is_dir() {
73 eprintln!("can't automatically convert {attr}: src isn't a directory");
74 return Ok(());
75 }
76
77 if let Some(mut source_root) = nix_eval(format!("{attr}.sourceRoot"))?.map(PathBuf::from) {
78 source_root = source_root.components().skip(1).collect();
79 src.push(source_root);
80 }
81
82 let cargo_lock_path = src.join("Cargo.lock");
83
84 if !cargo_lock_path.exists() {
85 eprintln!("can't automatically convert {attr}: src doesn't contain Cargo.lock");
86 return Ok(());
87 }
88
89 let cargo_lock_content = fs::read_to_string(cargo_lock_path)?;
90
91 let cargo_lock: CargoLock = basic_toml::from_str(&cargo_lock_content)?;
92
93 let mut git_dependencies = Vec::new();
94
95 for package in cargo_lock.packages.iter().filter(|p| {
96 p.source.is_some()
97 && p.checksum
98 .or_else(|| {
99 cargo_lock
100 .metadata
101 .as_ref()?
102 .get(
103 format!("checksum {} {} ({})", p.name, p.version, p.source.unwrap())
104 .as_str(),
105 )
106 .copied()
107 })
108 .is_none()
109 }) {
110 let (typ, original_url) = package
111 .source
112 .unwrap()
113 .split_once('+')
114 .expect("dependency should have well-formed source url");
115
116 if let Some(hash) = hashes.get(original_url) {
117 continue;
118 }
119
120 assert_eq!(
121 typ, "git",
122 "packages without checksums should be git dependencies"
123 );
124
125 let (mut url, rev) = original_url
126 .split_once('#')
127 .expect("git dependency should have commit");
128
129 // TODO: improve
130 if let Some((u, _)) = url.split_once('?') {
131 url = u;
132 }
133
134 let prefetch_output: PrefetchOutput = serde_json::from_slice(
135 &Command::new("nix-prefetch-git")
136 .args(["--url", url, "--rev", rev, "--quiet", "--fetch-submodules"])
137 .output()?
138 .stdout,
139 )?;
140
141 let output_hash = String::from_utf8(
142 Command::new("nix")
143 .args([
144 "--extra-experimental-features",
145 "nix-command",
146 "hash",
147 "to-sri",
148 "--type",
149 "sha256",
150 &prefetch_output.sha256,
151 ])
152 .output()?
153 .stdout,
154 )?;
155
156 let hash = output_hash.trim().to_string();
157
158 git_dependencies.push((
159 format!("{}-{}", package.name, package.version),
160 output_hash.trim().to_string().clone(),
161 ));
162
163 hashes.insert(original_url.to_string(), hash);
164 }
165
166 fs::write(
167 package_path.with_file_name("Cargo.lock"),
168 cargo_lock_content,
169 )?;
170
171 let mut package_lines: Vec<_> = fs::read_to_string(&package_path)?
172 .lines()
173 .map(String::from)
174 .collect();
175
176 let (cargo_deps_line_index, cargo_deps_line) = package_lines
177 .iter_mut()
178 .enumerate()
179 .find(|(_, l)| {
180 l.trim_start().starts_with("cargoHash") || l.trim_start().starts_with("cargoSha256")
181 })
182 .expect("package should contain cargoHash/cargoSha256");
183
184 let spaces = " ".repeat(cargo_deps_line.len() - cargo_deps_line.trim_start().len());
185
186 if git_dependencies.is_empty() {
187 *cargo_deps_line = format!("{spaces}cargoLock.lockFile = ./Cargo.lock;");
188 } else {
189 *cargo_deps_line = format!("{spaces}cargoLock = {{");
190
191 let mut index_iter = cargo_deps_line_index + 1..;
192
193 package_lines.insert(
194 index_iter.next().unwrap(),
195 format!("{spaces} lockFile = ./Cargo.lock;"),
196 );
197
198 package_lines.insert(
199 index_iter.next().unwrap(),
200 format!("{spaces} outputHashes = {{"),
201 );
202
203 for ((dep, hash), index) in git_dependencies.drain(..).zip(&mut index_iter) {
204 package_lines.insert(index, format!("{spaces} {dep:?} = {hash:?};"));
205 }
206
207 package_lines.insert(index_iter.next().unwrap(), format!("{spaces} }};"));
208 package_lines.insert(index_iter.next().unwrap(), format!("{spaces}}};"));
209 }
210
211 if package_lines.last().map(String::as_str) != Some("") {
212 package_lines.push(String::new());
213 }
214
215 fs::write(package_path, package_lines.join("\n"))?;
216
217 Ok(())
218}
219
220fn nix_eval(attr: impl AsRef<str>) -> anyhow::Result<Option<String>> {
221 let output = String::from_utf8(
222 Command::new("nix-instantiate")
223 .args(["--eval", "-A", attr.as_ref()])
224 .output()?
225 .stdout,
226 )?;
227
228 let trimmed = output.trim();
229
230 if trimmed.is_empty() || trimmed == "null" {
231 Ok(None)
232 } else {
233 Ok(Some(
234 trimmed
235 .strip_prefix('"')
236 .and_then(|p| p.strip_suffix('"'))
237 .ok_or_else(|| anyhow!("couldn't parse nix-instantiate output: {output:?}"))?
238 .to_string(),
239 ))
240 }
241}