logicaffeine_cli/project/
build.rs1use std::fmt::Write as FmtWrite;
25use std::fs;
26use std::path::{Path, PathBuf};
27use std::process::Command;
28
29use crate::compile::compile_project;
30use logicaffeine_compile::compile::{copy_runtime_crates, CompileError};
31
32use super::manifest::{Manifest, ManifestError};
33
34pub struct BuildConfig {
54 pub project_dir: PathBuf,
56 pub release: bool,
58}
59
60#[derive(Debug)]
65pub struct BuildResult {
66 pub target_dir: PathBuf,
68 pub binary_path: PathBuf,
70}
71
72#[derive(Debug)]
74pub enum BuildError {
75 Manifest(ManifestError),
77 Compile(CompileError),
79 Io(String),
81 Cargo(String),
83 NotFound(String),
85}
86
87impl std::fmt::Display for BuildError {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 match self {
90 BuildError::Manifest(e) => write!(f, "{}", e),
91 BuildError::Compile(e) => write!(f, "{}", e),
92 BuildError::Io(e) => write!(f, "IO error: {}", e),
93 BuildError::Cargo(e) => write!(f, "Cargo error: {}", e),
94 BuildError::NotFound(e) => write!(f, "Not found: {}", e),
95 }
96 }
97}
98
99impl std::error::Error for BuildError {}
100
101impl From<ManifestError> for BuildError {
102 fn from(e: ManifestError) -> Self {
103 BuildError::Manifest(e)
104 }
105}
106
107impl From<CompileError> for BuildError {
108 fn from(e: CompileError) -> Self {
109 BuildError::Compile(e)
110 }
111}
112
113pub fn find_project_root(start: &Path) -> Option<PathBuf> {
134 let mut current = if start.is_file() {
135 start.parent()?.to_path_buf()
136 } else {
137 start.to_path_buf()
138 };
139
140 loop {
141 if current.join("Largo.toml").exists() {
142 return Some(current);
143 }
144 if !current.pop() {
145 return None;
146 }
147 }
148}
149
150pub fn build(config: BuildConfig) -> Result<BuildResult, BuildError> {
169 let manifest = Manifest::load(&config.project_dir)?;
171
172 let entry_path = config.project_dir.join(&manifest.package.entry);
174 if entry_path.exists() {
175 return build_with_entry(&config, &manifest, &entry_path);
176 }
177
178 let md_path = entry_path.with_extension("md");
180 if md_path.exists() {
181 return build_with_entry(&config, &manifest, &md_path);
182 }
183
184 Err(BuildError::NotFound(format!(
185 "Entry point not found: {} (also tried .md)",
186 entry_path.display()
187 )))
188}
189
190fn build_with_entry(
191 config: &BuildConfig,
192 manifest: &Manifest,
193 entry_path: &Path,
194) -> Result<BuildResult, BuildError> {
195 let target_dir = config.project_dir.join("target");
197 let build_dir = if config.release {
198 target_dir.join("release")
199 } else {
200 target_dir.join("debug")
201 };
202 let rust_project_dir = build_dir.join("build");
203
204 if rust_project_dir.exists() {
206 fs::remove_dir_all(&rust_project_dir).map_err(|e| BuildError::Io(e.to_string()))?;
207 }
208 fs::create_dir_all(&rust_project_dir).map_err(|e| BuildError::Io(e.to_string()))?;
209
210 let output = compile_project(entry_path)?;
212
213 let src_dir = rust_project_dir.join("src");
215 fs::create_dir_all(&src_dir).map_err(|e| BuildError::Io(e.to_string()))?;
216
217 let main_rs = format!("use logicaffeine_data::*;\nuse logicaffeine_system::*;\n\n{}", output.rust_code);
218 fs::write(src_dir.join("main.rs"), main_rs).map_err(|e| BuildError::Io(e.to_string()))?;
219
220 let mut cargo_toml = format!(
222 r#"[package]
223name = "{}"
224version = "{}"
225edition = "2021"
226
227[dependencies]
228logicaffeine-data = {{ path = "./crates/logicaffeine_data" }}
229logicaffeine-system = {{ path = "./crates/logicaffeine_system", features = ["full"] }}
230tokio = {{ version = "1", features = ["rt-multi-thread", "macros"] }}
231"#,
232 manifest.package.name, manifest.package.version
233 );
234
235 for dep in &output.dependencies {
237 if dep.features.is_empty() {
238 let _ = writeln!(cargo_toml, "{} = \"{}\"", dep.name, dep.version);
239 } else {
240 let feats = dep.features.iter()
241 .map(|f| format!("\"{}\"", f))
242 .collect::<Vec<_>>()
243 .join(", ");
244 let _ = writeln!(
245 cargo_toml,
246 "{} = {{ version = \"{}\", features = [{}] }}",
247 dep.name, dep.version, feats
248 );
249 }
250 }
251
252 fs::write(rust_project_dir.join("Cargo.toml"), cargo_toml)
253 .map_err(|e| BuildError::Io(e.to_string()))?;
254
255 copy_runtime_crates(&rust_project_dir)?;
257
258 let mut cmd = Command::new("cargo");
260 cmd.arg("build").current_dir(&rust_project_dir);
261 if config.release {
262 cmd.arg("--release");
263 }
264
265 let output = cmd.output().map_err(|e| BuildError::Io(e.to_string()))?;
266
267 if !output.status.success() {
268 let stderr = String::from_utf8_lossy(&output.stderr);
269 return Err(BuildError::Cargo(stderr.to_string()));
270 }
271
272 let binary_name = if cfg!(windows) {
274 format!("{}.exe", manifest.package.name)
275 } else {
276 manifest.package.name.clone()
277 };
278 let cargo_target = if config.release { "release" } else { "debug" };
279 let binary_path = rust_project_dir
280 .join("target")
281 .join(cargo_target)
282 .join(&binary_name);
283
284 Ok(BuildResult {
285 target_dir: build_dir,
286 binary_path,
287 })
288}
289
290pub fn run(build_result: &BuildResult) -> Result<i32, BuildError> {
307 let mut child = Command::new(&build_result.binary_path)
308 .spawn()
309 .map_err(|e| BuildError::Io(e.to_string()))?;
310
311 let status = child.wait().map_err(|e| BuildError::Io(e.to_string()))?;
312
313 Ok(status.code().unwrap_or(1))
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319 use tempfile::tempdir;
320
321 #[test]
322 fn find_project_root_finds_largo_toml() {
323 let temp = tempdir().unwrap();
324 let sub = temp.path().join("a/b/c");
325 fs::create_dir_all(&sub).unwrap();
326 fs::write(temp.path().join("Largo.toml"), "[package]\nname=\"test\"\n").unwrap();
327
328 let found = find_project_root(&sub);
329 assert!(found.is_some());
330 assert_eq!(found.unwrap(), temp.path());
331 }
332
333 #[test]
334 fn find_project_root_returns_none_if_not_found() {
335 let temp = tempdir().unwrap();
336 let found = find_project_root(temp.path());
337 assert!(found.is_none());
338 }
339}