Maintain/
Build.rs

1//! # Dynamic Build Orchestrator
2//!
3//! This binary serves as a powerful, configurable pre-build step for a Tauri
4//! application. It is designed to be called from a shell script and dynamically
5//! modifies project configuration files (`Cargo.toml`, `tauri.conf.json`) based
6//! on environment variables and command-line arguments.
7//!
8//! Its primary responsibilities include:
9//! - Generating a unique `productName` and `identifier` for different build
10//!   "flavours" (e.g., for different dependencies, environments, or feature
11//!   sets).
12//! - Dynamically selecting a sidecar binary (like a specific Node.js version),
13//!
14//!   staging it in a temporary location, and configuring Tauri to bundle it.
15//! - Temporarily modifying configuration files, executing the final build
16//!   command (e.g., `pnpm tauri build`), and then restoring the original files
17//!   using a `Guard` pattern.
18
19#![allow(non_snake_case, non_upper_case_globals)]
20
21// --- Constants: File Paths and Delimiters ---
22
23/// Default project directory relative to the workspace root.
24pub const DirectoryDefault:&str = "Element/Mountain";
25
26/// Default project base name, used as a suffix for generated names.
27pub const NameDefault:&str = "Mountain";
28
29/// Default bundle identifier prefix.
30pub const PrefixDefault:&str = "land.editor.binary";
31
32/// Cargo configuration filename.
33pub const CargoFile:&str = "Cargo.toml";
34
35/// Tauri JSON5 configuration filename.
36pub const JsonfiveFile:&str = "tauri.conf.json5";
37
38/// Tauri JSON configuration filename.
39pub const JsonFile:&str = "tauri.conf.json";
40
41/// Suffix used for backup files created by the `Guard`.
42pub const BackupSuffix:&str = ".Backup";
43
44/// Delimiter for parts of the generated `productName`.
45pub const NameDelimiter:&str = "_";
46
47/// Delimiter for parts of the generated bundle `identifier`.
48pub const IdDelimiter:&str = ".";
49
50// --- Constants: Environment Variable Names ---
51
52/// Environment variable for the project directory.
53pub const DirEnv:&str = "MOUNTAIN_DIR";
54
55/// Environment variable for the original base name of the project.
56pub const NameEnv:&str = "MOUNTAIN_ORIGINAL_BASE_NAME";
57
58/// Environment variable for the bundle identifier prefix.
59pub const PrefixEnv:&str = "MOUNTAIN_BUNDLE_ID_PREFIX";
60
61/// Environment variable for the "Bundle" build flag.
62pub const BundleEnv:&str = "Bundle";
63
64/// Environment variable for the "Browser" build flag.
65pub const BrowserEnv:&str = "Browser";
66
67/// Environment variable for the "Compile" build flag.
68pub const CompileEnv:&str = "Compile";
69
70/// Environment variable for the "Clean" build flag.
71pub const CleanEnv:&str = "Clean";
72
73/// Environment variable for specifying a dependency flavour.
74pub const DependencyEnv:&str = "Dependency";
75
76/// Environment variable for the Node.js environment (`development` or
77/// `production`).
78pub const NodeEnv:&str = "NODE_ENV";
79
80/// Environment variable for selecting the Node.js sidecar version.
81pub const NodeVersionEnv:&str = "NODE_VERSION";
82
83/// Environment variable for setting the log level.
84pub const LogEnv:&str = "RUST_LOG";
85
86/// Represents all possible errors that can occur during the build script's
87/// execution.
88#[derive(Error, Debug)]
89pub enum Error {
90	#[error("IO: {0}")]
91	Io(#[from] io::Error),
92
93	#[error("Toml Editing: {0}")]
94	Edit(#[from] toml_edit::TomlError),
95
96	#[error("Toml Parsing: {0}")]
97	Parse(#[from] toml::de::Error),
98
99	#[error("Json: {0}")]
100	Json(#[from] serde_json::Error),
101
102	#[error("Json5: {0}")]
103	Jsonfive(#[from] json5::Error),
104
105	#[error("Missing Directory: {0}")]
106	Missing(PathBuf),
107
108	#[error("Command Failed: {0}")]
109	Shell(std::process::ExitStatus),
110
111	#[error("No Command Provided")]
112	NoCommand,
113
114	#[error("Tauri Configuration File Not Found")]
115	Config,
116
117	#[error("Backup File Exists: {0}")]
118	Exists(PathBuf),
119
120	#[error("UTF-8 Conversion: {0}")]
121	Utf(#[from] std::string::FromUtf8Error),
122
123	#[error("Environment Variable Missing: {0}")]
124	Environment(String),
125}
126
127/// Represents parsed command-line arguments and environment variables that
128/// control the build.
129#[derive(Parser, Debug, Clone)]
130#[clap(
131	author,
132	version,
133	about = "Prepares, builds, and restores project configurations."
134)]
135pub struct Argument {
136	/// The main directory of the project.
137	#[clap(long, env = DirEnv, default_value = DirectoryDefault)]
138	Directory:String,
139
140	/// The original base name of the project/package.
141	#[clap(long, env = NameEnv, default_value = NameDefault)]
142	Name:String,
143
144	/// The prefix for the application's bundle identifier.
145	#[clap(long, env = PrefixEnv, default_value = PrefixDefault)]
146	Prefix:String,
147
148	/// Flag or value indicating browser-specific build aspects.
149	#[clap(long, env = BrowserEnv)]
150	Browser:Option<String>,
151
152	/// Flag or value indicating bundling-specific aspects.
153	#[clap(long, env = BundleEnv)]
154	Bundle:Option<String>,
155
156	/// Flag or value indicating bundling-specific aspects.
157	#[clap(long, env = CompileEnv)]
158	Compile:Option<String>,
159
160	/// Flag or value indicating cleaning-specific aspects.
161	#[clap(long, env = CleanEnv)]
162	Clean:Option<String>,
163
164	/// Information about a dependency, often 'org/repo' or a boolean string.
165	#[clap(long, env = DependencyEnv)]
166	Dependency:Option<String>,
167
168	/// The Node.js environment (e.g., "development", "production").
169	#[clap(long, env = NodeEnv)]
170	Environment:Option<String>,
171
172	/// Specifies the Node.js sidecar version to bundle (e.g., "22").
173	#[clap(long, env = NodeVersionEnv)]
174	NodeVersion:Option<String>,
175
176	/// The build command and its arguments to execute.
177	#[clap(required = true, last = true)]
178	Command:Vec<String>,
179}
180
181/// Represents the `package` section of a `Cargo.toml` manifest.
182#[derive(Deserialize, Debug)]
183pub struct Manifest {
184	package:Meta,
185}
186
187/// Represents metadata within the `package` section of `Cargo.toml`.
188#[derive(Deserialize, Debug)]
189pub struct Meta {
190	version:String,
191}
192
193/// Manages the backup and restoration of a single file using the RAII pattern.
194/// Ensures that an original file is restored to its initial state when this
195/// struct goes out of scope.
196pub struct Guard {
197	Path:PathBuf,
198
199	Store:PathBuf,
200
201	Active:bool,
202
203	#[allow(dead_code)]
204	Note:String,
205}
206
207impl Guard {
208	pub fn New(OriginalPath:PathBuf, Description:String) -> Result<Self, Error> {
209		let BackupPath = OriginalPath.with_extension(format!(
210			"{}{}",
211			OriginalPath.extension().unwrap_or_default().to_str().unwrap_or(""),
212			BackupSuffix
213		));
214
215		if BackupPath.exists() {
216			error!("Backup file {} already exists.", BackupPath.display());
217
218			return Err(Error::Exists(BackupPath));
219		}
220
221		let mut BackupMade = false;
222
223		if OriginalPath.exists() {
224			fs::copy(&OriginalPath, &BackupPath)?;
225
226			info!(target: "Build::Guard", "Backed {} to {}", OriginalPath.display(), BackupPath.display());
227
228			BackupMade = true;
229		}
230
231		Ok(Self { Path:OriginalPath, Store:BackupPath, Active:BackupMade, Note:Description })
232	}
233
234	pub fn Path(&self) -> &Path { &self.Path }
235
236	pub fn Store(&self) -> &Path { &self.Store }
237}
238
239impl Drop for Guard {
240	fn drop(&mut self) {
241		if self.Active && self.Store.exists() {
242			info!(target: "Build::Guard", "Restoring {} from {}...", self.Path.display(), self.Store.display());
243
244			if let Ok(_) = fs::copy(&self.Store, &self.Path) {
245				info!(target: "Build::Guard", "Restore successful.");
246
247				if let Err(e) = fs::remove_file(&self.Store) {
248					error!(target: "Build::Guard", "Failed to delete backup {}: {}", self.Store.display(), e);
249				}
250			} else if let Err(e) = fs::copy(&self.Store, &self.Path) {
251				error!(target: "Build::Guard", "Restore FAILED: {}. {} is now inconsistent.", e, self.Path.display());
252			}
253		}
254	}
255}
256
257/// Dynamically modifies specific name fields within a `Cargo.toml` file. This
258/// includes `package.name`, `package.default-run`, and `bin.name`.
259pub fn TomlEdit(File:&Path, Old:&str, Current:&str) -> Result<bool, Error> {
260	debug!(target: "Build::Toml", "Attempting to modify TOML file: {}", File.display());
261
262	let Data = fs::read_to_string(File)?;
263
264	if Old == Current {
265		info!(target: "Build::Toml", "Old name '{}' is the same as current name '{}'. No changes needed for {}.", Old, Current, File.display());
266
267		return Ok(false);
268	}
269
270	debug!(target: "Build::Toml", "Old name: '{}', Current name: '{}'", Old, Current);
271
272	let mut Parsed:TomlDocument = Data.parse()?;
273
274	let mut PackageChange = false;
275
276	let mut LibraryChange = false;
277
278	let mut BinaryChange = false;
279
280	let mut DefaultChange = false;
281
282	if let Some(PackageTable) = Parsed.get_mut("package").and_then(|Item| Item.as_table_mut()) {
283		if let Some(NameItem) = PackageTable.get_mut("name") {
284			if NameItem.as_str() == Some(Old) {
285				*NameItem = TomlItem::Value(TomlValue::String(toml_edit::Formatted::new(Current.to_string())));
286
287				PackageChange = true;
288
289				debug!(target: "Build::Toml", "Changed package.name");
290			}
291		}
292
293		if let Some(RunItem) = PackageTable.get_mut("default-run") {
294			if RunItem.as_str() == Some(Old) {
295				*RunItem = TomlItem::Value(TomlValue::String(toml_edit::Formatted::new(Current.to_string())));
296
297				DefaultChange = true;
298
299				debug!(target: "Build::Toml", "Changed package.default-run");
300			}
301		}
302	}
303
304	if let Some(LibTable) = Parsed.get_mut("lib").and_then(|Item| Item.as_table_mut()) {
305		if let Some(NameItem) = LibTable.get_mut("name") {
306			if NameItem.as_str() == Some(Old) {
307				*NameItem = TomlItem::Value(TomlValue::String(toml_edit::Formatted::new(Current.to_string())));
308
309				LibraryChange = true;
310
311				debug!(target: "Build::Toml", "Changed lib.name");
312			}
313		}
314	}
315
316	if let Some(BinArray) = Parsed.get_mut("bin").and_then(|Item| Item.as_array_of_tables_mut()) {
317		for Table in BinArray.iter_mut() {
318			if let Some(NameItem) = Table.get_mut("name") {
319				if NameItem.as_str() == Some(Old) {
320					*NameItem = TomlItem::Value(TomlValue::String(toml_edit::Formatted::new(Current.to_string())));
321
322					BinaryChange = true;
323
324					debug!(target: "Build::Toml", "Changed a bin.name entry to '{}'", Current);
325
326					break;
327				}
328			}
329		}
330	}
331
332	if PackageChange || LibraryChange || BinaryChange || DefaultChange {
333		let Output = Parsed.to_string();
334
335		fs::write(File, Output)?;
336
337		let mut ModifiedItems = Vec::new();
338
339		if PackageChange {
340			ModifiedItems.push("package.name");
341		}
342
343		if DefaultChange {
344			ModifiedItems.push("package.default-run");
345		}
346
347		if LibraryChange {
348			ModifiedItems.push("lib.name");
349		}
350
351		if BinaryChange {
352			ModifiedItems.push("bin.name");
353		}
354
355		info!(target: "Build::Toml", "Temporarily changed {} in {} to: {}", ModifiedItems.join(", "), File.display(), Current);
356
357		Ok(true)
358	} else {
359		warn!(target: "Build::Toml", "Name '{}' not found in relevant sections of {}. No changes made to file.", Old, File.display());
360
361		Ok(false)
362	}
363}
364
365/// Dynamically modifies fields in a `tauri.conf.json` or `tauri.conf.json5`
366/// file, including the sidecar path.
367pub fn JsonEdit(File:&Path, Product:&str, Id:&str, Version:&str, SidecarPath:Option<&str>) -> Result<bool, Error> {
368	debug!(target: "Build::Json", "Attempting to modify JSON file: {}", File.display());
369
370	let Data = fs::read_to_string(File)?;
371
372	let mut Parsed:JsonValue = if File.extension().and_then(|s| s.to_str()) == Some("json5") {
373		json5::from_str(&Data)?
374	} else {
375		serde_json::from_str(&Data)?
376	};
377
378	let mut Modified = false;
379
380	let Root = Parsed
381		.as_object_mut()
382		.ok_or_else(|| Error::Io(io::Error::new(io::ErrorKind::InvalidData, "JSON root is not an object")))?;
383
384	if Root.get("version").and_then(JsonValue::as_str) != Some(Version) {
385		Root.insert("version".to_string(), JsonValue::String(Version.to_string()));
386
387		Modified = true;
388	}
389
390	if Root.get("productName").and_then(JsonValue::as_str) != Some(Product) {
391		Root.insert("productName".to_string(), JsonValue::String(Product.to_string()));
392
393		Modified = true;
394	}
395
396	if Root.get("identifier").and_then(JsonValue::as_str) != Some(Id) {
397		Root.insert("identifier".to_string(), JsonValue::String(Id.to_string()));
398
399		Modified = true;
400	}
401
402	if let Some(Path) = SidecarPath {
403		let Bundle = Root
404			.entry("bundle")
405			.or_insert_with(|| JsonValue::Object(Default::default()))
406			.as_object_mut()
407			.unwrap();
408
409		let Bins = Bundle
410			.entry("externalBin")
411			.or_insert_with(|| JsonValue::Array(Default::default()))
412			.as_array_mut()
413			.unwrap();
414
415		Bins.push(JsonValue::String(Path.to_string()));
416
417		Modified = true;
418	}
419
420	if Modified {
421		let mut Buffer = Vec::new();
422
423		let Formatter = serde_json::ser::PrettyFormatter::with_indent(b"\t");
424
425		let mut Serializer = serde_json::Serializer::with_formatter(&mut Buffer, Formatter);
426
427		Parsed.serialize(&mut Serializer)?;
428
429		fs::write(File, String::from_utf8(Buffer)?)?;
430
431		info!(target: "Build::Json", "Dynamically configured {}", File.display());
432	}
433
434	Ok(Modified)
435}
436
437/// Converts a kebab-case or snake_case string to `PascalCase`.
438pub fn Pascalize(Text:&str) -> String {
439	Text.split(|c:char| c == '-' || c == '_')
440		.filter(|s| !s.is_empty())
441		.map(|s| {
442			let mut c = s.chars();
443
444			c.next()
445				.map_or(String::new(), |f| f.to_uppercase().collect::<String>() + c.as_str())
446		})
447		.collect()
448}
449
450/// Converts a `PascalCase` string into a vector of its lowercase constituent
451/// words.
452fn WordsFromPascal(Text:&str) -> Vec<String> {
453	if Text.is_empty() {
454		return Vec::new();
455	}
456
457	let mut Words = Vec::new();
458
459	let mut CurrentWord = String::new();
460
461	let mut LastCharWasUppercase = false;
462
463	for Char in Text.chars() {
464		if Char.is_uppercase() {
465			if !CurrentWord.is_empty() && !LastCharWasUppercase {
466				Words.push(CurrentWord.to_ascii_lowercase());
467
468				CurrentWord.clear();
469			}
470
471			CurrentWord.push(Char);
472
473			LastCharWasUppercase = true;
474		} else {
475			CurrentWord.push(Char);
476
477			LastCharWasUppercase = false;
478		}
479	}
480
481	if !CurrentWord.is_empty() {
482		Words.push(CurrentWord.to_ascii_lowercase());
483	}
484
485	Words
486}
487
488/// Gets the Tauri-compatible target triple for the current build environment.
489fn GetTauriTargetTriple() -> String {
490	let Os = env::consts::OS;
491
492	let Arch = env::consts::ARCH;
493
494	match (Os, Arch) {
495		("windows", "x86_64") => "x86_64-pc-windows-msvc".to_string(),
496
497		("linux", "x86_64") => "x86_64-unknown-linux-gnu".to_string(),
498
499		("linux", "aarch64") => "aarch64-unknown-linux-gnu".to_string(),
500
501		("macos", "x86_64") => "x86_64-apple-darwin".to_string(),
502
503		("macos", "aarch64") => "aarch64-apple-darwin".to_string(),
504
505		_ => panic!("Unsupported OS-Arch for sidecar: {}-{}", Os, Arch),
506	}
507}
508
509/// Main orchestration logic for preparing and executing the build.
510pub fn Process(Argument:&Argument) -> Result<(), Error> {
511	info!(target: "Build", "Starting build orchestration...");
512
513	debug!(target: "Build", "Argument: {:?}", Argument);
514
515	let ProjectDir = PathBuf::from(&Argument.Directory);
516
517	if !ProjectDir.is_dir() {
518		return Err(Error::Missing(ProjectDir));
519	}
520
521	let CargoPath = ProjectDir.join(CargoFile);
522
523	let ConfigPath = {
524		let Jsonfive = ProjectDir.join(JsonfiveFile);
525
526		if Jsonfive.exists() { Jsonfive } else { ProjectDir.join(JsonFile) }
527	};
528
529	if !ConfigPath.exists() {
530		return Err(Error::Config);
531	}
532
533	let _CargoGuard = Guard::New(CargoPath.clone(), "Cargo.toml".to_string())?;
534
535	let _ConfigGuard = Guard::New(ConfigPath.clone(), "Tauri config".to_string())?;
536
537	let mut NamePartsForProductName = Vec::new();
538
539	let mut NamePartsForId = Vec::new();
540
541	if let Some(NodeValue) = &Argument.Environment {
542		if !NodeValue.is_empty() {
543			let PascalEnv = Pascalize(NodeValue);
544
545			if !PascalEnv.is_empty() {
546				NamePartsForProductName.push(format!("{}NodeEnvironment", PascalEnv));
547
548				NamePartsForId.extend(WordsFromPascal(&PascalEnv));
549
550				NamePartsForId.push("node".to_string());
551
552				NamePartsForId.push("environment".to_string());
553			}
554		}
555	}
556
557	if let Some(DependencyValue) = &Argument.Dependency {
558		if !DependencyValue.is_empty() {
559			let (PascalDepBase, IdDepWords) = if DependencyValue.eq_ignore_ascii_case("true") {
560				("Generic".to_string(), vec!["generic".to_string()])
561			} else if let Some((Org, Repo)) = DependencyValue.split_once('/') {
562				(format!("{}{}", Pascalize(Org), Pascalize(Repo)), {
563					let mut w = WordsFromPascal(&Pascalize(Org));
564
565					w.extend(WordsFromPascal(&Pascalize(Repo)));
566
567					w
568				})
569			} else {
570				(Pascalize(DependencyValue), WordsFromPascal(&Pascalize(DependencyValue)))
571			};
572
573			if !PascalDepBase.is_empty() {
574				NamePartsForProductName.push(format!("{}Dependency", PascalDepBase));
575
576				NamePartsForId.extend(IdDepWords);
577
578				NamePartsForId.push("dependency".to_string());
579			}
580		}
581	}
582
583	if let Some(Version) = &Argument.NodeVersion {
584		if !Version.is_empty() {
585			let PascalVersion = format!("{}NodeVersion", Version);
586
587			NamePartsForProductName.push(PascalVersion.clone());
588
589			NamePartsForId.push("node".to_string());
590
591			NamePartsForId.push(Version.to_string());
592		}
593	}
594
595	if Argument.Bundle.as_ref().map_or(false, |v| v == "true") {
596		NamePartsForProductName.push("Bundle".to_string());
597
598		NamePartsForId.push("bundle".to_string());
599	}
600
601	if Argument.Clean.as_ref().map_or(false, |v| v == "true") {
602		NamePartsForProductName.push("Clean".to_string());
603
604		NamePartsForId.push("clean".to_string());
605	}
606
607	if Argument.Browser.as_ref().map_or(false, |v| v == "true") {
608		NamePartsForProductName.push("Browser".to_string());
609
610		NamePartsForId.push("browser".to_string());
611	}
612
613	if Argument.Compile.as_ref().map_or(false, |v| v == "true") {
614		NamePartsForProductName.push("Compile".to_string());
615
616		NamePartsForId.push("compile".to_string());
617	}
618
619	let ProductNamePrefix = NamePartsForProductName.join(NameDelimiter);
620
621	let FinalName = if !ProductNamePrefix.is_empty() {
622		format!("{}{}{}", ProductNamePrefix, NameDelimiter, Argument.Name)
623	} else {
624		Argument.Name.clone()
625	};
626
627	info!(target: "Build", "Final generated product name: '{}'", FinalName);
628
629	NamePartsForId.extend(WordsFromPascal(&Argument.Name));
630
631	let IdSuffix = NamePartsForId
632		.into_iter()
633		.filter(|s| !s.is_empty())
634		.collect::<Vec<String>>()
635		.join(IdDelimiter);
636
637	let FinalId = format!("{}{}{}", Argument.Prefix, IdDelimiter, IdSuffix);
638
639	info!(target: "Build", "Generated bundle identifier: '{}'", FinalId);
640
641	if FinalName != Argument.Name {
642		TomlEdit(&CargoPath, &Argument.Name, &FinalName)?;
643	}
644
645	let AppVersion = toml::from_str::<Manifest>(&fs::read_to_string(&CargoPath)?)?.package.version;
646
647	// --- Sidecar Selection and Staging Logic ---
648	let sidecar_bundle_path_for_tauri = if let Some(version) = &Argument.NodeVersion {
649		info!(target: "Build", "Selected Node.js version: {}", version);
650
651		let target_triple = GetTauriTargetTriple();
652
653		// Path to the pre-downloaded Node executable
654		let source_executable_path = if cfg!(target_os = "windows") {
655			PathBuf::from(format!("./Element/SideCar/{}/NODE/{}/node.exe", target_triple, version))
656		} else {
657			PathBuf::from(format!("./Element/SideCar/{}/NODE/{}/bin/node", target_triple, version))
658		};
659
660		// Define a consistent, temporary directory inside `src-tauri` for the staged
661		// binary
662		let temp_sidecar_dir = ProjectDir.join("Binary");
663
664		fs::create_dir_all(&temp_sidecar_dir)?;
665
666		// Define the consistent name for the binary that Tauri will bundle
667		let dest_executable_path = if cfg!(target_os = "windows") {
668			temp_sidecar_dir.join(format!("node-{}.exe", target_triple))
669		} else {
670			temp_sidecar_dir.join(format!("node-{}", target_triple))
671		};
672
673		info!(
674			target: "Build",
675
676			"Staging sidecar from {} to {}",
677
678			source_executable_path.display(),
679
680			dest_executable_path.display()
681		);
682
683		// Perform the copy
684		fs::copy(&source_executable_path, &dest_executable_path)?;
685
686		// On non-windows, make sure the copied binary is executable
687		#[cfg(not(target_os = "windows"))]
688		{
689			use std::os::unix::fs::PermissionsExt;
690
691			let mut perms = fs::metadata(&dest_executable_path)?.permissions();
692
693			perms.set_mode(0o755); // rwxr-xr-x
694			fs::set_permissions(&dest_executable_path, perms)?;
695		}
696
697		Some("Binary/node".to_string())
698	} else {
699		info!(target: "Build", "No Node.js flavour selected for bundling.");
700
701		None
702	};
703
704	// --- End Sidecar Logic ---
705
706	JsonEdit(
707		&ConfigPath,
708		&FinalName,
709		&FinalId,
710		&AppVersion,
711		sidecar_bundle_path_for_tauri.as_deref(),
712	)?;
713
714	if Argument.Command.is_empty() {
715		return Err(Error::NoCommand);
716	}
717
718	let mut ShellCommand = if cfg!(target_os = "windows") {
719		let mut cmd = ProcessCommand::new("cmd");
720
721		cmd.arg("/C").args(&Argument.Command);
722
723		cmd
724	} else {
725		let mut cmd = ProcessCommand::new(&Argument.Command[0]);
726
727		cmd.args(&Argument.Command[1..]);
728
729		cmd
730	};
731
732	info!(target: "Build::Exec", "Executing final build command: {:?}", ShellCommand);
733
734	let Status = ShellCommand
735		.current_dir(env::current_dir()?)
736		.stdout(Stdio::inherit())
737		.stderr(Stdio::inherit())
738		.status()?;
739
740	if !Status.success() {
741		let temp_sidecar_dir = ProjectDir.join("bin");
742
743		if temp_sidecar_dir.exists() {
744			let _ = fs::remove_dir_all(&temp_sidecar_dir);
745		}
746
747		return Err(Error::Shell(Status));
748	}
749
750	// Final cleanup of the temporary sidecar directory after a successful build
751	let temp_sidecar_dir = ProjectDir.join("bin");
752
753	if temp_sidecar_dir.exists() {
754		fs::remove_dir_all(&temp_sidecar_dir)?;
755
756		info!(target: "Build", "Cleaned up temporary sidecar directory.");
757	}
758
759	info!(target: "Build", "Build orchestration completed successfully.");
760
761	Ok(())
762}
763
764/// Sets up the global logger for the application.
765pub fn Logger() {
766	let LevelText = env::var(LogEnv).unwrap_or_else(|_| "info".to_string());
767
768	let LogLevel = LevelText.parse::<LevelFilter>().unwrap_or(LevelFilter::Info);
769
770	env_logger::Builder::new()
771		.filter_level(LogLevel)
772		.format(|Buffer, Record| {
773			let LevelStyle = match Record.level() {
774				log::Level::Error => "ERROR".red().bold(),
775
776				log::Level::Warn => "WARN".yellow().bold(),
777
778				log::Level::Info => "INFO".green(),
779
780				log::Level::Debug => "DEBUG".blue(),
781
782				log::Level::Trace => "TRACE".magenta(),
783			};
784
785			writeln!(Buffer, "[{}] [{}]: {}", "Build".red(), LevelStyle, Record.args())
786		})
787		.parse_default_env()
788		.init();
789}
790
791/// Verifies if all required environment variables are set.
792pub fn VerifyEnv() -> Result<(), Error> { Ok(()) }
793
794/// The main entry point of the binary.
795pub fn Fn() {
796	Logger();
797
798	if let Err(Error) = VerifyEnv() {
799		error!("Failed environment variable check: {}", Error);
800
801		std::process::exit(1);
802	}
803
804	let Argument = Argument::parse();
805
806	debug!("Parsed arguments: {:?}", Argument);
807
808	match Process(&Argument) {
809		Ok(_) => info!("Build process completed successfully."),
810
811		Err(e) => {
812			error!("Build process failed: {}", e);
813
814			std::process::exit(1);
815		},
816	}
817}
818
819/// Main executable function.
820#[allow(unused)]
821fn main() { Fn(); }
822
823use std::{
824	env,
825	fs,
826	io::{self, Write as IoWriter},
827	path::{Path, PathBuf},
828	process::{Command as ProcessCommand, Stdio},
829};
830
831use clap::{self, Parser};
832use colored::*;
833use log::{LevelFilter, debug, error, info, warn};
834use serde::{Deserialize, Serialize};
835use serde_json::Value as JsonValue;
836use thiserror::Error;
837use toml_edit::{DocumentMut as TomlDocument, Item as TomlItem, Value as TomlValue};