Skip to main content

palladium/package/
build.rs

1// Build system for Palladium
2// "Forging packages into legendary artifacts"
3
4use super::{PackageManager, PackageManifest};
5use crate::driver::Driver;
6use crate::errors::{CompileError, Result};
7use std::collections::{HashMap, HashSet};
8use std::fs;
9use std::path::{Path, PathBuf};
10use std::time::SystemTime;
11
12/// Build configuration
13#[derive(Debug, Clone)]
14pub struct BuildConfig {
15    /// Release mode (optimized)
16    pub release: bool,
17    /// Target directory
18    pub target_dir: PathBuf,
19    /// Enable LLVM backend
20    pub use_llvm: bool,
21    /// Verbose output
22    pub verbose: bool,
23    /// Number of parallel jobs
24    pub jobs: usize,
25    /// Features to enable
26    pub features: HashSet<String>,
27}
28
29impl Default for BuildConfig {
30    fn default() -> Self {
31        Self {
32            release: false,
33            target_dir: PathBuf::from("target"),
34            use_llvm: false,
35            verbose: false,
36            jobs: num_cpus::get(),
37            features: HashSet::new(),
38        }
39    }
40}
41
42/// Build context for tracking dependencies and artifacts
43pub struct BuildContext {
44    /// Build configuration
45    config: BuildConfig,
46    /// Package manifests by name
47    packages: HashMap<String, PackageManifest>,
48    /// Build graph (package -> dependencies)
49    dependency_graph: HashMap<String, Vec<String>>,
50    /// Artifact cache (file -> last modified time)
51    artifact_cache: HashMap<PathBuf, SystemTime>,
52}
53
54impl BuildContext {
55    pub fn new(config: BuildConfig) -> Self {
56        Self {
57            config,
58            packages: HashMap::new(),
59            dependency_graph: HashMap::new(),
60            artifact_cache: HashMap::new(),
61        }
62    }
63
64    /// Load a package and its dependencies
65    pub fn load_package(&mut self, manifest_path: &Path) -> Result<String> {
66        let manifest = PackageManager::load_manifest(manifest_path)?;
67        let name = manifest.name.clone();
68
69        // Add to packages
70        self.packages.insert(name.clone(), manifest.clone());
71
72        // Build dependency list
73        let mut deps = Vec::new();
74        for dep_name in manifest.dependencies.keys() {
75            deps.push(dep_name.clone());
76            // TODO: Resolve and load dependency packages
77        }
78
79        self.dependency_graph.insert(name.clone(), deps);
80
81        Ok(name)
82    }
83
84    /// Get build order using topological sort
85    pub fn get_build_order(&self) -> Result<Vec<String>> {
86        let mut order = Vec::new();
87        let mut visited = HashSet::new();
88        let mut visiting = HashSet::new();
89
90        for package in self.packages.keys() {
91            if !visited.contains(package) {
92                self.visit_package(package, &mut visited, &mut visiting, &mut order)?;
93            }
94        }
95
96        Ok(order)
97    }
98
99    /// DFS visit for topological sort
100    fn visit_package(
101        &self,
102        package: &str,
103        visited: &mut HashSet<String>,
104        visiting: &mut HashSet<String>,
105        order: &mut Vec<String>,
106    ) -> Result<()> {
107        if visiting.contains(package) {
108            return Err(CompileError::Generic(format!(
109                "Circular dependency detected: {}",
110                package
111            )));
112        }
113
114        if visited.contains(package) {
115            return Ok(());
116        }
117
118        visiting.insert(package.to_string());
119
120        if let Some(deps) = self.dependency_graph.get(package) {
121            for dep in deps {
122                self.visit_package(dep, visited, visiting, order)?;
123            }
124        }
125
126        visiting.remove(package);
127        visited.insert(package.to_string());
128        order.push(package.to_string());
129
130        Ok(())
131    }
132
133    /// Check if a file needs rebuilding
134    pub fn needs_rebuild(&self, source: &Path, target: &Path) -> bool {
135        if !target.exists() {
136            return true;
137        }
138
139        let source_modified = fs::metadata(source)
140            .and_then(|m| m.modified())
141            .unwrap_or(SystemTime::UNIX_EPOCH);
142
143        let target_modified = fs::metadata(target)
144            .and_then(|m| m.modified())
145            .unwrap_or(SystemTime::UNIX_EPOCH);
146
147        source_modified > target_modified
148    }
149
150    /// Build a single package
151    pub fn build_package(&mut self, package_name: &str) -> Result<PathBuf> {
152        let manifest = self
153            .packages
154            .get(package_name)
155            .ok_or_else(|| CompileError::Generic(format!("Package '{}' not found", package_name)))?
156            .clone();
157
158        if self.config.verbose {
159            println!("๐Ÿ“ฆ Building package '{}'", package_name);
160        }
161
162        // Create output directory
163        let output_dir = self
164            .config
165            .target_dir
166            .join(if self.config.release {
167                "release"
168            } else {
169                "debug"
170            })
171            .join("deps");
172
173        if !output_dir.exists() {
174            fs::create_dir_all(&output_dir)?;
175        }
176
177        // Determine what to build
178        let mut built_artifacts = Vec::new();
179
180        // Build library if present
181        if let Some(lib_path) = &manifest.lib {
182            let artifact = self.build_library(&manifest, lib_path, &output_dir)?;
183            built_artifacts.push(artifact);
184        }
185
186        // Build binaries
187        for binary in &manifest.bin {
188            let artifact = self.build_binary(&manifest, &binary.path, &binary.name, &output_dir)?;
189            built_artifacts.push(artifact);
190        }
191
192        // Build main if present and no explicit binaries
193        if manifest.bin.is_empty() {
194            if let Some(main_path) = &manifest.main {
195                let artifact =
196                    self.build_binary(&manifest, main_path, &manifest.name, &output_dir)?;
197                built_artifacts.push(artifact);
198            } else if Path::new("src/main.pd").exists() {
199                let artifact =
200                    self.build_binary(&manifest, "src/main.pd", &manifest.name, &output_dir)?;
201                built_artifacts.push(artifact);
202            }
203        }
204
205        if built_artifacts.is_empty() {
206            return Err(CompileError::Generic(format!(
207                "No build targets found for package '{}'",
208                package_name
209            )));
210        }
211
212        Ok(built_artifacts[0].clone())
213    }
214
215    /// Build a library
216    fn build_library(
217        &mut self,
218        manifest: &PackageManifest,
219        lib_path: &str,
220        output_dir: &Path,
221    ) -> Result<PathBuf> {
222        let source_path = Path::new(lib_path);
223        let output_name = format!("lib{}", manifest.name);
224
225        self.compile_file(source_path, &output_name, output_dir, true)
226    }
227
228    /// Build a binary
229    fn build_binary(
230        &mut self,
231        _manifest: &PackageManifest,
232        bin_path: &str,
233        name: &str,
234        output_dir: &Path,
235    ) -> Result<PathBuf> {
236        let source_path = Path::new(bin_path);
237
238        self.compile_file(source_path, name, output_dir, false)
239    }
240
241    /// Compile a single file
242    fn compile_file(
243        &mut self,
244        source_path: &Path,
245        output_name: &str,
246        output_dir: &Path,
247        _is_lib: bool,
248    ) -> Result<PathBuf> {
249        let output_path = if self.config.use_llvm {
250            output_dir.join(format!("{}.ll", output_name))
251        } else {
252            output_dir.join(format!("{}.c", output_name))
253        };
254
255        // Check if rebuild is needed
256        if !self.needs_rebuild(source_path, &output_path) {
257            if self.config.verbose {
258                println!("   โญ๏ธ  {} is up to date", source_path.display());
259            }
260            return Ok(output_path);
261        }
262
263        if self.config.verbose {
264            println!("   ๐Ÿ”จ Compiling {}", source_path.display());
265        }
266
267        // Create driver with appropriate settings
268        let mut driver = Driver::new();
269        if self.config.use_llvm {
270            driver = driver.with_llvm();
271        }
272
273        // Compile the file
274        let temp_output = driver.compile_file(source_path)?;
275
276        // Move to final location
277        fs::rename(&temp_output, &output_path)?;
278
279        // Update cache
280        if let Ok(metadata) = fs::metadata(&output_path) {
281            if let Ok(modified) = metadata.modified() {
282                self.artifact_cache.insert(output_path.clone(), modified);
283            }
284        }
285
286        Ok(output_path)
287    }
288}
289
290/// Build system entry point
291pub struct BuildSystem {
292    context: BuildContext,
293}
294
295impl BuildSystem {
296    pub fn new(config: BuildConfig) -> Self {
297        Self {
298            context: BuildContext::new(config),
299        }
300    }
301
302    /// Build the current project
303    pub fn build(&mut self) -> Result<()> {
304        let start_time = std::time::Instant::now();
305
306        // Load root package
307        let _root_package = self.context.load_package(Path::new("package.pd"))?;
308
309        // Get build order
310        let build_order = self.context.get_build_order()?;
311
312        println!("๐Ÿ—๏ธ  Building {} package(s)", build_order.len());
313
314        // Build packages in order
315        for package in build_order {
316            self.context.build_package(&package)?;
317        }
318
319        let elapsed = start_time.elapsed();
320        println!("โœ… Build completed in {:.2}s", elapsed.as_secs_f64());
321
322        Ok(())
323    }
324
325    /// Clean build artifacts
326    pub fn clean(&self) -> Result<()> {
327        let target_dir = &self.context.config.target_dir;
328
329        if target_dir.exists() {
330            println!("๐Ÿงน Cleaning {}", target_dir.display());
331            fs::remove_dir_all(target_dir)?;
332        }
333
334        println!("โœ… Clean complete");
335        Ok(())
336    }
337
338    /// Run the built executable
339    pub fn run(&mut self, args: Vec<String>) -> Result<()> {
340        // First build
341        self.build()?;
342
343        // Find the main executable
344        let manifest = PackageManager::load_manifest(Path::new("package.pd"))?;
345        let exe_name = &manifest.name;
346
347        let exe_dir = self
348            .context
349            .config
350            .target_dir
351            .join(if self.context.config.release {
352                "release"
353            } else {
354                "debug"
355            })
356            .join("deps");
357
358        let c_file = exe_dir.join(format!("{}.c", exe_name));
359        let exe_file = exe_dir.join(exe_name);
360
361        // Compile C to executable if needed
362        if self.needs_executable_rebuild(&c_file, &exe_file) {
363            println!("๐Ÿ”— Linking {}", exe_name);
364
365            // Get the runtime library path
366            let runtime_path = PathBuf::from("runtime/palladium_runtime.c");
367            
368            let mut gcc_cmd = std::process::Command::new("gcc");
369            gcc_cmd.arg(&c_file).arg(&runtime_path).arg("-o").arg(&exe_file);
370
371            if self.context.config.release {
372                gcc_cmd.arg("-O3");
373            }
374
375            let output = gcc_cmd.output()?;
376
377            if !output.status.success() {
378                let stderr = String::from_utf8_lossy(&output.stderr);
379                return Err(CompileError::Generic(format!(
380                    "Linking failed:\n{}",
381                    stderr
382                )));
383            }
384        }
385
386        // Run the executable
387        println!("๐Ÿš€ Running {}", exe_name);
388        println!("โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€");
389
390        let mut cmd = std::process::Command::new(&exe_file);
391        cmd.args(&args);
392
393        let status = cmd.status()?;
394
395        println!("โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€");
396
397        if !status.success() {
398            std::process::exit(status.code().unwrap_or(1));
399        }
400
401        Ok(())
402    }
403
404    /// Check if executable needs rebuilding
405    fn needs_executable_rebuild(&self, source: &Path, target: &Path) -> bool {
406        self.context.needs_rebuild(source, target)
407    }
408
409    /// Run tests
410    pub fn test(&mut self, filter: Option<&str>) -> Result<()> {
411        println!("๐Ÿงช Running tests...");
412
413        let manifest = PackageManager::load_manifest(Path::new("package.pd"))?;
414
415        // Find test files
416        let mut test_files = Vec::new();
417
418        // Explicit test targets
419        for test in &manifest.tests {
420            test_files.push((test.name.clone(), PathBuf::from(&test.path)));
421        }
422
423        // Auto-discover tests in tests/ directory
424        let tests_dir = Path::new("tests");
425        if tests_dir.exists() {
426            for entry in fs::read_dir(tests_dir)? {
427                let entry = entry?;
428                let path = entry.path();
429                if path.extension().is_some_and(|ext| ext == "pd") {
430                    let name = path
431                        .file_stem()
432                        .and_then(|s| s.to_str())
433                        .unwrap_or("unknown")
434                        .to_string();
435
436                    if let Some(filter) = filter {
437                        if !name.contains(filter) {
438                            continue;
439                        }
440                    }
441
442                    test_files.push((name, path));
443                }
444            }
445        }
446
447        if test_files.is_empty() {
448            println!("No tests found");
449            return Ok(());
450        }
451
452        println!("Found {} test(s)", test_files.len());
453
454        let mut passed = 0;
455        let mut failed = 0;
456
457        for (name, path) in test_files {
458            print!("test {} ... ", name);
459
460            match self.run_test(&path) {
461                Ok(()) => {
462                    println!("โœ… ok");
463                    passed += 1;
464                }
465                Err(e) => {
466                    println!("โŒ FAILED");
467                    println!("  Error: {}", e);
468                    failed += 1;
469                }
470            }
471        }
472
473        println!("\nTest results: {} passed, {} failed", passed, failed);
474
475        if failed > 0 {
476            Err(CompileError::Generic(format!("{} test(s) failed", failed)))
477        } else {
478            Ok(())
479        }
480    }
481
482    /// Run a single test file
483    fn run_test(&mut self, test_path: &Path) -> Result<()> {
484        // Compile the test
485        let output_dir = self.context.config.target_dir.join("debug").join("tests");
486        if !output_dir.exists() {
487            fs::create_dir_all(&output_dir)?;
488        }
489
490        let test_name = test_path
491            .file_stem()
492            .and_then(|s| s.to_str())
493            .unwrap_or("test");
494
495        let output_path = self
496            .context
497            .compile_file(test_path, test_name, &output_dir, false)?;
498
499        // Link to executable
500        let exe_path = output_dir.join(test_name);
501
502        // Get the runtime library path
503        let runtime_path = PathBuf::from("runtime/palladium_runtime.c");
504        
505        let gcc_output = std::process::Command::new("gcc")
506            .arg(&output_path)
507            .arg(&runtime_path)
508            .arg("-o")
509            .arg(&exe_path)
510            .output()?;
511
512        if !gcc_output.status.success() {
513            let stderr = String::from_utf8_lossy(&gcc_output.stderr);
514            return Err(CompileError::Generic(format!(
515                "Test compilation failed:\n{}",
516                stderr
517            )));
518        }
519
520        // Run the test
521        let output = std::process::Command::new(&exe_path).output()?;
522
523        if output.status.success() {
524            Ok(())
525        } else {
526            let stderr = String::from_utf8_lossy(&output.stderr);
527            let stdout = String::from_utf8_lossy(&output.stdout);
528            Err(CompileError::Generic(format!(
529                "Test failed with exit code {}\nstdout:\n{}\nstderr:\n{}",
530                output.status.code().unwrap_or(-1),
531                stdout,
532                stderr
533            )))
534        }
535    }
536}
537
538// Re-export num_cpus functionality
539pub mod num_cpus {
540    pub fn get() -> usize {
541        std::thread::available_parallelism()
542            .map(|n| n.get())
543            .unwrap_or(1)
544    }
545}