Skip to main content

oxilean_build/
lib.rs

1//! # OxiLean Build System & Package Manager
2//!
3//! This crate implements the build system and package manager for OxiLean,
4//! providing incremental compilation, dependency resolution, parallel build
5//! execution, and package registry integration.
6//!
7//! ## Modules
8//!
9//! - manifest: Package manifest parsing and metadata
10//! - resolver: PubGrub-style dependency resolution
11//! - incremental: Incremental compilation with fingerprinting
12//! - executor: DAG-based parallel build scheduling
13//! - registry: Package registry integration
14//! - scripts: Custom build scripts and hooks
15
16#![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
61// ============================================================
62// Top-level types defined in this lib file
63// ============================================================
64
65use std::collections::HashMap;
66use std::fmt;
67use std::path::PathBuf;
68
69// ============================================================
70// BuildConfig: top-level configuration
71// ============================================================
72
73/// Global build configuration.
74#[derive(Clone, Debug)]
75pub struct BuildConfig {
76    /// Root directory of the project.
77    pub root: PathBuf,
78    /// Output/artifact directory.
79    pub out_dir: PathBuf,
80    /// Build profile (debug, release, etc.).
81    pub profile: BuildProfileKind,
82    /// Number of parallel build jobs.
83    pub jobs: usize,
84    /// Whether to enable verbose output.
85    pub verbose: bool,
86    /// Whether to emit build warnings.
87    pub warnings: bool,
88    /// Extra compiler flags.
89    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    /// Create a release build configuration.
108    pub fn release() -> Self {
109        Self {
110            profile: BuildProfileKind::Release,
111            ..Self::default()
112        }
113    }
114
115    /// Set the number of parallel jobs.
116    pub fn with_jobs(mut self, n: usize) -> Self {
117        self.jobs = n.max(1);
118        self
119    }
120
121    /// Enable verbose output.
122    pub fn verbose(mut self) -> Self {
123        self.verbose = true;
124        self
125    }
126
127    /// Set the root directory.
128    pub fn with_root(mut self, root: impl Into<PathBuf>) -> Self {
129        self.root = root.into();
130        self
131    }
132}
133
134/// Returns a best-effort CPU count.
135fn num_cpus() -> usize {
136    std::thread::available_parallelism()
137        .map(|n| n.get())
138        .unwrap_or(4)
139}
140
141// ============================================================
142// BuildProfileKind
143// ============================================================
144
145/// Build profile variants.
146#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
147pub enum BuildProfileKind {
148    /// Debug build: fast compilation, no optimizations, all debug info.
149    #[default]
150    Debug,
151    /// Release build: full optimizations, stripped debug info.
152    Release,
153    /// Test build: debug + test harness.
154    Test,
155    /// Benchmark build: release + benchmark harness.
156    Bench,
157    /// Documentation build: no compilation, only doc extraction.
158    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// ============================================================
174// BuildTarget
175// ============================================================
176
177/// A build target (library, binary, test, etc.).
178#[derive(Clone, Debug, PartialEq)]
179pub struct BuildTarget {
180    /// Target name.
181    pub name: String,
182    /// Root source file.
183    pub src: PathBuf,
184    /// Kind of target.
185    pub kind: TargetKind,
186    /// Whether the target is enabled.
187    pub enabled: bool,
188    /// Dependencies on other targets.
189    pub deps: Vec<String>,
190}
191
192impl BuildTarget {
193    /// Create a library target.
194    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    /// Create a binary target.
205    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    /// Create a test target.
216    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    /// Add a dependency on another target.
227    pub fn depends_on(mut self, dep: &str) -> Self {
228        self.deps.push(dep.to_string());
229        self
230    }
231
232    /// Disable this target.
233    pub fn disabled(mut self) -> Self {
234        self.enabled = false;
235        self
236    }
237}
238
239/// The kind of a build target.
240#[derive(Clone, Copy, Debug, PartialEq, Eq)]
241pub enum TargetKind {
242    /// A library.
243    Lib,
244    /// An executable binary.
245    Bin,
246    /// A test suite.
247    Test,
248    /// A benchmark suite.
249    Bench,
250    /// Documentation.
251    Doc,
252    /// A build script.
253    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// ============================================================
270// BuildPlan
271// ============================================================
272
273/// A resolved build plan ready for execution.
274#[derive(Clone, Debug)]
275pub struct BuildPlan {
276    /// Build configuration.
277    pub config: BuildConfig,
278    /// Targets in topological build order.
279    pub targets: Vec<BuildTarget>,
280    /// Resolved dependency graph.
281    pub dep_graph: HashMap<String, Vec<String>>,
282}
283
284impl BuildPlan {
285    /// Create an empty build plan.
286    pub fn new(config: BuildConfig) -> Self {
287        Self {
288            config,
289            targets: Vec::new(),
290            dep_graph: HashMap::new(),
291        }
292    }
293
294    /// Add a target to the plan.
295    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    /// Number of targets in the plan.
303    pub fn target_count(&self) -> usize {
304        self.targets.len()
305    }
306
307    /// Find a target by name.
308    pub fn find_target(&self, name: &str) -> Option<&BuildTarget> {
309        self.targets.iter().find(|t| t.name == name)
310    }
311
312    /// Return targets in topological order.
313    pub fn topo_order(&self) -> Vec<&BuildTarget> {
314        self.targets.iter().collect()
315    }
316
317    /// Enabled targets only.
318    pub fn enabled_targets(&self) -> Vec<&BuildTarget> {
319        self.targets.iter().filter(|t| t.enabled).collect()
320    }
321}
322
323// ============================================================
324// CompilationUnit
325// ============================================================
326
327/// A single unit of compilation (one source file to one object).
328#[derive(Clone, Debug)]
329pub struct CompilationUnit {
330    /// Source file path.
331    pub source: PathBuf,
332    /// Output artifact path.
333    pub output: PathBuf,
334    /// Module name.
335    pub module_name: String,
336    /// Whether this unit was already compiled and up-to-date.
337    pub is_cached: bool,
338}
339
340impl CompilationUnit {
341    /// Create a new compilation unit.
342    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    /// Mark this unit as cached (up-to-date).
352    pub fn mark_cached(mut self) -> Self {
353        self.is_cached = true;
354        self
355    }
356
357    /// Extension of the output file.
358    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// ============================================================
367// BuildSummary
368// ============================================================
369
370/// Summary of a completed build.
371#[derive(Clone, Debug, Default)]
372pub struct BuildSummary {
373    /// Number of units compiled from scratch.
374    pub compiled: usize,
375    /// Number of units served from cache.
376    pub cached: usize,
377    /// Number of units that failed.
378    pub failed: usize,
379    /// Total wall-clock time in milliseconds.
380    pub elapsed_ms: u64,
381    /// Errors encountered.
382    pub errors: Vec<String>,
383    /// Warnings encountered.
384    pub warnings: Vec<String>,
385}
386
387impl BuildSummary {
388    /// Create a zero summary.
389    pub fn new() -> Self {
390        Self::default()
391    }
392
393    /// Whether the build succeeded.
394    pub fn is_success(&self) -> bool {
395        self.failed == 0 && self.errors.is_empty()
396    }
397
398    /// Total units processed.
399    pub fn total(&self) -> usize {
400        self.compiled + self.cached + self.failed
401    }
402
403    /// Add an error message.
404    pub fn add_error(&mut self, msg: &str) {
405        self.errors.push(msg.to_string());
406        self.failed += 1;
407    }
408
409    /// Add a warning message.
410    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// ============================================================
426// Tests
427// ============================================================
428
429#[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// ============================================================
601// BuildCache: cache of compilation results
602// ============================================================
603
604/// A simple cache of compiled module results.
605#[derive(Clone, Debug, Default)]
606pub struct BuildCache {
607    /// Cached entries: module_name -> output_path.
608    entries: std::collections::HashMap<String, std::path::PathBuf>,
609}
610
611impl BuildCache {
612    /// Create an empty cache.
613    pub fn new() -> Self {
614        Self::default()
615    }
616
617    /// Insert a cache entry.
618    pub fn insert(&mut self, module: &str, output: std::path::PathBuf) {
619        self.entries.insert(module.to_string(), output);
620    }
621
622    /// Look up a cached output path.
623    pub fn get(&self, module: &str) -> Option<&std::path::PathBuf> {
624        self.entries.get(module)
625    }
626
627    /// Whether a module is cached.
628    pub fn contains(&self, module: &str) -> bool {
629        self.entries.contains_key(module)
630    }
631
632    /// Number of cached entries.
633    pub fn len(&self) -> usize {
634        self.entries.len()
635    }
636
637    /// Whether the cache is empty.
638    pub fn is_empty(&self) -> bool {
639        self.entries.is_empty()
640    }
641
642    /// Remove an entry.
643    pub fn invalidate(&mut self, module: &str) {
644        self.entries.remove(module);
645    }
646
647    /// Clear all entries.
648    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// ============================================================
660// BuildLog: structured build log entries
661// ============================================================
662
663/// The level of a build log message.
664#[derive(Clone, Copy, Debug, PartialEq, Eq)]
665pub enum BuildLogLevel {
666    /// Verbose/trace output.
667    Trace,
668    /// Informational message.
669    Info,
670    /// Warning.
671    Warn,
672    /// Error.
673    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/// A single build log entry.
688#[derive(Clone, Debug)]
689pub struct BuildLogEntry {
690    /// Log level.
691    pub level: BuildLogLevel,
692    /// Log message.
693    pub message: String,
694    /// Optional target name context.
695    pub target: Option<String>,
696}
697
698impl BuildLogEntry {
699    /// Create an info entry.
700    pub fn info(msg: &str) -> Self {
701        Self {
702            level: BuildLogLevel::Info,
703            message: msg.to_string(),
704            target: None,
705        }
706    }
707
708    /// Create an error entry.
709    pub fn error(msg: &str) -> Self {
710        Self {
711            level: BuildLogLevel::Error,
712            message: msg.to_string(),
713            target: None,
714        }
715    }
716
717    /// Attach a target name.
718    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// ============================================================
735// Additional tests
736// ============================================================
737
738#[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
811// ============================================================
812// DependencyGraph: explicit dep graph operations
813// ============================================================
814
815/// Utility functions for build dependency graphs.
816pub struct DependencyGraph;
817
818impl DependencyGraph {
819    /// Compute the topological order of nodes given an adjacency list.
820    /// Returns `None` if there is a cycle.
821    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    /// Whether the dependency graph has any cycles.
855    pub fn has_cycle(deps: &std::collections::HashMap<String, Vec<String>>) -> bool {
856        Self::topo_sort(deps).is_none()
857    }
858}
859
860// ============================================================
861// BuildEnvironment: environment variables for the build
862// ============================================================
863
864/// Environment variables available to build scripts.
865#[derive(Clone, Debug, Default)]
866pub struct BuildEnvironment {
867    /// Variable name → value pairs.
868    vars: std::collections::HashMap<String, String>,
869}
870
871impl BuildEnvironment {
872    /// Create an empty build environment.
873    pub fn new() -> Self {
874        Self::default()
875    }
876
877    /// Set a variable.
878    pub fn set(&mut self, key: &str, value: &str) {
879        self.vars.insert(key.to_string(), value.to_string());
880    }
881
882    /// Get a variable.
883    pub fn get(&self, key: &str) -> Option<&str> {
884        self.vars.get(key).map(|s| s.as_str())
885    }
886
887    /// Number of variables.
888    pub fn len(&self) -> usize {
889        self.vars.len()
890    }
891
892    /// Whether the environment is empty.
893    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// ============================================================
905// Additional tests
906// ============================================================
907
908#[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        // DAG: a -> b -> c (a depends on b, b depends on c).
939        // Kahn's algorithm processes nodes with in-degree 0 first,
940        // so 'a' appears before 'b', and 'b' before 'c'.
941        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// ============================================================
999// BuildArtifact: describes a single output artifact
1000// ============================================================
1001
1002/// A compiled artifact produced by a build target.
1003#[derive(Clone, Debug)]
1004pub struct BuildArtifact {
1005    /// Name of the artifact.
1006    pub name: String,
1007    /// Path to the artifact.
1008    pub path: std::path::PathBuf,
1009    /// Kind of artifact.
1010    pub kind: ArtifactKind,
1011    /// Size in bytes (if known).
1012    pub size_bytes: Option<u64>,
1013}
1014
1015impl BuildArtifact {
1016    /// Create a new artifact.
1017    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    /// Attach a size.
1027    pub fn with_size(mut self, size: u64) -> Self {
1028        self.size_bytes = Some(size);
1029        self
1030    }
1031
1032    /// Return a human-readable size description.
1033    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/// The kind of a build artifact.
1050#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1051pub enum ArtifactKind {
1052    /// A compiled object file.
1053    Object,
1054    /// A static library.
1055    StaticLib,
1056    /// A dynamic library.
1057    DynLib,
1058    /// An executable binary.
1059    Executable,
1060    /// Documentation output.
1061    Docs,
1062    /// An OxiLean `.olean`-like export file.
1063    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// ============================================================
1080// BuildPhase: named stages of the build pipeline
1081// ============================================================
1082
1083/// A named phase in the build pipeline.
1084#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
1085pub enum BuildPhase {
1086    /// Parsing source files.
1087    Parse,
1088    /// Type checking.
1089    TypeCheck,
1090    /// Code generation.
1091    Codegen,
1092    /// Linking.
1093    Link,
1094    /// Packaging.
1095    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/// A phase timing record.
1111#[derive(Clone, Debug)]
1112pub struct PhaseTimings {
1113    /// Phase name.
1114    pub phase: BuildPhase,
1115    /// Elapsed time in milliseconds.
1116    pub elapsed_ms: u64,
1117}
1118
1119impl PhaseTimings {
1120    /// Create a new timing record.
1121    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// ============================================================
1204// BuildFeatureFlags: feature flags for the build system
1205// ============================================================
1206
1207/// Feature flags that can be toggled to change build behavior.
1208#[derive(Clone, Debug, Default)]
1209pub struct BuildFeatureFlags {
1210    /// Enable link-time optimization.
1211    pub lto: bool,
1212    /// Enable profile-guided optimization.
1213    pub pgo: bool,
1214    /// Enable SIMD instruction generation.
1215    pub simd: bool,
1216    /// Enable experimental parallel type-checking.
1217    pub parallel_type_check: bool,
1218    /// Enable debug assertions even in release builds.
1219    pub debug_assertions: bool,
1220    /// Enable incremental compilation.
1221    pub incremental: bool,
1222    /// Enable sanitizers (address, memory, etc.).
1223    pub sanitizers: Vec<String>,
1224}
1225
1226impl BuildFeatureFlags {
1227    /// Create default flags for a debug build.
1228    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    /// Create default flags for a release build.
1241    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    /// Add a sanitizer.
1254    pub fn with_sanitizer(mut self, name: &str) -> Self {
1255        self.sanitizers.push(name.to_string());
1256        self
1257    }
1258
1259    /// Whether any sanitizers are active.
1260    pub fn has_sanitizers(&self) -> bool {
1261        !self.sanitizers.is_empty()
1262    }
1263}
1264
1265// ============================================================
1266// BuildNotification
1267// ============================================================
1268
1269/// A notification event emitted during the build process.
1270#[derive(Clone, Debug)]
1271pub enum BuildNotification {
1272    /// Build started.
1273    Started,
1274    /// A target began compiling.
1275    TargetStarted(String),
1276    /// A target finished compiling.
1277    TargetFinished(String, bool), // (name, success)
1278    /// Build completed.
1279    Completed(bool), // success
1280    /// A warning was emitted.
1281    Warning(String),
1282    /// An error was emitted.
1283    Error(String),
1284}
1285
1286impl BuildNotification {
1287    /// Whether this notification indicates success.
1288    pub fn is_success(&self) -> bool {
1289        matches!(self, BuildNotification::Completed(true))
1290    }
1291
1292    /// Short label for display.
1293    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// ============================================================
1312// BuildEventLog
1313// ============================================================
1314
1315/// A log of `BuildNotification`s for a build session.
1316#[derive(Clone, Debug, Default)]
1317pub struct BuildEventLog {
1318    events: Vec<BuildNotification>,
1319}
1320
1321impl BuildEventLog {
1322    /// Create an empty log.
1323    pub fn new() -> Self {
1324        Self::default()
1325    }
1326
1327    /// Append a notification.
1328    pub fn push(&mut self, event: BuildNotification) {
1329        self.events.push(event);
1330    }
1331
1332    /// Number of events.
1333    pub fn len(&self) -> usize {
1334        self.events.len()
1335    }
1336
1337    /// Whether empty.
1338    pub fn is_empty(&self) -> bool {
1339        self.events.is_empty()
1340    }
1341
1342    /// Count events with the given label.
1343    pub fn count_by_label(&self, label: &str) -> usize {
1344        self.events.iter().filter(|e| e.label() == label).count()
1345    }
1346
1347    /// Whether any error events were recorded.
1348    pub fn has_errors(&self) -> bool {
1349        self.events
1350            .iter()
1351            .any(|e| matches!(e, BuildNotification::Error(_)))
1352    }
1353}
1354
1355// ============================================================
1356// BuildPathResolver
1357// ============================================================
1358
1359/// Utility for resolving relative paths within the build directory.
1360pub struct BuildPathResolver {
1361    root: std::path::PathBuf,
1362    out: std::path::PathBuf,
1363}
1364
1365impl BuildPathResolver {
1366    /// Create a resolver anchored at `root` with output in `out`.
1367    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    /// Source path relative to root.
1375    pub fn source_path(&self, rel: &str) -> std::path::PathBuf {
1376        self.root.join(rel)
1377    }
1378
1379    /// Output path relative to out directory.
1380    pub fn output_path(&self, rel: &str) -> std::path::PathBuf {
1381        self.out.join(rel)
1382    }
1383
1384    /// Object file path for a given module name.
1385    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    /// Interface file path for a given module name.
1391    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// ============================================================
1398// BuildMetrics (top-level)
1399// ============================================================
1400
1401/// Top-level build performance metrics.
1402#[derive(Clone, Debug, Default)]
1403pub struct BuildMetrics {
1404    /// Number of source files processed.
1405    pub files_processed: u64,
1406    /// Number of type errors found.
1407    pub type_errors: u64,
1408    /// Total parse time in milliseconds.
1409    pub parse_ms: u64,
1410    /// Total type-check time in milliseconds.
1411    pub typecheck_ms: u64,
1412    /// Total codegen time in milliseconds.
1413    pub codegen_ms: u64,
1414    /// Total link time in milliseconds.
1415    pub link_ms: u64,
1416    /// Peak RSS memory usage in bytes.
1417    pub peak_rss_bytes: u64,
1418}
1419
1420impl BuildMetrics {
1421    /// Create zeroed metrics.
1422    pub fn new() -> Self {
1423        Self::default()
1424    }
1425
1426    /// Total build time in milliseconds.
1427    pub fn total_ms(&self) -> u64 {
1428        self.parse_ms + self.typecheck_ms + self.codegen_ms + self.link_ms
1429    }
1430
1431    /// Whether any type errors were found.
1432    pub fn has_type_errors(&self) -> bool {
1433        self.type_errors > 0
1434    }
1435
1436    /// Human-readable report.
1437    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// ============================================================
1452// PackageId: unique identifier for a package
1453// ============================================================
1454
1455/// A unique package identifier (name + version string).
1456#[derive(Clone, Debug, PartialEq, Eq, Hash)]
1457pub struct PackageId {
1458    /// Package name.
1459    pub name: String,
1460    /// Version string.
1461    pub version: String,
1462}
1463
1464impl PackageId {
1465    /// Create a new package ID.
1466    pub fn new(name: &str, version: &str) -> Self {
1467        Self {
1468            name: name.to_string(),
1469            version: version.to_string(),
1470        }
1471    }
1472
1473    /// Short display form "name@version".
1474    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// ============================================================
1486// BuildGraphNode
1487// ============================================================
1488
1489/// A node in the build graph, representing one compilation unit.
1490#[derive(Clone, Debug)]
1491pub struct BuildGraphNode {
1492    /// Node identifier (usually module name).
1493    pub id: String,
1494    /// Source file.
1495    pub source: std::path::PathBuf,
1496    /// Dependency node IDs.
1497    pub deps: Vec<String>,
1498    /// Output artifact path.
1499    pub output: std::path::PathBuf,
1500    /// Whether this node was invalidated.
1501    pub invalidated: bool,
1502}
1503
1504impl BuildGraphNode {
1505    /// Create a node.
1506    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    /// Add a dependency.
1521    pub fn add_dep(&mut self, dep_id: &str) {
1522        self.deps.push(dep_id.to_string());
1523    }
1524
1525    /// Mark as invalidated.
1526    pub fn invalidate(&mut self) {
1527        self.invalidated = true;
1528    }
1529}
1530
1531// ============================================================
1532// BuildGraph
1533// ============================================================
1534
1535/// The build graph: a collection of nodes with dependencies.
1536pub struct BuildGraph {
1537    nodes: HashMap<String, BuildGraphNode>,
1538}
1539
1540impl BuildGraph {
1541    /// Create an empty graph.
1542    pub fn new() -> Self {
1543        Self {
1544            nodes: HashMap::new(),
1545        }
1546    }
1547
1548    /// Add a node.
1549    pub fn add_node(&mut self, node: BuildGraphNode) {
1550        self.nodes.insert(node.id.clone(), node);
1551    }
1552
1553    /// Get a node by ID.
1554    pub fn get(&self, id: &str) -> Option<&BuildGraphNode> {
1555        self.nodes.get(id)
1556    }
1557
1558    /// Get a node mutably.
1559    pub fn get_mut(&mut self, id: &str) -> Option<&mut BuildGraphNode> {
1560        self.nodes.get_mut(id)
1561    }
1562
1563    /// Number of nodes.
1564    pub fn node_count(&self) -> usize {
1565        self.nodes.len()
1566    }
1567
1568    /// Nodes in topological order (simple Kahn's algorithm).
1569    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    /// All invalidated nodes.
1599    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// ============================================================
1611// BuildStats
1612// ============================================================
1613
1614/// Statistics from a single build run.
1615#[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    /// Create zeroed stats.
1627    pub fn new() -> Self {
1628        Self::default()
1629    }
1630
1631    /// Success rate (targets_built / (built + skipped + failed)).
1632    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    /// Whether the build was error-free.
1642    pub fn is_clean(&self) -> bool {
1643        self.targets_failed == 0 && self.total_errors == 0
1644    }
1645
1646    /// Human-readable summary.
1647    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// ============================================================
1661// Additional tests
1662// ============================================================
1663
1664#[cfg(test)]
1665mod lib_extra_tests {
1666    use super::*;
1667
1668    // ── BuildFeatureFlags ──
1669    #[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    // ── BuildNotification ──
1693    #[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    // ── BuildEventLog ──
1708    #[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    // ── BuildPathResolver ──
1719    #[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    // ── BuildMetrics ──
1735    #[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    // ── PackageId ──
1754    #[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    // ── BuildGraphNode ──
1762    #[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    // ── BuildGraph ──
1778    #[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    // ── BuildStats ──
1800    #[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// ============================================================
1827// WorkspaceInfo: metadata about the workspace
1828// ============================================================
1829
1830/// High-level metadata about the OxiLean workspace.
1831#[derive(Clone, Debug)]
1832pub struct WorkspaceInfo {
1833    /// Workspace name.
1834    pub name: String,
1835    /// Root directory.
1836    pub root: std::path::PathBuf,
1837    /// Member package names.
1838    pub members: Vec<String>,
1839    /// Workspace-level feature flags.
1840    pub flags: BuildFeatureFlags,
1841}
1842
1843impl WorkspaceInfo {
1844    /// Create a workspace with the given name and root.
1845    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    /// Add a member package.
1855    pub fn add_member(&mut self, member: &str) {
1856        self.members.push(member.to_string());
1857    }
1858
1859    /// Number of member packages.
1860    pub fn member_count(&self) -> usize {
1861        self.members.len()
1862    }
1863
1864    /// Whether `pkg` is a member of the workspace.
1865    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
1881// ============================================================
1882// BuildSession
1883// ============================================================
1884
1885/// A stateful build session, tracking progress of a single build invocation.
1886pub struct BuildSession {
1887    /// Configuration.
1888    pub config: BuildConfig,
1889    /// The build plan.
1890    pub plan: BuildPlan,
1891    /// Events recorded during the build.
1892    pub events: BuildEventLog,
1893    /// Running statistics.
1894    pub stats: BuildStats,
1895    /// Current phase.
1896    pub phase: BuildPhase,
1897}
1898
1899impl BuildSession {
1900    /// Start a new session.
1901    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    /// Advance to the next phase.
1915    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    /// Record a target as built.
1925    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    /// Record a target as failed.
1934    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    /// Finalize the session.
1946    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    /// Whether the build succeeded.
1953    pub fn succeeded(&self) -> bool {
1954        self.stats.is_clean()
1955    }
1956}
1957
1958// ============================================================
1959// Additional tests
1960// ============================================================
1961
1962#[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
2033// ============================================================
2034// BuildPlugin: extension points for the build system
2035// ============================================================
2036
2037/// A hook that can be called at various build lifecycle points.
2038pub trait BuildPlugin: std::fmt::Debug {
2039    /// Name of the plugin.
2040    fn name(&self) -> &str;
2041    /// Called before any targets are built.
2042    fn on_build_start(&self, _config: &BuildConfig) {}
2043    /// Called after all targets are built.
2044    fn on_build_finish(&self, _summary: &BuildSummary) {}
2045    /// Called when a target starts building.
2046    fn on_target_start(&self, _target: &BuildTarget) {}
2047    /// Called when a target finishes building.
2048    fn on_target_finish(&self, _target: &BuildTarget, _success: bool) {}
2049}
2050
2051/// A no-op plugin for testing.
2052#[derive(Debug)]
2053pub struct NoopPlugin {
2054    pub name: String,
2055}
2056
2057impl NoopPlugin {
2058    /// Create a noop plugin.
2059    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
2072// ============================================================
2073// PluginRegistry
2074// ============================================================
2075
2076/// Registry of active build plugins.
2077pub struct PluginRegistry {
2078    plugins: Vec<Box<dyn BuildPlugin>>,
2079}
2080
2081impl PluginRegistry {
2082    /// Create an empty registry.
2083    pub fn new() -> Self {
2084        Self {
2085            plugins: Vec::new(),
2086        }
2087    }
2088
2089    /// Register a plugin.
2090    pub fn register(&mut self, plugin: Box<dyn BuildPlugin>) {
2091        self.plugins.push(plugin);
2092    }
2093
2094    /// Number of plugins.
2095    pub fn len(&self) -> usize {
2096        self.plugins.len()
2097    }
2098
2099    /// Whether empty.
2100    pub fn is_empty(&self) -> bool {
2101        self.plugins.is_empty()
2102    }
2103
2104    /// Fire `on_build_start` on all plugins.
2105    pub fn fire_build_start(&self, config: &BuildConfig) {
2106        for p in &self.plugins {
2107            p.on_build_start(config);
2108        }
2109    }
2110
2111    /// Fire `on_build_finish` on all plugins.
2112    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// ============================================================
2126// OxileanVersion: version information
2127// ============================================================
2128
2129/// Version information for the OxiLean build system.
2130#[derive(Clone, Debug)]
2131pub struct OxileanVersion {
2132    /// Major version.
2133    pub major: u32,
2134    /// Minor version.
2135    pub minor: u32,
2136    /// Patch version.
2137    pub patch: u32,
2138    /// Pre-release tag (if any).
2139    pub pre: Option<String>,
2140}
2141
2142impl OxileanVersion {
2143    /// Create a version.
2144    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    /// Create a pre-release version.
2154    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    /// Whether this is a pre-release version.
2164    pub fn is_pre_release(&self) -> bool {
2165        self.pre.is_some()
2166    }
2167
2168    /// Whether this version is at least `(major, minor, patch)`.
2169    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// ============================================================
2184// BuildSystemError
2185// ============================================================
2186
2187/// Errors that can occur in the build system.
2188#[derive(Clone, Debug)]
2189pub enum BuildSystemError {
2190    /// Configuration was invalid.
2191    InvalidConfig(String),
2192    /// A source file was not found.
2193    SourceNotFound(std::path::PathBuf),
2194    /// A dependency cycle was detected.
2195    DependencyCycle(Vec<String>),
2196    /// A compilation step failed.
2197    CompilationFailed { target: String, reason: String },
2198    /// An I/O error occurred.
2199    Io(String),
2200    /// A plugin error occurred.
2201    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
2223/// Convenience type alias for build system results.
2224pub type BuildResult<T> = Result<T, BuildSystemError>;
2225
2226// ============================================================
2227// Tests
2228// ============================================================
2229
2230#[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
2291/// Returns the current build system API version.
2292pub 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// ============================================================
2307// BuildOutputFilter
2308// ============================================================
2309
2310/// Filters build output to suppress or highlight certain messages.
2311#[derive(Clone, Debug)]
2312pub struct BuildOutputFilter {
2313    /// Suppress lines matching these substrings.
2314    pub suppress: Vec<String>,
2315    /// Highlight lines matching these substrings.
2316    pub highlight: Vec<String>,
2317    /// Minimum log level to show.
2318    pub min_level: BuildLogLevel,
2319}
2320
2321impl BuildOutputFilter {
2322    /// Create a default filter (show everything, highlight nothing).
2323    pub fn new() -> Self {
2324        Self {
2325            suppress: Vec::new(),
2326            highlight: Vec::new(),
2327            min_level: BuildLogLevel::Trace,
2328        }
2329    }
2330
2331    /// Add a suppression pattern.
2332    pub fn suppress(mut self, pattern: &str) -> Self {
2333        self.suppress.push(pattern.to_string());
2334        self
2335    }
2336
2337    /// Add a highlight pattern.
2338    pub fn highlight(mut self, pattern: &str) -> Self {
2339        self.highlight.push(pattern.to_string());
2340        self
2341    }
2342
2343    /// Set minimum level.
2344    pub fn min_level(mut self, level: BuildLogLevel) -> Self {
2345        self.min_level = level;
2346        self
2347    }
2348
2349    /// Whether a message at `level` with `text` should be shown.
2350    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    /// Whether a message should be highlighted.
2358    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// ============================================================
2370// BuildCompiler: stub representation of the compiler invocation
2371// ============================================================
2372
2373/// Represents a compiler invocation configuration.
2374#[derive(Clone, Debug)]
2375pub struct BuildCompiler {
2376    /// Path to the compiler executable.
2377    pub executable: std::path::PathBuf,
2378    /// Default flags.
2379    pub default_flags: Vec<String>,
2380    /// Environment variables to set.
2381    pub env: HashMap<String, String>,
2382}
2383
2384impl BuildCompiler {
2385    /// Create a compiler configuration.
2386    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    /// Add a default flag.
2395    pub fn add_flag(&mut self, flag: &str) {
2396        self.default_flags.push(flag.to_string());
2397    }
2398
2399    /// Set an environment variable.
2400    pub fn set_env(&mut self, key: &str, value: &str) {
2401        self.env.insert(key.to_string(), value.to_string());
2402    }
2403
2404    /// Construct a command line for compiling `source` to `output`.
2405    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// ============================================================
2416// Tests
2417// ============================================================
2418
2419#[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
2461/// Utility: compute a simple build fingerprint from a list of file paths.
2462pub 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// ============================================================
2494// BuildSystemCapabilities: what the system can do
2495// ============================================================
2496
2497/// A description of the build system's capabilities.
2498#[derive(Clone, Debug, Default)]
2499pub struct BuildSystemCapabilities {
2500    /// Whether incremental compilation is available.
2501    pub incremental: bool,
2502    /// Whether distributed builds are available.
2503    pub distributed: bool,
2504    /// Whether a remote cache is available.
2505    pub remote_cache: bool,
2506    /// Whether parallel builds are available.
2507    pub parallel: bool,
2508    /// Maximum supported parallel jobs.
2509    pub max_jobs: usize,
2510}
2511
2512impl BuildSystemCapabilities {
2513    /// Create capabilities for a full-featured build system.
2514    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    /// Create capabilities for a minimal build system.
2525    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}