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::{
95		CargoFile, CocoonEsbuildDefineEnv, IdDelimiter, JsonFile, JsonfiveFile,
96		NameDelimiter,
97	},
98	Definition::{Argument, Guard, Manifest},
99	GetTauriTargetTriple::GetTauriTargetTriple,
100	JsonEdit::JsonEdit,
101	Pascalize::Pascalize,
102	TomlEdit::TomlEdit,
103	WordsFromPascal::WordsFromPascal,
104};
105
106/// Main orchestration logic for preparing and executing the build.
107///
108/// This function is the core of the build system, coordinating all aspects
109/// of preparing, building, and restoring project configurations. It:
110///
111/// 1. Validates the project directory and configuration files
112/// 2. Creates guards to backup and restore configuration files
113/// 3. Generates a unique product name and bundle identifier based on build
114///    flags
115/// 4. Modifies Cargo.toml and Tauri configuration files
116/// 5. Optionally stages a Node.js sidecar binary
117/// 6. Executes the provided build command
118/// 7. Cleans up temporary files after successful build
119///
120/// # Parameters
121///
122/// * `Argument` - Parsed command-line arguments and environment variables
123///
124/// # Returns
125///
126/// Returns `Ok(())` on successful build completion or a `BuildError` if
127/// any step fails.
128///
129/// # Errors
130///
131/// * `BuildError::Missing` - If the project directory doesn't exist
132/// * `BuildError::Config` - If Tauri configuration file not found
133/// * `BuildError::Exists` - If a backup file already exists
134/// * `BuildError::Io` - For file operation failures
135/// * `BuildError::Edit` - For TOML editing failures
136/// * `BuildError::Json` / `BuildError::Jsonfive` - For JSON/JSON5 parsing
137///   failures
138/// * `BuildError::Parse` - For TOML parsing failures
139/// * `BuildError::Shell` - If the build command fails
140///
141/// # Build Flavor Generation
142///
143/// The product name and bundle identifier are generated by combining:
144///
145/// - **Environment**: Node.js environment (development, production, etc.)
146/// - **Dependency**: Dependency information (org/repo or generic)
147/// - **Node Version**: Node.js version if bundling a sidecar
148/// - **Build Flags**: Bundle, Clean, Browser, Compile, Debug
149///
150/// Example product name:
151/// `Development_GenDependency_22NodeVersion_Debug_Mountain`
152///
153/// Example bundle identifier:
154/// `land.editor.binary.development.generic.node.22.debug.mountain`
155///
156/// # Node.js Sidecar Bundling
157///
158/// If `NodeVersion` is specified:
159/// - The Node.js binary is copied from
160///   `Element/SideCar/{triple}/NODE/{version}/`
161/// - The binary is staged in the project's `Binary/` directory
162/// - The Tauri configuration is updated to include the sidecar
163/// - The binary is given appropriate permissions on Unix-like systems
164/// - The temporary directory is cleaned up after successful build
165///
166/// # File Safety
167///
168/// All configuration file modifications are protected by the Guard pattern:
169/// - Files are backed up before modification
170/// - Files are automatically restored on error or when the guard drops
171/// - This ensures the original state is preserved regardless of build outcome
172///
173/// # Example
174///
175/// ```no_run
176/// use crate::Maintain::Source::Build::{Argument, Process};
177/// let argument = Argument::parse();
178/// Process(&argument)?;
179/// ```
180pub fn Process(Argument:&Argument) -> Result<(), BuildError> {
181	info!(target: "Build", "Starting build orchestration...");
182
183	log::debug!(target: "Build", "Argument: {:?}", Argument);
184
185	// Tier fan-out observability. The shell helper
186	// `Maintain/Script/TierEnvironment.sh` exports `CargoFeatures` and
187	// `CocoonEsbuildDefine`; surface them here so a build transcript shows
188	// which tier set shipped into the binary without having to replay the
189	// shell environment.
190	if let Some(Features) = Argument.CargoFeatures.as_deref().filter(|v| !v.is_empty()) {
191		info!(target: "Build", "Cargo features: {}", Features);
192	}
193
194	if let Some(Defines) = Argument.CocoonEsbuildDefine.as_deref().filter(|v| !v.is_empty()) {
195		info!(target: "Build", "Cocoon esbuild defines: {}", Defines);
196	}
197
198	let ProjectDir = PathBuf::from(&Argument.Directory);
199
200	if !ProjectDir.is_dir() {
201		return Err(BuildError::Missing(ProjectDir));
202	}
203
204	let CargoPath = ProjectDir.join(CargoFile);
205
206	let ConfigPath = {
207		let Jsonfive = ProjectDir.join(JsonfiveFile);
208
209		if Jsonfive.exists() { Jsonfive } else { ProjectDir.join(JsonFile) }
210	};
211
212	if !ConfigPath.exists() {
213		return Err(BuildError::Config);
214	}
215
216	// Create guards for file backup and restoration
217	let mut CargoGuard = Guard::New(CargoPath.clone(), "Cargo.toml".to_string())?;
218
219	let mut ConfigGuard = Guard::New(ConfigPath.clone(), "Tauri config".to_string())?;
220
221	let mut NamePartsForProductName = Vec::new();
222
223	let mut NamePartsForId = Vec::new();
224
225	// Include Node.js environment in product name
226	if let Some(NodeValue) = &Argument.Environment {
227		if !NodeValue.is_empty() {
228			let PascalEnv = Pascalize(NodeValue);
229
230			if !PascalEnv.is_empty() {
231				NamePartsForProductName.push(format!("{}NodeEnvironment", PascalEnv));
232
233				NamePartsForId.extend(WordsFromPascal(&PascalEnv));
234
235				NamePartsForId.push("node".to_string());
236
237				NamePartsForId.push("environment".to_string());
238			}
239		}
240	}
241
242	// Include dependency information in product name
243	if let Some(DependencyValue) = &Argument.Dependency {
244		if !DependencyValue.is_empty() {
245			let (PascalDepBase, IdDepWords) = if DependencyValue.eq_ignore_ascii_case("true") {
246				("Generic".to_string(), vec!["generic".to_string()])
247			} else if let Some((Org, Repo)) = DependencyValue.split_once('/') {
248				(format!("{}{}", Pascalize(Org), Pascalize(Repo)), {
249					let mut w = WordsFromPascal(&Pascalize(Org));
250
251					w.extend(WordsFromPascal(&Pascalize(Repo)));
252
253					w
254				})
255			} else {
256				(Pascalize(DependencyValue), WordsFromPascal(&Pascalize(DependencyValue)))
257			};
258
259			if !PascalDepBase.is_empty() {
260				NamePartsForProductName.push(format!("{}Dependency", PascalDepBase));
261
262				NamePartsForId.extend(IdDepWords);
263
264				NamePartsForId.push("dependency".to_string());
265			}
266		}
267	}
268
269	// Include Node.js version in product name
270	if let Some(Version) = &Argument.NodeVersion {
271		if !Version.is_empty() {
272			let PascalVersion = format!("{}NodeVersion", Version);
273
274			NamePartsForProductName.push(PascalVersion.clone());
275
276			NamePartsForId.push("node".to_string());
277
278			NamePartsForId.push(Version.to_string());
279		}
280	}
281
282	// Include build flags in product name
283	if Argument.Bundle.as_ref().map_or(false, |v| v == "true") {
284		NamePartsForProductName.push("Bundle".to_string());
285
286		NamePartsForId.push("bundle".to_string());
287	}
288
289	if Argument.Clean.as_ref().map_or(false, |v| v == "true") {
290		NamePartsForProductName.push("Clean".to_string());
291
292		NamePartsForId.push("clean".to_string());
293	}
294
295	if Argument.Browser.as_ref().map_or(false, |v| v == "true") {
296		NamePartsForProductName.push("Browser".to_string());
297
298		NamePartsForId.push("browser".to_string());
299	}
300
301	if Argument.Compile.as_ref().map_or(false, |v| v == "true") {
302		NamePartsForProductName.push("Compile".to_string());
303
304		NamePartsForId.push("compile".to_string());
305	}
306
307	if Argument.Debug.as_ref().map_or(false, |v| v == "true")
308		|| Argument.Command.iter().any(|arg| arg.contains("--debug"))
309	{
310		NamePartsForProductName.push("Debug".to_string());
311
312		NamePartsForId.push("debug".to_string());
313	}
314
315	// Workbench-profile suffixes. These are what keep `debug-mountain` and
316	// `debug-electron` binaries separated on disk. Without them, both
317	// profiles would compile into the same `Target/debug/<LongName>_Mountain`
318	// binary (because the Cargo bin name is "Mountain"), so switching
319	// profiles couldn't run side-by-side and the bundler would thrash the
320	// same artefacts every rebuild.
321	if Argument.Mountain.as_ref().map_or(false, |v| v == "true") {
322		NamePartsForProductName.push("MountainProfile".to_string());
323		NamePartsForId.push("mountain".to_string());
324		NamePartsForId.push("profile".to_string());
325	}
326
327	if Argument.Electron.as_ref().map_or(false, |v| v == "true") {
328		NamePartsForProductName.push("ElectronProfile".to_string());
329		NamePartsForId.push("electron".to_string());
330		NamePartsForId.push("profile".to_string());
331	}
332
333	// Compiler variant (e.g. "Rest") - distinguishes the OXC build path
334	// from the default TypeScript compiler path so two binaries with the
335	// same workbench flavour but different compilers don't collide.
336	if let Some(Variant) = &Argument.Compiler {
337		if !Variant.is_empty() {
338			let PascalCompiler = Pascalize(Variant);
339			if !PascalCompiler.is_empty() {
340				NamePartsForProductName.push(format!("{}Compiler", PascalCompiler));
341				NamePartsForId.extend(WordsFromPascal(&PascalCompiler));
342				NamePartsForId.push("compiler".to_string());
343			}
344		}
345	}
346
347	// Generate final product name
348	let ProductNamePrefix = NamePartsForProductName.join(NameDelimiter);
349
350	let FinalName = if !ProductNamePrefix.is_empty() {
351		format!("{}{}{}", ProductNamePrefix, NameDelimiter, Argument.Name)
352	} else {
353		Argument.Name.clone()
354	};
355
356	info!(target: "Build", "Final generated product name: '{}'", FinalName);
357
358	// Generate final bundle identifier
359	NamePartsForId.extend(WordsFromPascal(&Argument.Name));
360
361	let IdSuffix = NamePartsForId
362		.into_iter()
363		.filter(|s| !s.is_empty())
364		.collect::<Vec<String>>()
365		.join(IdDelimiter);
366
367	let FinalId = format!("{}{}{}", Argument.Prefix, IdDelimiter, IdSuffix);
368
369	info!(target: "Build", "Generated bundle identifier: '{}'", FinalId);
370
371	// Update Cargo.toml if product name changed
372	if FinalName != Argument.Name {
373		TomlEdit(&CargoPath, &Argument.Name, &FinalName)?;
374	}
375
376	// Get version from Cargo.toml
377	let AppVersion = toml::from_str::<Manifest>(&fs::read_to_string(&CargoPath)?)?
378		.get_version()
379		.to_string();
380
381	// Update Tauri configuration and optionally bundle Node.js sidecar
382	JsonEdit(
383		&ConfigPath,
384		&FinalName,
385		&FinalId,
386		&AppVersion,
387		(if let Some(version) = &Argument.NodeVersion {
388			info!(target: "Build", "Selected Node.js version: {}", version);
389
390			let Triple = GetTauriTargetTriple();
391
392			// Path to the pre-downloaded Node executable
393			let Executable = if cfg!(target_os = "windows") {
394				PathBuf::from(format!("./Element/SideCar/{}/NODE/{}/node.exe", Triple, version))
395			} else {
396				PathBuf::from(format!("./Element/SideCar/{}/NODE/{}/bin/node", Triple, version))
397			};
398
399			// Define a consistent, temporary directory for the staged binary
400			let DirectorySideCarTemporary = ProjectDir.join("Binary");
401
402			fs::create_dir_all(&DirectorySideCarTemporary)?;
403
404			// Define the consistent name for the binary that Tauri will bundle
405			let PathExecutableDestination = if cfg!(target_os = "windows") {
406				DirectorySideCarTemporary.join(format!("node-{}.exe", Triple))
407			} else {
408				DirectorySideCarTemporary.join(format!("node-{}", Triple))
409			};
410
411			info!(
412				target: "Build",
413				"Staging sidecar from {} to {}",
414				Executable.display(),
415				PathExecutableDestination.display()
416			);
417
418			// Perform the copy
419			fs::copy(&Executable, &PathExecutableDestination)?;
420
421			// On non-windows, make sure the copied binary is executable
422			#[cfg(not(target_os = "windows"))]
423			{
424				use std::os::unix::fs::PermissionsExt;
425
426				let mut Permission = fs::metadata(&PathExecutableDestination)?.permissions();
427
428				// rwxr-xr-x
429				Permission.set_mode(0o755);
430
431				fs::set_permissions(&PathExecutableDestination, Permission)?;
432			}
433
434			Some("Binary/node".to_string())
435		} else {
436			info!(target: "Build", "No Node.js flavour selected for bundling.");
437
438			None
439		})
440		.as_deref(),
441	)?;
442
443	// Execute the build command
444	if Argument.Command.is_empty() {
445		return Err(BuildError::NoCommand);
446	}
447
448	// Materialise the command into an owned Vec so we can append
449	// `--features <list>` to `pnpm tauri build [--debug]` invocations
450	// without mutating the parsed `Argument`. The guard below keeps the
451	// append scoped to tauri builds - other commands (e.g. cargo, direct
452	// tooling) pass through unchanged.
453	let mut CommandArguments:Vec<String> = Argument.Command.clone();
454
455	let IsTauriBuild = CommandArguments.len() >= 3
456		&& CommandArguments[0] == "pnpm"
457		&& CommandArguments[1] == "tauri"
458		&& CommandArguments[2] == "build";
459
460	if IsTauriBuild {
461		if let Some(Features) =
462			Argument.CargoFeatures.as_deref().filter(|v| !v.is_empty())
463		{
464			let AlreadyPresent = CommandArguments
465				.iter()
466				.any(|a| a == "--features" || a == "-f");
467
468			if !AlreadyPresent {
469				info!(
470					target: "Build",
471					"Forwarding Cargo features to `tauri build`: {}",
472					Features
473				);
474				CommandArguments.push("--features".to_string());
475				CommandArguments.push(Features.to_string());
476			}
477		}
478	}
479
480	let mut ShellCommand = if cfg!(target_os = "windows") {
481		let mut Command = ProcessCommand::new("cmd");
482
483		Command.arg("/C").args(&CommandArguments);
484
485		Command
486	} else {
487		let mut Command = ProcessCommand::new(&CommandArguments[0]);
488
489		Command.args(&CommandArguments[1..]);
490
491		Command
492	};
493
494	// Re-assert `CocoonEsbuildDefine` on the child environment so Cocoon's
495	// esbuild step sees the tier `define` blob even if a wrapper ever calls
496	// `.env_clear()` on our `ProcessCommand`. `ProcessCommand` inherits the
497	// parent env by default, so without a clear this is belt-and-braces.
498	if let Some(Defines) =
499		Argument.CocoonEsbuildDefine.as_deref().filter(|v| !v.is_empty())
500	{
501		ShellCommand.env(CocoonEsbuildDefineEnv, Defines);
502	}
503
504	info!(target: "Build::Exec", "Executing final build command: {:?}", ShellCommand);
505
506	let Status = ShellCommand
507		.current_dir(env::current_dir()?)
508		.stdout(Stdio::inherit())
509		.stderr(Stdio::inherit())
510		.status()?;
511
512	// Handle build failure
513	if !Status.success() {
514		let temp_sidecar_dir = ProjectDir.join("bin");
515
516		if temp_sidecar_dir.exists() {
517			let _ = fs::remove_dir_all(&temp_sidecar_dir);
518		}
519
520		return Err(BuildError::Shell(Status));
521	}
522
523	// Final cleanup of the temporary sidecar directory after a successful build
524	let DirectorySideCarTemporary = ProjectDir.join("bin");
525
526	if DirectorySideCarTemporary.exists() {
527		fs::remove_dir_all(&DirectorySideCarTemporary)?;
528
529		info!(target: "Build", "Cleaned up temporary sidecar directory.");
530	}
531
532	// Guards drop here, restoring Cargo.toml and tauri.conf.json to their
533	// original state and deleting the .Backup files.  The binary has already
534	// been compiled with the generated product name so restoring the source
535	// files is safe and required for the next build to succeed.
536	drop(CargoGuard);
537	drop(ConfigGuard);
538
539	info!(target: "Build", "Build orchestration completed successfully.");
540
541	Ok(())
542}