logicaffeine_cli/project/
build.rs1use std::fs;
25use std::path::{Path, PathBuf};
26use std::process::Command;
27
28use crate::compile::compile_project;
29use logicaffeine_compile::compile::{copy_runtime_crates, CompileError};
30
31use super::manifest::{Manifest, ManifestError};
32
33pub struct BuildConfig {
53 pub project_dir: PathBuf,
55 pub release: bool,
57}
58
59#[derive(Debug)]
64pub struct BuildResult {
65 pub target_dir: PathBuf,
67 pub binary_path: PathBuf,
69}
70
71#[derive(Debug)]
73pub enum BuildError {
74 Manifest(ManifestError),
76 Compile(CompileError),
78 Io(String),
80 Cargo(String),
82 NotFound(String),
84}
85
86impl std::fmt::Display for BuildError {
87 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88 match self {
89 BuildError::Manifest(e) => write!(f, "{}", e),
90 BuildError::Compile(e) => write!(f, "{}", e),
91 BuildError::Io(e) => write!(f, "IO error: {}", e),
92 BuildError::Cargo(e) => write!(f, "Cargo error: {}", e),
93 BuildError::NotFound(e) => write!(f, "Not found: {}", e),
94 }
95 }
96}
97
98impl std::error::Error for BuildError {}
99
100impl From<ManifestError> for BuildError {
101 fn from(e: ManifestError) -> Self {
102 BuildError::Manifest(e)
103 }
104}
105
106impl From<CompileError> for BuildError {
107 fn from(e: CompileError) -> Self {
108 BuildError::Compile(e)
109 }
110}
111
112pub fn find_project_root(start: &Path) -> Option<PathBuf> {
133 let mut current = if start.is_file() {
134 start.parent()?.to_path_buf()
135 } else {
136 start.to_path_buf()
137 };
138
139 loop {
140 if current.join("Largo.toml").exists() {
141 return Some(current);
142 }
143 if !current.pop() {
144 return None;
145 }
146 }
147}
148
149pub fn build(config: BuildConfig) -> Result<BuildResult, BuildError> {
168 let manifest = Manifest::load(&config.project_dir)?;
170
171 let entry_path = config.project_dir.join(&manifest.package.entry);
173 if entry_path.exists() {
174 return build_with_entry(&config, &manifest, &entry_path);
175 }
176
177 let md_path = entry_path.with_extension("md");
179 if md_path.exists() {
180 return build_with_entry(&config, &manifest, &md_path);
181 }
182
183 Err(BuildError::NotFound(format!(
184 "Entry point not found: {} (also tried .md)",
185 entry_path.display()
186 )))
187}
188
189fn build_with_entry(
190 config: &BuildConfig,
191 manifest: &Manifest,
192 entry_path: &Path,
193) -> Result<BuildResult, BuildError> {
194 let target_dir = config.project_dir.join("target");
196 let build_dir = if config.release {
197 target_dir.join("release")
198 } else {
199 target_dir.join("debug")
200 };
201 let rust_project_dir = build_dir.join("build");
202
203 if rust_project_dir.exists() {
205 fs::remove_dir_all(&rust_project_dir).map_err(|e| BuildError::Io(e.to_string()))?;
206 }
207 fs::create_dir_all(&rust_project_dir).map_err(|e| BuildError::Io(e.to_string()))?;
208
209 let rust_code = compile_project(entry_path)?;
211
212 let src_dir = rust_project_dir.join("src");
214 fs::create_dir_all(&src_dir).map_err(|e| BuildError::Io(e.to_string()))?;
215
216 let main_rs = format!("use logicaffeine_data::*;\nuse logicaffeine_system::*;\n\n{}", rust_code);
217 fs::write(src_dir.join("main.rs"), main_rs).map_err(|e| BuildError::Io(e.to_string()))?;
218
219 let cargo_toml = format!(
221 r#"[package]
222name = "{}"
223version = "{}"
224edition = "2021"
225
226[dependencies]
227logicaffeine-data = {{ path = "./crates/logicaffeine_data" }}
228logicaffeine-system = {{ path = "./crates/logicaffeine_system", features = ["full"] }}
229tokio = {{ version = "1", features = ["rt-multi-thread", "macros"] }}
230"#,
231 manifest.package.name, manifest.package.version
232 );
233 fs::write(rust_project_dir.join("Cargo.toml"), cargo_toml)
234 .map_err(|e| BuildError::Io(e.to_string()))?;
235
236 copy_runtime_crates(&rust_project_dir)?;
238
239 let mut cmd = Command::new("cargo");
241 cmd.arg("build").current_dir(&rust_project_dir);
242 if config.release {
243 cmd.arg("--release");
244 }
245
246 let output = cmd.output().map_err(|e| BuildError::Io(e.to_string()))?;
247
248 if !output.status.success() {
249 let stderr = String::from_utf8_lossy(&output.stderr);
250 return Err(BuildError::Cargo(stderr.to_string()));
251 }
252
253 let binary_name = if cfg!(windows) {
255 format!("{}.exe", manifest.package.name)
256 } else {
257 manifest.package.name.clone()
258 };
259 let cargo_target = if config.release { "release" } else { "debug" };
260 let binary_path = rust_project_dir
261 .join("target")
262 .join(cargo_target)
263 .join(&binary_name);
264
265 Ok(BuildResult {
266 target_dir: build_dir,
267 binary_path,
268 })
269}
270
271pub fn run(build_result: &BuildResult) -> Result<i32, BuildError> {
288 let mut child = Command::new(&build_result.binary_path)
289 .spawn()
290 .map_err(|e| BuildError::Io(e.to_string()))?;
291
292 let status = child.wait().map_err(|e| BuildError::Io(e.to_string()))?;
293
294 Ok(status.code().unwrap_or(1))
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300 use tempfile::tempdir;
301
302 #[test]
303 fn find_project_root_finds_largo_toml() {
304 let temp = tempdir().unwrap();
305 let sub = temp.path().join("a/b/c");
306 fs::create_dir_all(&sub).unwrap();
307 fs::write(temp.path().join("Largo.toml"), "[package]\nname=\"test\"\n").unwrap();
308
309 let found = find_project_root(&sub);
310 assert!(found.is_some());
311 assert_eq!(found.unwrap(), temp.path());
312 }
313
314 #[test]
315 fn find_project_root_returns_none_if_not_found() {
316 let temp = tempdir().unwrap();
317 let found = find_project_root(temp.path());
318 assert!(found.is_none());
319 }
320}