1#![forbid(unsafe_code)]
9#![deny(missing_docs)]
10
11use std::env;
12use std::path::PathBuf;
13use std::{
14 collections::{BTreeMap, HashMap, HashSet, VecDeque},
15 path::Path,
16};
17
18use anyhow::format_err;
19use anyhow::Context;
20use anyhow::Error;
21use cargo_metadata::Metadata;
22use cargo_metadata::PackageId;
23use metadata::MergedMetadata;
24use serde::Deserialize;
25use serde::Serialize;
26
27use crate::metadata::IndexedMetadata;
28use crate::resolve::{CrateDerivation, ResolvedSource};
29use itertools::Itertools;
30use resolve::CratesIoSource;
31
32mod command;
33pub mod config;
34mod lock;
35mod metadata;
36pub mod nix_build;
37mod prefetch;
38pub mod render;
39mod resolve;
40pub mod sources;
41#[cfg(test)]
42pub mod test;
43pub mod util;
44
45#[derive(Debug, Deserialize, Serialize)]
47pub struct BuildInfo {
48 pub root_package_id: Option<PackageId>,
50 pub workspace_members: BTreeMap<String, PackageId>,
52 pub registries: BTreeMap<String, String>,
54 pub crates: Vec<CrateDerivation>,
56 pub indexed_metadata: IndexedMetadata,
58 pub info: GenerateInfo,
60 pub config: GenerateConfig,
62}
63
64impl BuildInfo {
65 pub fn for_config(info: &GenerateInfo, config: &GenerateConfig) -> Result<BuildInfo, Error> {
67 let merged = {
68 let mut metadatas = Vec::new();
69 for cargo_toml in &config.cargo_toml {
70 metadatas.push(cargo_metadata(config, cargo_toml)?);
71 }
72 metadata::MergedMetadata::merge(metadatas)?
73 };
74
75 let indexed_metadata = IndexedMetadata::new_from_merged(&merged).map_err(|e| {
76 format_err!(
77 "while indexing metadata for {:#?}: {}",
78 config
79 .cargo_toml
80 .iter()
81 .map(|p| p.to_string_lossy())
82 .collect::<Vec<_>>(),
83 e
84 )
85 })?;
86 let mut default_nix = BuildInfo::new(info, config, indexed_metadata)?;
87
88 default_nix.prune_unneeded_crates();
89
90 prefetch_and_fill_crates_sha256(config, &merged, &mut default_nix)?;
91
92 prefetch_and_fill_registries(config, &mut default_nix)?;
93
94 Ok(default_nix)
95 }
96
97 fn prune_unneeded_crates(&mut self) {
98 let mut queue: VecDeque<&PackageId> = self
99 .root_package_id
100 .iter()
101 .chain(self.workspace_members.values())
102 .collect();
103 let mut reachable = HashSet::new();
104 let indexed_crates: BTreeMap<_, _> =
105 self.crates.iter().map(|c| (&c.package_id, c)).collect();
106 while let Some(next_package_id) = queue.pop_back() {
107 if !reachable.insert(next_package_id.clone()) {
108 continue;
109 }
110
111 queue.extend(
112 indexed_crates
113 .get(next_package_id)
114 .iter()
115 .flat_map(|c| {
116 c.dependencies
117 .iter()
118 .chain(c.build_dependencies.iter())
119 .chain(c.dev_dependencies.iter())
120 })
121 .map(|d| &d.package_id),
122 );
123 }
124 self.crates.retain(|c| reachable.contains(&c.package_id));
125 }
126
127 fn new(
128 info: &GenerateInfo,
129 config: &GenerateConfig,
130 metadata: IndexedMetadata,
131 ) -> Result<BuildInfo, Error> {
132 let crate2nix_json = crate::config::Config::read_from_or_default(
133 &config
134 .crate_hashes_json
135 .parent()
136 .expect("crate-hashes.json has parent dir")
137 .join("crate2nix.json"),
138 )?;
139
140 Ok(BuildInfo {
141 root_package_id: metadata.root.clone(),
142 workspace_members: metadata
143 .workspace_members
144 .iter()
145 .flat_map(|pkg_id| {
146 metadata
147 .pkgs_by_id
148 .get(pkg_id)
149 .map(|pkg| (pkg.name.clone(), pkg_id.clone()))
150 })
151 .collect(),
152 registries: BTreeMap::new(),
153 crates: metadata
154 .pkgs_by_id
155 .values()
156 .map(|package| {
157 CrateDerivation::resolve(config, &crate2nix_json, &metadata, package)
158 })
159 .collect::<Result<_, Error>>()?,
160 indexed_metadata: metadata,
161 info: info.clone(),
162 config: config.clone(),
163 })
164 }
165}
166
167fn cargo_metadata(config: &GenerateConfig, cargo_toml: &Path) -> Result<Metadata, Error> {
169 let mut cmd = cargo_metadata::MetadataCommand::new();
170 let mut other_options = config.other_metadata_options.clone();
171 other_options.push("--locked".into());
172 cmd.manifest_path(cargo_toml).other_options(&*other_options);
173 cmd.exec().map_err(|e| {
174 format_err!(
175 "while retrieving metadata about {}: {}",
176 &cargo_toml.to_string_lossy(),
177 e
178 )
179 })
180}
181
182fn prefetch_and_fill_crates_sha256(
184 config: &GenerateConfig,
185 merged: &MergedMetadata,
186 default_nix: &mut BuildInfo,
187) -> Result<(), Error> {
188 let mut from_lock_file: HashMap<PackageId, String> =
189 extract_hashes_from_lockfile(config, merged, default_nix)?;
190 for (_package_id, hash) in from_lock_file.iter_mut() {
191 let bytes =
192 hex::decode(&hash).map_err(|e| format_err!("while decoding '{}': {}", hash, e))?;
193 *hash = nix_base32::to_nix_base32(&bytes);
194 }
195
196 let prefetched = prefetch::prefetch(
197 config,
198 &from_lock_file,
199 &default_nix.crates,
200 &default_nix.indexed_metadata.id_shortener,
201 )
202 .map_err(|e| format_err!("while prefetching crates for calculating sha256: {}", e))?;
203
204 for package in default_nix.crates.iter_mut() {
205 if package.source.sha256().is_none() {
206 if let Some(hash) = prefetched
207 .get(
208 default_nix
209 .indexed_metadata
210 .id_shortener
211 .lengthen_ref(&package.package_id),
212 )
213 .or_else(|| from_lock_file.get(&package.package_id))
214 {
215 package.source = package.source.with_sha256(hash.clone());
216 }
217 }
218 }
219
220 Ok(())
221}
222
223fn prefetch_and_fill_registries(
225 config: &GenerateConfig,
226 default_nix: &mut BuildInfo,
227) -> Result<(), Error> {
228 default_nix.registries = prefetch::prefetch_registries(config, &default_nix.crates)
229 .map_err(|e| format_err!("while prefetching crates for calculating sha256: {}", e))?;
230
231 Ok(())
232}
233
234fn extract_hashes_from_lockfile(
235 config: &GenerateConfig,
236 merged: &MergedMetadata,
237 default_nix: &mut BuildInfo,
238) -> Result<HashMap<PackageId, String>, Error> {
239 if !config.use_cargo_lock_checksums {
240 return Ok(HashMap::new());
241 }
242
243 let mut hashes: HashMap<PackageId, String> = HashMap::new();
244
245 for cargo_toml in &config.cargo_toml {
246 let lock_file_path = cargo_toml.parent().unwrap().join("Cargo.lock");
247 let lock_file = crate::lock::EncodableResolve::load_lock_file(&lock_file_path)?;
248 lock_file
249 .get_hashes_by_package_id(merged, &mut hashes)
250 .context(format!(
251 "while parsing checksums from Lockfile {}",
252 &lock_file_path.to_string_lossy()
253 ))?;
254 }
255
256 let hashes_with_shortened_ids: HashMap<PackageId, String> = hashes
257 .into_iter()
258 .map(|(package_id, hash)| {
259 (
260 default_nix
261 .indexed_metadata
262 .id_shortener
263 .shorten_owned(package_id),
264 hash,
265 )
266 })
267 .collect();
268
269 let mut missing_hashes = Vec::new();
270 for package in default_nix.crates.iter_mut().filter(|c| match &c.source {
271 ResolvedSource::CratesIo(CratesIoSource { sha256, .. }) if sha256.is_none() => {
272 !hashes_with_shortened_ids.contains_key(&c.package_id)
273 }
274 _ => false,
275 }) {
276 missing_hashes.push(format!("{} {}", package.crate_name, package.version));
277 }
278 if !missing_hashes.is_empty() {
279 eprintln!(
280 "Did not find all crates.io hashes in Cargo.lock. Hashes for e.g. {} are missing.\n\
281 This is probably a bug.",
282 missing_hashes.iter().take(10).join(", ")
283 );
284 }
285 Ok(hashes_with_shortened_ids)
286}
287
288#[derive(Debug, Deserialize, Serialize, Clone)]
290pub struct GenerateInfo {
291 pub crate2nix_version: String,
293 pub crate2nix_arguments: Vec<String>,
295}
296
297impl Default for GenerateInfo {
298 fn default() -> GenerateInfo {
299 GenerateInfo {
300 crate2nix_version: env!("CARGO_PKG_VERSION").to_string(),
301 crate2nix_arguments: env::args().skip(1).collect(),
302 }
303 }
304}
305
306#[derive(Debug, Deserialize, Serialize, Clone)]
308pub struct GenerateConfig {
309 pub cargo_toml: Vec<PathBuf>,
311 pub use_cargo_lock_checksums: bool,
313 pub output: PathBuf,
315 pub crate_hashes_json: PathBuf,
318 pub registry_hashes_json: PathBuf,
321 pub nixpkgs_path: String,
323 pub other_metadata_options: Vec<String>,
325 pub read_crate_hashes: bool,
327}