cargo_e/
e_command_builder.rs

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