nbuild_core/models/
mod.rs

1//! Models to reason about the cargo inputs and the nix outputs
2
3use std::{cell::RefCell, collections::BTreeMap, path::PathBuf, rc::Rc};
4
5use cargo_lock::Version;
6use tracing::{instrument, trace};
7
8pub mod cargo;
9pub mod nix;
10
11/// Where does the crate's code come from
12#[derive(Debug, PartialEq, Clone)]
13pub enum Source {
14    /// It is a local path
15    ///
16    /// ```toml
17    /// [dependencies]
18    /// dependency = { path = "/local/path" }
19    /// ```
20    Local(PathBuf),
21
22    /// It is from crates.io
23    ///
24    /// ```toml
25    /// [dependencies]
26    /// dependency = "0.2.0"
27    /// ```
28    CratesIo(String),
29}
30
31/// Convert the cargo package to a nix package for output
32impl From<cargo::Package> for nix::Package {
33    fn from(package: cargo::Package) -> Self {
34        let mut converted = Default::default();
35
36        let result = cargo_to_nix(package, &mut converted);
37
38        // Drop what was converted so that we can unwrap from the Rc
39        drop(converted);
40
41        Rc::try_unwrap(result).unwrap().into_inner()
42    }
43}
44
45/// Recursively convert a cargo package to a nix package. Also ensure a crate is only converted once by using the
46/// `converted` cache to lookup crates that have already been converted.
47#[instrument(skip_all, fields(name = %cargo_package.name))]
48fn cargo_to_nix(
49    cargo_package: cargo::Package,
50    converted: &mut BTreeMap<(String, Version), Rc<RefCell<nix::Package>>>,
51) -> Rc<RefCell<nix::Package>> {
52    let cargo::Package {
53        name,
54        lib_name,
55        version,
56        source,
57        lib_path,
58        build_path,
59        proc_macro,
60        features: _, // We only care about the features that were enabled at the end
61        enabled_features,
62        dependencies,
63        build_dependencies,
64        edition,
65    } = cargo_package;
66
67    match converted.get(&(name.clone(), version.clone())) {
68        Some(package) => Rc::clone(package),
69        None => {
70            let dependencies = dependencies
71                .iter()
72                .filter(|d| !d.optional)
73                .map(|dependency| convert_dependency(dependency, converted))
74                .collect();
75            let build_dependencies = build_dependencies
76                .iter()
77                .filter(|d| !d.optional)
78                .map(|dependency| convert_dependency(dependency, converted))
79                .collect();
80
81            // Handle libs that rename themselves
82            let lib_name = lib_name.and_then(|n| if n == name { None } else { Some(n) });
83
84            // Handle libs with a custom `lib.rs` paths
85            let lib_path = lib_path.and_then(|p| if p == "src/lib.rs" { None } else { Some(p) });
86
87            // Handle custom `build.rs` paths
88            let build_path = build_path.and_then(|p| if p == "build.rs" { None } else { Some(p) });
89
90            // The features array needs to stay deterministic to prevent unneeded rebuilds, so we sort it
91            let mut features = enabled_features.into_iter().collect::<Vec<_>>();
92            features.sort();
93
94            let package = RefCell::new(nix::Package {
95                name: name.clone(),
96                version: version.clone(),
97                source,
98                lib_name,
99                lib_path,
100                build_path,
101                proc_macro,
102                features,
103                dependencies,
104                build_dependencies,
105                edition,
106                printed: false,
107            })
108            .into();
109
110            converted.insert((name, version), Rc::clone(&package));
111
112            package
113        }
114    }
115}
116
117fn convert_dependency(
118    dependency: &cargo::Dependency,
119    converted: &mut BTreeMap<(String, Version), Rc<RefCell<nix::Package>>>,
120) -> nix::Dependency {
121    let cargo_package = Rc::clone(&dependency.package).borrow().clone();
122    let package = cargo_to_nix(cargo_package, converted);
123
124    let rename = if dependency.name == package.borrow().name {
125        None
126    } else {
127        trace!(dependency_name = dependency.name, "activating rename");
128
129        Some(dependency.name.to_string())
130    };
131
132    nix::Dependency { package, rename }
133}
134
135#[cfg(test)]
136mod tests {
137    use std::{
138        cell::RefCell,
139        collections::{HashMap, HashSet},
140        path::PathBuf,
141        rc::Rc,
142        str::FromStr,
143    };
144
145    use crate::models::{cargo, nix};
146
147    use pretty_assertions::assert_eq;
148
149    #[test]
150    fn cargo_to_nix() {
151        let workspace = PathBuf::from_str(env!("CARGO_MANIFEST_DIR"))
152            .unwrap()
153            .join("tests")
154            .join("workspace");
155        let path = workspace.join("parent");
156
157        let libc = RefCell::new(cargo::Package {
158            name: "libc".to_string(),
159            version: "0.2.144".parse().unwrap(),
160            source: "libc_sha".into(),
161            lib_name: Some("libc".to_string()),
162            lib_path: Some("src/lib.rs".into()),
163            build_path: Some("build.rs".into()),
164            proc_macro: false,
165            dependencies: Default::default(),
166            build_dependencies: Default::default(),
167            features: HashMap::from([
168                ("std".to_string(), vec![]),
169                ("default".to_string(), vec!["std".to_string()]),
170                ("use_std".to_string(), vec!["std".to_string()]),
171                ("extra_traits".to_string(), vec![]),
172                ("align".to_string(), vec![]),
173                (
174                    "rustc-dep-of-std".to_string(),
175                    vec!["align".to_string(), "rustc-std-workspace-core".to_string()],
176                ),
177                ("const-extern-fn".to_string(), vec![]),
178                (
179                    "rustc-std-workspace-core".to_string(),
180                    vec!["dep:rustc-std-workspace-core".to_string()],
181                ),
182            ]),
183            enabled_features: Default::default(),
184            edition: "2015".to_string(),
185        })
186        .into();
187        let optional = RefCell::new(cargo::Package {
188            name: "optional".to_string(),
189            version: "1.0.0".parse().unwrap(),
190            source: "optional_sha".into(),
191            lib_name: Some("optional".to_string()),
192            lib_path: Some("src/lib.rs".into()),
193            build_path: None,
194            proc_macro: false,
195            dependencies: Default::default(),
196            build_dependencies: Default::default(),
197            features: HashMap::from([
198                ("std".to_string(), vec![]),
199                ("default".to_string(), vec!["std".to_string()]),
200            ]),
201            enabled_features: Default::default(),
202            edition: "2021".to_string(),
203        })
204        .into();
205
206        let input = cargo::Package {
207            name: "parent".to_string(),
208            lib_name: None,
209            version: "0.1.0".parse().unwrap(),
210            source: path.clone().into(),
211            lib_path: None,
212            build_path: None,
213            proc_macro: false,
214            dependencies: vec![
215                cargo::Dependency {
216                    name: "child".to_string(),
217                    package: RefCell::new(cargo::Package {
218                        name: "child".to_string(),
219                        version: "0.1.0".parse().unwrap(),
220                        source: workspace.join("child").into(),
221                        lib_name: Some("child".to_string()),
222                        lib_path: Some("src/lib.rs".into()),
223                        build_path: None,
224                        proc_macro: false,
225                        dependencies: vec![
226                            cargo::Dependency {
227                                name: "fnv".to_string(),
228                                package: RefCell::new(cargo::Package {
229                                    name: "fnv".to_string(),
230                                    version: "1.0.7".parse().unwrap(),
231                                    source: "fnv_sha".into(),
232                                    lib_name: Some("fnv".to_string()),
233                                    lib_path: Some("lib.rs".into()),
234                                    build_path: None,
235                                    proc_macro: false,
236                                    dependencies: Default::default(),
237                                    build_dependencies: Default::default(),
238                                    features: HashMap::from([
239                                        ("default".to_string(), vec!["std".to_string()]),
240                                        ("std".to_string(), vec![]),
241                                    ]),
242                                    enabled_features: Default::default(),
243                                    edition: "2015".to_string(),
244                                })
245                                .into(),
246                                optional: false,
247                                uses_default_features: true,
248                                features: Default::default(),
249                            },
250                            cargo::Dependency {
251                                name: "itoa".to_string(),
252                                package: RefCell::new(cargo::Package {
253                                    name: "itoa".to_string(),
254                                    version: "1.0.6".parse().unwrap(),
255                                    source: "itoa_sha".into(),
256                                    lib_name: Some("itoa".to_string()),
257                                    lib_path: Some("src/lib.rs".into()),
258                                    build_path: None,
259                                    proc_macro: false,
260                                    dependencies: Default::default(),
261                                    build_dependencies: Default::default(),
262                                    features: HashMap::from([(
263                                        "no-panic".to_string(),
264                                        vec!["dep:no-panic".to_string()],
265                                    )]),
266                                    enabled_features: Default::default(),
267                                    edition: "2018".to_string(),
268                                })
269                                .into(),
270                                optional: false,
271                                uses_default_features: true,
272                                features: Default::default(),
273                            },
274                            cargo::Dependency {
275                                name: "libc".to_string(),
276                                package: Rc::clone(&libc),
277                                optional: false,
278                                uses_default_features: true,
279                                features: Default::default(),
280                            },
281                            cargo::Dependency {
282                                name: "optional".to_string(),
283                                package: Rc::clone(&optional),
284                                optional: true,
285                                uses_default_features: true,
286                                features: Default::default(),
287                            },
288                            cargo::Dependency {
289                                name: "new_name".to_string(),
290                                package: RefCell::new(cargo::Package {
291                                    name: "rename".to_string(),
292                                    version: "0.1.0".parse().unwrap(),
293                                    source: workspace.join("rename").into(),
294                                    lib_name: Some("lib_rename".to_string()),
295                                    lib_path: Some("src/lib.rs".into()),
296                                    build_path: None,
297                                    proc_macro: false,
298                                    dependencies: Default::default(),
299                                    build_dependencies: Default::default(),
300                                    features: Default::default(),
301                                    enabled_features: Default::default(),
302                                    edition: "2021".to_string(),
303                                })
304                                .into(),
305                                optional: false,
306                                uses_default_features: true,
307                                features: Default::default(),
308                            },
309                            cargo::Dependency {
310                                name: "rustversion".to_string(),
311                                package: RefCell::new(cargo::Package {
312                                    name: "rustversion".to_string(),
313                                    version: "1.0.12".parse().unwrap(),
314                                    source: "rustversion_sha".into(),
315                                    lib_name: Some("rustversion".to_string()),
316                                    lib_path: Some("src/lib.rs".into()),
317                                    build_path: Some("build/build.rs".into()),
318                                    proc_macro: true,
319                                    dependencies: Default::default(),
320                                    build_dependencies: Default::default(),
321                                    features: Default::default(),
322                                    enabled_features: Default::default(),
323                                    edition: "2018".to_string(),
324                                })
325                                .into(),
326                                optional: false,
327                                uses_default_features: true,
328                                features: Default::default(),
329                            },
330                        ],
331                        build_dependencies: vec![cargo::Dependency {
332                            name: "arbitrary".to_string(),
333                            package: RefCell::new(cargo::Package {
334                                name: "arbitrary".to_string(),
335                                version: "1.3.0".parse().unwrap(),
336                                source: "arbitrary_sha".into(),
337                                lib_name: Some("arbitrary".to_string()),
338                                lib_path: Some("src/lib.rs".into()),
339                                build_path: None,
340                                proc_macro: false,
341                                dependencies: Default::default(),
342                                build_dependencies: Default::default(),
343                                features: HashMap::from([
344                                    ("derive".to_string(), vec!["derive_arbitrary".to_string()]),
345                                    (
346                                        "derive_arbitrary".to_string(),
347                                        vec!["dep:derive_arbitrary".to_string()],
348                                    ),
349                                ]),
350                                enabled_features: Default::default(),
351                                edition: "2018".to_string(),
352                            })
353                            .into(),
354                            optional: false,
355                            uses_default_features: true,
356                            features: Default::default(),
357                        }],
358                        features: HashMap::from([
359                            (
360                                "default".to_string(),
361                                vec!["one".to_string(), "two".to_string()],
362                            ),
363                            ("one".to_string(), vec!["new_name".to_string()]),
364                            ("two".to_string(), vec![]),
365                            ("new_name".to_string(), vec!["dep:new_name".to_string()]),
366                        ]),
367                        enabled_features: HashSet::from([
368                            "one".to_string(),
369                            "new_name".to_string(),
370                        ]),
371                        edition: "2021".to_string(),
372                    })
373                    .into(),
374                    optional: false,
375                    uses_default_features: false,
376                    features: vec!["one".to_string()],
377                },
378                cargo::Dependency {
379                    name: "itoa".to_string(),
380                    package: RefCell::new(cargo::Package {
381                        name: "itoa".to_string(),
382                        version: "0.4.8".parse().unwrap(),
383                        source: "itoa_sha".into(),
384                        lib_name: Some("itoa".to_string()),
385                        lib_path: Some("src/lib.rs".into()),
386                        build_path: None,
387                        proc_macro: false,
388                        dependencies: Default::default(),
389                        build_dependencies: Default::default(),
390                        features: HashMap::from([
391                            ("default".to_string(), vec!["std".to_string()]),
392                            ("no-panic".to_string(), vec!["dep:no-panic".to_string()]),
393                            ("std".to_string(), vec![]),
394                            ("i128".to_string(), vec![]),
395                        ]),
396                        enabled_features: Default::default(),
397                        edition: "2018".to_string(),
398                    })
399                    .into(),
400                    optional: false,
401                    uses_default_features: true,
402                    features: Default::default(),
403                },
404                cargo::Dependency {
405                    name: "libc".to_string(),
406                    package: libc,
407                    optional: false,
408                    uses_default_features: true,
409                    features: Default::default(),
410                },
411                cargo::Dependency {
412                    name: "optional".to_string(),
413                    package: optional,
414                    optional: true,
415                    uses_default_features: true,
416                    features: Default::default(),
417                },
418                cargo::Dependency {
419                    name: "targets".to_string(),
420                    package: RefCell::new(cargo::Package {
421                        name: "targets".to_string(),
422                        version: "0.1.0".parse().unwrap(),
423                        source: workspace.join("targets").into(),
424                        lib_name: Some("targets".to_string()),
425                        lib_path: Some("src/lib.rs".into()),
426                        build_path: None,
427                        proc_macro: false,
428                        dependencies: Default::default(),
429                        build_dependencies: Default::default(),
430                        features: HashMap::from([
431                            ("unix".to_string(), vec![]),
432                            ("windows".to_string(), vec![]),
433                        ]),
434                        enabled_features: HashSet::from(["unix".to_string()]),
435                        edition: "2021".to_string(),
436                    })
437                    .into(),
438                    optional: false,
439                    uses_default_features: true,
440                    features: vec!["unix".to_string()],
441                },
442            ],
443            build_dependencies: Default::default(),
444            features: Default::default(),
445            enabled_features: Default::default(),
446            edition: "2021".to_string(),
447        };
448
449        let actual: nix::Package = input.into();
450
451        let libc = RefCell::new(nix::Package {
452            name: "libc".to_string(),
453            version: "0.2.144".parse().unwrap(),
454            source: "libc_sha".into(),
455            lib_name: None,
456            lib_path: None,
457            build_path: None,
458            proc_macro: false,
459            dependencies: Default::default(),
460            build_dependencies: Default::default(),
461            features: Default::default(),
462            edition: "2015".to_string(),
463            printed: false,
464        })
465        .into();
466        let expected = nix::Package {
467            name: "parent".to_string(),
468            version: "0.1.0".parse().unwrap(),
469            source: path.into(),
470            lib_name: None,
471            lib_path: None,
472            build_path: None,
473            proc_macro: false,
474            dependencies: vec![
475                nix::Package {
476                    name: "child".to_string(),
477                    version: "0.1.0".parse().unwrap(),
478                    source: workspace.join("child").into(),
479                    lib_name: None,
480                    lib_path: None,
481                    build_path: None,
482                    proc_macro: false,
483                    dependencies: vec![
484                        nix::Package {
485                            name: "fnv".to_string(),
486                            version: "1.0.7".parse().unwrap(),
487                            source: "fnv_sha".into(),
488                            lib_name: None,
489                            lib_path: Some("lib.rs".into()),
490                            build_path: None,
491                            proc_macro: false,
492                            dependencies: Default::default(),
493                            build_dependencies: Default::default(),
494                            features: Default::default(),
495                            edition: "2015".to_string(),
496                            printed: false,
497                        }
498                        .into(),
499                        nix::Package {
500                            name: "itoa".to_string(),
501                            version: "1.0.6".parse().unwrap(),
502                            source: "itoa_sha".into(),
503                            lib_name: None,
504                            lib_path: None,
505                            build_path: None,
506                            proc_macro: false,
507                            dependencies: Default::default(),
508                            build_dependencies: Default::default(),
509                            features: Default::default(),
510                            edition: "2018".to_string(),
511                            printed: false,
512                        }
513                        .into(),
514                        nix::Dependency {
515                            package: Rc::clone(&libc),
516                            rename: None,
517                        },
518                        nix::Dependency {
519                            package: RefCell::new(nix::Package {
520                                name: "rename".to_string(),
521                                version: "0.1.0".parse().unwrap(),
522                                source: workspace.join("rename").into(),
523                                lib_name: Some("lib_rename".to_string()),
524                                lib_path: None,
525                                build_path: None,
526                                proc_macro: false,
527                                dependencies: Default::default(),
528                                build_dependencies: Default::default(),
529                                features: Default::default(),
530                                edition: "2021".to_string(),
531                                printed: false,
532                            })
533                            .into(),
534                            rename: Some("new_name".to_string()),
535                        },
536                        nix::Package {
537                            name: "rustversion".to_string(),
538                            version: "1.0.12".parse().unwrap(),
539                            source: "rustversion_sha".into(),
540                            lib_name: None,
541                            lib_path: None,
542                            build_path: Some("build/build.rs".into()),
543                            proc_macro: true,
544                            dependencies: Default::default(),
545                            build_dependencies: Default::default(),
546                            features: Default::default(),
547                            edition: "2018".to_string(),
548                            printed: false,
549                        }
550                        .into(),
551                    ],
552                    build_dependencies: vec![nix::Package {
553                        name: "arbitrary".to_string(),
554                        version: "1.3.0".parse().unwrap(),
555                        source: "arbitrary_sha".into(),
556                        lib_name: None,
557                        lib_path: None,
558                        build_path: None,
559                        proc_macro: false,
560                        dependencies: Default::default(),
561                        build_dependencies: Default::default(),
562                        features: Default::default(),
563                        edition: "2018".to_string(),
564                        printed: false,
565                    }
566                    .into()],
567                    features: vec!["new_name".to_string(), "one".to_string()],
568                    edition: "2021".to_string(),
569                    printed: false,
570                }
571                .into(),
572                nix::Package {
573                    name: "itoa".to_string(),
574                    version: "0.4.8".parse().unwrap(),
575                    source: "itoa_sha".into(),
576                    lib_name: None,
577                    lib_path: None,
578                    build_path: None,
579                    proc_macro: false,
580                    dependencies: Default::default(),
581                    build_dependencies: Default::default(),
582                    features: Default::default(),
583                    edition: "2018".to_string(),
584                    printed: false,
585                }
586                .into(),
587                nix::Dependency {
588                    package: libc,
589                    rename: None,
590                },
591                nix::Package {
592                    name: "targets".to_string(),
593                    version: "0.1.0".parse().unwrap(),
594                    source: workspace.join("targets").into(),
595                    lib_name: None,
596                    lib_path: None,
597                    build_path: None,
598                    proc_macro: false,
599                    dependencies: Default::default(),
600                    build_dependencies: Default::default(),
601                    features: vec!["unix".to_string()],
602                    edition: "2021".to_string(),
603                    printed: false,
604                }
605                .into(),
606            ],
607            build_dependencies: Default::default(),
608            features: Default::default(),
609            edition: "2021".to_string(),
610            printed: false,
611        };
612
613        assert_eq!(actual, expected);
614
615        // Make sure the libcs are linked - ie, the version for both libcs should change with this one assignment
616        actual.dependencies[2].package.borrow_mut().version = "0.2.0".parse().unwrap();
617
618        assert_eq!(
619            actual.dependencies[2],
620            actual.dependencies[0].package.borrow().dependencies[2]
621        );
622    }
623}