Maintain/Build/Process.rs
1//=============================================================================//
2// File Path: Element/Maintain/Source/Build/Process.rs
3//=============================================================================//
4// Module: Process
5//
6// Brief Description: Main orchestration logic for preparing and executing the build.
7//
8// RESPONSIBILITIES:
9// ================
10//
11// Primary:
12// - Orchestrate the entire build process from start to finish
13// - Generate product names and bundle identifiers
14// - Modify configuration files for specific build flavors
15// - Stage and bundle Node.js sidecar binaries if needed
16// - Execute the final build command
17//
18// Secondary:
19// - Provide detailed logging of build orchestration steps
20// - Ensure cleanup of temporary files
21//
22// ARCHITECTURAL ROLE:
23// ===================
24//
25// Position:
26// - Core/Orchestration layer
27// - Build process coordination
28//
29// Dependencies (What this module requires):
30// - External crates: std (env, fs, path, process, os), log, toml
31// - Internal modules: Constant::*, Definition::*, Error::BuildError, Function::*
32// - Traits implemented: None
33//
34// Dependents (What depends on this module):
35// - Main entry point
36// - Fn function
37//
38// IMPLEMENTATION DETAILS:
39// =======================
40//
41// Design Patterns:
42// - Orchestration pattern
43// - Guard pattern (for file backup/restoration)
44//
45// Performance Considerations:
46// - Complexity: O(n) - file I/O operations dominate
47// - Memory usage patterns: Moderate (stores configuration data in memory)
48// - Hot path optimizations: None needed (build time is user-facing)
49//
50// Thread Safety:
51// - Thread-safe: No (not designed for concurrent execution)
52// - Synchronization mechanisms used: None
53// - Interior mutability considerations: None
54//
55// Error Handling:
56// - Error types returned: BuildError (various)
57// - Recovery strategies: Guard restores files on error
58//
59// EXAMPLES:
60// =========
61//
62// Example 1: Basic build orchestration
63/// ```rust
64/// use crate::Maintain::Source::Build::Process;
65/// use crate::Maintain::Source::Build::Argument;
66/// let argument = Argument::parse();
67/// Process(&argument)?;
68/// ```
69//
70// Example 2: Handling build errors
71/// ```rust
72/// use crate::Maintain::Source::Build::Process;
73/// match Process(&argument) {
74/// Ok(_) => println!("Build succeeded"),
75/// Err(e) => println!("Build failed: {}", e),
76/// }
77/// ```
78//
79//=============================================================================//
80// IMPLEMENTATION
81//=============================================================================//
82
83use crate::Build::Error::Error as BuildError;
84use crate::Build::Definition::{Argument, Guard, Manifest};
85use crate::Build::JsonEdit::JsonEdit;
86use crate::Build::TomlEdit::TomlEdit;
87use crate::Build::WordsFromPascal::WordsFromPascal;
88use crate::Build::Pascalize::Pascalize;
89use crate::Build::GetTauriTargetTriple::GetTauriTargetTriple;
90
91use crate::Build::Constant::{
92CargoFile, JsonFile, JsonfiveFile,
93NameDelimiter, IdDelimiter,
94};
95
96use log::info;
97use std::{
98 env, fs,
99 path::PathBuf,
100 process::{Command as ProcessCommand, Stdio},
101};
102use toml;
103
104/// Main orchestration logic for preparing and executing the build.
105///
106/// This function is the core of the build system, coordinating all aspects
107/// of preparing, building, and restoring project configurations. It:
108///
109/// 1. Validates the project directory and configuration files
110/// 2. Creates guards to backup and restore configuration files
111/// 3. Generates a unique product name and bundle identifier based on build flags
112/// 4. Modifies Cargo.toml and Tauri configuration files
113/// 5. Optionally stages a Node.js sidecar binary
114/// 6. Executes the provided build command
115/// 7. Cleans up temporary files after successful build
116///
117/// # Parameters
118///
119/// * `Argument` - Parsed command-line arguments and environment variables
120///
121/// # Returns
122///
123/// Returns `Ok(())` on successful build completion or a `BuildError` if
124/// any step fails.
125///
126/// # Errors
127///
128/// * `BuildError::Missing` - If the project directory doesn't exist
129/// * `BuildError::Config` - If Tauri configuration file not found
130/// * `BuildError::Exists` - If a backup file already exists
131/// * `BuildError::Io` - For file operation failures
132/// * `BuildError::Edit` - For TOML editing failures
133/// * `BuildError::Json` / `BuildError::Jsonfive` - For JSON/JSON5 parsing failures
134/// * `BuildError::Parse` - For TOML parsing failures
135/// * `BuildError::Shell` - If the build command fails
136///
137/// # Build Flavor Generation
138///
139/// The product name and bundle identifier are generated by combining:
140///
141/// - **Environment**: Node.js environment (development, production, etc.)
142/// - **Dependency**: Dependency information (org/repo or generic)
143/// - **Node Version**: Node.js version if bundling a sidecar
144/// - **Build Flags**: Bundle, Clean, Browser, Compile, Debug
145///
146/// Example product name: `Development_GenDependency_22NodeVersion_Debug_Mountain`
147///
148/// Example bundle identifier: `land.editor.binary.development.generic.node.22.debug.mountain`
149///
150/// # Node.js Sidecar Bundling
151///
152/// If `NodeVersion` is specified:
153/// - The Node.js binary is copied from `Element/SideCar/{triple}/NODE/{version}/`
154/// - The binary is staged in the project's `Binary/` directory
155/// - The Tauri configuration is updated to include the sidecar
156/// - The binary is given appropriate permissions on Unix-like systems
157/// - The temporary directory is cleaned up after successful build
158///
159/// # File Safety
160///
161/// All configuration file modifications are protected by the Guard pattern:
162/// - Files are backed up before modification
163/// - Files are automatically restored on error or when the guard drops
164/// - This ensures the original state is preserved regardless of build outcome
165///
166/// # Example
167///
168/// ```no_run
169/// use crate::Maintain::Source::Build::Process;
170/// use crate::Maintain::Source::Build::Argument;
171/// let argument = Argument::parse();
172/// Process(&argument)?;
173/// ```
174pub fn Process(Argument: &Argument) -> Result<(), BuildError> {
175 info!(target: "Build", "Starting build orchestration...");
176
177 log::debug!(target: "Build", "Argument: {:?}", Argument);
178
179 let ProjectDir = PathBuf::from(&Argument.Directory);
180
181 if !ProjectDir.is_dir() {
182 return Err(BuildError::Missing(ProjectDir));
183 }
184
185 let CargoPath = ProjectDir.join(CargoFile);
186
187 let ConfigPath = {
188 let Jsonfive = ProjectDir.join(JsonfiveFile);
189
190 if Jsonfive.exists() {
191 Jsonfive
192 } else {
193 ProjectDir.join(JsonFile)
194 }
195 };
196
197 if !ConfigPath.exists() {
198 return Err(BuildError::Config);
199 }
200
201 // Create guards for file backup and restoration
202 let _CargoGuard = Guard::New(CargoPath.clone(), "Cargo.toml".to_string())?;
203
204 let _ConfigGuard = Guard::New(ConfigPath.clone(), "Tauri config".to_string())?;
205
206 let mut NamePartsForProductName = Vec::new();
207
208 let mut NamePartsForId = Vec::new();
209
210 // Include Node.js environment in product name
211 if let Some(NodeValue) = &Argument.Environment {
212 if !NodeValue.is_empty() {
213 let PascalEnv = Pascalize(NodeValue);
214
215 if !PascalEnv.is_empty() {
216 NamePartsForProductName.push(format!("{}NodeEnvironment", PascalEnv));
217
218 NamePartsForId.extend(WordsFromPascal(&PascalEnv));
219
220 NamePartsForId.push("node".to_string());
221
222 NamePartsForId.push("environment".to_string());
223 }
224 }
225 }
226
227 // Include dependency information in product name
228 if let Some(DependencyValue) = &Argument.Dependency {
229 if !DependencyValue.is_empty() {
230 let (PascalDepBase, IdDepWords) = if DependencyValue.eq_ignore_ascii_case("true") {
231 ("Generic".to_string(), vec!["generic".to_string()])
232 } else if let Some((Org, Repo)) = DependencyValue.split_once('/') {
233 (
234 format!("{}{}", Pascalize(Org), Pascalize(Repo)),
235 {
236 let mut w = WordsFromPascal(&Pascalize(Org));
237
238 w.extend(WordsFromPascal(&Pascalize(Repo)));
239
240 w
241 },
242 )
243 } else {
244 (Pascalize(DependencyValue), WordsFromPascal(&Pascalize(DependencyValue)))
245 };
246
247 if !PascalDepBase.is_empty() {
248 NamePartsForProductName.push(format!("{}Dependency", PascalDepBase));
249
250 NamePartsForId.extend(IdDepWords);
251
252 NamePartsForId.push("dependency".to_string());
253 }
254 }
255 }
256
257 // Include Node.js version in product name
258 if let Some(Version) = &Argument.NodeVersion {
259 if !Version.is_empty() {
260 let PascalVersion = format!("{}NodeVersion", Version);
261
262 NamePartsForProductName.push(PascalVersion.clone());
263
264 NamePartsForId.push("node".to_string());
265
266 NamePartsForId.push(Version.to_string());
267 }
268 }
269
270 // Include build flags in product name
271 if Argument.Bundle.as_ref().map_or(false, |v| v == "true") {
272 NamePartsForProductName.push("Bundle".to_string());
273
274 NamePartsForId.push("bundle".to_string());
275 }
276
277 if Argument.Clean.as_ref().map_or(false, |v| v == "true") {
278 NamePartsForProductName.push("Clean".to_string());
279
280 NamePartsForId.push("clean".to_string());
281 }
282
283 if Argument.Browser.as_ref().map_or(false, |v| v == "true") {
284 NamePartsForProductName.push("Browser".to_string());
285
286 NamePartsForId.push("browser".to_string());
287 }
288
289 if Argument.Compile.as_ref().map_or(false, |v| v == "true") {
290 NamePartsForProductName.push("Compile".to_string());
291
292 NamePartsForId.push("compile".to_string());
293 }
294
295 if Argument.Debug.as_ref().map_or(false, |v| v == "true")
296 || Argument.Command.iter().any(|arg| arg.contains("--debug"))
297 {
298 NamePartsForProductName.push("Debug".to_string());
299
300 NamePartsForId.push("debug".to_string());
301 }
302
303 // Generate final product name
304 let ProductNamePrefix = NamePartsForProductName.join(NameDelimiter);
305
306 let FinalName = if !ProductNamePrefix.is_empty() {
307 format!("{}{}{}", ProductNamePrefix, NameDelimiter, Argument.Name)
308 } else {
309 Argument.Name.clone()
310 };
311
312 info!(target: "Build", "Final generated product name: '{}'", FinalName);
313
314 // Generate final bundle identifier
315 NamePartsForId.extend(WordsFromPascal(&Argument.Name));
316
317 let IdSuffix = NamePartsForId
318 .into_iter()
319 .filter(|s| !s.is_empty())
320 .collect::<Vec<String>>()
321 .join(IdDelimiter);
322
323 let FinalId = format!("{}{}{}", Argument.Prefix, IdDelimiter, IdSuffix);
324
325 info!(target: "Build", "Generated bundle identifier: '{}'", FinalId);
326
327 // Update Cargo.toml if product name changed
328 if FinalName != Argument.Name {
329 TomlEdit(&CargoPath, &Argument.Name, &FinalName)?;
330 }
331
332 // Get version from Cargo.toml
333 let AppVersion = toml::from_str::<Manifest>(&fs::read_to_string(&CargoPath)?)?.get_version().to_string();
334
335 // Update Tauri configuration and optionally bundle Node.js sidecar
336 JsonEdit(
337 &ConfigPath,
338 &FinalName,
339 &FinalId,
340 &AppVersion,
341 (if let Some(version) = &Argument.NodeVersion {
342 info!(target: "Build", "Selected Node.js version: {}", version);
343
344 let Triple = GetTauriTargetTriple();
345
346 // Path to the pre-downloaded Node executable
347 let Executable = if cfg!(target_os = "windows") {
348 PathBuf::from(format!("./Element/SideCar/{}/NODE/{}/node.exe", Triple, version))
349 } else {
350 PathBuf::from(format!("./Element/SideCar/{}/NODE/{}/bin/node", Triple, version))
351 };
352
353 // Define a consistent, temporary directory for the staged binary
354 let DirectorySideCarTemporary = ProjectDir.join("Binary");
355
356 fs::create_dir_all(&DirectorySideCarTemporary)?;
357
358 // Define the consistent name for the binary that Tauri will bundle
359 let PathExecutableDestination = if cfg!(target_os = "windows") {
360 DirectorySideCarTemporary.join(format!("node-{}.exe", Triple))
361 } else {
362 DirectorySideCarTemporary.join(format!("node-{}", Triple))
363 };
364
365 info!(
366 target: "Build",
367 "Staging sidecar from {} to {}",
368 Executable.display(),
369 PathExecutableDestination.display()
370 );
371
372 // Perform the copy
373 fs::copy(&Executable, &PathExecutableDestination)?;
374
375 // On non-windows, make sure the copied binary is executable
376 #[cfg(not(target_os = "windows"))]
377 {
378 use std::os::unix::fs::PermissionsExt;
379
380 let mut Permission = fs::metadata(&PathExecutableDestination)?.permissions();
381
382 // rwxr-xr-x
383 Permission.set_mode(0o755);
384
385 fs::set_permissions(&PathExecutableDestination, Permission)?;
386 }
387
388 Some("Binary/node".to_string())
389 } else {
390 info!(target: "Build", "No Node.js flavour selected for bundling.");
391
392 None
393 })
394 .as_deref(),
395 )?;
396
397 // Execute the build command
398 if Argument.Command.is_empty() {
399 return Err(BuildError::NoCommand);
400 }
401
402 let mut ShellCommand = if cfg!(target_os = "windows") {
403 let mut Command = ProcessCommand::new("cmd");
404
405 Command.arg("/C").args(&Argument.Command);
406
407 Command
408 } else {
409 let mut Command = ProcessCommand::new(&Argument.Command[0]);
410
411 Command.args(&Argument.Command[1..]);
412
413 Command
414 };
415
416 info!(target: "Build::Exec", "Executing final build command: {:?}", ShellCommand);
417
418 let Status = ShellCommand
419 .current_dir(env::current_dir()?)
420 .stdout(Stdio::inherit())
421 .stderr(Stdio::inherit())
422 .status()?;
423
424 // Handle build failure
425 if !Status.success() {
426 let temp_sidecar_dir = ProjectDir.join("bin");
427
428 if temp_sidecar_dir.exists() {
429 let _ = fs::remove_dir_all(&temp_sidecar_dir);
430 }
431
432 return Err(BuildError::Shell(Status));
433 }
434
435 // Final cleanup of the temporary sidecar directory after a successful build
436 let DirectorySideCarTemporary = ProjectDir.join("bin");
437
438 if DirectorySideCarTemporary.exists() {
439 fs::remove_dir_all(&DirectorySideCarTemporary)?;
440
441 info!(target: "Build", "Cleaned up temporary sidecar directory.");
442 }
443
444 info!(target: "Build", "Build orchestration completed successfully.");
445
446 Ok(())
447}