1 #![warn(clippy::pedantic)]
2 #![allow(clippy::too_many_lines)]
5 use serde::Deserialize;
6 use std::{collections::HashMap, env, fs, path::PathBuf, process::Command};
10 #[serde(rename = "package", borrow)]
11 packages: Vec<Package<'a>>,
12 metadata: Option<HashMap<&'a str, &'a str>>,
15 #[derive(Deserialize)]
19 source: Option<&'a str>,
20 checksum: Option<&'a str>,
23 #[derive(Deserialize)]
24 struct PrefetchOutput {
28 fn main() -> anyhow::Result<()> {
29 let mut hashes = HashMap::new();
31 let attr_count = env::args().len() - 1;
33 for (i, attr) in env::args().skip(1).enumerate() {
34 println!("converting {attr} ({}/{attr_count})", i + 1);
36 convert(&attr, &mut hashes)?;
42 fn 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)));
46 if package_path.is_none() {
47 eprintln!("can't automatically convert {attr}: doesn't exist");
51 let package_path = package_path.unwrap();
53 if package_path.with_file_name("Cargo.lock").exists() {
54 eprintln!("skipping {attr}: already has a vendored Cargo.lock");
58 let mut src = PathBuf::from(
60 Command::new("nix-build")
62 .arg(format!("{attr}.src"))
70 eprintln!("can't automatically convert {attr}: src doesn't exist (bad attr?)");
72 } else if !src.metadata()?.is_dir() {
73 eprintln!("can't automatically convert {attr}: src isn't a directory");
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);
82 let cargo_lock_path = src.join("Cargo.lock");
84 if !cargo_lock_path.exists() {
85 eprintln!("can't automatically convert {attr}: src doesn't contain Cargo.lock");
89 let cargo_lock_content = fs::read_to_string(cargo_lock_path)?;
91 let cargo_lock: CargoLock = basic_toml::from_str(&cargo_lock_content)?;
93 let mut git_dependencies = Vec::new();
95 for package in cargo_lock.packages.iter().filter(|p| {
103 format!("checksum {} {} ({})", p.name, p.version, p.source.unwrap())
110 let (typ, original_url) = package
114 .expect("dependency should have well-formed source url");
116 if let Some(hash) = hashes.get(original_url) {
122 "packages without checksums should be git dependencies"
125 let (mut url, rev) = original_url
127 .expect("git dependency should have commit");
130 if let Some((u, _)) = url.split_once('?') {
134 let prefetch_output: PrefetchOutput = serde_json::from_slice(
135 &Command::new("nix-prefetch-git")
136 .args(["--url", url, "--rev", rev, "--quiet", "--fetch-submodules"])
141 let output_hash = String::from_utf8(
144 "--extra-experimental-features",
150 &prefetch_output.sha256,
156 let hash = output_hash.trim().to_string();
158 git_dependencies.push((
159 format!("{}-{}", package.name, package.version),
160 output_hash.trim().to_string().clone(),
163 hashes.insert(original_url.to_string(), hash);
167 package_path.with_file_name("Cargo.lock"),
171 let mut package_lines: Vec<_> = fs::read_to_string(&package_path)?
176 let (cargo_deps_line_index, cargo_deps_line) = package_lines
180 l.trim_start().starts_with("cargoHash") || l.trim_start().starts_with("cargoSha256")
182 .expect("package should contain cargoHash/cargoSha256");
184 let spaces = " ".repeat(cargo_deps_line.len() - cargo_deps_line.trim_start().len());
186 if git_dependencies.is_empty() {
187 *cargo_deps_line = format!("{spaces}cargoLock.lockFile = ./Cargo.lock;");
189 *cargo_deps_line = format!("{spaces}cargoLock = {{");
191 let mut index_iter = cargo_deps_line_index + 1..;
193 package_lines.insert(
194 index_iter.next().unwrap(),
195 format!("{spaces} lockFile = ./Cargo.lock;"),
198 package_lines.insert(
199 index_iter.next().unwrap(),
200 format!("{spaces} outputHashes = {{"),
203 for ((dep, hash), index) in git_dependencies.drain(..).zip(&mut index_iter) {
204 package_lines.insert(index, format!("{spaces} {dep:?} = {hash:?};"));
207 package_lines.insert(index_iter.next().unwrap(), format!("{spaces} }};"));
208 package_lines.insert(index_iter.next().unwrap(), format!("{spaces}}};"));
211 if package_lines.last().map(String::as_str) != Some("") {
212 package_lines.push(String::new());
215 fs::write(package_path, package_lines.join("\n"))?;
220 fn 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()])
228 let trimmed = output.trim();
230 if trimmed.is_empty() || trimmed == "null" {
236 .and_then(|p| p.strip_suffix('"'))
237 .ok_or_else(|| anyhow!("couldn't parse nix-instantiate output: {output:?}"))?