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}