1#![allow(non_snake_case, non_upper_case_globals)]
20
21pub const DirectoryDefault:&str = "Element/Mountain";
25
26pub const NameDefault:&str = "Mountain";
28
29pub const PrefixDefault:&str = "land.editor.binary";
31
32pub const CargoFile:&str = "Cargo.toml";
34
35pub const JsonfiveFile:&str = "tauri.conf.json5";
37
38pub const JsonFile:&str = "tauri.conf.json";
40
41pub const BackupSuffix:&str = ".Backup";
43
44pub const NameDelimiter:&str = "_";
46
47pub const IdDelimiter:&str = ".";
49
50pub const DirEnv:&str = "MOUNTAIN_DIR";
54
55pub const NameEnv:&str = "MOUNTAIN_ORIGINAL_BASE_NAME";
57
58pub const PrefixEnv:&str = "MOUNTAIN_BUNDLE_ID_PREFIX";
60
61pub const BundleEnv:&str = "Bundle";
63
64pub const BrowserEnv:&str = "Browser";
66
67pub const CompileEnv:&str = "Compile";
69
70pub const CleanEnv:&str = "Clean";
72
73pub const DependencyEnv:&str = "Dependency";
75
76pub const NodeEnv:&str = "NODE_ENV";
79
80pub const NodeVersionEnv:&str = "NODE_VERSION";
82
83pub const LogEnv:&str = "RUST_LOG";
85
86#[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#[derive(Parser, Debug, Clone)]
130#[clap(
131 author,
132 version,
133 about = "Prepares, builds, and restores project configurations."
134)]
135pub struct Argument {
136 #[clap(long, env = DirEnv, default_value = DirectoryDefault)]
138 Directory:String,
139
140 #[clap(long, env = NameEnv, default_value = NameDefault)]
142 Name:String,
143
144 #[clap(long, env = PrefixEnv, default_value = PrefixDefault)]
146 Prefix:String,
147
148 #[clap(long, env = BrowserEnv)]
150 Browser:Option<String>,
151
152 #[clap(long, env = BundleEnv)]
154 Bundle:Option<String>,
155
156 #[clap(long, env = CompileEnv)]
158 Compile:Option<String>,
159
160 #[clap(long, env = CleanEnv)]
162 Clean:Option<String>,
163
164 #[clap(long, env = DependencyEnv)]
166 Dependency:Option<String>,
167
168 #[clap(long, env = NodeEnv)]
170 Environment:Option<String>,
171
172 #[clap(long, env = NodeVersionEnv)]
174 NodeVersion:Option<String>,
175
176 #[clap(required = true, last = true)]
178 Command:Vec<String>,
179}
180
181#[derive(Deserialize, Debug)]
183pub struct Manifest {
184 package:Meta,
185}
186
187#[derive(Deserialize, Debug)]
189pub struct Meta {
190 version:String,
191}
192
193pub 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
257pub 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
365pub 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
437pub 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
450fn 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
488fn 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
509pub 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 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 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 let temp_sidecar_dir = ProjectDir.join("Binary");
663
664 fs::create_dir_all(&temp_sidecar_dir)?;
665
666 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 fs::copy(&source_executable_path, &dest_executable_path)?;
685
686 #[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); 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 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 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
764pub 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
791pub fn VerifyEnv() -> Result<(), Error> { Ok(()) }
793
794pub 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#[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};