1use std::{
66 collections::BTreeMap,
67 env,
68 fs,
69 path::PathBuf,
70 process::{Command as ProcessCommand, Stdio},
71};
72
73use log::info;
74use toml;
75
76use crate::Build::Error::Error as BuildError;
94use crate::Build::{
95 Constant::{
96 CargoFile,
97 CocoonEsbuildDefineEnv,
98 IdDelimiter,
99 JsonFile,
100 JsonfiveFile,
101 LandDisableEnv,
102 LandInspectEnv,
103 LandRecordEnv,
104 LandTraceEnv,
105 NameDelimiter,
106 PlistFile,
107 },
108 Definition::{Argument, Guard, Manifest},
109 GetTauriTargetTriple::GetTauriTargetTriple,
110 JsonEdit::JsonEdit,
111 Pascalize::Pascalize,
112 PlistEdit::PlistEdit,
113 TomlEdit::TomlEdit,
114 WordsFromPascal::WordsFromPascal,
115};
116
117pub fn Process(Argument:&Argument) -> Result<(), BuildError> {
192 info!(target: "Build", "Starting build orchestration...");
193
194 log::debug!(target: "Build", "Argument: {:?}", Argument);
195
196 if let Some(Features) = Argument.CargoFeatures.as_deref().filter(|v| !v.is_empty()) {
202 info!(target: "Build", "Cargo features: {}", Features);
203 }
204
205 if let Some(Defines) = Argument.CocoonEsbuildDefine.as_deref().filter(|v| !v.is_empty()) {
206 info!(target: "Build", "Cocoon esbuild defines: {}", Defines);
207 }
208
209 let ProjectDir = PathBuf::from(&Argument.Directory);
210
211 if !ProjectDir.is_dir() {
212 return Err(BuildError::Missing(ProjectDir));
213 }
214
215 let CargoPath = ProjectDir.join(CargoFile);
216
217 let ConfigPath = {
218 let Jsonfive = ProjectDir.join(JsonfiveFile);
219
220 if Jsonfive.exists() { Jsonfive } else { ProjectDir.join(JsonFile) }
221 };
222
223 if !ConfigPath.exists() {
224 return Err(BuildError::Config);
225 }
226
227 let mut CargoGuard = Guard::New(CargoPath.clone(), "Cargo.toml".to_string())?;
229
230 let mut ConfigGuard = Guard::New(ConfigPath.clone(), "Tauri config".to_string())?;
231
232 let mut NamePartsForProductName = Vec::new();
233
234 let mut NamePartsForId = Vec::new();
235
236 if let Some(NodeValue) = &Argument.Environment {
238 if !NodeValue.is_empty() {
239 let PascalEnv = Pascalize(NodeValue);
240
241 if !PascalEnv.is_empty() {
242 NamePartsForProductName.push(format!("{}NodeEnvironment", PascalEnv));
243
244 NamePartsForId.extend(WordsFromPascal(&PascalEnv));
245
246 NamePartsForId.push("node".to_string());
247
248 NamePartsForId.push("environment".to_string());
249 }
250 }
251 }
252
253 if let Some(DependencyValue) = &Argument.Dependency {
255 if !DependencyValue.is_empty() {
256 let (PascalDepBase, IdDepWords) = if DependencyValue.eq_ignore_ascii_case("true") {
257 ("Generic".to_string(), vec!["generic".to_string()])
258 } else if let Some((Org, Repo)) = DependencyValue.split_once('/') {
259 (format!("{}{}", Pascalize(Org), Pascalize(Repo)), {
260 let mut w = WordsFromPascal(&Pascalize(Org));
261
262 w.extend(WordsFromPascal(&Pascalize(Repo)));
263
264 w
265 })
266 } else {
267 (Pascalize(DependencyValue), WordsFromPascal(&Pascalize(DependencyValue)))
268 };
269
270 if !PascalDepBase.is_empty() {
271 NamePartsForProductName.push(format!("{}Dependency", PascalDepBase));
272
273 NamePartsForId.extend(IdDepWords);
274
275 NamePartsForId.push("dependency".to_string());
276 }
277 }
278 }
279
280 if let Some(Version) = &Argument.NodeVersion {
282 if !Version.is_empty() {
283 let PascalVersion = format!("{}NodeVersion", Version);
284
285 NamePartsForProductName.push(PascalVersion.clone());
286
287 NamePartsForId.push("node".to_string());
288
289 NamePartsForId.push(Version.to_string());
290 }
291 }
292
293 if Argument.Bundle.as_ref().map_or(false, |v| v == "true") {
295 NamePartsForProductName.push("Bundle".to_string());
296
297 NamePartsForId.push("bundle".to_string());
298 }
299
300 if Argument.Clean.as_ref().map_or(false, |v| v == "true") {
301 NamePartsForProductName.push("Clean".to_string());
302
303 NamePartsForId.push("clean".to_string());
304 }
305
306 if Argument.Browser.as_ref().map_or(false, |v| v == "true") {
307 NamePartsForProductName.push("Browser".to_string());
308
309 NamePartsForId.push("browser".to_string());
310 }
311
312 if Argument.Compile.as_ref().map_or(false, |v| v == "true") {
313 NamePartsForProductName.push("Compile".to_string());
314
315 NamePartsForId.push("compile".to_string());
316 }
317
318 if Argument.Debug.as_ref().map_or(false, |v| v == "true")
319 || Argument.Command.iter().any(|arg| arg.contains("--debug"))
320 {
321 NamePartsForProductName.push("Debug".to_string());
322
323 NamePartsForId.push("debug".to_string());
324 }
325
326 if Argument.Mountain.as_ref().map_or(false, |v| v == "true") {
333 NamePartsForProductName.push("MountainProfile".to_string());
334
335 NamePartsForId.push("mountain".to_string());
336
337 NamePartsForId.push("profile".to_string());
338 }
339
340 if Argument.Electron.as_ref().map_or(false, |v| v == "true") {
341 NamePartsForProductName.push("ElectronProfile".to_string());
342
343 NamePartsForId.push("electron".to_string());
344
345 NamePartsForId.push("profile".to_string());
346 }
347
348 if let Some(Variant) = &Argument.Compiler {
352 if !Variant.is_empty() {
353 let PascalCompiler = Pascalize(Variant);
354
355 if !PascalCompiler.is_empty() {
356 NamePartsForProductName.push(format!("{}Compiler", PascalCompiler));
357
358 NamePartsForId.extend(WordsFromPascal(&PascalCompiler));
359
360 NamePartsForId.push("compiler".to_string());
361 }
362 }
363 }
364
365 let ProductNamePrefix = NamePartsForProductName.join(NameDelimiter);
367
368 let FinalName = if !ProductNamePrefix.is_empty() {
369 format!("{}{}{}", ProductNamePrefix, NameDelimiter, Argument.Name)
370 } else {
371 Argument.Name.clone()
372 };
373
374 info!(target: "Build", "Final generated product name: '{}'", FinalName);
375
376 NamePartsForId.extend(WordsFromPascal(&Argument.Name));
378
379 let IdSuffix = NamePartsForId
380 .into_iter()
381 .filter(|s| !s.is_empty())
382 .collect::<Vec<String>>()
383 .join(IdDelimiter);
384
385 let FinalId = format!("{}{}{}", Argument.Prefix, IdDelimiter, IdSuffix);
386
387 info!(target: "Build", "Generated bundle identifier: '{}'", FinalId);
388
389 if FinalName != Argument.Name {
391 TomlEdit(&CargoPath, &Argument.Name, &FinalName)?;
392 }
393
394 let AppVersion = toml::from_str::<Manifest>(&fs::read_to_string(&CargoPath)?)?
396 .get_version()
397 .to_string();
398
399 JsonEdit(
401 &ConfigPath,
402 &FinalName,
403 &FinalId,
404 &AppVersion,
405 (if let Some(version) = &Argument.NodeVersion {
406 info!(target: "Build", "Selected Node.js version: {}", version);
407
408 let Triple = GetTauriTargetTriple();
409
410 let Executable = if cfg!(target_os = "windows") {
412 PathBuf::from(format!("./Element/SideCar/{}/NODE/{}/node.exe", Triple, version))
413 } else {
414 PathBuf::from(format!("./Element/SideCar/{}/NODE/{}/bin/node", Triple, version))
415 };
416
417 let DirectorySideCarTemporary = ProjectDir.join("Binary");
419
420 fs::create_dir_all(&DirectorySideCarTemporary)?;
421
422 let PathExecutableDestination = if cfg!(target_os = "windows") {
424 DirectorySideCarTemporary.join(format!("node-{}.exe", Triple))
425 } else {
426 DirectorySideCarTemporary.join(format!("node-{}", Triple))
427 };
428
429 info!(
430 target: "Build",
431
432 "Staging sidecar from {} to {}",
433
434 Executable.display(),
435
436 PathExecutableDestination.display()
437 );
438
439 fs::copy(&Executable, &PathExecutableDestination)?;
441
442 #[cfg(not(target_os = "windows"))]
444 {
445 use std::os::unix::fs::PermissionsExt;
446
447 let mut Permission = fs::metadata(&PathExecutableDestination)?.permissions();
448
449 Permission.set_mode(0o755);
451
452 fs::set_permissions(&PathExecutableDestination, Permission)?;
453 }
454
455 Some("Binary/node".to_string())
456 } else {
457 info!(target: "Build", "No Node.js flavour selected for bundling.");
458
459 None
460 })
461 .as_deref(),
462 )?;
463
464 #[cfg(target_os = "macos")]
471 {
472 let PlistPath = ProjectDir.join(PlistFile);
473
474 if PlistPath.exists() {
475 let PlistEnvVars = BuildPlistEnvironment();
476
477 if !PlistEnvVars.is_empty() {
478 let mut PlistGuard = Guard::New(PlistPath.clone(), "Info.plist".to_string())?;
479
480 let _ = PlistEdit(&PlistPath, &PlistEnvVars);
481
482 PlistGuard.disarm();
483 }
484 }
485 }
486
487 if Argument.Command.is_empty() {
489 return Err(BuildError::NoCommand);
490 }
491
492 let mut CommandArguments:Vec<String> = Argument.Command.clone();
498
499 let IsTauriBuild = CommandArguments.len() >= 3
500 && CommandArguments[0] == "pnpm"
501 && CommandArguments[1] == "tauri"
502 && CommandArguments[2] == "build";
503
504 if IsTauriBuild {
505 if let Some(Features) = Argument.CargoFeatures.as_deref().filter(|v| !v.is_empty()) {
506 let AlreadyPresent = CommandArguments.iter().any(|a| a == "--features" || a == "-f");
507
508 if !AlreadyPresent {
509 info!(
510 target: "Build",
511
512 "Forwarding Cargo features to `tauri build`: {}",
513
514 Features
515 );
516
517 CommandArguments.push("--features".to_string());
518
519 CommandArguments.push(Features.to_string());
520 }
521 }
522 }
523
524 let mut ShellCommand = if cfg!(target_os = "windows") {
525 let mut Command = ProcessCommand::new("cmd");
526
527 Command.arg("/C").args(&CommandArguments);
528
529 Command
530 } else {
531 let mut Command = ProcessCommand::new(&CommandArguments[0]);
532
533 Command.args(&CommandArguments[1..]);
534
535 Command
536 };
537
538 if let Some(Defines) = Argument.CocoonEsbuildDefine.as_deref().filter(|v| !v.is_empty()) {
543 ShellCommand.env(CocoonEsbuildDefineEnv, Defines);
544 }
545
546 info!(target: "Build::Exec", "Executing final build command: {:?}", ShellCommand);
547
548 let Status = ShellCommand
549 .current_dir(env::current_dir()?)
550 .stdout(Stdio::inherit())
551 .stderr(Stdio::inherit())
552 .status()?;
553
554 if !Status.success() {
556 let temp_sidecar_dir = ProjectDir.join("bin");
557
558 if temp_sidecar_dir.exists() {
559 let _ = fs::remove_dir_all(&temp_sidecar_dir);
560 }
561
562 return Err(BuildError::Shell(Status));
563 }
564
565 let DirectorySideCarTemporary = ProjectDir.join("bin");
567
568 if DirectorySideCarTemporary.exists() {
569 fs::remove_dir_all(&DirectorySideCarTemporary)?;
570
571 info!(target: "Build", "Cleaned up temporary sidecar directory.");
572 }
573
574 drop(CargoGuard);
579
580 drop(ConfigGuard);
581
582 info!(target: "Build", "Build orchestration completed successfully.");
583
584 Ok(())
585}
586
587fn BuildPlistEnvironment() -> BTreeMap<String, String> {
598 let mut EnvVars = BTreeMap::new();
599
600 let SkipKeys = ["CargoFeatures", "CocoonEsbuildDefine", "NODE_ENV"];
603
604 for Source in [".env.Land", ".env.Land.Sample"] {
607 let Path = PathBuf::from(Source);
608
609 if Path.exists() {
610 if let Ok(Content) = fs::read_to_string(&Path) {
611 info!(target: "Build::Plist", "Loading LSEnvironment vars from {}", Source);
612
613 for Line in Content.lines() {
614 let Trimmed = Line.trim();
615
616 if Trimmed.is_empty() || Trimmed.starts_with('#') {
617 continue;
618 }
619
620 if let Some((Key, Value)) = Trimmed.split_once('=') {
621 let CleanKey = Key.trim();
622
623 let CleanValue = Value.trim().trim_matches('"').trim_matches('\'');
624
625 if SkipKeys.contains(&CleanKey) {
627 continue;
628 }
629
630 EnvVars.insert(CleanKey.to_string(), CleanValue.to_string());
631 }
632 }
633 }
634
635 break;
636 }
637 }
638
639 for Key in [LandTraceEnv, LandRecordEnv, LandInspectEnv, LandDisableEnv] {
644 if !EnvVars.contains_key(Key) {
645 if let Ok(Value) = env::var(Key) {
646 EnvVars.insert(Key.to_string(), Value);
647 }
648 }
649 }
650
651 EnvVars
652}