Skip to main content

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