Skip to main content

crate2nix/
lib.rs

1//! # crate2nix
2//!
3//! Internal library for the crate2nix binary. This is not meant to be used separately, I just enjoy
4//! writing doc tests ;)
5//!
6//! [Repository](https://github.com/kolloch/crate2nix)
7
8#![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/// The resolved build info and the input for rendering the build.nix.tera template.
46#[derive(Debug, Deserialize, Serialize)]
47pub struct BuildInfo {
48    /// The package ID of the root crate.
49    pub root_package_id: Option<PackageId>,
50    /// Workspaces member package IDs by package names.
51    pub workspace_members: BTreeMap<String, PackageId>,
52    /// Registries used by the crates.
53    pub registries: BTreeMap<String, String>,
54    /// Build info for all crates needed for this build.
55    pub crates: Vec<CrateDerivation>,
56    /// For convenience include the source for tests.
57    pub indexed_metadata: IndexedMetadata,
58    /// The generation configuration.
59    pub info: GenerateInfo,
60    /// The generation configuration.
61    pub config: GenerateConfig,
62}
63
64impl BuildInfo {
65    /// Return the `NixBuildInfo` data ready for rendering the nix build file.
66    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
167/// Call `cargo metadata` and return result.
168fn 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
182/// Prefetch hashes when necessary.
183fn 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
223/// Prefetch hashes when necessary.
224fn 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/// Some info about the crate2nix invocation.
289#[derive(Debug, Deserialize, Serialize, Clone)]
290pub struct GenerateInfo {
291    /// The version of this `crate2nix` instance.
292    pub crate2nix_version: String,
293    /// The arguments that were passed to `crate2nix`.
294    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/// Configuration for the default.nix generation.
307#[derive(Debug, Deserialize, Serialize, Clone)]
308pub struct GenerateConfig {
309    /// The path to `Cargo.toml`.
310    pub cargo_toml: Vec<PathBuf>,
311    /// Whether to inspect `Cargo.lock` for checksums so that we do not need to prefetch them.
312    pub use_cargo_lock_checksums: bool,
313    /// The path of the generated `Cargo.nix` file.
314    pub output: PathBuf,
315    /// The path of the `crate-hashes.json` file which is used to look up hashes and/or store
316    /// prefetched hashes at.
317    pub crate_hashes_json: PathBuf,
318    /// The path of the `registry-hashes.json` file which is used to look up hashes and/or store
319    /// prefetched hashes at.
320    pub registry_hashes_json: PathBuf,
321    /// The nix expression for the nixpkgs path to use.
322    pub nixpkgs_path: String,
323    /// Additional arguments to pass to `cargo metadata`.
324    pub other_metadata_options: Vec<String>,
325    /// Whether to read a `crate-hashes.json` file.
326    pub read_crate_hashes: bool,
327}