1use 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#[derive(Debug, Clone)]
14pub struct BuildConfig {
15 pub release: bool,
17 pub target_dir: PathBuf,
19 pub use_llvm: bool,
21 pub verbose: bool,
23 pub jobs: usize,
25 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
42pub struct BuildContext {
44 config: BuildConfig,
46 packages: HashMap<String, PackageManifest>,
48 dependency_graph: HashMap<String, Vec<String>>,
50 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 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 self.packages.insert(name.clone(), manifest.clone());
71
72 let mut deps = Vec::new();
74 for dep_name in manifest.dependencies.keys() {
75 deps.push(dep_name.clone());
76 }
78
79 self.dependency_graph.insert(name.clone(), deps);
80
81 Ok(name)
82 }
83
84 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 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 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 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 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 let mut built_artifacts = Vec::new();
179
180 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 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 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 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 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 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 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 let mut driver = Driver::new();
269 if self.config.use_llvm {
270 driver = driver.with_llvm();
271 }
272
273 let temp_output = driver.compile_file(source_path)?;
275
276 fs::rename(&temp_output, &output_path)?;
278
279 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
290pub 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 pub fn build(&mut self) -> Result<()> {
304 let start_time = std::time::Instant::now();
305
306 let _root_package = self.context.load_package(Path::new("package.pd"))?;
308
309 let build_order = self.context.get_build_order()?;
311
312 println!("๐๏ธ Building {} package(s)", build_order.len());
313
314 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 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 pub fn run(&mut self, args: Vec<String>) -> Result<()> {
340 self.build()?;
342
343 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 if self.needs_executable_rebuild(&c_file, &exe_file) {
363 println!("๐ Linking {}", exe_name);
364
365 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 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 fn needs_executable_rebuild(&self, source: &Path, target: &Path) -> bool {
406 self.context.needs_rebuild(source, target)
407 }
408
409 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 let mut test_files = Vec::new();
417
418 for test in &manifest.tests {
420 test_files.push((test.name.clone(), PathBuf::from(&test.path)));
421 }
422
423 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 fn run_test(&mut self, test_path: &Path) -> Result<()> {
484 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 let exe_path = output_dir.join(test_name);
501
502 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 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
538pub mod num_cpus {
540 pub fn get() -> usize {
541 std::thread::available_parallelism()
542 .map(|n| n.get())
543 .unwrap_or(1)
544 }
545}