⚠️ VeridianOS Kernel Documentation - This is low-level kernel code. All functions are unsafe unless explicitly marked otherwise. no_std

veridian_kernel/pkg/
build_system.rs

1//! Build Orchestrator
2//!
3//! Source-to-binary package build pipeline with dependency resolution,
4//! build sandboxing via namespaces, and integration with the ports system.
5
6#[cfg(feature = "alloc")]
7use alloc::{
8    collections::BTreeMap,
9    string::{String, ToString},
10    vec,
11    vec::Vec,
12};
13
14use crate::error::KernelError;
15
16/// Build stage in the pipeline
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum BuildStage {
19    /// Fetching source
20    Fetch,
21    /// Verifying source integrity
22    Verify,
23    /// Extracting archive
24    Extract,
25    /// Patching source
26    Patch,
27    /// Running configure
28    Configure,
29    /// Compiling
30    Compile,
31    /// Running tests
32    Test,
33    /// Installing to staging directory
34    Install,
35    /// Creating binary package
36    Package,
37    /// Build complete
38    Done,
39    /// Build failed
40    Failed,
41}
42
43/// Build configuration for a single package
44#[cfg(feature = "alloc")]
45#[derive(Debug, Clone)]
46pub struct BuildConfig {
47    pub name: String,
48    pub version: String,
49    pub source_url: String,
50    pub checksum_sha256: String,
51    pub build_type: super::ports::BuildType,
52    pub dependencies: Vec<String>,
53    pub build_dependencies: Vec<String>,
54    pub configure_flags: Vec<String>,
55    pub make_flags: Vec<String>,
56    pub patches: Vec<String>,
57    pub install_prefix: String,
58}
59
60#[cfg(feature = "alloc")]
61impl BuildConfig {
62    pub fn new(name: &str, version: &str) -> Self {
63        Self {
64            name: name.to_string(),
65            version: version.to_string(),
66            source_url: String::new(),
67            checksum_sha256: String::new(),
68            build_type: super::ports::BuildType::Autotools,
69            dependencies: Vec::new(),
70            build_dependencies: Vec::new(),
71            configure_flags: Vec::new(),
72            make_flags: Vec::new(),
73            patches: Vec::new(),
74            install_prefix: String::from("/usr"),
75        }
76    }
77}
78
79/// Build job status
80#[cfg(feature = "alloc")]
81#[derive(Debug, Clone)]
82pub struct BuildJob {
83    pub config: BuildConfig,
84    pub stage: BuildStage,
85    pub log: Vec<String>,
86    pub exit_code: Option<i32>,
87    pub staging_dir: String,
88    pub build_dir: String,
89}
90
91#[cfg(feature = "alloc")]
92impl BuildJob {
93    pub fn new(config: BuildConfig) -> Self {
94        let staging = alloc::format!("/tmp/build/{}-{}/staging", config.name, config.version);
95        let build = alloc::format!("/tmp/build/{}-{}/build", config.name, config.version);
96        Self {
97            config,
98            stage: BuildStage::Fetch,
99            log: Vec::new(),
100            exit_code: None,
101            staging_dir: staging,
102            build_dir: build,
103        }
104    }
105
106    pub fn log_message(&mut self, msg: &str) {
107        self.log.push(msg.to_string());
108    }
109
110    pub fn is_complete(&self) -> bool {
111        matches!(self.stage, BuildStage::Done | BuildStage::Failed)
112    }
113}
114
115/// Dependency graph for topological sort
116#[cfg(feature = "alloc")]
117#[derive(Default)]
118pub struct DependencyGraph {
119    nodes: Vec<String>,
120    edges: BTreeMap<String, Vec<String>>,
121}
122
123#[cfg(feature = "alloc")]
124impl DependencyGraph {
125    pub fn new() -> Self {
126        Self {
127            nodes: Vec::new(),
128            edges: BTreeMap::new(),
129        }
130    }
131
132    pub fn add_package(&mut self, name: &str, deps: &[String]) {
133        if !self.nodes.contains(&name.to_string()) {
134            self.nodes.push(name.to_string());
135        }
136        self.edges.insert(name.to_string(), deps.to_vec());
137
138        for dep in deps {
139            if !self.nodes.contains(dep) {
140                self.nodes.push(dep.clone());
141            }
142        }
143    }
144
145    /// Topological sort using Kahn's algorithm
146    pub fn sort(&self) -> Result<Vec<String>, KernelError> {
147        let mut in_degree: BTreeMap<String, usize> = BTreeMap::new();
148        for node in &self.nodes {
149            in_degree.insert(node.clone(), 0);
150        }
151
152        for deps in self.edges.values() {
153            for dep in deps {
154                *in_degree.entry(dep.clone()).or_insert(0) += 1;
155            }
156        }
157
158        // Incorrect: edges are name->deps meaning name depends on deps
159        // in_degree should count how many things depend on a node
160        // Actually for build order: if A depends on B, B must come first
161        // So we need reverse edges for in-degree calculation
162
163        let mut reverse_in_degree: BTreeMap<String, usize> = BTreeMap::new();
164        for node in &self.nodes {
165            reverse_in_degree.insert(node.clone(), 0);
166        }
167
168        // If "app" depends on ["libfoo", "libbar"], then app has in-degree 2
169        for (name, deps) in &self.edges {
170            reverse_in_degree.insert(name.clone(), deps.len());
171        }
172
173        let mut queue: Vec<String> = Vec::new();
174        for (node, &deg) in &reverse_in_degree {
175            if deg == 0 {
176                queue.push(node.clone());
177            }
178        }
179
180        let mut result = Vec::new();
181        while let Some(node) = queue.pop() {
182            result.push(node.clone());
183
184            // Find all packages that depend on this node and reduce their in-degree
185            for (name, deps) in &self.edges {
186                if deps.contains(&node) {
187                    if let Some(deg) = reverse_in_degree.get_mut(name) {
188                        *deg = deg.saturating_sub(1);
189                        if *deg == 0 {
190                            queue.push(name.clone());
191                        }
192                    }
193                }
194            }
195        }
196
197        if result.len() != self.nodes.len() {
198            return Err(KernelError::InvalidArgument {
199                name: "dependency",
200                value: "cycle detected",
201            });
202        }
203
204        Ok(result)
205    }
206}
207
208/// Build sandbox configuration
209#[cfg(feature = "alloc")]
210#[derive(Debug, Clone)]
211pub struct BuildSandbox {
212    pub root_dir: String,
213    pub use_namespace: bool,
214    pub allowed_paths: Vec<String>,
215    pub env_vars: BTreeMap<String, String>,
216}
217
218#[cfg(feature = "alloc")]
219impl BuildSandbox {
220    pub fn new(root: &str) -> Self {
221        let mut env = BTreeMap::new();
222        env.insert("PATH".to_string(), "/usr/bin:/bin".to_string());
223        env.insert("HOME".to_string(), "/tmp/build".to_string());
224
225        Self {
226            root_dir: root.to_string(),
227            use_namespace: true,
228            allowed_paths: vec![
229                "/usr/include".to_string(),
230                "/usr/lib".to_string(),
231                "/usr/bin".to_string(),
232            ],
233            env_vars: env,
234        }
235    }
236}
237
238/// Build orchestrator that manages the full pipeline
239#[cfg(feature = "alloc")]
240#[derive(Default)]
241pub struct BuildOrchestrator {
242    jobs: Vec<BuildJob>,
243    completed: Vec<String>,
244}
245
246#[cfg(feature = "alloc")]
247impl BuildOrchestrator {
248    pub fn new() -> Self {
249        Self {
250            jobs: Vec::new(),
251            completed: Vec::new(),
252        }
253    }
254
255    pub fn add_job(&mut self, config: BuildConfig) {
256        self.jobs.push(BuildJob::new(config));
257    }
258
259    /// Execute the build pipeline for a single job
260    pub fn execute_job(&mut self, idx: usize) -> Result<(), KernelError> {
261        if idx >= self.jobs.len() {
262            return Err(KernelError::InvalidArgument {
263                name: "dependency",
264                value: "cycle detected",
265            });
266        }
267
268        let job = &mut self.jobs[idx];
269
270        // Check dependencies are satisfied
271        for dep in &job.config.dependencies {
272            if !self.completed.contains(dep) {
273                job.log_message(&alloc::format!("Missing dependency: {}", dep));
274                job.stage = BuildStage::Failed;
275                return Err(KernelError::NotFound {
276                    resource: "dependency",
277                    id: 0,
278                });
279            }
280        }
281
282        // Execute stages sequentially
283        job.stage = BuildStage::Fetch;
284        job.log_message(&alloc::format!(
285            "Fetching {} v{}",
286            job.config.name,
287            job.config.version
288        ));
289
290        job.stage = BuildStage::Verify;
291        job.log_message("Verifying source checksum");
292
293        job.stage = BuildStage::Extract;
294        job.log_message("Extracting source archive");
295
296        job.stage = BuildStage::Patch;
297        for patch in &job.config.patches.clone() {
298            job.log_message(&alloc::format!("Applying patch: {}", patch));
299        }
300
301        job.stage = BuildStage::Configure;
302        let configure_cmd = job.config.build_type.configure_command();
303        if !configure_cmd.is_empty() {
304            job.log_message(&alloc::format!("Running: {}", configure_cmd));
305        }
306
307        job.stage = BuildStage::Compile;
308        let build_cmd = job.config.build_type.build_command();
309        if !build_cmd.is_empty() {
310            job.log_message(&alloc::format!("Running: {}", build_cmd));
311        }
312
313        job.stage = BuildStage::Install;
314        job.log_message(&alloc::format!("Installing to {}", job.staging_dir));
315
316        job.stage = BuildStage::Done;
317        job.exit_code = Some(0);
318
319        let name = job.config.name.clone();
320        self.completed.push(name);
321
322        Ok(())
323    }
324
325    pub fn job_count(&self) -> usize {
326        self.jobs.len()
327    }
328
329    pub fn completed_count(&self) -> usize {
330        self.completed.len()
331    }
332
333    pub fn get_job(&self, idx: usize) -> Option<&BuildJob> {
334        self.jobs.get(idx)
335    }
336}
337
338// ---------------------------------------------------------------------------
339// Tests
340// ---------------------------------------------------------------------------
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345
346    #[test]
347    fn test_build_stage_eq() {
348        assert_eq!(BuildStage::Fetch, BuildStage::Fetch);
349        assert_ne!(BuildStage::Fetch, BuildStage::Compile);
350    }
351
352    #[test]
353    fn test_build_config_new() {
354        let config = BuildConfig::new("hello", "1.0");
355        assert_eq!(config.name, "hello");
356        assert_eq!(config.version, "1.0");
357        assert_eq!(config.install_prefix, "/usr");
358        assert!(config.dependencies.is_empty());
359    }
360
361    #[test]
362    fn test_build_job_new() {
363        let config = BuildConfig::new("test-pkg", "2.0");
364        let job = BuildJob::new(config);
365        assert_eq!(job.stage, BuildStage::Fetch);
366        assert!(!job.is_complete());
367        assert!(job.exit_code.is_none());
368    }
369
370    #[test]
371    fn test_build_job_logging() {
372        let config = BuildConfig::new("test", "1.0");
373        let mut job = BuildJob::new(config);
374        job.log_message("Starting build");
375        job.log_message("Build complete");
376        assert_eq!(job.log.len(), 2);
377        assert_eq!(job.log[0], "Starting build");
378    }
379
380    #[test]
381    fn test_build_job_complete() {
382        let config = BuildConfig::new("test", "1.0");
383        let mut job = BuildJob::new(config);
384        job.stage = BuildStage::Done;
385        assert!(job.is_complete());
386        job.stage = BuildStage::Failed;
387        assert!(job.is_complete());
388        job.stage = BuildStage::Compile;
389        assert!(!job.is_complete());
390    }
391
392    #[test]
393    fn test_dependency_graph_empty() {
394        let graph = DependencyGraph::new();
395        let result = graph.sort().unwrap();
396        assert!(result.is_empty());
397    }
398
399    #[test]
400    fn test_dependency_graph_single() {
401        let mut graph = DependencyGraph::new();
402        graph.add_package("hello", &[]);
403        let result = graph.sort().unwrap();
404        assert_eq!(result, vec!["hello"]);
405    }
406
407    #[test]
408    fn test_dependency_graph_chain() {
409        let mut graph = DependencyGraph::new();
410        graph.add_package("app", &["libfoo".to_string()]);
411        graph.add_package("libfoo", &["libc".to_string()]);
412        graph.add_package("libc", &[]);
413
414        let result = graph.sort().unwrap();
415        // libc must come before libfoo, libfoo before app
416        let libc_pos = result.iter().position(|n| n == "libc").unwrap();
417        let libfoo_pos = result.iter().position(|n| n == "libfoo").unwrap();
418        let app_pos = result.iter().position(|n| n == "app").unwrap();
419        assert!(libc_pos < libfoo_pos);
420        assert!(libfoo_pos < app_pos);
421    }
422
423    #[test]
424    fn test_dependency_graph_diamond() {
425        let mut graph = DependencyGraph::new();
426        graph.add_package("app", &["liba".to_string(), "libb".to_string()]);
427        graph.add_package("liba", &["libc".to_string()]);
428        graph.add_package("libb", &["libc".to_string()]);
429        graph.add_package("libc", &[]);
430
431        let result = graph.sort().unwrap();
432        let libc_pos = result.iter().position(|n| n == "libc").unwrap();
433        let app_pos = result.iter().position(|n| n == "app").unwrap();
434        assert!(libc_pos < app_pos);
435    }
436
437    #[test]
438    fn test_build_sandbox() {
439        let sandbox = BuildSandbox::new("/tmp/sandbox");
440        assert_eq!(sandbox.root_dir, "/tmp/sandbox");
441        assert!(sandbox.use_namespace);
442        assert!(sandbox.env_vars.contains_key("PATH"));
443    }
444
445    #[test]
446    fn test_orchestrator_basic() {
447        let mut orch = BuildOrchestrator::new();
448        assert_eq!(orch.job_count(), 0);
449
450        let config = BuildConfig::new("test", "1.0");
451        orch.add_job(config);
452        assert_eq!(orch.job_count(), 1);
453        assert_eq!(orch.completed_count(), 0);
454    }
455
456    #[test]
457    fn test_orchestrator_execute() {
458        let mut orch = BuildOrchestrator::new();
459        let config = BuildConfig::new("test", "1.0");
460        orch.add_job(config);
461
462        orch.execute_job(0).unwrap();
463        assert_eq!(orch.completed_count(), 1);
464        let job = orch.get_job(0).unwrap();
465        assert_eq!(job.stage, BuildStage::Done);
466        assert_eq!(job.exit_code, Some(0));
467    }
468
469    #[test]
470    fn test_orchestrator_missing_dep() {
471        let mut orch = BuildOrchestrator::new();
472        let mut config = BuildConfig::new("app", "1.0");
473        config.dependencies.push("missing-lib".to_string());
474        orch.add_job(config);
475
476        let result = orch.execute_job(0);
477        assert!(result.is_err());
478    }
479
480    #[test]
481    fn test_orchestrator_invalid_index() {
482        let mut orch = BuildOrchestrator::new();
483        let result = orch.execute_job(99);
484        assert!(result.is_err());
485    }
486
487    #[test]
488    fn test_orchestrator_with_deps() {
489        let mut orch = BuildOrchestrator::new();
490
491        let libc = BuildConfig::new("libc", "1.0");
492        orch.add_job(libc);
493
494        let mut app = BuildConfig::new("app", "1.0");
495        app.dependencies.push("libc".to_string());
496        orch.add_job(app);
497
498        // Build libc first
499        orch.execute_job(0).unwrap();
500        // Then app (libc is now in completed)
501        orch.execute_job(1).unwrap();
502        assert_eq!(orch.completed_count(), 2);
503    }
504}