1use std::path::Path;
6
7use super::ExternalDependency;
8
9#[derive(Debug, Clone)]
11pub struct ReleaseProfile {
12 pub opt_level: u8,
14 pub lto: bool,
16 pub codegen_units: u32,
18 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 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#[derive(Debug, Clone)]
47pub struct ManifestConfig<'a> {
48 pub name: &'a str,
50 pub version: &'a str,
52 pub edition: &'a str,
54 pub lib_crate_types: Option<&'a [&'a str]>,
56 pub release_profile: Option<ReleaseProfile>,
58 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
75pub 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 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 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 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 toml.push_str("[dependencies]\n");
120
121 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 for dep in dependencies {
130 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 if config.standalone_workspace {
140 toml.push('\n');
141 toml.push_str("[workspace]\n");
142 }
143
144 toml
145}
146
147fn format_dependency(toml: &mut String, dep: &ExternalDependency, notebook_dir: Option<&Path>) {
149 if let Some(path) = &dep.path {
150 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 let serde_count = toml.matches("serde").count();
290 assert_eq!(serde_count, 1); }
292}