1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
//! # crate2nix
//!
//! Internal library for the crate2nix binary. This is not meant to be used separately, I just enjoy
//! writing doc tests ;)
//!
//! [Repository](https://github.com/kolloch/crate2nix)

#![forbid(unsafe_code)]
#![deny(missing_docs)]

use std::env;
use std::path::PathBuf;
use std::{
    collections::{BTreeMap, HashMap, HashSet, VecDeque},
    path::Path,
};

use anyhow::format_err;
use anyhow::Context;
use anyhow::Error;
use cargo_metadata::Metadata;
use cargo_metadata::PackageId;
use metadata::MergedMetadata;
use serde::Deserialize;
use serde::Serialize;

use crate::metadata::IndexedMetadata;
use crate::resolve::{CrateDerivation, ResolvedSource};
use itertools::Itertools;
use resolve::CratesIoSource;

mod command;
pub mod config;
mod lock;
mod metadata;
pub mod nix_build;
mod prefetch;
pub mod render;
mod resolve;
pub mod sources;
#[cfg(test)]
pub mod test;
pub mod util;

/// The resolved build info and the input for rendering the build.nix.tera template.
#[derive(Debug, Deserialize, Serialize)]
pub struct BuildInfo {
    /// The package ID of the root crate.
    pub root_package_id: Option<PackageId>,
    /// Workspaces member package IDs by package names.
    pub workspace_members: BTreeMap<String, PackageId>,
    /// Build info for all crates needed for this build.
    pub crates: Vec<CrateDerivation>,
    /// For convenience include the source for tests.
    pub indexed_metadata: IndexedMetadata,
    /// The generation configuration.
    pub info: GenerateInfo,
    /// The generation configuration.
    pub config: GenerateConfig,
}

impl BuildInfo {
    /// Return the `NixBuildInfo` data ready for rendering the nix build file.
    pub fn for_config(info: &GenerateInfo, config: &GenerateConfig) -> Result<BuildInfo, Error> {
        let merged = {
            let mut metadatas = Vec::new();
            for cargo_toml in &config.cargo_toml {
                metadatas.push(cargo_metadata(config, cargo_toml)?);
            }
            metadata::MergedMetadata::merge(metadatas)?
        };

        let indexed_metadata = IndexedMetadata::new_from_merged(&merged).map_err(|e| {
            format_err!(
                "while indexing metadata for {:#?}: {}",
                config
                    .cargo_toml
                    .iter()
                    .map(|p| p.to_string_lossy())
                    .collect::<Vec<_>>(),
                e
            )
        })?;
        let mut default_nix = BuildInfo::new(info, config, indexed_metadata)?;

        default_nix.prune_unneeded_crates();

        prefetch_and_fill_crates_sha256(config, &merged, &mut default_nix)?;

        Ok(default_nix)
    }

    fn prune_unneeded_crates(&mut self) {
        let mut queue: VecDeque<&PackageId> = self
            .root_package_id
            .iter()
            .chain(self.workspace_members.values())
            .collect();
        let mut reachable = HashSet::new();
        let indexed_crates: BTreeMap<_, _> =
            self.crates.iter().map(|c| (&c.package_id, c)).collect();
        while let Some(next_package_id) = queue.pop_back() {
            if !reachable.insert(next_package_id.clone()) {
                continue;
            }

            queue.extend(
                indexed_crates
                    .get(next_package_id)
                    .iter()
                    .flat_map(|c| {
                        c.dependencies
                            .iter()
                            .chain(c.build_dependencies.iter())
                            .chain(c.dev_dependencies.iter())
                    })
                    .map(|d| &d.package_id),
            );
        }
        self.crates.retain(|c| reachable.contains(&c.package_id));
    }

    fn new(
        info: &GenerateInfo,
        config: &GenerateConfig,
        metadata: IndexedMetadata,
    ) -> Result<BuildInfo, Error> {
        let crate2nix_json = crate::config::Config::read_from_or_default(
            &config
                .crate_hashes_json
                .parent()
                .expect("crate-hashes.json has parent dir")
                .join("crate2nix.json"),
        )?;

        Ok(BuildInfo {
            root_package_id: metadata.root.clone(),
            workspace_members: metadata
                .workspace_members
                .iter()
                .flat_map(|pkg_id| {
                    metadata
                        .pkgs_by_id
                        .get(pkg_id)
                        .map(|pkg| (pkg.name.clone(), pkg_id.clone()))
                })
                .collect(),
            crates: metadata
                .pkgs_by_id
                .values()
                .map(|package| {
                    CrateDerivation::resolve(config, &crate2nix_json, &metadata, package)
                })
                .collect::<Result<_, Error>>()?,
            indexed_metadata: metadata,
            info: info.clone(),
            config: config.clone(),
        })
    }
}

/// Call `cargo metadata` and return result.
fn cargo_metadata(config: &GenerateConfig, cargo_toml: &Path) -> Result<Metadata, Error> {
    let mut cmd = cargo_metadata::MetadataCommand::new();
    let mut other_options = config.other_metadata_options.clone();
    other_options.push("--locked".into());
    cmd.manifest_path(cargo_toml).other_options(&*other_options);
    cmd.exec().map_err(|e| {
        format_err!(
            "while retrieving metadata about {}: {}",
            &cargo_toml.to_string_lossy(),
            e
        )
    })
}

/// Prefetch hashes when necessary.
fn prefetch_and_fill_crates_sha256(
    config: &GenerateConfig,
    merged: &MergedMetadata,
    default_nix: &mut BuildInfo,
) -> Result<(), Error> {
    let mut from_lock_file: HashMap<PackageId, String> =
        extract_hashes_from_lockfile(config, merged, default_nix)?;
    for (_package_id, hash) in from_lock_file.iter_mut() {
        let bytes =
            hex::decode(&hash).map_err(|e| format_err!("while decoding '{}': {}", hash, e))?;
        *hash = nix_base32::to_nix_base32(&bytes);
    }

    let prefetched = prefetch::prefetch(
        config,
        &from_lock_file,
        &default_nix.crates,
        &default_nix.indexed_metadata.id_shortener,
    )
    .map_err(|e| format_err!("while prefetching crates for calculating sha256: {}", e))?;

    for package in default_nix.crates.iter_mut() {
        if package.source.sha256().is_none() {
            if let Some(hash) = prefetched
                .get(
                    default_nix
                        .indexed_metadata
                        .id_shortener
                        .lengthen_ref(&package.package_id),
                )
                .or_else(|| from_lock_file.get(&package.package_id))
            {
                package.source = package.source.with_sha256(hash.clone());
            }
        }
    }

    Ok(())
}

fn extract_hashes_from_lockfile(
    config: &GenerateConfig,
    merged: &MergedMetadata,
    default_nix: &mut BuildInfo,
) -> Result<HashMap<PackageId, String>, Error> {
    if !config.use_cargo_lock_checksums {
        return Ok(HashMap::new());
    }

    let mut hashes: HashMap<PackageId, String> = HashMap::new();

    for cargo_toml in &config.cargo_toml {
        let lock_file_path = cargo_toml.parent().unwrap().join("Cargo.lock");
        let lock_file = crate::lock::EncodableResolve::load_lock_file(&lock_file_path)?;
        lock_file
            .get_hashes_by_package_id(merged, &mut hashes)
            .context(format!(
                "while parsing checksums from Lockfile {}",
                &lock_file_path.to_string_lossy()
            ))?;
    }

    let hashes_with_shortened_ids: HashMap<PackageId, String> = hashes
        .into_iter()
        .map(|(package_id, hash)| {
            (
                default_nix
                    .indexed_metadata
                    .id_shortener
                    .shorten_owned(package_id),
                hash,
            )
        })
        .collect();

    let mut missing_hashes = Vec::new();
    for package in default_nix.crates.iter_mut().filter(|c| match &c.source {
        ResolvedSource::CratesIo(CratesIoSource { sha256, .. }) if sha256.is_none() => {
            !hashes_with_shortened_ids.contains_key(&c.package_id)
        }
        _ => false,
    }) {
        missing_hashes.push(format!("{} {}", package.crate_name, package.version));
    }
    if !missing_hashes.is_empty() {
        eprintln!(
            "Did not find all crates.io hashes in Cargo.lock. Hashes for e.g. {} are missing.\n\
             This is probably a bug.",
            missing_hashes.iter().take(10).join(", ")
        );
    }
    Ok(hashes_with_shortened_ids)
}

/// Some info about the crate2nix invocation.
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct GenerateInfo {
    /// The version of this `crate2nix` instance.
    pub crate2nix_version: String,
    /// The arguments that were passed to `crate2nix`.
    pub crate2nix_arguments: Vec<String>,
}

impl Default for GenerateInfo {
    fn default() -> GenerateInfo {
        GenerateInfo {
            crate2nix_version: env!("CARGO_PKG_VERSION").to_string(),
            crate2nix_arguments: env::args().skip(1).collect(),
        }
    }
}

/// Configuration for the default.nix generation.
#[derive(Debug, Deserialize, Serialize, Clone)]
pub struct GenerateConfig {
    /// The path to `Cargo.toml`.
    pub cargo_toml: Vec<PathBuf>,
    /// Whether to inspect `Cargo.lock` for checksums so that we do not need to prefetch them.
    pub use_cargo_lock_checksums: bool,
    /// The path of the generated `Cargo.nix` file.
    pub output: PathBuf,
    /// The path of the `crate-hashes.json` file which is used to look up hashes and/or store
    /// prefetched hashes at.
    pub crate_hashes_json: PathBuf,
    /// The nix expression for the nixpkgs path to use.
    pub nixpkgs_path: String,
    /// Additional arguments to pass to `cargo metadata`.
    pub other_metadata_options: Vec<String>,
    /// Whether to read a `crate-hashes.json` file.
    pub read_crate_hashes: bool,
}