Skip to main content

dampen_cli/commands/
build.rs

1#![allow(clippy::print_stderr, clippy::print_stdout)]
2
3//! Build command - generates production Rust code from Dampen UI files
4
5use std::path::Path;
6
7/// Build command arguments
8#[derive(clap::Args)]
9pub struct BuildArgs {
10    /// Input directory containing .dampen files (default: ui/)
11    #[arg(short, long, default_value = "ui")]
12    input: String,
13
14    /// Output file for generated code (default: src/ui_generated.rs)
15    #[arg(short, long, default_value = "src/ui_generated.rs")]
16    output: String,
17
18    /// Model struct name (default: Model)
19    #[arg(long, default_value = "Model")]
20    model: String,
21
22    /// Message enum name (default: Message)
23    #[arg(long, default_value = "Message")]
24    message: String,
25
26    /// Verbose output
27    #[arg(short, long)]
28    verbose: bool,
29
30    /// Package to build (if workspace has multiple packages)
31    #[arg(short, long)]
32    package: Option<String>,
33
34    /// Additional features to enable (beyond codegen)
35    #[arg(long, value_delimiter = ',')]
36    features: Vec<String>,
37
38    /// Build in release mode with codegen
39    #[arg(long)]
40    release: bool,
41}
42
43/// Execute the build command
44///
45/// Builds the application:
46/// - Debug mode (default): Interpreted mode with hot-reload support
47/// - Release mode (--release): Codegen mode with full optimizations
48///
49/// # Mode Behavior
50///
51/// - **Debug Mode** (default): Interpreted mode, fast iteration for development
52/// - **Release Mode** (--release): Codegen mode with zero runtime overhead
53///
54/// # Examples
55///
56/// ```bash
57/// # Debug build (interpreted)
58/// dampen build
59///
60/// # Release build (codegen optimized)
61/// dampen build --release
62///
63/// # Build specific package
64/// dampen build -p my-app
65///
66/// # Enable additional features
67/// dampen build --features tokio,logging
68/// ```
69pub fn execute(args: &BuildArgs) -> Result<(), String> {
70    execute_production_build(args)
71}
72
73fn execute_production_build(args: &BuildArgs) -> Result<(), String> {
74    use std::process::Command;
75
76    // Run checks first (strict=false so warnings don't block, but errors do)
77    if args.verbose {
78        eprintln!("Running pre-flight checks...");
79    }
80
81    // Use the input directory specified in args (default is "ui")
82    // Or pass None if we want auto-discovery logic (but args.input has a default value)
83
84    // Resolve UI directory based on package argument if present
85    let check_input = if let Some(ref pkg) = args.package {
86        crate::commands::check::resolve_package_ui_path(pkg)
87            .map(|p| p.to_string_lossy().to_string())
88    } else {
89        None
90    };
91
92    if let Err(e) = crate::commands::check::run_checks(check_input, false, args.verbose) {
93        return Err(format!("Pre-flight check failed: {}", e));
94    }
95
96    let mode = if args.release {
97        "codegen"
98    } else {
99        "interpreted"
100    };
101
102    if args.verbose {
103        eprintln!("Running {} build...", mode);
104    }
105
106    // Check build.rs only for codegen mode
107    // If package is specified, also check in examples/ directory
108    let build_rs_exists = Path::new("build.rs").exists()
109        || args
110            .package
111            .as_ref()
112            .is_some_and(|pkg| Path::new("examples").join(pkg).join("build.rs").exists());
113
114    if args.release && !build_rs_exists {
115        return Err(
116            "build.rs not found. Codegen mode requires build.rs for code generation.\n\
117             Tip: Use 'dampen build' (without --release) for interpreted mode,\n\
118             or ensure you're in the correct project directory."
119                .to_string(),
120        );
121    }
122
123    if !Path::new("Cargo.toml").exists() {
124        return Err("Cargo.toml not found. Are you in a Rust project directory?".to_string());
125    }
126
127    let mut cmd = Command::new("cargo");
128    cmd.arg("build");
129
130    if let Some(ref package) = args.package {
131        cmd.arg("-p").arg(package);
132    }
133
134    if args.verbose {
135        cmd.arg("--verbose");
136    }
137
138    // Build features list based on mode
139    let all_features = if args.release {
140        // Release mode: codegen with optimizations
141        cmd.arg("--release");
142        cmd.arg("--no-default-features");
143        let mut features = vec!["codegen".to_string()];
144        features.extend(args.features.clone());
145        features
146    } else {
147        // Debug mode: interpreted (NEW BEHAVIOR)
148        let mut features = vec!["interpreted".to_string()];
149        features.extend(args.features.clone());
150        features
151    };
152
153    cmd.arg("--features").arg(all_features.join(","));
154
155    if args.verbose {
156        let features_str = all_features.join(",");
157        let cargo_cmd = if args.release {
158            format!(
159                "cargo build --release --no-default-features --features {}",
160                features_str
161            )
162        } else {
163            format!("cargo build --features {}", features_str)
164        };
165        eprintln!("Executing: {}", cargo_cmd);
166    }
167
168    let status = cmd
169        .status()
170        .map_err(|e| format!("Failed to execute cargo: {}", e))?;
171
172    if !status.success() {
173        return Err("Build failed".to_string());
174    }
175
176    if args.verbose {
177        let output_dir = if args.release {
178            "target/release/"
179        } else {
180            "target/debug/"
181        };
182        eprintln!("Build successful! Binary is in {}", output_dir);
183    }
184
185    if args.release {
186        eprintln!("Release build (codegen) completed successfully!");
187    } else {
188        eprintln!("Debug build (interpreted) completed successfully!");
189        eprintln!(
190            "Use 'dampen build --release' or 'dampen release' for optimized production builds."
191        );
192    }
193
194    Ok(())
195}
196
197/// Execute build command with release mode enabled
198/// This is a helper function for the release command
199pub fn execute_release_build(
200    package: Option<String>,
201    features: Vec<String>,
202    verbose: bool,
203    target_dir: Option<String>,
204) -> Result<(), String> {
205    use std::process::Command;
206
207    // Run checks first
208    if verbose {
209        eprintln!("Running pre-flight checks...");
210    }
211
212    // Resolve UI directory based on package argument if present
213    let check_input = if let Some(ref pkg) = package {
214        crate::commands::check::resolve_package_ui_path(pkg)
215            .map(|p| p.to_string_lossy().to_string())
216    } else {
217        None
218    };
219
220    if let Err(e) = crate::commands::check::run_checks(check_input, false, verbose) {
221        return Err(format!("Pre-flight check failed: {}", e));
222    }
223
224    let mode = "codegen";
225
226    if verbose {
227        eprintln!("Running {} build...", mode);
228    }
229
230    // Check build.rs only for codegen mode
231    // If package is specified, also check in examples/ directory
232    let build_rs_exists = Path::new("build.rs").exists()
233        || package
234            .as_ref()
235            .is_some_and(|pkg| Path::new("examples").join(pkg).join("build.rs").exists());
236
237    if !build_rs_exists {
238        return Err(
239            "build.rs not found. Codegen mode requires build.rs for code generation.\n\
240             Tip: Use 'dampen build' (without --release) for interpreted mode,\n\
241             or ensure you're in the correct project directory."
242                .to_string(),
243        );
244    }
245
246    if !Path::new("Cargo.toml").exists() {
247        return Err("Cargo.toml not found. Are you in a Rust project directory?".to_string());
248    }
249
250    let mut cmd = Command::new("cargo");
251    cmd.arg("build");
252    cmd.arg("--release");
253
254    if let Some(ref pkg) = package {
255        cmd.arg("-p").arg(pkg);
256    }
257
258    if let Some(ref dir) = target_dir {
259        cmd.arg("--target-dir").arg(dir);
260    }
261
262    if verbose {
263        cmd.arg("--verbose");
264    }
265
266    // Release mode: codegen with optimizations
267    cmd.arg("--no-default-features");
268    let mut all_features = vec!["codegen".to_string()];
269    all_features.extend(features);
270    cmd.arg("--features").arg(all_features.join(","));
271
272    if verbose {
273        let features_str = all_features.join(",");
274        eprintln!(
275            "Executing: cargo build --release --no-default-features --features {}",
276            features_str
277        );
278    }
279
280    let status = cmd
281        .status()
282        .map_err(|e| format!("Failed to execute cargo: {}", e))?;
283
284    if !status.success() {
285        return Err("Build failed".to_string());
286    }
287
288    if verbose {
289        eprintln!("Build successful! Binary is in target/release/");
290    }
291
292    eprintln!("Release build (codegen) completed successfully!");
293
294    Ok(())
295}