1use std::{cell::RefCell, fs, rc::Rc};
4
5use cargo_metadata::{camino::Utf8PathBuf, semver::Version};
6
7use super::Source;
8
9#[derive(Debug, PartialEq)]
13pub struct Package {
14 pub(super) name: String,
15 pub(super) version: Version,
16 pub(super) source: Source,
17 pub(super) lib_name: Option<String>,
18 pub(super) lib_path: Option<Utf8PathBuf>,
19 pub(super) build_path: Option<Utf8PathBuf>,
20 pub(super) proc_macro: bool,
21 pub(super) features: Vec<String>,
22 pub(super) dependencies: Vec<Dependency>,
23 pub(super) build_dependencies: Vec<Dependency>,
24 pub(super) edition: String,
25 pub(super) printed: bool,
26}
27
28#[derive(Debug, PartialEq)]
30pub struct Dependency {
31 pub(super) package: Rc<RefCell<Package>>,
32 pub(super) rename: Option<String>,
33}
34
35impl Package {
36 pub fn into_file(self) -> Result<(), std::io::Error> {
38 let expr = self.into_derivative();
39
40 fs::write(".nbuild.nix", expr)
41 }
42
43 pub fn name(&self) -> &str {
45 &self.name
46 }
47
48 pub fn into_derivative(self) -> String {
50 let Self {
51 name,
52 version,
53 source,
54 lib_name: _,
55 lib_path: _,
56 build_path: _,
57 proc_macro: _,
58 features: _,
59 dependencies,
60 build_dependencies,
61 edition,
62 printed: _,
63 } = self;
64
65 let mut build_details = Default::default();
67
68 let dep_idents: Vec<_> = dependencies
69 .into_iter()
70 .map(|d| {
71 let identifier = d.package.borrow().identifier();
72 Self::to_details(&d, &mut build_details);
73 identifier
74 })
75 .collect();
76
77 let build_deps = if build_dependencies.is_empty() {
78 Default::default()
79 } else {
80 let dep_idents: Vec<_> = build_dependencies
81 .into_iter()
82 .map(|d| {
83 let identifier = d.package.borrow().identifier();
84 Self::to_details(&d, &mut build_details);
85 identifier
86 })
87 .collect();
88 format!("\n buildDependencies = [{}];", dep_idents.join(" "))
89 };
90
91 format!(
92 r#"{{ pkgs ? import <nixpkgs> {{
93 overlays = [ (import (builtins.fetchTarball "https://github.com/oxalica/rust-overlay/archive/master.tar.gz")) ];
94}} }}:
95
96let
97 sourceFilter = name: type:
98 let
99 baseName = builtins.baseNameOf (builtins.toString name);
100 in
101 ! (
102 # Filter out git
103 baseName == ".gitignore"
104 || (type == "directory" && baseName == ".git")
105
106 # Filter out build results
107 || (
108 type == "directory" && baseName == "target"
109 )
110
111 # Filter out nix-build result symlinks
112 || (
113 type == "symlink" && pkgs.lib.hasPrefix "result" baseName
114 )
115 );
116 rustVersion = pkgs.rust-bin.stable."1.68.0".default;
117 defaultCrateOverrides = pkgs.defaultCrateOverrides // {{
118 opentelemetry-proto = attrs: {{ buildInputs = [ pkgs.protobuf ]; }};
119 }};
120 fetchCrate = {{ crateName, version, sha256 }}: pkgs.fetchurl {{
121 # https://www.pietroalbini.org/blog/downloading-crates-io/
122 # Not rate-limited, CDN URL.
123 name = "${{crateName}}-${{version}}.tar.gz";
124 url = "https://static.crates.io/crates/${{crateName}}/${{crateName}}-${{version}}.crate";
125 inherit sha256;
126 }};
127 buildRustCrate = pkgs.buildRustCrate.override {{
128 rustc = rustVersion;
129 inherit defaultCrateOverrides fetchCrate;
130 }};
131 preBuild = "rustc -vV";
132
133 # Core
134 {} = buildRustCrate rec {{
135 crateName = "{}";
136 version = "{}";
137
138 {}
139
140 dependencies = [
141 {}
142 ];{}
143 edition = "{}";
144 codegenUnits = 16;
145 extraRustcOpts = [ "-C embed-bitcode=no" ];
146 inherit preBuild;
147 }};
148
149 # Dependencies
150{}
151in
152{}
153"#,
154 name,
155 name,
156 version,
157 Self::get_source(&source),
158 dep_idents.join("\n "),
159 build_deps,
160 edition,
161 build_details.join("\n"),
162 name
163 )
164 }
165
166 fn to_details(dependency: &Dependency, build_details: &mut Vec<String>) {
168 let mut this = dependency.package.borrow_mut();
169
170 if this.printed {
172 return;
173 }
174
175 let features = if this.features.is_empty() {
176 Default::default()
177 } else {
178 format!(
179 "\n features = [{}];",
180 this.features
181 .iter()
182 .map(|f| format!("\"{f}\""))
183 .collect::<Vec<_>>()
184 .join(" ")
185 )
186 };
187
188 let lib_name = if let Some(lib_name) = &this.lib_name {
189 format!("\n libName = \"{lib_name}\";")
190 } else {
191 Default::default()
192 };
193 let lib_path = if let Some(lib_path) = &this.lib_path {
194 format!("\n libPath = \"{lib_path}\";")
195 } else {
196 Default::default()
197 };
198 let build_path = if let Some(build_path) = &this.build_path {
199 format!("\n build = \"{build_path}\";")
200 } else {
201 Default::default()
202 };
203 let proc_macro = if this.proc_macro {
204 "\n procMacro = true;"
205 } else {
206 Default::default()
207 };
208
209 let mut renames = Vec::new();
210
211 let deps = if this.dependencies.is_empty() {
212 Default::default()
213 } else {
214 let dep_idents: Vec<_> = this
215 .dependencies
216 .iter()
217 .map(|d| {
218 if let Some(rename) = &d.rename {
219 renames.push((
220 d.package.borrow().name.clone(),
221 rename.clone(),
222 d.package.borrow().version.to_string(),
223 ));
224 }
225
226 d.package.borrow().identifier()
227 })
228 .collect();
229 format!("\n dependencies = [{}];", dep_idents.join(" "))
230 };
231 let build_deps = if this.build_dependencies.is_empty() {
232 Default::default()
233 } else {
234 let dep_idents: Vec<_> = this
235 .build_dependencies
236 .iter()
237 .map(|d| {
238 if let Some(rename) = &d.rename {
239 renames.push((
240 d.package.borrow().name.clone(),
241 rename.clone(),
242 d.package.borrow().version.to_string(),
243 ));
244 }
245
246 d.package.borrow().identifier()
247 })
248 .collect();
249 format!("\n buildDependencies = [{}];", dep_idents.join(" "))
250 };
251
252 let crate_renames = if renames.is_empty() {
253 Default::default()
254 } else {
255 let renames = renames
256 .into_iter()
257 .map(|(name, rename, version)| {
258 format!("\"{name}\" = [{{ rename = \"{rename}\"; version = \"{version}\"; }}];")
259 })
260 .collect::<Vec<_>>()
261 .join(" ");
262
263 format!("\n crateRenames = {{{renames}}};")
264 };
265
266 let details = format!(
267 r#" {} = buildRustCrate rec {{
268 crateName = "{}";{}
269 version = "{}";
270
271 {}{}{}{}{}{}{}{}
272 edition = "{}";
273 crateBin = [];
274 codegenUnits = 16;
275 extraRustcOpts = [ "-C embed-bitcode=no" ];
276 inherit preBuild;
277 }};"#,
278 this.identifier(),
279 this.name,
280 lib_name,
281 this.version,
282 Self::get_source(&this.source),
283 lib_path,
284 build_path,
285 proc_macro,
286 deps,
287 build_deps,
288 crate_renames,
289 features,
290 this.edition,
291 );
292
293 build_details.push(details);
294
295 for dependency in this
296 .dependencies
297 .iter()
298 .chain(this.build_dependencies.iter())
299 {
300 Self::to_details(dependency, build_details);
301 }
302
303 this.printed = true;
304 }
305
306 fn identifier(&self) -> String {
308 format!(
309 "{}_{}",
310 self.name,
311 self.version.to_string().replace(['.', '+'], "_")
312 )
313 }
314
315 fn get_source(source: &Source) -> String {
317 match source {
318 Source::Local(path) => format!(
319 "src = pkgs.lib.cleanSourceWith {{ filter = sourceFilter; src = {}; }};",
320 path.display()
321 ),
322 Source::CratesIo(sha256) => format!("sha256 = \"{sha256}\";"),
323 }
324 }
325}
326
327#[cfg(test)]
328mod tests {
329 use std::{path::PathBuf, str::FromStr};
330
331 use super::*;
332
333 use pretty_assertions::assert_eq;
334
335 impl From<Package> for Dependency {
336 fn from(package: Package) -> Self {
337 Self {
338 package: Rc::new(RefCell::new(package)),
339 rename: None,
340 }
341 }
342 }
343
344 impl From<PathBuf> for Source {
345 fn from(path: PathBuf) -> Self {
346 Self::Local(path)
347 }
348 }
349
350 impl From<&str> for Source {
351 fn from(sha: &str) -> Self {
352 Self::CratesIo(sha.to_string())
353 }
354 }
355
356 #[test]
357 fn simple_package() {
358 let package = Package {
359 name: "simple".to_string(),
360 version: "0.1.0".parse().unwrap(),
361 source: PathBuf::from_str("/cargo-nbuild/nbuild-core/tests/simple")
362 .unwrap()
363 .into(),
364 lib_name: None,
365 lib_path: None,
366 build_path: None,
367 proc_macro: false,
368 dependencies: vec![Package {
369 name: "itoa".to_string(),
370 version: "1.0.6".parse().unwrap(),
371 source: "itoa_sha".into(),
372 lib_name: None,
373 lib_path: None,
374 build_path: None,
375 proc_macro: false,
376 dependencies: Default::default(),
377 build_dependencies: Default::default(),
378 features: Default::default(),
379 edition: "2018".to_string(),
380 printed: false,
381 }
382 .into()],
383 build_dependencies: vec![Package {
384 name: "arbitrary".to_string(),
385 version: "1.3.0".parse().unwrap(),
386 source: "arbitrary_sha".into(),
387 lib_name: None,
388 lib_path: None,
389 build_path: None,
390 proc_macro: false,
391 dependencies: Default::default(),
392 build_dependencies: Default::default(),
393 features: Default::default(),
394 edition: "2018".to_string(),
395 printed: false,
396 }
397 .into()],
398 features: Default::default(),
399 edition: "2021".to_string(),
400 printed: false,
401 };
402
403 let actual = package.into_derivative();
404
405 assert_eq!(
406 actual,
407 r#"{ pkgs ? import <nixpkgs> {
408 overlays = [ (import (builtins.fetchTarball "https://github.com/oxalica/rust-overlay/archive/master.tar.gz")) ];
409} }:
410
411let
412 sourceFilter = name: type:
413 let
414 baseName = builtins.baseNameOf (builtins.toString name);
415 in
416 ! (
417 # Filter out git
418 baseName == ".gitignore"
419 || (type == "directory" && baseName == ".git")
420
421 # Filter out build results
422 || (
423 type == "directory" && baseName == "target"
424 )
425
426 # Filter out nix-build result symlinks
427 || (
428 type == "symlink" && pkgs.lib.hasPrefix "result" baseName
429 )
430 );
431 rustVersion = pkgs.rust-bin.stable."1.68.0".default;
432 defaultCrateOverrides = pkgs.defaultCrateOverrides // {
433 opentelemetry-proto = attrs: { buildInputs = [ pkgs.protobuf ]; };
434 };
435 fetchCrate = { crateName, version, sha256 }: pkgs.fetchurl {
436 # https://www.pietroalbini.org/blog/downloading-crates-io/
437 # Not rate-limited, CDN URL.
438 name = "${crateName}-${version}.tar.gz";
439 url = "https://static.crates.io/crates/${crateName}/${crateName}-${version}.crate";
440 inherit sha256;
441 };
442 buildRustCrate = pkgs.buildRustCrate.override {
443 rustc = rustVersion;
444 inherit defaultCrateOverrides fetchCrate;
445 };
446 preBuild = "rustc -vV";
447
448 # Core
449 simple = buildRustCrate rec {
450 crateName = "simple";
451 version = "0.1.0";
452
453 src = pkgs.lib.cleanSourceWith { filter = sourceFilter; src = /cargo-nbuild/nbuild-core/tests/simple; };
454
455 dependencies = [
456 itoa_1_0_6
457 ];
458 buildDependencies = [arbitrary_1_3_0];
459 edition = "2021";
460 codegenUnits = 16;
461 extraRustcOpts = [ "-C embed-bitcode=no" ];
462 inherit preBuild;
463 };
464
465 # Dependencies
466 itoa_1_0_6 = buildRustCrate rec {
467 crateName = "itoa";
468 version = "1.0.6";
469
470 sha256 = "itoa_sha";
471 edition = "2018";
472 crateBin = [];
473 codegenUnits = 16;
474 extraRustcOpts = [ "-C embed-bitcode=no" ];
475 inherit preBuild;
476 };
477 arbitrary_1_3_0 = buildRustCrate rec {
478 crateName = "arbitrary";
479 version = "1.3.0";
480
481 sha256 = "arbitrary_sha";
482 edition = "2018";
483 crateBin = [];
484 codegenUnits = 16;
485 extraRustcOpts = [ "-C embed-bitcode=no" ];
486 inherit preBuild;
487 };
488in
489simple
490"#
491 );
492 }
493
494 #[test]
495 fn workspace() {
496 let base = PathBuf::from_str("/cargo-nbuild/nbuild-core/tests/workspace").unwrap();
497
498 let libc = RefCell::new(Package {
499 name: "libc".to_string(),
500 version: "0.2.144".parse().unwrap(),
501 source: "sha".into(),
502 lib_name: None,
503 lib_path: None,
504 build_path: None,
505 proc_macro: false,
506 dependencies: Default::default(),
507 build_dependencies: Default::default(),
508 features: Default::default(),
509 edition: "2015".to_string(),
510 printed: false,
511 })
512 .into();
513
514 let package = Package {
515 name: "parent".to_string(),
516 version: "0.1.0".parse().unwrap(),
517 source: base.join("parent").into(),
518 lib_name: None,
519 lib_path: None,
520 build_path: None,
521 proc_macro: false,
522 dependencies: vec![
523 Package {
524 name: "child".to_string(),
525 version: "0.1.0".parse().unwrap(),
526 source: base.join("child").into(),
527 lib_name: None,
528 lib_path: None,
529 build_path: None,
530 proc_macro: false,
531 dependencies: vec![
532 Package {
533 name: "fnv".to_string(),
534 version: "1.0.7".parse().unwrap(),
535 source: "sha".into(),
536 lib_name: None,
537 lib_path: Some("lib.rs".into()),
538 build_path: None,
539 proc_macro: false,
540 dependencies: Default::default(),
541 build_dependencies: Default::default(),
542 features: Default::default(),
543 edition: "2015".to_string(),
544 printed: false,
545 }
546 .into(),
547 Package {
548 name: "itoa".to_string(),
549 version: "1.0.6".parse().unwrap(),
550 source: "sha".into(),
551 lib_name: None,
552 lib_path: None,
553 build_path: None,
554 proc_macro: false,
555 dependencies: Default::default(),
556 build_dependencies: Default::default(),
557 features: Default::default(),
558 edition: "2018".to_string(),
559 printed: false,
560 }
561 .into(),
562 Dependency {
563 package: Rc::clone(&libc),
564 rename: None,
565 },
566 Dependency {
567 package: RefCell::new(Package {
568 name: "rename".to_string(),
569 version: "0.1.0".parse().unwrap(),
570 source: base.join("rename").into(),
571 lib_name: Some("lib_rename".to_string()),
572 lib_path: None,
573 build_path: None,
574 proc_macro: false,
575 dependencies: Default::default(),
576 build_dependencies: Default::default(),
577 features: Default::default(),
578 edition: "2021".to_string(),
579 printed: false,
580 })
581 .into(),
582 rename: Some("new_name".to_string()),
583 },
584 Package {
585 name: "rustversion".to_string(),
586 version: "1.0.12".parse().unwrap(),
587 source: "sha".into(),
588 lib_name: None,
589 lib_path: None,
590 build_path: Some("build/build.rs".into()),
591 proc_macro: true,
592 dependencies: Default::default(),
593 build_dependencies: Default::default(),
594 features: Default::default(),
595 edition: "2018".to_string(),
596 printed: false,
597 }
598 .into(),
599 ],
600 build_dependencies: vec![Package {
601 name: "arbitrary".to_string(),
602 version: "1.3.0".parse().unwrap(),
603 source: "sha".into(),
604 lib_name: None,
605 lib_path: None,
606 build_path: None,
607 proc_macro: false,
608 dependencies: Default::default(),
609 build_dependencies: Default::default(),
610 features: Default::default(),
611 edition: "2018".to_string(),
612 printed: false,
613 }
614 .into()],
615 features: vec!["one".to_string()],
616 edition: "2021".to_string(),
617 printed: false,
618 }
619 .into(),
620 Package {
621 name: "itoa".to_string(),
622 version: "0.4.8".parse().unwrap(),
623 source: "sha".into(),
624 lib_name: None,
625 lib_path: None,
626 build_path: None,
627 proc_macro: false,
628 dependencies: Default::default(),
629 build_dependencies: Default::default(),
630 features: Default::default(),
631 edition: "2018".to_string(),
632 printed: false,
633 }
634 .into(),
635 Dependency {
636 package: libc,
637 rename: None,
638 },
639 Package {
640 name: "targets".to_string(),
641 version: "0.1.0".parse().unwrap(),
642 source: base.join("targets").into(),
643 lib_name: None,
644 lib_path: None,
645 build_path: None,
646 proc_macro: false,
647 dependencies: Default::default(),
648 build_dependencies: Default::default(),
649 features: vec!["unix".to_string()],
650 edition: "2021".to_string(),
651 printed: false,
652 }
653 .into(),
654 ],
655 build_dependencies: Default::default(),
656 features: Default::default(),
657 edition: "2021".to_string(),
658 printed: false,
659 };
660
661 let actual = package.into_derivative();
662
663 assert_eq!(
664 actual,
665 r#"{ pkgs ? import <nixpkgs> {
666 overlays = [ (import (builtins.fetchTarball "https://github.com/oxalica/rust-overlay/archive/master.tar.gz")) ];
667} }:
668
669let
670 sourceFilter = name: type:
671 let
672 baseName = builtins.baseNameOf (builtins.toString name);
673 in
674 ! (
675 # Filter out git
676 baseName == ".gitignore"
677 || (type == "directory" && baseName == ".git")
678
679 # Filter out build results
680 || (
681 type == "directory" && baseName == "target"
682 )
683
684 # Filter out nix-build result symlinks
685 || (
686 type == "symlink" && pkgs.lib.hasPrefix "result" baseName
687 )
688 );
689 rustVersion = pkgs.rust-bin.stable."1.68.0".default;
690 defaultCrateOverrides = pkgs.defaultCrateOverrides // {
691 opentelemetry-proto = attrs: { buildInputs = [ pkgs.protobuf ]; };
692 };
693 fetchCrate = { crateName, version, sha256 }: pkgs.fetchurl {
694 # https://www.pietroalbini.org/blog/downloading-crates-io/
695 # Not rate-limited, CDN URL.
696 name = "${crateName}-${version}.tar.gz";
697 url = "https://static.crates.io/crates/${crateName}/${crateName}-${version}.crate";
698 inherit sha256;
699 };
700 buildRustCrate = pkgs.buildRustCrate.override {
701 rustc = rustVersion;
702 inherit defaultCrateOverrides fetchCrate;
703 };
704 preBuild = "rustc -vV";
705
706 # Core
707 parent = buildRustCrate rec {
708 crateName = "parent";
709 version = "0.1.0";
710
711 src = pkgs.lib.cleanSourceWith { filter = sourceFilter; src = /cargo-nbuild/nbuild-core/tests/workspace/parent; };
712
713 dependencies = [
714 child_0_1_0
715 itoa_0_4_8
716 libc_0_2_144
717 targets_0_1_0
718 ];
719 edition = "2021";
720 codegenUnits = 16;
721 extraRustcOpts = [ "-C embed-bitcode=no" ];
722 inherit preBuild;
723 };
724
725 # Dependencies
726 child_0_1_0 = buildRustCrate rec {
727 crateName = "child";
728 version = "0.1.0";
729
730 src = pkgs.lib.cleanSourceWith { filter = sourceFilter; src = /cargo-nbuild/nbuild-core/tests/workspace/child; };
731 dependencies = [fnv_1_0_7 itoa_1_0_6 libc_0_2_144 rename_0_1_0 rustversion_1_0_12];
732 buildDependencies = [arbitrary_1_3_0];
733 crateRenames = {"rename" = [{ rename = "new_name"; version = "0.1.0"; }];};
734 features = ["one"];
735 edition = "2021";
736 crateBin = [];
737 codegenUnits = 16;
738 extraRustcOpts = [ "-C embed-bitcode=no" ];
739 inherit preBuild;
740 };
741 fnv_1_0_7 = buildRustCrate rec {
742 crateName = "fnv";
743 version = "1.0.7";
744
745 sha256 = "sha";
746 libPath = "lib.rs";
747 edition = "2015";
748 crateBin = [];
749 codegenUnits = 16;
750 extraRustcOpts = [ "-C embed-bitcode=no" ];
751 inherit preBuild;
752 };
753 itoa_1_0_6 = buildRustCrate rec {
754 crateName = "itoa";
755 version = "1.0.6";
756
757 sha256 = "sha";
758 edition = "2018";
759 crateBin = [];
760 codegenUnits = 16;
761 extraRustcOpts = [ "-C embed-bitcode=no" ];
762 inherit preBuild;
763 };
764 libc_0_2_144 = buildRustCrate rec {
765 crateName = "libc";
766 version = "0.2.144";
767
768 sha256 = "sha";
769 edition = "2015";
770 crateBin = [];
771 codegenUnits = 16;
772 extraRustcOpts = [ "-C embed-bitcode=no" ];
773 inherit preBuild;
774 };
775 rename_0_1_0 = buildRustCrate rec {
776 crateName = "rename";
777 libName = "lib_rename";
778 version = "0.1.0";
779
780 src = pkgs.lib.cleanSourceWith { filter = sourceFilter; src = /cargo-nbuild/nbuild-core/tests/workspace/rename; };
781 edition = "2021";
782 crateBin = [];
783 codegenUnits = 16;
784 extraRustcOpts = [ "-C embed-bitcode=no" ];
785 inherit preBuild;
786 };
787 rustversion_1_0_12 = buildRustCrate rec {
788 crateName = "rustversion";
789 version = "1.0.12";
790
791 sha256 = "sha";
792 build = "build/build.rs";
793 procMacro = true;
794 edition = "2018";
795 crateBin = [];
796 codegenUnits = 16;
797 extraRustcOpts = [ "-C embed-bitcode=no" ];
798 inherit preBuild;
799 };
800 arbitrary_1_3_0 = buildRustCrate rec {
801 crateName = "arbitrary";
802 version = "1.3.0";
803
804 sha256 = "sha";
805 edition = "2018";
806 crateBin = [];
807 codegenUnits = 16;
808 extraRustcOpts = [ "-C embed-bitcode=no" ];
809 inherit preBuild;
810 };
811 itoa_0_4_8 = buildRustCrate rec {
812 crateName = "itoa";
813 version = "0.4.8";
814
815 sha256 = "sha";
816 edition = "2018";
817 crateBin = [];
818 codegenUnits = 16;
819 extraRustcOpts = [ "-C embed-bitcode=no" ];
820 inherit preBuild;
821 };
822 targets_0_1_0 = buildRustCrate rec {
823 crateName = "targets";
824 version = "0.1.0";
825
826 src = pkgs.lib.cleanSourceWith { filter = sourceFilter; src = /cargo-nbuild/nbuild-core/tests/workspace/targets; };
827 features = ["unix"];
828 edition = "2021";
829 crateBin = [];
830 codegenUnits = 16;
831 extraRustcOpts = [ "-C embed-bitcode=no" ];
832 inherit preBuild;
833 };
834in
835parent
836"#
837 );
838 }
839}