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, CocoonEsbuildDefineEnv, 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	// Tier fan-out observability. The shell helper
183	// `Maintain/Script/TierEnvironment.sh` exports `CargoFeatures` and
184	// `CocoonEsbuildDefine`; surface them here so a build transcript shows
185	// which tier set shipped into the binary without having to replay the
186	// shell environment.
187	if let Some(Features) = Argument.CargoFeatures.as_deref().filter(|v| !v.is_empty()) {
188		info!(target: "Build", "Cargo features: {}", Features);
189	}
190
191	if let Some(Defines) = Argument.CocoonEsbuildDefine.as_deref().filter(|v| !v.is_empty()) {
192		info!(target: "Build", "Cocoon esbuild defines: {}", Defines);
193	}
194
195	let ProjectDir = PathBuf::from(&Argument.Directory);
196
197	if !ProjectDir.is_dir() {
198		return Err(BuildError::Missing(ProjectDir));
199	}
200
201	let CargoPath = ProjectDir.join(CargoFile);
202
203	let ConfigPath = {
204		let Jsonfive = ProjectDir.join(JsonfiveFile);
205
206		if Jsonfive.exists() { Jsonfive } else { ProjectDir.join(JsonFile) }
207	};
208
209	if !ConfigPath.exists() {
210		return Err(BuildError::Config);
211	}
212
213	// Create guards for file backup and restoration
214	let mut CargoGuard = Guard::New(CargoPath.clone(), "Cargo.toml".to_string())?;
215
216	let mut ConfigGuard = Guard::New(ConfigPath.clone(), "Tauri config".to_string())?;
217
218	let mut NamePartsForProductName = Vec::new();
219
220	let mut NamePartsForId = Vec::new();
221
222	// Include Node.js environment in product name
223	if let Some(NodeValue) = &Argument.Environment {
224		if !NodeValue.is_empty() {
225			let PascalEnv = Pascalize(NodeValue);
226
227			if !PascalEnv.is_empty() {
228				NamePartsForProductName.push(format!("{}NodeEnvironment", PascalEnv));
229
230				NamePartsForId.extend(WordsFromPascal(&PascalEnv));
231
232				NamePartsForId.push("node".to_string());
233
234				NamePartsForId.push("environment".to_string());
235			}
236		}
237	}
238
239	// Include dependency information in product name
240	if let Some(DependencyValue) = &Argument.Dependency {
241		if !DependencyValue.is_empty() {
242			let (PascalDepBase, IdDepWords) = if DependencyValue.eq_ignore_ascii_case("true") {
243				("Generic".to_string(), vec!["generic".to_string()])
244			} else if let Some((Org, Repo)) = DependencyValue.split_once('/') {
245				(format!("{}{}", Pascalize(Org), Pascalize(Repo)), {
246					let mut w = WordsFromPascal(&Pascalize(Org));
247
248					w.extend(WordsFromPascal(&Pascalize(Repo)));
249
250					w
251				})
252			} else {
253				(Pascalize(DependencyValue), WordsFromPascal(&Pascalize(DependencyValue)))
254			};
255
256			if !PascalDepBase.is_empty() {
257				NamePartsForProductName.push(format!("{}Dependency", PascalDepBase));
258
259				NamePartsForId.extend(IdDepWords);
260
261				NamePartsForId.push("dependency".to_string());
262			}
263		}
264	}
265
266	// Include Node.js version in product name
267	if let Some(Version) = &Argument.NodeVersion {
268		if !Version.is_empty() {
269			let PascalVersion = format!("{}NodeVersion", Version);
270
271			NamePartsForProductName.push(PascalVersion.clone());
272
273			NamePartsForId.push("node".to_string());
274
275			NamePartsForId.push(Version.to_string());
276		}
277	}
278
279	// Include build flags in product name
280	if Argument.Bundle.as_ref().map_or(false, |v| v == "true") {
281		NamePartsForProductName.push("Bundle".to_string());
282
283		NamePartsForId.push("bundle".to_string());
284	}
285
286	if Argument.Clean.as_ref().map_or(false, |v| v == "true") {
287		NamePartsForProductName.push("Clean".to_string());
288
289		NamePartsForId.push("clean".to_string());
290	}
291
292	if Argument.Browser.as_ref().map_or(false, |v| v == "true") {
293		NamePartsForProductName.push("Browser".to_string());
294
295		NamePartsForId.push("browser".to_string());
296	}
297
298	if Argument.Compile.as_ref().map_or(false, |v| v == "true") {
299		NamePartsForProductName.push("Compile".to_string());
300
301		NamePartsForId.push("compile".to_string());
302	}
303
304	if Argument.Debug.as_ref().map_or(false, |v| v == "true")
305		|| Argument.Command.iter().any(|arg| arg.contains("--debug"))
306	{
307		NamePartsForProductName.push("Debug".to_string());
308
309		NamePartsForId.push("debug".to_string());
310	}
311
312	// Workbench-profile suffixes. These are what keep `debug-mountain` and
313	// `debug-electron` binaries separated on disk. Without them, both
314	// profiles would compile into the same `Target/debug/<LongName>_Mountain`
315	// binary (because the Cargo bin name is "Mountain"), so switching
316	// profiles couldn't run side-by-side and the bundler would thrash the
317	// same artefacts every rebuild.
318	if Argument.Mountain.as_ref().map_or(false, |v| v == "true") {
319		NamePartsForProductName.push("MountainProfile".to_string());
320		NamePartsForId.push("mountain".to_string());
321		NamePartsForId.push("profile".to_string());
322	}
323
324	if Argument.Electron.as_ref().map_or(false, |v| v == "true") {
325		NamePartsForProductName.push("ElectronProfile".to_string());
326		NamePartsForId.push("electron".to_string());
327		NamePartsForId.push("profile".to_string());
328	}
329
330	// Compiler variant (e.g. "Rest") - distinguishes the OXC build path
331	// from the default TypeScript compiler path so two binaries with the
332	// same workbench flavour but different compilers don't collide.
333	if let Some(Variant) = &Argument.Compiler {
334		if !Variant.is_empty() {
335			let PascalCompiler = Pascalize(Variant);
336			if !PascalCompiler.is_empty() {
337				NamePartsForProductName.push(format!("{}Compiler", PascalCompiler));
338				NamePartsForId.extend(WordsFromPascal(&PascalCompiler));
339				NamePartsForId.push("compiler".to_string());
340			}
341		}
342	}
343
344	// Generate final product name
345	let ProductNamePrefix = NamePartsForProductName.join(NameDelimiter);
346
347	let FinalName = if !ProductNamePrefix.is_empty() {
348		format!("{}{}{}", ProductNamePrefix, NameDelimiter, Argument.Name)
349	} else {
350		Argument.Name.clone()
351	};
352
353	info!(target: "Build", "Final generated product name: '{}'", FinalName);
354
355	// Generate final bundle identifier
356	NamePartsForId.extend(WordsFromPascal(&Argument.Name));
357
358	let IdSuffix = NamePartsForId
359		.into_iter()
360		.filter(|s| !s.is_empty())
361		.collect::<Vec<String>>()
362		.join(IdDelimiter);
363
364	let FinalId = format!("{}{}{}", Argument.Prefix, IdDelimiter, IdSuffix);
365
366	info!(target: "Build", "Generated bundle identifier: '{}'", FinalId);
367
368	// Update Cargo.toml if product name changed
369	if FinalName != Argument.Name {
370		TomlEdit(&CargoPath, &Argument.Name, &FinalName)?;
371	}
372
373	// Get version from Cargo.toml
374	let AppVersion = toml::from_str::<Manifest>(&fs::read_to_string(&CargoPath)?)?
375		.get_version()
376		.to_string();
377
378	// Update Tauri configuration and optionally bundle Node.js sidecar
379	JsonEdit(
380		&ConfigPath,
381		&FinalName,
382		&FinalId,
383		&AppVersion,
384		(if let Some(version) = &Argument.NodeVersion {
385			info!(target: "Build", "Selected Node.js version: {}", version);
386
387			let Triple = GetTauriTargetTriple();
388
389			// Path to the pre-downloaded Node executable
390			let Executable = if cfg!(target_os = "windows") {
391				PathBuf::from(format!("./Element/SideCar/{}/NODE/{}/node.exe", Triple, version))
392			} else {
393				PathBuf::from(format!("./Element/SideCar/{}/NODE/{}/bin/node", Triple, version))
394			};
395
396			// Define a consistent, temporary directory for the staged binary
397			let DirectorySideCarTemporary = ProjectDir.join("Binary");
398
399			fs::create_dir_all(&DirectorySideCarTemporary)?;
400
401			// Define the consistent name for the binary that Tauri will bundle
402			let PathExecutableDestination = if cfg!(target_os = "windows") {
403				DirectorySideCarTemporary.join(format!("node-{}.exe", Triple))
404			} else {
405				DirectorySideCarTemporary.join(format!("node-{}", Triple))
406			};
407
408			info!(
409				target: "Build",
410				"Staging sidecar from {} to {}",
411				Executable.display(),
412				PathExecutableDestination.display()
413			);
414
415			// Perform the copy
416			fs::copy(&Executable, &PathExecutableDestination)?;
417
418			// On non-windows, make sure the copied binary is executable
419			#[cfg(not(target_os = "windows"))]
420			{
421				use std::os::unix::fs::PermissionsExt;
422
423				let mut Permission = fs::metadata(&PathExecutableDestination)?.permissions();
424
425				// rwxr-xr-x
426				Permission.set_mode(0o755);
427
428				fs::set_permissions(&PathExecutableDestination, Permission)?;
429			}
430
431			Some("Binary/node".to_string())
432		} else {
433			info!(target: "Build", "No Node.js flavour selected for bundling.");
434
435			None
436		})
437		.as_deref(),
438	)?;
439
440	// Execute the build command
441	if Argument.Command.is_empty() {
442		return Err(BuildError::NoCommand);
443	}
444
445	// Materialise the command into an owned Vec so we can append
446	// `--features <list>` to `pnpm tauri build [--debug]` invocations
447	// without mutating the parsed `Argument`. The guard below keeps the
448	// append scoped to tauri builds - other commands (e.g. cargo, direct
449	// tooling) pass through unchanged.
450	let mut CommandArguments:Vec<String> = Argument.Command.clone();
451
452	let IsTauriBuild = CommandArguments.len() >= 3
453		&& CommandArguments[0] == "pnpm"
454		&& CommandArguments[1] == "tauri"
455		&& CommandArguments[2] == "build";
456
457	if IsTauriBuild {
458		if let Some(Features) = Argument.CargoFeatures.as_deref().filter(|v| !v.is_empty()) {
459			let AlreadyPresent = CommandArguments.iter().any(|a| a == "--features" || a == "-f");
460
461			if !AlreadyPresent {
462				info!(
463					target: "Build",
464					"Forwarding Cargo features to `tauri build`: {}",
465					Features
466				);
467				CommandArguments.push("--features".to_string());
468				CommandArguments.push(Features.to_string());
469			}
470		}
471	}
472
473	let mut ShellCommand = if cfg!(target_os = "windows") {
474		let mut Command = ProcessCommand::new("cmd");
475
476		Command.arg("/C").args(&CommandArguments);
477
478		Command
479	} else {
480		let mut Command = ProcessCommand::new(&CommandArguments[0]);
481
482		Command.args(&CommandArguments[1..]);
483
484		Command
485	};
486
487	// Re-assert `CocoonEsbuildDefine` on the child environment so Cocoon's
488	// esbuild step sees the tier `define` blob even if a wrapper ever calls
489	// `.env_clear()` on our `ProcessCommand`. `ProcessCommand` inherits the
490	// parent env by default, so without a clear this is belt-and-braces.
491	if let Some(Defines) = Argument.CocoonEsbuildDefine.as_deref().filter(|v| !v.is_empty()) {
492		ShellCommand.env(CocoonEsbuildDefineEnv, Defines);
493	}
494
495	info!(target: "Build::Exec", "Executing final build command: {:?}", ShellCommand);
496
497	let Status = ShellCommand
498		.current_dir(env::current_dir()?)
499		.stdout(Stdio::inherit())
500		.stderr(Stdio::inherit())
501		.status()?;
502
503	// Handle build failure
504	if !Status.success() {
505		let temp_sidecar_dir = ProjectDir.join("bin");
506
507		if temp_sidecar_dir.exists() {
508			let _ = fs::remove_dir_all(&temp_sidecar_dir);
509		}
510
511		return Err(BuildError::Shell(Status));
512	}
513
514	// Final cleanup of the temporary sidecar directory after a successful build
515	let DirectorySideCarTemporary = ProjectDir.join("bin");
516
517	if DirectorySideCarTemporary.exists() {
518		fs::remove_dir_all(&DirectorySideCarTemporary)?;
519
520		info!(target: "Build", "Cleaned up temporary sidecar directory.");
521	}
522
523	// Guards drop here, restoring Cargo.toml and tauri.conf.json to their
524	// original state and deleting the .Backup files.  The binary has already
525	// been compiled with the generated product name so restoring the source
526	// files is safe and required for the next build to succeed.
527	drop(CargoGuard);
528	drop(ConfigGuard);
529
530	info!(target: "Build", "Build orchestration completed successfully.");
531
532	Ok(())
533}