cargo_e/
e_command_builder.rs

1use std::collections::HashSet;
2use std::path::{Path, PathBuf};
3use std::process::Command;
4use which::which;
5use std::io::Read;
6
7use crate::e_target::{CargoTarget, TargetKind, TargetOrigin};
8
9/// A builder that constructs a Cargo command for a given target.
10#[derive(Clone)]
11pub struct CargoCommandBuilder {
12    pub args: Vec<String>,
13    pub alternate_cmd: Option<String>,
14    pub execution_dir: Option<PathBuf>,
15    pub suppressed_flags: HashSet<String>,
16}
17impl Default for CargoCommandBuilder {
18    fn default() -> Self {
19        Self::new()
20    }
21}
22impl CargoCommandBuilder {
23    /// Creates a new, empty builder.
24    pub fn new() -> Self {
25        CargoCommandBuilder {
26            args: Vec::new(),
27            alternate_cmd: None,
28            execution_dir: None,
29            suppressed_flags: HashSet::new(),
30        }
31    }
32
33    // /// Configures the command based on the provided CargoTarget.
34    // pub fn with_target(mut self, target: &CargoTarget) -> Self {
35    //     match target.kind {
36    //         CargoTargetKind::Example => {
37    //             self.args.push("run".into());
38    //             self.args.push("--example".into());
39    //             self.args.push(target.name.clone());
40    //         }
41    //         CargoTargetKind::Binary => {
42    //             self.args.push("run".into());
43    //             self.args.push("--bin".into());
44    //             self.args.push(target.name.clone());
45    //         }
46    //         CargoTargetKind::Test => {
47    //             self.args.push("test".into());
48    //             self.args.push(target.name.clone());
49    //         }
50    //         CargoTargetKind::Manifest => {
51    //             // For a manifest target, you might simply want to open or browse it.
52    //             // Adjust the behavior as needed.
53    //             self.args.push("manifest".into());
54    //         }
55    //     }
56
57    //     // If the target is "extended", add a --manifest-path flag
58    //     if target.extended {
59    //         self.args.push("--manifest-path".into());
60    //         self.args.push(target.manifest_path.clone());
61    //     }
62
63    //     // Optionally use the origin information if available.
64
65    //     if let Some(TargetOrigin::SubProject(ref path)) = target.origin {
66    //         self.args.push("--manifest-path".into());
67    //         self.args.push(path.display().to_string());
68    //     }
69
70    //     self
71    // }
72
73    /// Configure the command based on the target kind.
74    pub fn with_target(mut self, target: &CargoTarget) -> Self {
75        println!("Target origin: {:?}", target.origin.clone().unwrap());
76        match target.kind {
77            TargetKind::Unknown => {
78                return self;
79            }
80            TargetKind::Bench => {
81                // To run benchmarks, use the "bench" command.
82                self.alternate_cmd = Some("bench".to_string());
83                self.args.push(target.name.clone());
84            }
85            TargetKind::Test => {
86                self.args.push("test".into());
87                // Pass the target's name as a filter to run specific tests.
88                self.args.push(target.name.clone());
89            }
90            TargetKind::Example | TargetKind::ExtendedExample => {
91                self.args.push("run".into());
92                //self.args.push("--message-format=json".into());
93                self.args.push("--example".into());
94                self.args.push(target.name.clone());
95                self.args.push("--manifest-path".into());
96                self.args.push(
97                    target
98                        .manifest_path
99                        .clone()
100                        .to_str()
101                        .unwrap_or_default()
102                        .to_owned(),
103                );
104            }
105            TargetKind::Binary | TargetKind::ExtendedBinary => {
106                self.args.push("run".into());
107                self.args.push("--bin".into());
108                self.args.push(target.name.clone());
109                self.args.push("--manifest-path".into());
110                self.args.push(
111                    target
112                        .manifest_path
113                        .clone()
114                        .to_str()
115                        .unwrap_or_default()
116                        .to_owned(),
117                );
118            }
119            TargetKind::Manifest => {
120                self.suppressed_flags.insert("quiet".to_string());
121                self.args.push("run".into());
122                self.args.push("--manifest-path".into());
123                self.args.push(
124                    target
125                        .manifest_path
126                        .clone()
127                        .to_str()
128                        .unwrap_or_default()
129                        .to_owned(),
130                );
131            }
132            TargetKind::ManifestTauriExample => {
133                self.suppressed_flags.insert("quiet".to_string());
134                self.args.push("run".into());
135                self.args.push("--example".into());
136                self.args.push(target.name.clone());
137                self.args.push("--manifest-path".into());
138                self.args.push(
139                    target
140                        .manifest_path
141                        .clone()
142                        .to_str()
143                        .unwrap_or_default()
144                        .to_owned(),
145                );
146            }
147            TargetKind::ManifestTauri => {
148                self.suppressed_flags.insert("quiet".to_string());
149                // Helper closure to check for tauri.conf.json in a directory.
150                let has_tauri_conf = |dir: &Path| -> bool { dir.join("tauri.conf.json").exists() };
151
152                // Try candidate's parent (if origin is SingleFile or DefaultBinary).
153                let candidate_dir_opt = match &target.origin {
154                    Some(TargetOrigin::SingleFile(path))
155                    | Some(TargetOrigin::DefaultBinary(path)) => path.parent(),
156                    _ => None,
157                };
158
159                if let Some(candidate_dir) = candidate_dir_opt {
160                    if has_tauri_conf(candidate_dir) {
161                        println!("Using candidate directory: {}", candidate_dir.display());
162                        self.execution_dir = Some(candidate_dir.to_path_buf());
163                    } else if let Some(manifest_parent) = target.manifest_path.parent() {
164                        if has_tauri_conf(manifest_parent) {
165                            println!("Using manifest parent: {}", manifest_parent.display());
166                            self.execution_dir = Some(manifest_parent.to_path_buf());
167                        } else if let Some(grandparent) = manifest_parent.parent() {
168                            if has_tauri_conf(grandparent) {
169                                println!("Using manifest grandparent: {}", grandparent.display());
170                                self.execution_dir = Some(grandparent.to_path_buf());
171                            } else {
172                                println!("No tauri.conf.json found in candidate, manifest parent, or grandparent; defaulting to manifest parent: {}", manifest_parent.display());
173                                self.execution_dir = Some(manifest_parent.to_path_buf());
174                            }
175                        } else {
176                            println!("No grandparent for manifest; defaulting to candidate directory: {}", candidate_dir.display());
177                            self.execution_dir = Some(candidate_dir.to_path_buf());
178                        }
179                    } else {
180                        println!(
181                            "No manifest parent found for: {}",
182                            target.manifest_path.display()
183                        );
184                    }
185                } else if let Some(manifest_parent) = target.manifest_path.parent() {
186                    if has_tauri_conf(manifest_parent) {
187                        println!("Using manifest parent: {}", manifest_parent.display());
188                        self.execution_dir = Some(manifest_parent.to_path_buf());
189                    } else if let Some(grandparent) = manifest_parent.parent() {
190                        if has_tauri_conf(grandparent) {
191                            println!("Using manifest grandparent: {}", grandparent.display());
192                            self.execution_dir = Some(grandparent.to_path_buf());
193                        } else {
194                            println!(
195                                "No tauri.conf.json found; defaulting to manifest parent: {}",
196                                manifest_parent.display()
197                            );
198                            self.execution_dir = Some(manifest_parent.to_path_buf());
199                        }
200                    }
201                } else {
202                    println!(
203                        "No manifest parent found for: {}",
204                        target.manifest_path.display()
205                    );
206                }
207                self.args.push("tauri".into());
208                self.args.push("dev".into());
209            }
210            TargetKind::ManifestLeptos => {
211
212                let readme_path = target.manifest_path.parent()
213                .map(|p| p.join("README.md"))
214                .filter(|p| p.exists())
215                .or_else(|| target.manifest_path.parent()
216                    .map(|p| p.join("readme.md"))
217                    .filter(|p| p.exists())
218                );
219            
220            if let Some(readme) = readme_path {
221                if let Ok(mut file) = std::fs::File::open(&readme) {
222                    let mut contents = String::new();
223                    if file.read_to_string(&mut contents).is_ok() && contents.contains("cargo leptos watch") {
224                        // Use cargo leptos watch
225                        println!("Detected 'cargo leptos watch' in {}", readme.display());
226                        self.alternate_cmd = Some("cargo".to_string());
227                        self.args.push("leptos".into());
228                        self.args.push("watch".into());
229                        self = self.with_required_features(&target.manifest_path, target);
230                        return self;
231                    }
232                }
233            }
234            
235            // fallback to trunk
236            let exe_path = match which("trunk") {
237                Ok(path) => path,
238                Err(err) => {
239                    eprintln!("Error: 'trunk' not found in PATH: {}", err);
240                    return self;
241                }
242            };
243            
244            if let Some(manifest_parent) = target.manifest_path.parent() {
245                println!("Manifest path: {}", target.manifest_path.display());
246                println!(
247                    "Execution directory (same as manifest folder): {}",
248                    manifest_parent.display()
249                );
250                self.execution_dir = Some(manifest_parent.to_path_buf());
251            } else {
252                println!(
253                    "No manifest parent found for: {}",
254                    target.manifest_path.display()
255                );
256            }
257            
258            self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
259            self.args.push("serve".into());
260            self.args.push("--open".into());
261            self = self.with_required_features(&target.manifest_path, target);
262            }
263            TargetKind::ManifestDioxus => {
264                let exe_path = match which("dx") {
265                    Ok(path) => path,
266                    Err(err) => {
267                        eprintln!("Error: 'dx' not found in PATH: {}", err);
268                        return self;
269                    }
270                };
271                // For Dioxus targets, print the manifest path and set the execution directory
272                // to be the same directory as the manifest.
273                if let Some(manifest_parent) = target.manifest_path.parent() {
274                    println!("Manifest path: {}", target.manifest_path.display());
275                    println!(
276                        "Execution directory (same as manifest folder): {}",
277                        manifest_parent.display()
278                    );
279                    self.execution_dir = Some(manifest_parent.to_path_buf());
280                } else {
281                    println!(
282                        "No manifest parent found for: {}",
283                        target.manifest_path.display()
284                    );
285                }
286                self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
287                self.args.push("serve".into());
288                self = self.with_required_features(&target.manifest_path, target);
289            }
290            TargetKind::ManifestDioxusExample => {
291                let exe_path = match which("dx") {
292                    Ok(path) => path,
293                    Err(err) => {
294                        eprintln!("Error: 'dx' not found in PATH: {}", err);
295                        return self;
296                    }
297                };
298                // For Dioxus targets, print the manifest path and set the execution directory
299                // to be the same directory as the manifest.
300                if let Some(manifest_parent) = target.manifest_path.parent() {
301                    println!("Manifest path: {}", target.manifest_path.display());
302                    println!(
303                        "Execution directory (same as manifest folder): {}",
304                        manifest_parent.display()
305                    );
306                    self.execution_dir = Some(manifest_parent.to_path_buf());
307                } else {
308                    println!(
309                        "No manifest parent found for: {}",
310                        target.manifest_path.display()
311                    );
312                }
313                self.alternate_cmd = Some(exe_path.as_os_str().to_string_lossy().to_string());
314                self.args.push("serve".into());
315                self.args.push("--example".into());
316                self.args.push(target.name.clone());
317                self = self.with_required_features(&target.manifest_path, target);
318            }
319        }
320        self
321    }
322
323    /// Configure the command using CLI options.
324    pub fn with_cli(mut self, cli: &crate::Cli) -> Self {
325        if cli.quiet && !self.suppressed_flags.contains("quiet") {
326            // Insert --quiet right after "run" if present.
327            if let Some(pos) = self.args.iter().position(|arg| arg == "run") {
328                self.args.insert(pos + 1, "--quiet".into());
329            } else {
330                self.args.push("--quiet".into());
331            }
332        }
333        if cli.release {
334            // Insert --release right after the initial "run" command if applicable.
335            // For example, if the command already contains "run", insert "--release" after it.
336            if let Some(pos) = self.args.iter().position(|arg| arg == "run") {
337                self.args.insert(pos + 1, "--release".into());
338            } else {
339                // If not running a "run" command (like in the Tauri case), simply push it.
340                self.args.push("--release".into());
341            }
342        }
343        // Append extra arguments (if any) after a "--" separator.
344        if !cli.extra.is_empty() {
345            self.args.push("--".into());
346            self.args.extend(cli.extra.iter().cloned());
347        }
348        self
349    }
350    /// Append required features based on the manifest, target kind, and name.
351    /// This method queries your manifest helper function and, if features are found,
352    /// appends "--features" and the feature list.
353    pub fn with_required_features(mut self, manifest: &PathBuf, target: &CargoTarget) -> Self {
354        if let Some(features) = crate::e_manifest::get_required_features_from_manifest(
355            manifest,
356            &target.kind,
357            &target.name,
358        ) {
359            self.args.push("--features".to_string());
360            self.args.push(features);
361        }
362        self
363    }
364
365    /// Appends extra arguments to the command.
366    pub fn with_extra_args(mut self, extra: &[String]) -> Self {
367        if !extra.is_empty() {
368            // Use "--" to separate Cargo arguments from target-specific arguments.
369            self.args.push("--".into());
370            self.args.extend(extra.iter().cloned());
371        }
372        self
373    }
374
375    /// Builds the final vector of command-line arguments.
376    pub fn build(self) -> Vec<String> {
377        self.args
378    }
379
380    /// Optionally, builds a std::process::Command.
381    pub fn build_command(self) -> Command {
382        let mut cmd = if let Some(alternate) = self.alternate_cmd {
383            Command::new(alternate)
384        } else {
385            Command::new("cargo")
386        };
387        cmd.args(self.args);
388        if let Some(dir) = self.execution_dir {
389            cmd.current_dir(dir);
390        }
391        cmd
392    }
393}
394
395// --- Example usage ---
396#[cfg(test)]
397mod tests {
398    use crate::e_target::TargetOrigin;
399
400    use super::*;
401
402    #[test]
403    fn test_command_builder_example() {
404        let target = CargoTarget {
405            name: "my_example".to_string(),
406            display_name: "My Example".to_string(),
407            manifest_path: "Cargo.toml".into(),
408            kind: TargetKind::Example,
409            extended: true,
410            toml_specified: false,
411            origin: Some(TargetOrigin::SingleFile(PathBuf::from(
412                "examples/my_example.rs",
413            ))),
414        };
415
416        let extra_args = vec!["--flag".to_string(), "value".to_string()];
417
418        let args = CargoCommandBuilder::new()
419            .with_target(&target)
420            .with_extra_args(&extra_args)
421            .build();
422
423        // For an example target, we expect something like:
424        // cargo run --example my_example --manifest-path Cargo.toml -- --flag value
425        assert!(args.contains(&"run".to_string()));
426        assert!(args.contains(&"--example".to_string()));
427        assert!(args.contains(&"my_example".to_string()));
428        assert!(args.contains(&"--manifest-path".to_string()));
429        assert!(args.contains(&"Cargo.toml".to_string()));
430        assert!(args.contains(&"--".to_string()));
431        assert!(args.contains(&"--flag".to_string()));
432        assert!(args.contains(&"value".to_string()));
433    }
434}