fusabi_pm/
build.rs

1//! Build functionality for the Fusabi Package Manager.
2//!
3//! This module handles compilation of Fusabi packages including dependency resolution.
4
5use crate::manifest::{Dependency, DetailedDependency, Manifest};
6use std::collections::HashSet;
7use std::fs;
8use std::path::{Path, PathBuf};
9use thiserror::Error;
10
11/// Errors that can occur during the build process.
12#[derive(Debug, Error)]
13pub enum BuildError {
14    #[error("Manifest not found: {0}")]
15    ManifestNotFound(PathBuf),
16
17    #[error("Main source file not found: {0}")]
18    MainFileNotFound(PathBuf),
19
20    #[error("Failed to read file: {0}")]
21    IoError(#[from] std::io::Error),
22
23    #[error("Manifest error: {0}")]
24    ManifestError(#[from] crate::manifest::ManifestError),
25
26    #[error("Compilation error: {0}")]
27    CompileError(String),
28
29    #[error("Dependency not found: {name} (expected at {path})")]
30    DependencyNotFound { name: String, path: PathBuf },
31
32    #[error("Cyclic dependency detected: {0}")]
33    CyclicDependency(String),
34
35    #[error("Dependency resolution failed: {0}")]
36    DependencyResolutionFailed(String),
37}
38
39/// Result of a successful build.
40#[derive(Debug)]
41pub struct BuildResult {
42    /// Path to the output bytecode file.
43    pub output_path: PathBuf,
44    /// Size of the output in bytes.
45    pub output_size: usize,
46    /// List of resolved dependencies.
47    pub resolved_deps: Vec<ResolvedDependency>,
48}
49
50/// A resolved dependency with its path and metadata.
51#[derive(Debug, Clone)]
52pub struct ResolvedDependency {
53    /// Name of the dependency.
54    pub name: String,
55    /// Resolved path to the dependency.
56    pub path: PathBuf,
57    /// Version if available.
58    pub version: Option<String>,
59}
60
61/// Builder for Fusabi packages.
62pub struct PackageBuilder {
63    /// Root directory of the package.
64    project_root: PathBuf,
65    /// Path to fusabi_packages directory for installed dependencies.
66    packages_dir: PathBuf,
67    /// Verbose output flag.
68    verbose: bool,
69}
70
71impl PackageBuilder {
72    /// Creates a new package builder for the given project root.
73    pub fn new(project_root: PathBuf) -> Self {
74        let packages_dir = project_root.join("fusabi_packages");
75        Self {
76            project_root,
77            packages_dir,
78            verbose: false,
79        }
80    }
81
82    /// Enables verbose output.
83    pub fn verbose(mut self, verbose: bool) -> Self {
84        self.verbose = verbose;
85        self
86    }
87
88    /// Builds the package.
89    pub fn build(&self) -> Result<BuildResult, BuildError> {
90        let manifest_path = self.project_root.join("fusabi.toml");
91        if !manifest_path.exists() {
92            return Err(BuildError::ManifestNotFound(manifest_path));
93        }
94
95        let manifest = Manifest::load(&manifest_path)?;
96
97        if self.verbose {
98            println!("Building {}...", manifest.package.name);
99        }
100
101        // Resolve dependencies
102        let resolved_deps = self.resolve_dependencies(&manifest)?;
103
104        if self.verbose && !resolved_deps.is_empty() {
105            println!("Resolved {} dependencies:", resolved_deps.len());
106            for dep in &resolved_deps {
107                println!("  - {} @ {}", dep.name, dep.path.display());
108            }
109        }
110
111        // Find and read main source file
112        let main_path = self.project_root.join("src").join("main.fsx");
113        if !main_path.exists() {
114            return Err(BuildError::MainFileNotFound(main_path));
115        }
116
117        let source = fs::read_to_string(&main_path)?;
118
119        // Compile to bytecode
120        let bytecode = fusabi::compile_to_bytecode(&source)
121            .map_err(|e| BuildError::CompileError(e.to_string()))?;
122
123        // Create target directory
124        let target_dir = self.project_root.join("target");
125        if !target_dir.exists() {
126            fs::create_dir_all(&target_dir)?;
127        }
128
129        // Write bytecode
130        let output_path = target_dir.join(format!("{}.fzb", manifest.package.name));
131        let output_size = bytecode.len();
132        fs::write(&output_path, &bytecode)?;
133
134        if self.verbose {
135            println!("Build successful!");
136            println!("Output: {} ({} bytes)", output_path.display(), output_size);
137        }
138
139        Ok(BuildResult {
140            output_path,
141            output_size,
142            resolved_deps,
143        })
144    }
145
146    /// Resolves all dependencies for the manifest.
147    fn resolve_dependencies(
148        &self,
149        manifest: &Manifest,
150    ) -> Result<Vec<ResolvedDependency>, BuildError> {
151        let mut resolved = Vec::new();
152        let mut visited = HashSet::new();
153
154        for (name, dep) in &manifest.dependencies {
155            self.resolve_dependency(name, dep, &mut resolved, &mut visited)?;
156        }
157
158        Ok(resolved)
159    }
160
161    /// Resolves a single dependency recursively.
162    fn resolve_dependency(
163        &self,
164        name: &str,
165        dependency: &Dependency,
166        resolved: &mut Vec<ResolvedDependency>,
167        visited: &mut HashSet<String>,
168    ) -> Result<(), BuildError> {
169        // Check for cycles
170        if visited.contains(name) {
171            return Err(BuildError::CyclicDependency(name.to_string()));
172        }
173
174        // Check if already resolved
175        if resolved.iter().any(|d| d.name == name) {
176            return Ok(());
177        }
178
179        visited.insert(name.to_string());
180
181        let (dep_path, version) = self.locate_dependency(name, dependency)?;
182
183        // Check if the dependency has its own manifest with dependencies
184        let dep_manifest_path = dep_path.join("fusabi.toml");
185        if dep_manifest_path.exists() {
186            let dep_manifest = Manifest::load(&dep_manifest_path)?;
187
188            // Recursively resolve transitive dependencies
189            for (trans_name, trans_dep) in &dep_manifest.dependencies {
190                self.resolve_dependency(trans_name, trans_dep, resolved, visited)?;
191            }
192        }
193
194        resolved.push(ResolvedDependency {
195            name: name.to_string(),
196            path: dep_path,
197            version,
198        });
199
200        visited.remove(name);
201
202        Ok(())
203    }
204
205    /// Locates a dependency on the filesystem.
206    fn locate_dependency(
207        &self,
208        name: &str,
209        dependency: &Dependency,
210    ) -> Result<(PathBuf, Option<String>), BuildError> {
211        match dependency {
212            Dependency::Simple(version) => {
213                // Look in fusabi_packages directory
214                let dep_path = self.packages_dir.join(name);
215                if dep_path.exists() {
216                    Ok((dep_path, Some(version.clone())))
217                } else {
218                    Err(BuildError::DependencyNotFound {
219                        name: name.to_string(),
220                        path: dep_path,
221                    })
222                }
223            }
224            Dependency::Detailed(DetailedDependency {
225                path: Some(local_path),
226                version,
227                ..
228            }) => {
229                // Resolve relative path from project root
230                let dep_path = if Path::new(local_path).is_absolute() {
231                    PathBuf::from(local_path)
232                } else {
233                    self.project_root.join(local_path)
234                };
235
236                if dep_path.exists() {
237                    Ok((dep_path, version.clone()))
238                } else {
239                    Err(BuildError::DependencyNotFound {
240                        name: name.to_string(),
241                        path: dep_path,
242                    })
243                }
244            }
245            Dependency::Detailed(DetailedDependency {
246                git: Some(_git_url),
247                ..
248            }) => {
249                // Git dependencies should be cloned to fusabi_packages
250                let dep_path = self.packages_dir.join(name);
251                if dep_path.exists() {
252                    Ok((dep_path, None))
253                } else {
254                    Err(BuildError::DependencyResolutionFailed(format!(
255                        "Git dependency '{}' not installed. Run 'fpm install' first.",
256                        name
257                    )))
258                }
259            }
260            Dependency::Detailed(DetailedDependency { version, .. }) => {
261                // Fallback to packages directory
262                let dep_path = self.packages_dir.join(name);
263                if dep_path.exists() {
264                    Ok((dep_path, version.clone()))
265                } else {
266                    Err(BuildError::DependencyNotFound {
267                        name: name.to_string(),
268                        path: dep_path,
269                    })
270                }
271            }
272        }
273    }
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279    use crate::manifest::Package;
280    use tempfile::TempDir;
281
282    fn create_test_package(temp_dir: &TempDir, name: &str) -> PathBuf {
283        let pkg_dir = temp_dir.path().join(name);
284        fs::create_dir_all(pkg_dir.join("src")).unwrap();
285
286        let manifest = Manifest::new(Package::new(name.to_string(), "0.1.0".to_string()));
287        fs::write(pkg_dir.join("fusabi.toml"), manifest.to_toml().unwrap()).unwrap();
288
289        fs::write(pkg_dir.join("src/main.fsx"), "42").unwrap();
290
291        pkg_dir
292    }
293
294    #[test]
295    fn test_build_simple_package() {
296        let temp_dir = TempDir::new().unwrap();
297        let pkg_dir = create_test_package(&temp_dir, "test-pkg");
298
299        let builder = PackageBuilder::new(pkg_dir);
300        let result = builder.build();
301
302        assert!(result.is_ok());
303        let result = result.unwrap();
304        assert!(result.output_path.exists());
305        assert!(result.resolved_deps.is_empty());
306    }
307
308    #[test]
309    fn test_build_missing_manifest() {
310        let temp_dir = TempDir::new().unwrap();
311
312        let builder = PackageBuilder::new(temp_dir.path().to_path_buf());
313        let result = builder.build();
314
315        assert!(matches!(result, Err(BuildError::ManifestNotFound(_))));
316    }
317
318    #[test]
319    fn test_build_missing_main_file() {
320        let temp_dir = TempDir::new().unwrap();
321
322        let manifest = Manifest::new(Package::new("test".to_string(), "0.1.0".to_string()));
323        fs::write(
324            temp_dir.path().join("fusabi.toml"),
325            manifest.to_toml().unwrap(),
326        )
327        .unwrap();
328
329        let builder = PackageBuilder::new(temp_dir.path().to_path_buf());
330        let result = builder.build();
331
332        assert!(matches!(result, Err(BuildError::MainFileNotFound(_))));
333    }
334
335    #[test]
336    fn test_resolve_path_dependency() {
337        let temp_dir = TempDir::new().unwrap();
338
339        // Create the main package
340        let main_pkg = create_test_package(&temp_dir, "main-pkg");
341
342        // Create a dependency package
343        let dep_pkg = create_test_package(&temp_dir, "dep-pkg");
344
345        // Update main package manifest to depend on dep-pkg
346        let mut manifest = Manifest::load(main_pkg.join("fusabi.toml")).unwrap();
347        manifest.add_dependency(
348            "dep-pkg".to_string(),
349            Dependency::Detailed(DetailedDependency {
350                path: Some(dep_pkg.to_string_lossy().to_string()),
351                version: Some("0.1.0".to_string()),
352                git: None,
353                rev: None,
354                optional: false,
355            }),
356        );
357        fs::write(main_pkg.join("fusabi.toml"), manifest.to_toml().unwrap()).unwrap();
358
359        let builder = PackageBuilder::new(main_pkg);
360        let result = builder.build().unwrap();
361
362        assert_eq!(result.resolved_deps.len(), 1);
363        assert_eq!(result.resolved_deps[0].name, "dep-pkg");
364    }
365}