1#![allow(dead_code)]
17#![warn(clippy::all)]
18#![allow(clippy::result_large_err)]
19#![allow(unused_imports)]
20#![allow(clippy::field_reassign_with_default)]
21#![allow(clippy::ptr_arg)]
22#![allow(clippy::derivable_impls)]
23#![allow(clippy::should_implement_trait)]
24#![allow(clippy::collapsible_if)]
25#![allow(clippy::single_match)]
26#![allow(clippy::needless_ifs)]
27#![allow(clippy::useless_format)]
28#![allow(clippy::new_without_default)]
29#![allow(clippy::manual_strip)]
30#![allow(clippy::needless_borrows_for_generic_args)]
31#![allow(clippy::len_without_is_empty)]
32#![allow(clippy::type_complexity)]
33#![allow(clippy::manual_saturating_arithmetic)]
34#![allow(clippy::if_same_then_else)]
35#![allow(clippy::manual_is_variant_and)]
36#![allow(clippy::implicit_saturating_sub)]
37#![allow(clippy::incompatible_msrv)]
38#![allow(clippy::int_plus_one)]
39#![allow(clippy::manual_map)]
40#![allow(clippy::needless_bool)]
41#![allow(clippy::needless_else)]
42#![allow(clippy::clone_on_copy)]
43#![allow(clippy::manual_find)]
44#![allow(clippy::for_kv_map)]
45#![allow(clippy::manual_range_contains)]
46#![allow(clippy::double_ended_iterator_last)]
47#![allow(clippy::len_zero)]
48
49pub mod analytics;
50pub mod cache_eviction;
51pub mod distributed;
52pub mod executor;
53pub mod incremental;
54pub mod manifest;
55pub mod opt_incremental;
56pub mod registry;
57pub mod remote_cache;
58pub mod resolver;
59pub mod scripts;
60
61use std::collections::HashMap;
66use std::fmt;
67use std::path::PathBuf;
68
69#[derive(Clone, Debug)]
75pub struct BuildConfig {
76 pub root: PathBuf,
78 pub out_dir: PathBuf,
80 pub profile: BuildProfileKind,
82 pub jobs: usize,
84 pub verbose: bool,
86 pub warnings: bool,
88 pub extra_flags: Vec<String>,
90}
91
92impl Default for BuildConfig {
93 fn default() -> Self {
94 Self {
95 root: PathBuf::from("."),
96 out_dir: PathBuf::from("build"),
97 profile: BuildProfileKind::Debug,
98 jobs: num_cpus(),
99 verbose: false,
100 warnings: true,
101 extra_flags: Vec::new(),
102 }
103 }
104}
105
106impl BuildConfig {
107 pub fn release() -> Self {
109 Self {
110 profile: BuildProfileKind::Release,
111 ..Self::default()
112 }
113 }
114
115 pub fn with_jobs(mut self, n: usize) -> Self {
117 self.jobs = n.max(1);
118 self
119 }
120
121 pub fn verbose(mut self) -> Self {
123 self.verbose = true;
124 self
125 }
126
127 pub fn with_root(mut self, root: impl Into<PathBuf>) -> Self {
129 self.root = root.into();
130 self
131 }
132}
133
134fn num_cpus() -> usize {
136 std::thread::available_parallelism()
137 .map(|n| n.get())
138 .unwrap_or(4)
139}
140
141#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
147pub enum BuildProfileKind {
148 #[default]
150 Debug,
151 Release,
153 Test,
155 Bench,
157 Doc,
159}
160
161impl fmt::Display for BuildProfileKind {
162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163 match self {
164 BuildProfileKind::Debug => write!(f, "debug"),
165 BuildProfileKind::Release => write!(f, "release"),
166 BuildProfileKind::Test => write!(f, "test"),
167 BuildProfileKind::Bench => write!(f, "bench"),
168 BuildProfileKind::Doc => write!(f, "doc"),
169 }
170 }
171}
172
173#[derive(Clone, Debug, PartialEq)]
179pub struct BuildTarget {
180 pub name: String,
182 pub src: PathBuf,
184 pub kind: TargetKind,
186 pub enabled: bool,
188 pub deps: Vec<String>,
190}
191
192impl BuildTarget {
193 pub fn lib(name: &str, src: impl Into<PathBuf>) -> Self {
195 Self {
196 name: name.to_string(),
197 src: src.into(),
198 kind: TargetKind::Lib,
199 enabled: true,
200 deps: Vec::new(),
201 }
202 }
203
204 pub fn bin(name: &str, src: impl Into<PathBuf>) -> Self {
206 Self {
207 name: name.to_string(),
208 src: src.into(),
209 kind: TargetKind::Bin,
210 enabled: true,
211 deps: Vec::new(),
212 }
213 }
214
215 pub fn test(name: &str, src: impl Into<PathBuf>) -> Self {
217 Self {
218 name: name.to_string(),
219 src: src.into(),
220 kind: TargetKind::Test,
221 enabled: true,
222 deps: Vec::new(),
223 }
224 }
225
226 pub fn depends_on(mut self, dep: &str) -> Self {
228 self.deps.push(dep.to_string());
229 self
230 }
231
232 pub fn disabled(mut self) -> Self {
234 self.enabled = false;
235 self
236 }
237}
238
239#[derive(Clone, Copy, Debug, PartialEq, Eq)]
241pub enum TargetKind {
242 Lib,
244 Bin,
246 Test,
248 Bench,
250 Doc,
252 BuildScript,
254}
255
256impl fmt::Display for TargetKind {
257 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
258 match self {
259 TargetKind::Lib => write!(f, "lib"),
260 TargetKind::Bin => write!(f, "bin"),
261 TargetKind::Test => write!(f, "test"),
262 TargetKind::Bench => write!(f, "bench"),
263 TargetKind::Doc => write!(f, "doc"),
264 TargetKind::BuildScript => write!(f, "build-script"),
265 }
266 }
267}
268
269#[derive(Clone, Debug)]
275pub struct BuildPlan {
276 pub config: BuildConfig,
278 pub targets: Vec<BuildTarget>,
280 pub dep_graph: HashMap<String, Vec<String>>,
282}
283
284impl BuildPlan {
285 pub fn new(config: BuildConfig) -> Self {
287 Self {
288 config,
289 targets: Vec::new(),
290 dep_graph: HashMap::new(),
291 }
292 }
293
294 pub fn add_target(&mut self, target: BuildTarget) {
296 let name = target.name.clone();
297 let deps = target.deps.clone();
298 self.targets.push(target);
299 self.dep_graph.insert(name, deps);
300 }
301
302 pub fn target_count(&self) -> usize {
304 self.targets.len()
305 }
306
307 pub fn find_target(&self, name: &str) -> Option<&BuildTarget> {
309 self.targets.iter().find(|t| t.name == name)
310 }
311
312 pub fn topo_order(&self) -> Vec<&BuildTarget> {
314 self.targets.iter().collect()
315 }
316
317 pub fn enabled_targets(&self) -> Vec<&BuildTarget> {
319 self.targets.iter().filter(|t| t.enabled).collect()
320 }
321}
322
323#[derive(Clone, Debug)]
329pub struct CompilationUnit {
330 pub source: PathBuf,
332 pub output: PathBuf,
334 pub module_name: String,
336 pub is_cached: bool,
338}
339
340impl CompilationUnit {
341 pub fn new(source: impl Into<PathBuf>, output: impl Into<PathBuf>, module_name: &str) -> Self {
343 Self {
344 source: source.into(),
345 output: output.into(),
346 module_name: module_name.to_string(),
347 is_cached: false,
348 }
349 }
350
351 pub fn mark_cached(mut self) -> Self {
353 self.is_cached = true;
354 self
355 }
356
357 pub fn output_ext(&self) -> &str {
359 self.output
360 .extension()
361 .and_then(|e| e.to_str())
362 .unwrap_or("")
363 }
364}
365
366#[derive(Clone, Debug, Default)]
372pub struct BuildSummary {
373 pub compiled: usize,
375 pub cached: usize,
377 pub failed: usize,
379 pub elapsed_ms: u64,
381 pub errors: Vec<String>,
383 pub warnings: Vec<String>,
385}
386
387impl BuildSummary {
388 pub fn new() -> Self {
390 Self::default()
391 }
392
393 pub fn is_success(&self) -> bool {
395 self.failed == 0 && self.errors.is_empty()
396 }
397
398 pub fn total(&self) -> usize {
400 self.compiled + self.cached + self.failed
401 }
402
403 pub fn add_error(&mut self, msg: &str) {
405 self.errors.push(msg.to_string());
406 self.failed += 1;
407 }
408
409 pub fn add_warning(&mut self, msg: &str) {
411 self.warnings.push(msg.to_string());
412 }
413}
414
415impl fmt::Display for BuildSummary {
416 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
417 write!(
418 f,
419 "BuildSummary {{ compiled: {}, cached: {}, failed: {}, {}ms }}",
420 self.compiled, self.cached, self.failed, self.elapsed_ms
421 )
422 }
423}
424
425#[cfg(test)]
430mod tests {
431 use super::*;
432
433 #[test]
434 fn test_build_config_default() {
435 let cfg = BuildConfig::default();
436 assert_eq!(cfg.profile, BuildProfileKind::Debug);
437 assert!(cfg.jobs >= 1);
438 assert!(!cfg.verbose);
439 }
440
441 #[test]
442 fn test_build_config_release() {
443 let cfg = BuildConfig::release();
444 assert_eq!(cfg.profile, BuildProfileKind::Release);
445 }
446
447 #[test]
448 fn test_build_config_with_jobs() {
449 let cfg = BuildConfig::default().with_jobs(8);
450 assert_eq!(cfg.jobs, 8);
451 }
452
453 #[test]
454 fn test_build_config_with_jobs_min_one() {
455 let cfg = BuildConfig::default().with_jobs(0);
456 assert_eq!(cfg.jobs, 1);
457 }
458
459 #[test]
460 fn test_build_profile_display() {
461 assert_eq!(format!("{}", BuildProfileKind::Debug), "debug");
462 assert_eq!(format!("{}", BuildProfileKind::Release), "release");
463 assert_eq!(format!("{}", BuildProfileKind::Test), "test");
464 }
465
466 #[test]
467 fn test_target_kind_display() {
468 assert_eq!(format!("{}", TargetKind::Lib), "lib");
469 assert_eq!(format!("{}", TargetKind::Bin), "bin");
470 }
471
472 #[test]
473 fn test_build_target_lib() {
474 let t = BuildTarget::lib("mylib", "src/lib.ox");
475 assert_eq!(t.name, "mylib");
476 assert_eq!(t.kind, TargetKind::Lib);
477 assert!(t.enabled);
478 }
479
480 #[test]
481 fn test_build_target_bin() {
482 let t = BuildTarget::bin("mybinary", "src/main.ox");
483 assert_eq!(t.kind, TargetKind::Bin);
484 }
485
486 #[test]
487 fn test_build_target_test() {
488 let t = BuildTarget::test("mytest", "tests/test.ox");
489 assert_eq!(t.kind, TargetKind::Test);
490 }
491
492 #[test]
493 fn test_build_target_disabled() {
494 let t = BuildTarget::lib("lib", "src/lib.ox").disabled();
495 assert!(!t.enabled);
496 }
497
498 #[test]
499 fn test_build_target_depends_on() {
500 let t = BuildTarget::bin("app", "src/main.ox")
501 .depends_on("core")
502 .depends_on("util");
503 assert_eq!(t.deps.len(), 2);
504 assert!(t.deps.contains(&"core".to_string()));
505 }
506
507 #[test]
508 fn test_build_plan_add_target() {
509 let mut plan = BuildPlan::new(BuildConfig::default());
510 plan.add_target(BuildTarget::lib("lib", "src/lib.ox"));
511 assert_eq!(plan.target_count(), 1);
512 }
513
514 #[test]
515 fn test_build_plan_find_target() {
516 let mut plan = BuildPlan::new(BuildConfig::default());
517 plan.add_target(BuildTarget::lib("mylib", "src/lib.ox"));
518 assert!(plan.find_target("mylib").is_some());
519 assert!(plan.find_target("missing").is_none());
520 }
521
522 #[test]
523 fn test_build_plan_enabled_targets() {
524 let mut plan = BuildPlan::new(BuildConfig::default());
525 plan.add_target(BuildTarget::lib("lib1", "src/lib1.ox"));
526 plan.add_target(BuildTarget::lib("lib2", "src/lib2.ox").disabled());
527 let enabled = plan.enabled_targets();
528 assert_eq!(enabled.len(), 1);
529 }
530
531 #[test]
532 fn test_compilation_unit_new() {
533 let unit = CompilationUnit::new("src/foo.ox", "build/foo.o", "Foo");
534 assert_eq!(unit.module_name, "Foo");
535 assert!(!unit.is_cached);
536 }
537
538 #[test]
539 fn test_compilation_unit_mark_cached() {
540 let unit = CompilationUnit::new("src/foo.ox", "build/foo.o", "Foo").mark_cached();
541 assert!(unit.is_cached);
542 }
543
544 #[test]
545 fn test_build_summary_default() {
546 let s = BuildSummary::new();
547 assert!(s.is_success());
548 assert_eq!(s.total(), 0);
549 }
550
551 #[test]
552 fn test_build_summary_add_error() {
553 let mut s = BuildSummary::new();
554 s.add_error("compilation failed");
555 assert!(!s.is_success());
556 assert_eq!(s.failed, 1);
557 }
558
559 #[test]
560 fn test_build_summary_add_warning() {
561 let mut s = BuildSummary::new();
562 s.add_warning("unused variable");
563 assert!(s.is_success());
564 assert_eq!(s.warnings.len(), 1);
565 }
566
567 #[test]
568 fn test_build_summary_display() {
569 let s = BuildSummary {
570 compiled: 5,
571 cached: 3,
572 failed: 0,
573 elapsed_ms: 1200,
574 ..BuildSummary::default()
575 };
576 let text = format!("{}", s);
577 assert!(text.contains("compiled: 5"));
578 assert!(text.contains("cached: 3"));
579 assert!(text.contains("1200ms"));
580 }
581
582 #[test]
583 fn test_build_summary_total() {
584 let s = BuildSummary {
585 compiled: 4,
586 cached: 2,
587 failed: 1,
588 ..BuildSummary::default()
589 };
590 assert_eq!(s.total(), 7);
591 }
592
593 #[test]
594 fn test_build_config_verbose() {
595 let cfg = BuildConfig::default().verbose();
596 assert!(cfg.verbose);
597 }
598}
599
600#[derive(Clone, Debug, Default)]
606pub struct BuildCache {
607 entries: std::collections::HashMap<String, std::path::PathBuf>,
609}
610
611impl BuildCache {
612 pub fn new() -> Self {
614 Self::default()
615 }
616
617 pub fn insert(&mut self, module: &str, output: std::path::PathBuf) {
619 self.entries.insert(module.to_string(), output);
620 }
621
622 pub fn get(&self, module: &str) -> Option<&std::path::PathBuf> {
624 self.entries.get(module)
625 }
626
627 pub fn contains(&self, module: &str) -> bool {
629 self.entries.contains_key(module)
630 }
631
632 pub fn len(&self) -> usize {
634 self.entries.len()
635 }
636
637 pub fn is_empty(&self) -> bool {
639 self.entries.is_empty()
640 }
641
642 pub fn invalidate(&mut self, module: &str) {
644 self.entries.remove(module);
645 }
646
647 pub fn clear(&mut self) {
649 self.entries.clear();
650 }
651}
652
653impl std::fmt::Display for BuildCache {
654 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
655 write!(f, "BuildCache({} entries)", self.entries.len())
656 }
657}
658
659#[derive(Clone, Copy, Debug, PartialEq, Eq)]
665pub enum BuildLogLevel {
666 Trace,
668 Info,
670 Warn,
672 Error,
674}
675
676impl std::fmt::Display for BuildLogLevel {
677 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
678 match self {
679 BuildLogLevel::Trace => write!(f, "TRACE"),
680 BuildLogLevel::Info => write!(f, "INFO"),
681 BuildLogLevel::Warn => write!(f, "WARN"),
682 BuildLogLevel::Error => write!(f, "ERROR"),
683 }
684 }
685}
686
687#[derive(Clone, Debug)]
689pub struct BuildLogEntry {
690 pub level: BuildLogLevel,
692 pub message: String,
694 pub target: Option<String>,
696}
697
698impl BuildLogEntry {
699 pub fn info(msg: &str) -> Self {
701 Self {
702 level: BuildLogLevel::Info,
703 message: msg.to_string(),
704 target: None,
705 }
706 }
707
708 pub fn error(msg: &str) -> Self {
710 Self {
711 level: BuildLogLevel::Error,
712 message: msg.to_string(),
713 target: None,
714 }
715 }
716
717 pub fn for_target(mut self, target: &str) -> Self {
719 self.target = Some(target.to_string());
720 self
721 }
722}
723
724impl std::fmt::Display for BuildLogEntry {
725 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
726 if let Some(t) = &self.target {
727 write!(f, "[{}][{}] {}", self.level, t, self.message)
728 } else {
729 write!(f, "[{}] {}", self.level, self.message)
730 }
731 }
732}
733
734#[cfg(test)]
739mod extra_tests {
740 use super::*;
741
742 #[test]
743 fn test_build_cache_insert_and_get() {
744 let mut c = BuildCache::new();
745 c.insert("Foo", std::path::PathBuf::from("build/Foo.o"));
746 assert!(c.contains("Foo"));
747 assert_eq!(
748 c.get("Foo").expect("key should exist"),
749 &std::path::PathBuf::from("build/Foo.o")
750 );
751 }
752
753 #[test]
754 fn test_build_cache_invalidate() {
755 let mut c = BuildCache::new();
756 c.insert("Bar", std::path::PathBuf::from("build/Bar.o"));
757 c.invalidate("Bar");
758 assert!(!c.contains("Bar"));
759 }
760
761 #[test]
762 fn test_build_cache_clear() {
763 let mut c = BuildCache::new();
764 c.insert("X", std::path::PathBuf::from("build/X.o"));
765 c.clear();
766 assert!(c.is_empty());
767 }
768
769 #[test]
770 fn test_build_cache_display() {
771 let c = BuildCache::new();
772 let s = format!("{}", c);
773 assert!(s.contains("BuildCache"));
774 }
775
776 #[test]
777 fn test_build_log_level_display() {
778 assert_eq!(format!("{}", BuildLogLevel::Info), "INFO");
779 assert_eq!(format!("{}", BuildLogLevel::Error), "ERROR");
780 }
781
782 #[test]
783 fn test_build_log_entry_info() {
784 let e = BuildLogEntry::info("compiled foo");
785 assert_eq!(e.level, BuildLogLevel::Info);
786 assert!(e.target.is_none());
787 }
788
789 #[test]
790 fn test_build_log_entry_for_target() {
791 let e = BuildLogEntry::info("compiled").for_target("mylib");
792 assert_eq!(e.target.as_deref(), Some("mylib"));
793 }
794
795 #[test]
796 fn test_build_log_entry_display_with_target() {
797 let e = BuildLogEntry::error("fail").for_target("core");
798 let s = format!("{}", e);
799 assert!(s.contains("core"));
800 assert!(s.contains("fail"));
801 }
802
803 #[test]
804 fn test_build_log_entry_display_no_target() {
805 let e = BuildLogEntry::info("starting build");
806 let s = format!("{}", e);
807 assert!(s.contains("INFO"));
808 }
809}
810
811pub struct DependencyGraph;
817
818impl DependencyGraph {
819 pub fn topo_sort(deps: &std::collections::HashMap<String, Vec<String>>) -> Option<Vec<String>> {
822 let mut in_degree: std::collections::HashMap<&str, usize> =
823 deps.keys().map(|k| (k.as_str(), 0)).collect();
824 for edges in deps.values() {
825 for e in edges {
826 *in_degree.entry(e.as_str()).or_insert(0) += 1;
827 }
828 }
829 let mut queue: std::collections::VecDeque<&str> = in_degree
830 .iter()
831 .filter(|(_, &d)| d == 0)
832 .map(|(&k, _)| k)
833 .collect();
834 let mut order = Vec::new();
835 while let Some(node) = queue.pop_front() {
836 order.push(node.to_string());
837 if let Some(edges) = deps.get(node) {
838 for e in edges {
839 let d = in_degree.entry(e.as_str()).or_insert(0);
840 *d = d.saturating_sub(1);
841 if *d == 0 {
842 queue.push_back(e.as_str());
843 }
844 }
845 }
846 }
847 if order.len() == deps.len() {
848 Some(order)
849 } else {
850 None
851 }
852 }
853
854 pub fn has_cycle(deps: &std::collections::HashMap<String, Vec<String>>) -> bool {
856 Self::topo_sort(deps).is_none()
857 }
858}
859
860#[derive(Clone, Debug, Default)]
866pub struct BuildEnvironment {
867 vars: std::collections::HashMap<String, String>,
869}
870
871impl BuildEnvironment {
872 pub fn new() -> Self {
874 Self::default()
875 }
876
877 pub fn set(&mut self, key: &str, value: &str) {
879 self.vars.insert(key.to_string(), value.to_string());
880 }
881
882 pub fn get(&self, key: &str) -> Option<&str> {
884 self.vars.get(key).map(|s| s.as_str())
885 }
886
887 pub fn len(&self) -> usize {
889 self.vars.len()
890 }
891
892 pub fn is_empty(&self) -> bool {
894 self.vars.is_empty()
895 }
896}
897
898impl std::fmt::Display for BuildEnvironment {
899 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
900 write!(f, "BuildEnvironment({} vars)", self.vars.len())
901 }
902}
903
904#[cfg(test)]
909mod dep_tests {
910 use super::*;
911
912 fn simple_dag() -> std::collections::HashMap<String, Vec<String>> {
913 let mut m = std::collections::HashMap::new();
914 m.insert("a".to_string(), vec!["b".to_string()]);
915 m.insert("b".to_string(), vec!["c".to_string()]);
916 m.insert("c".to_string(), vec![]);
917 m
918 }
919
920 #[test]
921 fn test_topo_sort_acyclic() {
922 let dag = simple_dag();
923 let order = DependencyGraph::topo_sort(&dag);
924 assert!(order.is_some());
925 let o = order.expect("test operation should succeed");
926 let ai = o
927 .iter()
928 .position(|s| s == "a")
929 .expect("test operation should succeed");
930 let bi = o
931 .iter()
932 .position(|s| s == "b")
933 .expect("test operation should succeed");
934 let ci = o
935 .iter()
936 .position(|s| s == "c")
937 .expect("test operation should succeed");
938 assert!(
942 ai < bi && bi < ci,
943 "expected topological ordering a < b < c, got {:?}",
944 o
945 );
946 }
947
948 #[test]
949 fn test_has_cycle_no_cycle() {
950 let dag = simple_dag();
951 assert!(!DependencyGraph::has_cycle(&dag));
952 }
953
954 #[test]
955 fn test_has_cycle_with_cycle() {
956 let mut dag = std::collections::HashMap::new();
957 dag.insert("a".to_string(), vec!["b".to_string()]);
958 dag.insert("b".to_string(), vec!["a".to_string()]);
959 assert!(DependencyGraph::has_cycle(&dag));
960 }
961
962 #[test]
963 fn test_build_environment_set_get() {
964 let mut env = BuildEnvironment::new();
965 env.set("OXILEAN_ROOT", "/opt/oxilean");
966 assert_eq!(env.get("OXILEAN_ROOT"), Some("/opt/oxilean"));
967 }
968
969 #[test]
970 fn test_build_environment_missing_key() {
971 let env = BuildEnvironment::new();
972 assert_eq!(env.get("MISSING"), None);
973 }
974
975 #[test]
976 fn test_build_environment_len() {
977 let mut env = BuildEnvironment::new();
978 env.set("A", "1");
979 env.set("B", "2");
980 assert_eq!(env.len(), 2);
981 }
982
983 #[test]
984 fn test_build_environment_display() {
985 let env = BuildEnvironment::new();
986 let s = format!("{}", env);
987 assert!(s.contains("BuildEnvironment"));
988 }
989
990 #[test]
991 fn test_topo_sort_empty() {
992 let dag = std::collections::HashMap::new();
993 let order = DependencyGraph::topo_sort(&dag);
994 assert_eq!(order, Some(vec![]));
995 }
996}
997
998#[derive(Clone, Debug)]
1004pub struct BuildArtifact {
1005 pub name: String,
1007 pub path: std::path::PathBuf,
1009 pub kind: ArtifactKind,
1011 pub size_bytes: Option<u64>,
1013}
1014
1015impl BuildArtifact {
1016 pub fn new(name: &str, path: impl Into<std::path::PathBuf>, kind: ArtifactKind) -> Self {
1018 Self {
1019 name: name.to_string(),
1020 path: path.into(),
1021 kind,
1022 size_bytes: None,
1023 }
1024 }
1025
1026 pub fn with_size(mut self, size: u64) -> Self {
1028 self.size_bytes = Some(size);
1029 self
1030 }
1031
1032 pub fn size_display(&self) -> String {
1034 match self.size_bytes {
1035 Some(b) if b >= 1_048_576 => format!("{:.1} MB", b as f64 / 1_048_576.0),
1036 Some(b) if b >= 1024 => format!("{:.1} KB", b as f64 / 1024.0),
1037 Some(b) => format!("{} B", b),
1038 None => "unknown".to_string(),
1039 }
1040 }
1041}
1042
1043impl std::fmt::Display for BuildArtifact {
1044 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1045 write!(f, "{}[{}] -> {:?}", self.name, self.kind, self.path)
1046 }
1047}
1048
1049#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1051pub enum ArtifactKind {
1052 Object,
1054 StaticLib,
1056 DynLib,
1058 Executable,
1060 Docs,
1062 Export,
1064}
1065
1066impl std::fmt::Display for ArtifactKind {
1067 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1068 match self {
1069 ArtifactKind::Object => write!(f, "object"),
1070 ArtifactKind::StaticLib => write!(f, "static-lib"),
1071 ArtifactKind::DynLib => write!(f, "dyn-lib"),
1072 ArtifactKind::Executable => write!(f, "executable"),
1073 ArtifactKind::Docs => write!(f, "docs"),
1074 ArtifactKind::Export => write!(f, "export"),
1075 }
1076 }
1077}
1078
1079#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1085pub enum BuildPhase {
1086 Parse,
1088 TypeCheck,
1090 Codegen,
1092 Link,
1094 Package,
1096}
1097
1098impl std::fmt::Display for BuildPhase {
1099 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1100 match self {
1101 BuildPhase::Parse => write!(f, "parse"),
1102 BuildPhase::TypeCheck => write!(f, "type-check"),
1103 BuildPhase::Codegen => write!(f, "codegen"),
1104 BuildPhase::Link => write!(f, "link"),
1105 BuildPhase::Package => write!(f, "package"),
1106 }
1107 }
1108}
1109
1110#[derive(Clone, Debug)]
1112pub struct PhaseTimings {
1113 pub phase: BuildPhase,
1115 pub elapsed_ms: u64,
1117}
1118
1119impl PhaseTimings {
1120 pub fn new(phase: BuildPhase, elapsed_ms: u64) -> Self {
1122 Self { phase, elapsed_ms }
1123 }
1124}
1125
1126impl std::fmt::Display for PhaseTimings {
1127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1128 write!(f, "{}: {}ms", self.phase, self.elapsed_ms)
1129 }
1130}
1131
1132#[cfg(test)]
1133mod artifact_tests {
1134 use super::*;
1135
1136 #[test]
1137 fn test_artifact_new() {
1138 let a = BuildArtifact::new("mylib.a", "build/mylib.a", ArtifactKind::StaticLib);
1139 assert_eq!(a.name, "mylib.a");
1140 assert_eq!(a.kind, ArtifactKind::StaticLib);
1141 assert!(a.size_bytes.is_none());
1142 }
1143
1144 #[test]
1145 fn test_artifact_with_size_bytes() {
1146 let a = BuildArtifact::new("foo", "build/foo", ArtifactKind::Executable).with_size(512);
1147 assert_eq!(a.size_display(), "512 B");
1148 }
1149
1150 #[test]
1151 fn test_artifact_with_size_kb() {
1152 let a = BuildArtifact::new("foo", "f", ArtifactKind::Object).with_size(2048);
1153 assert!(a.size_display().contains("KB"));
1154 }
1155
1156 #[test]
1157 fn test_artifact_with_size_mb() {
1158 let a = BuildArtifact::new("foo", "f", ArtifactKind::DynLib).with_size(2_097_152);
1159 assert!(a.size_display().contains("MB"));
1160 }
1161
1162 #[test]
1163 fn test_artifact_display() {
1164 let a = BuildArtifact::new("lib", "build/lib.a", ArtifactKind::StaticLib);
1165 let s = format!("{}", a);
1166 assert!(s.contains("lib"));
1167 }
1168
1169 #[test]
1170 fn test_artifact_kind_display() {
1171 assert_eq!(format!("{}", ArtifactKind::Object), "object");
1172 assert_eq!(format!("{}", ArtifactKind::Export), "export");
1173 }
1174
1175 #[test]
1176 fn test_build_phase_ordering() {
1177 assert!(BuildPhase::Parse < BuildPhase::TypeCheck);
1178 assert!(BuildPhase::TypeCheck < BuildPhase::Codegen);
1179 assert!(BuildPhase::Codegen < BuildPhase::Link);
1180 }
1181
1182 #[test]
1183 fn test_phase_timings_display() {
1184 let t = PhaseTimings::new(BuildPhase::Parse, 150);
1185 let s = format!("{}", t);
1186 assert!(s.contains("parse"));
1187 assert!(s.contains("150ms"));
1188 }
1189
1190 #[test]
1191 fn test_artifact_size_unknown() {
1192 let a = BuildArtifact::new("x", "x", ArtifactKind::Docs);
1193 assert_eq!(a.size_display(), "unknown");
1194 }
1195
1196 #[test]
1197 fn test_build_phase_display() {
1198 assert_eq!(format!("{}", BuildPhase::TypeCheck), "type-check");
1199 assert_eq!(format!("{}", BuildPhase::Package), "package");
1200 }
1201}
1202
1203#[derive(Clone, Debug, Default)]
1209pub struct BuildFeatureFlags {
1210 pub lto: bool,
1212 pub pgo: bool,
1214 pub simd: bool,
1216 pub parallel_type_check: bool,
1218 pub debug_assertions: bool,
1220 pub incremental: bool,
1222 pub sanitizers: Vec<String>,
1224}
1225
1226impl BuildFeatureFlags {
1227 pub fn debug_defaults() -> Self {
1229 Self {
1230 lto: false,
1231 pgo: false,
1232 simd: false,
1233 parallel_type_check: true,
1234 debug_assertions: true,
1235 incremental: true,
1236 sanitizers: Vec::new(),
1237 }
1238 }
1239
1240 pub fn release_defaults() -> Self {
1242 Self {
1243 lto: true,
1244 pgo: false,
1245 simd: true,
1246 parallel_type_check: true,
1247 debug_assertions: false,
1248 incremental: false,
1249 sanitizers: Vec::new(),
1250 }
1251 }
1252
1253 pub fn with_sanitizer(mut self, name: &str) -> Self {
1255 self.sanitizers.push(name.to_string());
1256 self
1257 }
1258
1259 pub fn has_sanitizers(&self) -> bool {
1261 !self.sanitizers.is_empty()
1262 }
1263}
1264
1265#[derive(Clone, Debug)]
1271pub enum BuildNotification {
1272 Started,
1274 TargetStarted(String),
1276 TargetFinished(String, bool), Completed(bool), Warning(String),
1282 Error(String),
1284}
1285
1286impl BuildNotification {
1287 pub fn is_success(&self) -> bool {
1289 matches!(self, BuildNotification::Completed(true))
1290 }
1291
1292 pub fn label(&self) -> &'static str {
1294 match self {
1295 BuildNotification::Started => "started",
1296 BuildNotification::TargetStarted(_) => "target-started",
1297 BuildNotification::TargetFinished(_, _) => "target-finished",
1298 BuildNotification::Completed(_) => "completed",
1299 BuildNotification::Warning(_) => "warning",
1300 BuildNotification::Error(_) => "error",
1301 }
1302 }
1303}
1304
1305impl std::fmt::Display for BuildNotification {
1306 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1307 write!(f, "[{}]", self.label())
1308 }
1309}
1310
1311#[derive(Clone, Debug, Default)]
1317pub struct BuildEventLog {
1318 events: Vec<BuildNotification>,
1319}
1320
1321impl BuildEventLog {
1322 pub fn new() -> Self {
1324 Self::default()
1325 }
1326
1327 pub fn push(&mut self, event: BuildNotification) {
1329 self.events.push(event);
1330 }
1331
1332 pub fn len(&self) -> usize {
1334 self.events.len()
1335 }
1336
1337 pub fn is_empty(&self) -> bool {
1339 self.events.is_empty()
1340 }
1341
1342 pub fn count_by_label(&self, label: &str) -> usize {
1344 self.events.iter().filter(|e| e.label() == label).count()
1345 }
1346
1347 pub fn has_errors(&self) -> bool {
1349 self.events
1350 .iter()
1351 .any(|e| matches!(e, BuildNotification::Error(_)))
1352 }
1353}
1354
1355pub struct BuildPathResolver {
1361 root: std::path::PathBuf,
1362 out: std::path::PathBuf,
1363}
1364
1365impl BuildPathResolver {
1366 pub fn new(root: impl Into<std::path::PathBuf>, out: impl Into<std::path::PathBuf>) -> Self {
1368 Self {
1369 root: root.into(),
1370 out: out.into(),
1371 }
1372 }
1373
1374 pub fn source_path(&self, rel: &str) -> std::path::PathBuf {
1376 self.root.join(rel)
1377 }
1378
1379 pub fn output_path(&self, rel: &str) -> std::path::PathBuf {
1381 self.out.join(rel)
1382 }
1383
1384 pub fn object_path(&self, module: &str) -> std::path::PathBuf {
1386 let rel = format!("{}.o", module.replace('.', "/"));
1387 self.out.join("obj").join(rel)
1388 }
1389
1390 pub fn interface_path(&self, module: &str) -> std::path::PathBuf {
1392 let rel = format!("{}.olean", module.replace('.', "/"));
1393 self.out.join("iface").join(rel)
1394 }
1395}
1396
1397#[derive(Clone, Debug, Default)]
1403pub struct BuildMetrics {
1404 pub files_processed: u64,
1406 pub type_errors: u64,
1408 pub parse_ms: u64,
1410 pub typecheck_ms: u64,
1412 pub codegen_ms: u64,
1414 pub link_ms: u64,
1416 pub peak_rss_bytes: u64,
1418}
1419
1420impl BuildMetrics {
1421 pub fn new() -> Self {
1423 Self::default()
1424 }
1425
1426 pub fn total_ms(&self) -> u64 {
1428 self.parse_ms + self.typecheck_ms + self.codegen_ms + self.link_ms
1429 }
1430
1431 pub fn has_type_errors(&self) -> bool {
1433 self.type_errors > 0
1434 }
1435
1436 pub fn report(&self) -> String {
1438 format!(
1439 "files={} type_errors={} parse={}ms typecheck={}ms codegen={}ms link={}ms total={}ms",
1440 self.files_processed,
1441 self.type_errors,
1442 self.parse_ms,
1443 self.typecheck_ms,
1444 self.codegen_ms,
1445 self.link_ms,
1446 self.total_ms(),
1447 )
1448 }
1449}
1450
1451#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1457pub struct PackageId {
1458 pub name: String,
1460 pub version: String,
1462}
1463
1464impl PackageId {
1465 pub fn new(name: &str, version: &str) -> Self {
1467 Self {
1468 name: name.to_string(),
1469 version: version.to_string(),
1470 }
1471 }
1472
1473 pub fn to_slug(&self) -> String {
1475 format!("{}@{}", self.name, self.version)
1476 }
1477}
1478
1479impl std::fmt::Display for PackageId {
1480 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1481 write!(f, "{}", self.to_slug())
1482 }
1483}
1484
1485#[derive(Clone, Debug)]
1491pub struct BuildGraphNode {
1492 pub id: String,
1494 pub source: std::path::PathBuf,
1496 pub deps: Vec<String>,
1498 pub output: std::path::PathBuf,
1500 pub invalidated: bool,
1502}
1503
1504impl BuildGraphNode {
1505 pub fn new(
1507 id: &str,
1508 source: impl Into<std::path::PathBuf>,
1509 output: impl Into<std::path::PathBuf>,
1510 ) -> Self {
1511 Self {
1512 id: id.to_string(),
1513 source: source.into(),
1514 deps: Vec::new(),
1515 output: output.into(),
1516 invalidated: false,
1517 }
1518 }
1519
1520 pub fn add_dep(&mut self, dep_id: &str) {
1522 self.deps.push(dep_id.to_string());
1523 }
1524
1525 pub fn invalidate(&mut self) {
1527 self.invalidated = true;
1528 }
1529}
1530
1531pub struct BuildGraph {
1537 nodes: HashMap<String, BuildGraphNode>,
1538}
1539
1540impl BuildGraph {
1541 pub fn new() -> Self {
1543 Self {
1544 nodes: HashMap::new(),
1545 }
1546 }
1547
1548 pub fn add_node(&mut self, node: BuildGraphNode) {
1550 self.nodes.insert(node.id.clone(), node);
1551 }
1552
1553 pub fn get(&self, id: &str) -> Option<&BuildGraphNode> {
1555 self.nodes.get(id)
1556 }
1557
1558 pub fn get_mut(&mut self, id: &str) -> Option<&mut BuildGraphNode> {
1560 self.nodes.get_mut(id)
1561 }
1562
1563 pub fn node_count(&self) -> usize {
1565 self.nodes.len()
1566 }
1567
1568 pub fn topo_order(&self) -> Vec<&BuildGraphNode> {
1570 let mut in_degree: HashMap<&str, usize> =
1571 self.nodes.keys().map(|k| (k.as_str(), 0)).collect();
1572 for node in self.nodes.values() {
1573 for dep in &node.deps {
1574 *in_degree.entry(dep.as_str()).or_insert(0) += 1;
1575 }
1576 }
1577 let mut queue: std::collections::VecDeque<&str> = in_degree
1578 .iter()
1579 .filter(|(_, &d)| d == 0)
1580 .map(|(&k, _)| k)
1581 .collect();
1582 let mut order = Vec::new();
1583 while let Some(id) = queue.pop_front() {
1584 if let Some(node) = self.nodes.get(id) {
1585 order.push(node);
1586 for dep in &node.deps {
1587 let d = in_degree.entry(dep.as_str()).or_insert(0);
1588 *d = d.saturating_sub(1);
1589 if *d == 0 {
1590 queue.push_back(dep.as_str());
1591 }
1592 }
1593 }
1594 }
1595 order
1596 }
1597
1598 pub fn invalidated_nodes(&self) -> Vec<&BuildGraphNode> {
1600 self.nodes.values().filter(|n| n.invalidated).collect()
1601 }
1602}
1603
1604impl Default for BuildGraph {
1605 fn default() -> Self {
1606 Self::new()
1607 }
1608}
1609
1610#[derive(Clone, Debug, Default)]
1616pub struct BuildStats {
1617 pub targets_built: usize,
1618 pub targets_skipped: usize,
1619 pub targets_failed: usize,
1620 pub total_warnings: usize,
1621 pub total_errors: usize,
1622 pub wall_time_ms: u64,
1623}
1624
1625impl BuildStats {
1626 pub fn new() -> Self {
1628 Self::default()
1629 }
1630
1631 pub fn success_rate(&self) -> f64 {
1633 let total = self.targets_built + self.targets_skipped + self.targets_failed;
1634 if total == 0 {
1635 1.0
1636 } else {
1637 self.targets_built as f64 / total as f64
1638 }
1639 }
1640
1641 pub fn is_clean(&self) -> bool {
1643 self.targets_failed == 0 && self.total_errors == 0
1644 }
1645
1646 pub fn summary(&self) -> String {
1648 format!(
1649 "built={} skipped={} failed={} warnings={} errors={} wall={}ms",
1650 self.targets_built,
1651 self.targets_skipped,
1652 self.targets_failed,
1653 self.total_warnings,
1654 self.total_errors,
1655 self.wall_time_ms,
1656 )
1657 }
1658}
1659
1660#[cfg(test)]
1665mod lib_extra_tests {
1666 use super::*;
1667
1668 #[test]
1670 fn feature_flags_debug_defaults() {
1671 let flags = BuildFeatureFlags::debug_defaults();
1672 assert!(flags.debug_assertions);
1673 assert!(flags.incremental);
1674 assert!(!flags.lto);
1675 }
1676
1677 #[test]
1678 fn feature_flags_release_defaults() {
1679 let flags = BuildFeatureFlags::release_defaults();
1680 assert!(flags.lto);
1681 assert!(flags.simd);
1682 assert!(!flags.debug_assertions);
1683 }
1684
1685 #[test]
1686 fn feature_flags_sanitizer() {
1687 let flags = BuildFeatureFlags::debug_defaults().with_sanitizer("address");
1688 assert!(flags.has_sanitizers());
1689 assert_eq!(flags.sanitizers.len(), 1);
1690 }
1691
1692 #[test]
1694 fn build_notification_label() {
1695 assert_eq!(BuildNotification::Started.label(), "started");
1696 assert_eq!(BuildNotification::Error("e".to_string()).label(), "error");
1697 assert!(BuildNotification::Completed(true).is_success());
1698 assert!(!BuildNotification::Completed(false).is_success());
1699 }
1700
1701 #[test]
1702 fn build_notification_display() {
1703 let n = BuildNotification::Warning("unused var".to_string());
1704 assert!(format!("{}", n).contains("warning"));
1705 }
1706
1707 #[test]
1709 fn build_event_log_push_and_count() {
1710 let mut log = BuildEventLog::new();
1711 log.push(BuildNotification::Started);
1712 log.push(BuildNotification::Error("fail".to_string()));
1713 assert_eq!(log.len(), 2);
1714 assert!(log.has_errors());
1715 assert_eq!(log.count_by_label("error"), 1);
1716 }
1717
1718 #[test]
1720 fn build_path_resolver_paths() {
1721 let res = BuildPathResolver::new("/project", "/project/build");
1722 let src = res.source_path("src/Main.lean");
1723 assert!(src
1724 .to_str()
1725 .expect("conversion should succeed")
1726 .contains("src/Main.lean"));
1727 let obj = res.object_path("Mathlib.Data.Nat");
1728 assert!(obj
1729 .to_str()
1730 .expect("conversion should succeed")
1731 .contains(".o"));
1732 }
1733
1734 #[test]
1736 fn build_metrics_total_ms() {
1737 let m = BuildMetrics {
1738 parse_ms: 100,
1739 typecheck_ms: 200,
1740 codegen_ms: 50,
1741 link_ms: 25,
1742 ..Default::default()
1743 };
1744 assert_eq!(m.total_ms(), 375);
1745 }
1746
1747 #[test]
1748 fn build_metrics_report_nonempty() {
1749 let m = BuildMetrics::new();
1750 assert!(!m.report().is_empty());
1751 }
1752
1753 #[test]
1755 fn package_id_slug() {
1756 let id = PackageId::new("oxilean-core", "0.1.1");
1757 assert_eq!(id.to_slug(), "oxilean-core@0.1.1");
1758 assert_eq!(format!("{}", id), "oxilean-core@0.1.1");
1759 }
1760
1761 #[test]
1763 fn build_graph_node_add_dep() {
1764 let mut node = BuildGraphNode::new("ModA", "src/A.lean", "build/A.o");
1765 node.add_dep("ModB");
1766 assert_eq!(node.deps.len(), 1);
1767 }
1768
1769 #[test]
1770 fn build_graph_node_invalidate() {
1771 let mut node = BuildGraphNode::new("X", "X.lean", "X.o");
1772 assert!(!node.invalidated);
1773 node.invalidate();
1774 assert!(node.invalidated);
1775 }
1776
1777 #[test]
1779 fn build_graph_topo_order() {
1780 let mut graph = BuildGraph::new();
1781 let mut a = BuildGraphNode::new("A", "A.lean", "A.o");
1782 a.add_dep("B");
1783 graph.add_node(a);
1784 graph.add_node(BuildGraphNode::new("B", "B.lean", "B.o"));
1785 let order = graph.topo_order();
1786 assert_eq!(order.len(), 2);
1787 }
1788
1789 #[test]
1790 fn build_graph_invalidated_nodes() {
1791 let mut graph = BuildGraph::new();
1792 let mut node = BuildGraphNode::new("X", "X.lean", "X.o");
1793 node.invalidate();
1794 graph.add_node(node);
1795 graph.add_node(BuildGraphNode::new("Y", "Y.lean", "Y.o"));
1796 assert_eq!(graph.invalidated_nodes().len(), 1);
1797 }
1798
1799 #[test]
1801 fn build_stats_success_rate() {
1802 let s = BuildStats {
1803 targets_built: 8,
1804 targets_skipped: 1,
1805 targets_failed: 1,
1806 ..Default::default()
1807 };
1808 assert!((s.success_rate() - 0.8).abs() < 1e-9);
1809 }
1810
1811 #[test]
1812 fn build_stats_is_clean() {
1813 let mut s = BuildStats::new();
1814 assert!(s.is_clean());
1815 s.targets_failed = 1;
1816 assert!(!s.is_clean());
1817 }
1818
1819 #[test]
1820 fn build_stats_summary_nonempty() {
1821 let s = BuildStats::new();
1822 assert!(!s.summary().is_empty());
1823 }
1824}
1825
1826#[derive(Clone, Debug)]
1832pub struct WorkspaceInfo {
1833 pub name: String,
1835 pub root: std::path::PathBuf,
1837 pub members: Vec<String>,
1839 pub flags: BuildFeatureFlags,
1841}
1842
1843impl WorkspaceInfo {
1844 pub fn new(name: &str, root: impl Into<std::path::PathBuf>) -> Self {
1846 Self {
1847 name: name.to_string(),
1848 root: root.into(),
1849 members: Vec::new(),
1850 flags: BuildFeatureFlags::default(),
1851 }
1852 }
1853
1854 pub fn add_member(&mut self, member: &str) {
1856 self.members.push(member.to_string());
1857 }
1858
1859 pub fn member_count(&self) -> usize {
1861 self.members.len()
1862 }
1863
1864 pub fn is_member(&self, pkg: &str) -> bool {
1866 self.members.iter().any(|m| m == pkg)
1867 }
1868}
1869
1870impl std::fmt::Display for WorkspaceInfo {
1871 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1872 write!(
1873 f,
1874 "Workspace({}, {} members)",
1875 self.name,
1876 self.members.len()
1877 )
1878 }
1879}
1880
1881pub struct BuildSession {
1887 pub config: BuildConfig,
1889 pub plan: BuildPlan,
1891 pub events: BuildEventLog,
1893 pub stats: BuildStats,
1895 pub phase: BuildPhase,
1897}
1898
1899impl BuildSession {
1900 pub fn start(config: BuildConfig) -> Self {
1902 let plan = BuildPlan::new(config.clone());
1903 let mut events = BuildEventLog::new();
1904 events.push(BuildNotification::Started);
1905 Self {
1906 config,
1907 plan,
1908 events,
1909 stats: BuildStats::new(),
1910 phase: BuildPhase::Parse,
1911 }
1912 }
1913
1914 pub fn advance_phase(&mut self) {
1916 self.phase = match self.phase {
1917 BuildPhase::Parse => BuildPhase::TypeCheck,
1918 BuildPhase::TypeCheck => BuildPhase::Codegen,
1919 BuildPhase::Codegen => BuildPhase::Link,
1920 BuildPhase::Link | BuildPhase::Package => BuildPhase::Package,
1921 };
1922 }
1923
1924 pub fn record_built(&mut self, target_name: &str) {
1926 self.stats.targets_built += 1;
1927 self.events.push(BuildNotification::TargetFinished(
1928 target_name.to_string(),
1929 true,
1930 ));
1931 }
1932
1933 pub fn record_failed(&mut self, target_name: &str, error: &str) {
1935 self.stats.targets_failed += 1;
1936 self.stats.total_errors += 1;
1937 self.events.push(BuildNotification::TargetFinished(
1938 target_name.to_string(),
1939 false,
1940 ));
1941 self.events
1942 .push(BuildNotification::Error(error.to_string()));
1943 }
1944
1945 pub fn finish(&mut self, wall_ms: u64) {
1947 self.stats.wall_time_ms = wall_ms;
1948 let ok = self.stats.is_clean();
1949 self.events.push(BuildNotification::Completed(ok));
1950 }
1951
1952 pub fn succeeded(&self) -> bool {
1954 self.stats.is_clean()
1955 }
1956}
1957
1958#[cfg(test)]
1963mod session_tests {
1964 use super::*;
1965
1966 #[test]
1967 fn workspace_info_members() {
1968 let mut ws = WorkspaceInfo::new("oxilean", "/opt/oxilean");
1969 ws.add_member("oxilean-core");
1970 ws.add_member("oxilean-meta");
1971 assert_eq!(ws.member_count(), 2);
1972 assert!(ws.is_member("oxilean-core"));
1973 assert!(!ws.is_member("missing"));
1974 }
1975
1976 #[test]
1977 fn workspace_info_display() {
1978 let ws = WorkspaceInfo::new("my-workspace", "/ws");
1979 let s = format!("{}", ws);
1980 assert!(s.contains("Workspace(my-workspace"));
1981 }
1982
1983 #[test]
1984 fn build_session_record_built() {
1985 let cfg = BuildConfig::default();
1986 let mut session = BuildSession::start(cfg);
1987 session.record_built("mylib");
1988 assert_eq!(session.stats.targets_built, 1);
1989 assert!(session.events.count_by_label("target-finished") >= 1);
1990 }
1991
1992 #[test]
1993 fn build_session_record_failed() {
1994 let cfg = BuildConfig::default();
1995 let mut session = BuildSession::start(cfg);
1996 session.record_failed("bad-lib", "type error");
1997 assert_eq!(session.stats.targets_failed, 1);
1998 assert!(session.events.has_errors());
1999 assert!(!session.succeeded());
2000 }
2001
2002 #[test]
2003 fn build_session_advance_phase() {
2004 let cfg = BuildConfig::default();
2005 let mut session = BuildSession::start(cfg);
2006 assert_eq!(session.phase, BuildPhase::Parse);
2007 session.advance_phase();
2008 assert_eq!(session.phase, BuildPhase::TypeCheck);
2009 session.advance_phase();
2010 assert_eq!(session.phase, BuildPhase::Codegen);
2011 }
2012
2013 #[test]
2014 fn build_session_finish_success() {
2015 let cfg = BuildConfig::default();
2016 let mut session = BuildSession::start(cfg);
2017 session.record_built("lib");
2018 session.finish(1500);
2019 assert_eq!(session.stats.wall_time_ms, 1500);
2020 assert!(session.succeeded());
2021 }
2022
2023 #[test]
2024 fn build_session_finish_failure() {
2025 let cfg = BuildConfig::default();
2026 let mut session = BuildSession::start(cfg);
2027 session.record_failed("lib", "oops");
2028 session.finish(500);
2029 assert!(!session.succeeded());
2030 }
2031}
2032
2033pub trait BuildPlugin: std::fmt::Debug {
2039 fn name(&self) -> &str;
2041 fn on_build_start(&self, _config: &BuildConfig) {}
2043 fn on_build_finish(&self, _summary: &BuildSummary) {}
2045 fn on_target_start(&self, _target: &BuildTarget) {}
2047 fn on_target_finish(&self, _target: &BuildTarget, _success: bool) {}
2049}
2050
2051#[derive(Debug)]
2053pub struct NoopPlugin {
2054 pub name: String,
2055}
2056
2057impl NoopPlugin {
2058 pub fn new(name: &str) -> Self {
2060 Self {
2061 name: name.to_string(),
2062 }
2063 }
2064}
2065
2066impl BuildPlugin for NoopPlugin {
2067 fn name(&self) -> &str {
2068 &self.name
2069 }
2070}
2071
2072pub struct PluginRegistry {
2078 plugins: Vec<Box<dyn BuildPlugin>>,
2079}
2080
2081impl PluginRegistry {
2082 pub fn new() -> Self {
2084 Self {
2085 plugins: Vec::new(),
2086 }
2087 }
2088
2089 pub fn register(&mut self, plugin: Box<dyn BuildPlugin>) {
2091 self.plugins.push(plugin);
2092 }
2093
2094 pub fn len(&self) -> usize {
2096 self.plugins.len()
2097 }
2098
2099 pub fn is_empty(&self) -> bool {
2101 self.plugins.is_empty()
2102 }
2103
2104 pub fn fire_build_start(&self, config: &BuildConfig) {
2106 for p in &self.plugins {
2107 p.on_build_start(config);
2108 }
2109 }
2110
2111 pub fn fire_build_finish(&self, summary: &BuildSummary) {
2113 for p in &self.plugins {
2114 p.on_build_finish(summary);
2115 }
2116 }
2117}
2118
2119impl Default for PluginRegistry {
2120 fn default() -> Self {
2121 Self::new()
2122 }
2123}
2124
2125#[derive(Clone, Debug)]
2131pub struct OxileanVersion {
2132 pub major: u32,
2134 pub minor: u32,
2136 pub patch: u32,
2138 pub pre: Option<String>,
2140}
2141
2142impl OxileanVersion {
2143 pub fn new(major: u32, minor: u32, patch: u32) -> Self {
2145 Self {
2146 major,
2147 minor,
2148 patch,
2149 pre: None,
2150 }
2151 }
2152
2153 pub fn pre(major: u32, minor: u32, patch: u32, pre: &str) -> Self {
2155 Self {
2156 major,
2157 minor,
2158 patch,
2159 pre: Some(pre.to_string()),
2160 }
2161 }
2162
2163 pub fn is_pre_release(&self) -> bool {
2165 self.pre.is_some()
2166 }
2167
2168 pub fn is_at_least(&self, major: u32, minor: u32, patch: u32) -> bool {
2170 (self.major, self.minor, self.patch) >= (major, minor, patch)
2171 }
2172}
2173
2174impl std::fmt::Display for OxileanVersion {
2175 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2176 match &self.pre {
2177 Some(pre) => write!(f, "{}.{}.{}-{}", self.major, self.minor, self.patch, pre),
2178 None => write!(f, "{}.{}.{}", self.major, self.minor, self.patch),
2179 }
2180 }
2181}
2182
2183#[derive(Clone, Debug)]
2189pub enum BuildSystemError {
2190 InvalidConfig(String),
2192 SourceNotFound(std::path::PathBuf),
2194 DependencyCycle(Vec<String>),
2196 CompilationFailed { target: String, reason: String },
2198 Io(String),
2200 Plugin { plugin: String, message: String },
2202}
2203
2204impl std::fmt::Display for BuildSystemError {
2205 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2206 match self {
2207 BuildSystemError::InvalidConfig(msg) => write!(f, "InvalidConfig: {}", msg),
2208 BuildSystemError::SourceNotFound(p) => write!(f, "SourceNotFound: {:?}", p),
2209 BuildSystemError::DependencyCycle(cycle) => {
2210 write!(f, "DependencyCycle: {}", cycle.join(" -> "))
2211 }
2212 BuildSystemError::CompilationFailed { target, reason } => {
2213 write!(f, "CompilationFailed[{}]: {}", target, reason)
2214 }
2215 BuildSystemError::Io(msg) => write!(f, "IoError: {}", msg),
2216 BuildSystemError::Plugin { plugin, message } => {
2217 write!(f, "PluginError[{}]: {}", plugin, message)
2218 }
2219 }
2220 }
2221}
2222
2223pub type BuildResult<T> = Result<T, BuildSystemError>;
2225
2226#[cfg(test)]
2231mod plugin_tests {
2232 use super::*;
2233
2234 #[test]
2235 fn noop_plugin_name() {
2236 let p = NoopPlugin::new("formatter");
2237 assert_eq!(p.name(), "formatter");
2238 }
2239
2240 #[test]
2241 fn plugin_registry_register_and_fire() {
2242 let mut reg = PluginRegistry::new();
2243 reg.register(Box::new(NoopPlugin::new("p1")));
2244 reg.register(Box::new(NoopPlugin::new("p2")));
2245 assert_eq!(reg.len(), 2);
2246 let cfg = BuildConfig::default();
2247 reg.fire_build_start(&cfg);
2248 let summary = BuildSummary::new();
2249 reg.fire_build_finish(&summary);
2250 }
2251
2252 #[test]
2253 fn oxilean_version_display() {
2254 let v = OxileanVersion::new(0, 1, 1);
2255 assert_eq!(format!("{}", v), "0.1.1");
2256 }
2257
2258 #[test]
2259 fn oxilean_version_pre_release() {
2260 let v = OxileanVersion::pre(1, 0, 0, "alpha.1");
2261 assert!(v.is_pre_release());
2262 assert_eq!(format!("{}", v), "1.0.0-alpha.1");
2263 }
2264
2265 #[test]
2266 fn oxilean_version_is_at_least() {
2267 let v = OxileanVersion::new(0, 2, 0);
2268 assert!(v.is_at_least(0, 1, 0));
2269 assert!(v.is_at_least(0, 2, 0));
2270 assert!(!v.is_at_least(0, 3, 0));
2271 }
2272
2273 #[test]
2274 fn build_system_error_display() {
2275 let e = BuildSystemError::InvalidConfig("missing field".to_string());
2276 assert!(format!("{}", e).contains("InvalidConfig"));
2277
2278 let e2 = BuildSystemError::DependencyCycle(vec!["A".to_string(), "B".to_string()]);
2279 assert!(format!("{}", e2).contains("A -> B"));
2280 }
2281
2282 #[test]
2283 fn build_result_ok_err() {
2284 let ok: BuildResult<u32> = Ok(42);
2285 assert!(ok.is_ok());
2286 let err: BuildResult<u32> = Err(BuildSystemError::Io("disk full".to_string()));
2287 assert!(err.is_err());
2288 }
2289}
2290
2291pub fn build_system_api_version() -> OxileanVersion {
2293 OxileanVersion::new(0, 1, 1)
2294}
2295
2296#[cfg(test)]
2297mod api_version_test {
2298 use super::*;
2299 #[test]
2300 fn api_version_nonempty() {
2301 let v = build_system_api_version();
2302 assert!(format!("{}", v).len() > 0);
2303 }
2304}
2305
2306#[derive(Clone, Debug)]
2312pub struct BuildOutputFilter {
2313 pub suppress: Vec<String>,
2315 pub highlight: Vec<String>,
2317 pub min_level: BuildLogLevel,
2319}
2320
2321impl BuildOutputFilter {
2322 pub fn new() -> Self {
2324 Self {
2325 suppress: Vec::new(),
2326 highlight: Vec::new(),
2327 min_level: BuildLogLevel::Trace,
2328 }
2329 }
2330
2331 pub fn suppress(mut self, pattern: &str) -> Self {
2333 self.suppress.push(pattern.to_string());
2334 self
2335 }
2336
2337 pub fn highlight(mut self, pattern: &str) -> Self {
2339 self.highlight.push(pattern.to_string());
2340 self
2341 }
2342
2343 pub fn min_level(mut self, level: BuildLogLevel) -> Self {
2345 self.min_level = level;
2346 self
2347 }
2348
2349 pub fn should_show(&self, level: BuildLogLevel, text: &str) -> bool {
2351 if (level as u8) < (self.min_level as u8) {
2352 return false;
2353 }
2354 !self.suppress.iter().any(|s| text.contains(s.as_str()))
2355 }
2356
2357 pub fn should_highlight(&self, text: &str) -> bool {
2359 self.highlight.iter().any(|h| text.contains(h.as_str()))
2360 }
2361}
2362
2363impl Default for BuildOutputFilter {
2364 fn default() -> Self {
2365 Self::new()
2366 }
2367}
2368
2369#[derive(Clone, Debug)]
2375pub struct BuildCompiler {
2376 pub executable: std::path::PathBuf,
2378 pub default_flags: Vec<String>,
2380 pub env: HashMap<String, String>,
2382}
2383
2384impl BuildCompiler {
2385 pub fn new(executable: impl Into<std::path::PathBuf>) -> Self {
2387 Self {
2388 executable: executable.into(),
2389 default_flags: Vec::new(),
2390 env: HashMap::new(),
2391 }
2392 }
2393
2394 pub fn add_flag(&mut self, flag: &str) {
2396 self.default_flags.push(flag.to_string());
2397 }
2398
2399 pub fn set_env(&mut self, key: &str, value: &str) {
2401 self.env.insert(key.to_string(), value.to_string());
2402 }
2403
2404 pub fn command_line(&self, source: &str, output: &str) -> Vec<String> {
2406 let mut cmd = vec![self.executable.to_string_lossy().to_string()];
2407 cmd.extend(self.default_flags.iter().cloned());
2408 cmd.push(source.to_string());
2409 cmd.push("-o".to_string());
2410 cmd.push(output.to_string());
2411 cmd
2412 }
2413}
2414
2415#[cfg(test)]
2420mod filter_compiler_tests {
2421 use super::*;
2422
2423 #[test]
2424 fn output_filter_should_show() {
2425 let filter = BuildOutputFilter::new()
2426 .suppress("note:")
2427 .min_level(BuildLogLevel::Warn);
2428 assert!(!filter.should_show(BuildLogLevel::Info, "just info"));
2429 assert!(filter.should_show(BuildLogLevel::Warn, "this is a warning"));
2430 assert!(!filter.should_show(BuildLogLevel::Info, "note: unused import"));
2431 }
2432
2433 #[test]
2434 fn output_filter_highlight() {
2435 let filter = BuildOutputFilter::new().highlight("error[");
2436 assert!(filter.should_highlight("error[E0001]: something"));
2437 assert!(!filter.should_highlight("just a warning"));
2438 }
2439
2440 #[test]
2441 fn build_compiler_command_line() {
2442 let mut compiler = BuildCompiler::new("/usr/bin/oxileanc");
2443 compiler.add_flag("--opt");
2444 let cmd = compiler.command_line("src/Main.lean", "build/Main.o");
2445 assert!(cmd.contains(&"--opt".to_string()));
2446 assert!(cmd.contains(&"-o".to_string()));
2447 assert!(cmd.contains(&"src/Main.lean".to_string()));
2448 }
2449
2450 #[test]
2451 fn build_compiler_env() {
2452 let mut compiler = BuildCompiler::new("/usr/bin/oxileanc");
2453 compiler.set_env("LEAN_PATH", "/lean/lib");
2454 assert_eq!(
2455 compiler.env.get("LEAN_PATH").map(|s| s.as_str()),
2456 Some("/lean/lib")
2457 );
2458 }
2459}
2460
2461pub fn fingerprint_file_list(files: &[&str]) -> u64 {
2463 let mut h: u64 = 0xcbf29ce484222325;
2464 for &file in files {
2465 for b in file.bytes() {
2466 h ^= b as u64;
2467 h = h.wrapping_mul(0x100000001b3);
2468 }
2469 h ^= b'/' as u64;
2470 h = h.wrapping_mul(0x100000001b3);
2471 }
2472 h
2473}
2474
2475#[cfg(test)]
2476mod fingerprint_test {
2477 use super::*;
2478 #[test]
2479 fn fingerprint_file_list_deterministic() {
2480 let h1 = fingerprint_file_list(&["a.lean", "b.lean"]);
2481 let h2 = fingerprint_file_list(&["a.lean", "b.lean"]);
2482 assert_eq!(h1, h2);
2483 }
2484
2485 #[test]
2486 fn fingerprint_file_list_different_inputs() {
2487 let h1 = fingerprint_file_list(&["a.lean"]);
2488 let h2 = fingerprint_file_list(&["b.lean"]);
2489 assert_ne!(h1, h2);
2490 }
2491}
2492
2493#[derive(Clone, Debug, Default)]
2499pub struct BuildSystemCapabilities {
2500 pub incremental: bool,
2502 pub distributed: bool,
2504 pub remote_cache: bool,
2506 pub parallel: bool,
2508 pub max_jobs: usize,
2510}
2511
2512impl BuildSystemCapabilities {
2513 pub fn full() -> Self {
2515 Self {
2516 incremental: true,
2517 distributed: true,
2518 remote_cache: true,
2519 parallel: true,
2520 max_jobs: 256,
2521 }
2522 }
2523
2524 pub fn minimal() -> Self {
2526 Self {
2527 incremental: false,
2528 distributed: false,
2529 remote_cache: false,
2530 parallel: false,
2531 max_jobs: 1,
2532 }
2533 }
2534}
2535
2536impl std::fmt::Display for BuildSystemCapabilities {
2537 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2538 write!(
2539 f,
2540 "Capabilities[incremental={} distributed={} remote_cache={} parallel={} max_jobs={}]",
2541 self.incremental, self.distributed, self.remote_cache, self.parallel, self.max_jobs,
2542 )
2543 }
2544}
2545
2546#[cfg(test)]
2547mod capabilities_tests {
2548 use super::*;
2549
2550 #[test]
2551 fn full_capabilities() {
2552 let caps = BuildSystemCapabilities::full();
2553 assert!(caps.incremental && caps.distributed && caps.parallel);
2554 assert_eq!(caps.max_jobs, 256);
2555 }
2556
2557 #[test]
2558 fn minimal_capabilities() {
2559 let caps = BuildSystemCapabilities::minimal();
2560 assert!(!caps.incremental);
2561 assert_eq!(caps.max_jobs, 1);
2562 }
2563
2564 #[test]
2565 fn capabilities_display() {
2566 let caps = BuildSystemCapabilities::full();
2567 let s = format!("{}", caps);
2568 assert!(s.contains("Capabilities["));
2569 }
2570}