1use 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#[derive(Debug, PartialEq, Clone)]
13pub enum Source {
14 Local(PathBuf),
21
22 CratesIo(String),
29}
30
31impl 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(converted);
40
41 Rc::try_unwrap(result).unwrap().into_inner()
42 }
43}
44
45#[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: _, 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 let lib_name = lib_name.and_then(|n| if n == name { None } else { Some(n) });
83
84 let lib_path = lib_path.and_then(|p| if p == "src/lib.rs" { None } else { Some(p) });
86
87 let build_path = build_path.and_then(|p| if p == "build.rs" { None } else { Some(p) });
89
90 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 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}