1use crate::manifest::{Dependency, DetailedDependency, Manifest};
6use std::collections::HashSet;
7use std::fs;
8use std::path::{Path, PathBuf};
9use thiserror::Error;
10
11#[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#[derive(Debug)]
41pub struct BuildResult {
42 pub output_path: PathBuf,
44 pub output_size: usize,
46 pub resolved_deps: Vec<ResolvedDependency>,
48}
49
50#[derive(Debug, Clone)]
52pub struct ResolvedDependency {
53 pub name: String,
55 pub path: PathBuf,
57 pub version: Option<String>,
59}
60
61pub struct PackageBuilder {
63 project_root: PathBuf,
65 packages_dir: PathBuf,
67 verbose: bool,
69}
70
71impl PackageBuilder {
72 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 pub fn verbose(mut self, verbose: bool) -> Self {
84 self.verbose = verbose;
85 self
86 }
87
88 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 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 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 let bytecode = fusabi::compile_to_bytecode(&source)
121 .map_err(|e| BuildError::CompileError(e.to_string()))?;
122
123 let target_dir = self.project_root.join("target");
125 if !target_dir.exists() {
126 fs::create_dir_all(&target_dir)?;
127 }
128
129 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 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 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 if visited.contains(name) {
171 return Err(BuildError::CyclicDependency(name.to_string()));
172 }
173
174 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 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 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 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 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 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 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 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 let main_pkg = create_test_package(&temp_dir, "main-pkg");
341
342 let dep_pkg = create_test_package(&temp_dir, "dep-pkg");
344
345 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}