venus_core/compile/
cargo_generator.rs

1//! Cargo manifest generation utilities.
2//!
3//! Shared logic for generating Cargo.toml files for Universe and production builds.
4
5use std::path::Path;
6
7use super::ExternalDependency;
8
9/// Options for the release profile in Cargo.toml.
10#[derive(Debug, Clone)]
11pub struct ReleaseProfile {
12    /// Optimization level (0-3).
13    pub opt_level: u8,
14    /// Enable Link-Time Optimization.
15    pub lto: bool,
16    /// Number of codegen units.
17    pub codegen_units: u32,
18    /// Panic strategy ("unwind" or "abort").
19    pub panic: &'static str,
20}
21
22impl Default for ReleaseProfile {
23    fn default() -> Self {
24        Self {
25            opt_level: 3,
26            lto: false,
27            codegen_units: 16,
28            panic: "unwind",
29        }
30    }
31}
32
33impl ReleaseProfile {
34    /// Create a profile optimized for production binaries.
35    pub fn production() -> Self {
36        Self {
37            opt_level: 3,
38            lto: true,
39            codegen_units: 1,
40            panic: "abort",
41        }
42    }
43}
44
45/// Configuration for generating a Cargo manifest.
46#[derive(Debug, Clone)]
47pub struct ManifestConfig<'a> {
48    /// Package name.
49    pub name: &'a str,
50    /// Package version.
51    pub version: &'a str,
52    /// Rust edition.
53    pub edition: &'a str,
54    /// Library crate types (if building a library).
55    pub lib_crate_types: Option<&'a [&'a str]>,
56    /// Release profile settings.
57    pub release_profile: Option<ReleaseProfile>,
58    /// Whether to add an empty [workspace] table.
59    pub standalone_workspace: bool,
60}
61
62impl<'a> Default for ManifestConfig<'a> {
63    fn default() -> Self {
64        Self {
65            name: "generated",
66            version: "0.1.0",
67            edition: "2021",
68            lib_crate_types: None,
69            release_profile: None,
70            standalone_workspace: false,
71        }
72    }
73}
74
75/// Generate a Cargo.toml manifest.
76///
77/// # Arguments
78///
79/// * `config` - Manifest configuration
80/// * `dependencies` - List of dependencies to include
81/// * `always_include_serde` - Whether to include serde/bincode (for production builds where user code may need them)
82/// * `notebook_dir` - Base directory for resolving relative path dependencies
83pub fn generate_cargo_toml(
84    config: &ManifestConfig<'_>,
85    dependencies: &[ExternalDependency],
86    always_include_serde: bool,
87    notebook_dir: Option<&Path>,
88) -> String {
89    let mut toml = String::new();
90
91    // Package section
92    toml.push_str("[package]\n");
93    toml.push_str(&format!("name = \"{}\"\n", config.name));
94    toml.push_str(&format!("version = \"{}\"\n", config.version));
95    toml.push_str(&format!("edition = \"{}\"\n", config.edition));
96    toml.push('\n');
97
98    // Library section (if applicable)
99    if let Some(crate_types) = config.lib_crate_types {
100        toml.push_str("[lib]\n");
101        let types: Vec<_> = crate_types.iter().map(|t| format!("\"{}\"", t)).collect();
102        toml.push_str(&format!("crate-type = [{}]\n", types.join(", ")));
103        toml.push('\n');
104    }
105
106    // Release profile (if applicable)
107    if let Some(profile) = &config.release_profile {
108        toml.push_str("[profile.release]\n");
109        toml.push_str(&format!("opt-level = {}\n", profile.opt_level));
110        if profile.lto {
111            toml.push_str("lto = true\n");
112        }
113        toml.push_str(&format!("codegen-units = {}\n", profile.codegen_units));
114        toml.push_str(&format!("panic = \"{}\"\n", profile.panic));
115        toml.push('\n');
116    }
117
118    // Dependencies section
119    toml.push_str("[dependencies]\n");
120
121    // Include serde/bincode for user code that might use them (production builds only).
122    // Note: Interactive execution uses rkyv via universe.rs, not this code path.
123    if always_include_serde {
124        toml.push_str("bincode = \"1.3\"\n");
125        toml.push_str("serde = { version = \"1.0\", features = [\"derive\"] }\n");
126    }
127
128    // Add user dependencies
129    for dep in dependencies {
130        // Skip serde/bincode if we already added them
131        if always_include_serde && (dep.name == "serde" || dep.name == "bincode") {
132            continue;
133        }
134
135        format_dependency(&mut toml, dep, notebook_dir);
136    }
137
138    // Standalone workspace table (prevents being part of parent workspace)
139    if config.standalone_workspace {
140        toml.push('\n');
141        toml.push_str("[workspace]\n");
142    }
143
144    toml
145}
146
147/// Format a single external dependency entry.
148fn format_dependency(toml: &mut String, dep: &ExternalDependency, notebook_dir: Option<&Path>) {
149    if let Some(path) = &dep.path {
150        // Convert relative paths to absolute if notebook_dir is provided
151        let abs_path = if path.is_relative() {
152            notebook_dir
153                .map(|dir| dir.join(path))
154                .and_then(|p| p.canonicalize().ok())
155                .unwrap_or_else(|| path.clone())
156        } else {
157            path.clone()
158        };
159
160        toml.push_str(&format!(
161            "{} = {{ path = \"{}\" }}\n",
162            dep.name,
163            abs_path.display()
164        ));
165    } else if let Some(version) = &dep.version {
166        if dep.features.is_empty() {
167            toml.push_str(&format!("{} = \"{}\"\n", dep.name, version));
168        } else {
169            let features: Vec<_> = dep.features.iter().map(|f| format!("\"{}\"", f)).collect();
170            toml.push_str(&format!(
171                "{} = {{ version = \"{}\", features = [{}] }}\n",
172                dep.name,
173                version,
174                features.join(", ")
175            ));
176        }
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use std::path::PathBuf;
184
185    fn make_deps() -> Vec<ExternalDependency> {
186        vec![
187            ExternalDependency {
188                name: "tokio".to_string(),
189                version: Some("1".to_string()),
190                features: vec!["full".to_string()],
191                path: None,
192            },
193            ExternalDependency {
194                name: "anyhow".to_string(),
195                version: Some("1.0".to_string()),
196                features: vec![],
197                path: None,
198            },
199        ]
200    }
201
202    #[test]
203    fn test_basic_manifest() {
204        let config = ManifestConfig {
205            name: "my_crate",
206            ..Default::default()
207        };
208        let deps = make_deps();
209        let toml = generate_cargo_toml(&config, &deps, false, None);
210
211        assert!(toml.contains("[package]"));
212        assert!(toml.contains("name = \"my_crate\""));
213        assert!(toml.contains("tokio = { version = \"1\", features = [\"full\"] }"));
214        assert!(toml.contains("anyhow = \"1.0\""));
215    }
216
217    #[test]
218    fn test_with_serde() {
219        let config = ManifestConfig::default();
220        let deps = vec![];
221        let toml = generate_cargo_toml(&config, &deps, true, None);
222
223        assert!(toml.contains("bincode = \"1.3\""));
224        assert!(toml.contains("serde = { version = \"1.0\", features = [\"derive\"] }"));
225    }
226
227    #[test]
228    fn test_with_lib_crate_types() {
229        let config = ManifestConfig {
230            lib_crate_types: Some(&["cdylib", "rlib"]),
231            ..Default::default()
232        };
233        let toml = generate_cargo_toml(&config, &[], false, None);
234
235        assert!(toml.contains("[lib]"));
236        assert!(toml.contains("crate-type = [\"cdylib\", \"rlib\"]"));
237    }
238
239    #[test]
240    fn test_with_release_profile() {
241        let config = ManifestConfig {
242            release_profile: Some(ReleaseProfile::production()),
243            ..Default::default()
244        };
245        let toml = generate_cargo_toml(&config, &[], false, None);
246
247        assert!(toml.contains("[profile.release]"));
248        assert!(toml.contains("opt-level = 3"));
249        assert!(toml.contains("lto = true"));
250        assert!(toml.contains("codegen-units = 1"));
251        assert!(toml.contains("panic = \"abort\""));
252    }
253
254    #[test]
255    fn test_standalone_workspace() {
256        let config = ManifestConfig {
257            standalone_workspace: true,
258            ..Default::default()
259        };
260        let toml = generate_cargo_toml(&config, &[], false, None);
261
262        assert!(toml.contains("[workspace]"));
263    }
264
265    #[test]
266    fn test_path_dependency() {
267        let deps = vec![ExternalDependency {
268            name: "local_crate".to_string(),
269            version: None,
270            features: vec![],
271            path: Some(PathBuf::from("/absolute/path/to/crate")),
272        }];
273        let toml = generate_cargo_toml(&ManifestConfig::default(), &deps, false, None);
274
275        assert!(toml.contains("local_crate = { path = \"/absolute/path/to/crate\" }"));
276    }
277
278    #[test]
279    fn test_skips_duplicate_serde() {
280        let deps = vec![ExternalDependency {
281            name: "serde".to_string(),
282            version: Some("1.0".to_string()),
283            features: vec!["derive".to_string()],
284            path: None,
285        }];
286        let toml = generate_cargo_toml(&ManifestConfig::default(), &deps, true, None);
287
288        // Should only have one serde entry (the auto-added one)
289        let serde_count = toml.matches("serde").count();
290        assert_eq!(serde_count, 1); // Only the auto-added one
291    }
292}