1use std::collections::HashSet;
6use std::fs;
7use std::path::{Path, PathBuf};
8use std::process::Command;
9
10use crate::error::{Error, Result};
11use crate::graph::{CellInfo, CellParser, GraphEngine};
12
13use super::cargo_generator::{generate_cargo_toml, ManifestConfig, ReleaseProfile};
14use super::dependency_parser::DependencyParser;
15use super::source_processor::NotebookSourceProcessor;
16use super::CompilerConfig;
17
18pub struct ProductionBuilder {
24 config: CompilerConfig,
26
27 cells: Vec<CellInfo>,
29
30 graph: GraphEngine,
32
33 parser: DependencyParser,
35
36 source: String,
38
39 notebook_path: PathBuf,
41}
42
43impl ProductionBuilder {
44 pub fn new(config: CompilerConfig) -> Self {
46 Self {
47 config,
48 cells: Vec::new(),
49 graph: GraphEngine::new(),
50 parser: DependencyParser::new(),
51 source: String::new(),
52 notebook_path: PathBuf::new(),
53 }
54 }
55
56 pub fn load(&mut self, path: impl AsRef<Path>) -> Result<()> {
66 let path = path.as_ref();
67 self.notebook_path = path.to_path_buf();
68 self.source = fs::read_to_string(path)?;
69
70 let mut parser = CellParser::new();
72 let parse_result = parser.parse_file(path)?;
73 self.cells = parse_result.code_cells;
74
75 self.validate_unique_cell_names()?;
77
78 self.graph = GraphEngine::new();
80 for cell in &mut self.cells {
81 let real_id = self.graph.add_cell(cell.clone());
82 cell.id = real_id;
83 }
84 self.graph.resolve_dependencies()?;
85
86 self.parser.parse(&self.source);
88
89 Ok(())
90 }
91
92 fn validate_unique_cell_names(&self) -> Result<()> {
94 let mut seen = HashSet::new();
95 for cell in &self.cells {
96 if !seen.insert(&cell.name) {
97 return Err(Error::Compilation {
98 cell_id: Some(cell.name.clone()),
99 message: format!(
100 "Duplicate cell name '{}' in notebook '{}'",
101 cell.name,
102 self.notebook_path.display()
103 ),
104 });
105 }
106 }
107 Ok(())
108 }
109
110 pub fn build(&self, output_path: impl AsRef<Path>, release: bool) -> Result<PathBuf> {
124 let output_path = output_path.as_ref();
125 let build_dir = self.config.build_dir.join("production");
126
127 fs::create_dir_all(&build_dir)?;
128
129 let cargo_toml = self.generate_cargo_toml()?;
131 fs::write(build_dir.join("Cargo.toml"), cargo_toml)?;
132
133 let main_rs = self.generate_main_rs()?;
135 let src_dir = build_dir.join("src");
136 fs::create_dir_all(&src_dir)?;
137 fs::write(src_dir.join("main.rs"), main_rs)?;
138
139 let mut cmd = Command::new("cargo");
141 cmd.current_dir(&build_dir).arg("build");
142
143 if release {
144 cmd.arg("--release");
145 }
146
147 cmd.arg("--message-format=short");
149
150 let output = cmd.output().map_err(|e| Error::Compilation {
151 cell_id: None,
152 message: format!(
153 "Failed to run cargo (working dir: {}): {}",
154 build_dir.display(),
155 e
156 ),
157 })?;
158
159 if !output.status.success() {
160 let stderr = String::from_utf8_lossy(&output.stderr);
161 return Err(Error::Compilation {
162 cell_id: None,
163 message: format!(
164 "Production build failed for '{}':\n{}",
165 self.notebook_path.display(),
166 stderr
167 ),
168 });
169 }
170
171 let profile = if release { "release" } else { "debug" };
173 let binary_name = self.binary_name();
174 let built_binary = build_dir
175 .join("target")
176 .join(profile)
177 .join(&binary_name);
178
179 fs::copy(&built_binary, output_path)?;
180
181 #[cfg(unix)]
183 {
184 use std::os::unix::fs::PermissionsExt;
185 let mut perms = fs::metadata(output_path)?.permissions();
186 perms.set_mode(0o755);
187 fs::set_permissions(output_path, perms)?;
188 }
189
190 Ok(output_path.to_path_buf())
191 }
192
193 pub fn cell_count(&self) -> usize {
195 self.cells.len()
196 }
197
198 pub fn dependency_count(&self) -> usize {
200 self.parser.dependencies().len()
201 }
202
203 fn generate_cargo_toml(&self) -> Result<String> {
205 let name = self
207 .notebook_path
208 .file_stem()
209 .and_then(|s| s.to_str())
210 .unwrap_or("notebook")
211 .replace('-', "_");
212
213 let notebook_dir = self.notebook_path.parent().ok_or_else(|| Error::Compilation {
215 cell_id: None,
216 message: format!(
217 "Could not determine parent directory for notebook: {}",
218 self.notebook_path.display()
219 ),
220 })?;
221
222 for dep in self.parser.dependencies() {
224 if let Some(path) = &dep.path
225 && path.is_relative() {
226 let full_path = notebook_dir.join(path);
227 full_path.canonicalize().map_err(|e| Error::Compilation {
228 cell_id: None,
229 message: format!(
230 "Failed to resolve path dependency '{}' ({}): {}",
231 dep.name,
232 full_path.display(),
233 e
234 ),
235 })?;
236 }
237 }
238
239 let config = ManifestConfig {
240 name: &name,
241 version: "0.1.0",
242 edition: "2021",
243 lib_crate_types: None,
244 release_profile: Some(ReleaseProfile::production()),
245 standalone_workspace: true,
246 };
247
248 Ok(generate_cargo_toml(
249 &config,
250 self.parser.dependencies(),
251 true, Some(notebook_dir),
253 ))
254 }
255
256 fn generate_main_rs(&self) -> Result<String> {
258 let mut code = String::new();
259
260 code.push_str("//! Generated by Venus - standalone notebook binary.\n");
262 code.push_str("//!\n");
263 code.push_str(&format!(
264 "//! Source: {}\n",
265 self.notebook_path.display()
266 ));
267 code.push('\n');
268 code.push_str("#![allow(unused_imports)]\n");
269 code.push_str("#![allow(dead_code)]\n");
270 code.push_str("#![allow(clippy::ptr_arg)]\n");
271 code.push('\n');
272
273 let processed_source =
275 NotebookSourceProcessor::process_for_production(&self.source).map_err(|e| {
276 Error::Compilation {
277 cell_id: None,
278 message: format!(
279 "Failed to parse notebook source '{}': {}",
280 self.notebook_path.display(),
281 e
282 ),
283 }
284 })?;
285 code.push_str(&processed_source);
286 code.push('\n');
287
288 code.push_str("fn main() {\n");
290 code.push_str(" println!(\"═══════════════════════════════════════════════════\");\n");
291 code.push_str(&format!(
292 " println!(\" Venus Notebook: {}\");\n",
293 self.notebook_path
294 .file_name()
295 .and_then(|s| s.to_str())
296 .unwrap_or("notebook")
297 ));
298 code.push_str(" println!(\"═══════════════════════════════════════════════════\");\n");
299 code.push_str(" println!();\n\n");
300
301 let order = self.graph.topological_order()?;
303
304 for cell_id in &order {
305 let cell = self
306 .cells
307 .iter()
308 .find(|c| c.id == *cell_id)
309 .ok_or_else(|| Error::CellNotFound(format!("{:?}", cell_id)))?;
310
311 let args: Vec<String> = cell
313 .dependencies
314 .iter()
315 .map(|dep| {
316 if dep.is_ref {
317 if dep.is_mut {
318 format!("&mut {}", dep.param_name)
319 } else {
320 format!("&{}", dep.param_name)
321 }
322 } else {
323 dep.param_name.clone()
324 }
325 })
326 .collect();
327
328 code.push_str(&format!(" println!(\"▶ Running: {}\");\n", cell.name));
330
331 code.push_str(&format!(
333 " let {} = {}({});\n",
334 cell.name,
335 cell.name,
336 args.join(", ")
337 ));
338
339 code.push_str(&format!(
341 " println!(\" → {{:?}}\", {});\n",
342 cell.name
343 ));
344 code.push_str(" println!();\n");
345 }
346
347 code.push_str(" println!(\"═══════════════════════════════════════════════════\");\n");
348 code.push_str(&format!(
349 " println!(\" Completed {} cell(s)\");\n",
350 order.len()
351 ));
352 code.push_str(" println!(\"═══════════════════════════════════════════════════\");\n");
353 code.push_str("}\n");
354
355 Ok(code)
356 }
357
358 fn binary_name(&self) -> String {
360 let name = self
361 .notebook_path
362 .file_stem()
363 .and_then(|s| s.to_str())
364 .unwrap_or("notebook")
365 .replace('-', "_");
366
367 #[cfg(target_os = "windows")]
368 {
369 format!("{}.exe", name)
370 }
371 #[cfg(not(target_os = "windows"))]
372 {
373 name
374 }
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 #[test]
383 fn test_binary_name() {
384 let config = CompilerConfig::default();
385 let mut builder = ProductionBuilder::new(config);
386 builder.notebook_path = PathBuf::from("my-notebook.rs");
387
388 let name = builder.binary_name();
389
390 #[cfg(target_os = "windows")]
391 assert_eq!(name, "my_notebook.exe");
392
393 #[cfg(not(target_os = "windows"))]
394 assert_eq!(name, "my_notebook");
395 }
396
397 #[test]
398 fn test_validate_unique_cell_names() {
399 use crate::graph::{CellId, SourceSpan};
400
401 let config = CompilerConfig::default();
402 let mut builder = ProductionBuilder::new(config);
403 builder.notebook_path = PathBuf::from("test.rs");
404
405 let span = SourceSpan {
406 start_line: 1,
407 start_col: 0,
408 end_line: 1,
409 end_col: 10,
410 };
411
412 builder.cells = vec![
414 CellInfo {
415 id: CellId::new(0),
416 name: "foo".to_string(),
417 display_name: "foo".to_string(),
418 dependencies: vec![],
419 return_type: "i32".to_string(),
420 doc_comment: None,
421 source_code: String::new(),
422 source_file: PathBuf::new(),
423 span: span.clone(),
424 },
425 CellInfo {
426 id: CellId::new(1),
427 name: "bar".to_string(),
428 display_name: "bar".to_string(),
429 dependencies: vec![],
430 return_type: "i32".to_string(),
431 doc_comment: None,
432 source_code: String::new(),
433 source_file: PathBuf::new(),
434 span: span.clone(),
435 },
436 ];
437 assert!(builder.validate_unique_cell_names().is_ok());
438
439 builder.cells.push(CellInfo {
441 id: CellId::new(2),
442 name: "foo".to_string(), display_name: "foo".to_string(),
444 dependencies: vec![],
445 return_type: "i32".to_string(),
446 doc_comment: None,
447 source_code: String::new(),
448 source_file: PathBuf::new(),
449 span,
450 });
451 assert!(builder.validate_unique_cell_names().is_err());
452 }
453}