cargo_e/
e_command_builder.rs

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