Merge pull request #329823 from ExpidusOS/fix/pkgsllvm/elfutils
[NixPkgs.git] / maintainers / scripts / convert-to-import-cargo-lock / src / main.rs
blob6eb6768d14e965b734193424ca2f0a3500091ea8
1 #![warn(clippy::pedantic)]
2 #![allow(clippy::too_many_lines)]
4 use anyhow::anyhow;
5 use serde::Deserialize;
6 use std::{collections::HashMap, env, fs, path::PathBuf, process::Command};
8 #[derive(Deserialize)]
9 struct CargoLock<'a> {
10     #[serde(rename = "package", borrow)]
11     packages: Vec<Package<'a>>,
12     metadata: Option<HashMap<&'a str, &'a str>>,
15 #[derive(Deserialize)]
16 struct Package<'a> {
17     name: &'a str,
18     version: &'a str,
19     source: Option<&'a str>,
20     checksum: Option<&'a str>,
23 #[derive(Deserialize)]
24 struct PrefetchOutput {
25     sha256: String,
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)?;
37     }
39     Ok(())
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");
48         return Ok(());
49     }
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");
55         return Ok(());
56     }
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     );
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     }
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     }
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");
86         return Ok(());
87     }
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| {
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");
116         if let Some(hash) = hashes.get(original_url) {
117             continue;
118         }
120         assert_eq!(
121             typ, "git",
122             "packages without checksums should be git dependencies"
123         );
125         let (mut url, rev) = original_url
126             .split_once('#')
127             .expect("git dependency should have commit");
129         // TODO: improve
130         if let Some((u, _)) = url.split_once('?') {
131             url = u;
132         }
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         )?;
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         )?;
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(),
161         ));
163         hashes.insert(original_url.to_string(), hash);
164     }
166     fs::write(
167         package_path.with_file_name("Cargo.lock"),
168         cargo_lock_content,
169     )?;
171     let mut package_lines: Vec<_> = fs::read_to_string(&package_path)?
172         .lines()
173         .map(String::from)
174         .collect();
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");
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;");
188     } else {
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;"),
196         );
198         package_lines.insert(
199             index_iter.next().unwrap(),
200             format!("{spaces}  outputHashes = {{"),
201         );
203         for ((dep, hash), index) in git_dependencies.drain(..).zip(&mut index_iter) {
204             package_lines.insert(index, format!("{spaces}    {dep:?} = {hash:?};"));
205         }
207         package_lines.insert(index_iter.next().unwrap(), format!("{spaces}  }};"));
208         package_lines.insert(index_iter.next().unwrap(), format!("{spaces}}};"));
209     }
211     if package_lines.last().map(String::as_str) != Some("") {
212         package_lines.push(String::new());
213     }
215     fs::write(package_path, package_lines.join("\n"))?;
217     Ok(())
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()])
224             .output()?
225             .stdout,
226     )?;
228     let trimmed = output.trim();
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     }