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}