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