1#![allow(
2 non_snake_case,
3 non_camel_case_types,
4 non_upper_case_globals,
5 dead_code,
6 unused_imports,
7 unused_variables,
8 unused_assignments
9)]
10
11#[derive(Clone, Debug)]
40struct PlatformTarget {
41 DownloadIdentifier:String,
44
45 ArchiveExtension:String,
47
48 TauriTargetTriple:String,
51}
52
53#[derive(Clone, Debug, PartialEq)]
56enum ArchiveType {
57 Zip,
58
59 TarGz,
60}
61
62#[derive(Deserialize, Debug)]
65struct NodeVersionInfo {
66 version:String,
67}
68
69#[derive(Clone, Debug)]
73struct DownloadTask {
74 SidecarName:String,
76
77 MajorVersion:String,
79
80 FullVersion:String,
82
83 DownloadURL:String,
85
86 TempParentDirectory:PathBuf,
88
89 DestinationDirectory:PathBuf,
91
92 ArchiveType:ArchiveType,
94
95 ExtractedFolderName:String,
97
98 TauriTargetTriple:String,
100}
101
102#[derive(Serialize, Deserialize, Debug, Default)]
107struct DownloadCache {
108 Entries:HashMap<String, String>,
112}
113
114impl DownloadCache {
115 fn Load(CachePath:&Path) -> Self {
118 if !CachePath.exists() {
119 info!("Cache file not found. A new one will be created.");
120
121 return DownloadCache::default();
122 }
123
124 let FileContents = match fs::read_to_string(CachePath) {
125 Ok(Contents) => Contents,
126
127 Err(Error) => {
128 warn!("Failed to read cache file: {}. Starting with an empty cache.", Error);
129
130 return DownloadCache::default();
131 },
132 };
133
134 match serde_json::from_str(&FileContents) {
135 Ok(Cache) => {
136 info!("Successfully loaded download cache.");
137
138 Cache
139 },
140
141 Err(Error) => {
142 warn!("Failed to parse cache file: {}. Starting with an empty cache.", Error);
143
144 DownloadCache::default()
145 },
146 }
147 }
148
149 fn Save(&self, CachePath:&Path) -> Result<()> {
153 let SortedEntries:BTreeMap<_, _> = self.Entries.iter().collect();
155
156 let CacheToSerialize = serde_json::json!({
158
159 "Entries": SortedEntries
160 });
161
162 let mut Buffer = Vec::new();
164
165 let Formatter = serde_json::ser::PrettyFormatter::with_indent(b" ");
167
168 let mut Serializer = serde_json::Serializer::with_formatter(&mut Buffer, Formatter);
170
171 CacheToSerialize.serialize(&mut Serializer)?;
173
174 fs::write(CachePath, &Buffer)
176 .with_context(|| format!("Failed to write tab-formatted cache to {:?}", CachePath))?;
177
178 Ok(())
179 }
180}
181
182fn GetBaseSidecarDirectory() -> Result<PathBuf> {
191 let CurrentExePath = env::current_exe().context("Failed to get the path of the current executable.")?;
193
194 let mut CurrentDir = CurrentExePath
196 .parent()
197 .context("Executable must be in a directory (not the root).")?;
198
199 loop {
200 let LibraryRsPath = CurrentDir.join("Source").join("Library.rs");
203
204 if LibraryRsPath.exists() {
205 return Ok(CurrentDir.to_path_buf());
206 }
207
208 let CargoTomlPath = CurrentDir.join("Cargo.toml");
211
212 if CargoTomlPath.exists() {
213 if let Ok(CargoContents) = fs::read_to_string(&CargoTomlPath) {
214 if let Ok(Toml) = toml::from_str::<toml::Value>(&CargoContents) {
215 if let Some(Package) = Toml.get("package") {
216 if let Some(PackageName) = Package.get("name").and_then(|v| v.as_str()) {
217 if PackageName == "SideCar" {
218 let SourceDir = CurrentDir.join("Source");
220
221 if SourceDir.exists() {
222 return Ok(CurrentDir.to_path_buf());
223 }
224 }
225 }
226 }
227 }
228 }
229 }
230
231 let SubdirCargoTomlPath = CurrentDir.join("Element").join("SideCar").join("Cargo.toml");
235
236 if SubdirCargoTomlPath.exists() {
237 if let Ok(CargoContents) = fs::read_to_string(&SubdirCargoTomlPath) {
238 if let Ok(Toml) = toml::from_str::<toml::Value>(&CargoContents) {
239 if let Some(Package) = Toml.get("package") {
240 if let Some(PackageName) = Package.get("name").and_then(|v| v.as_str()) {
241 if PackageName == "SideCar" {
242 let SourceDir = CurrentDir.join("Element").join("SideCar").join("Source");
244
245 if SourceDir.exists() {
246 return Ok(CurrentDir.join("Element").join("SideCar"));
248 }
249 }
250 }
251 }
252 }
253 }
254 }
255
256 let NextDir = match CurrentDir.parent() {
258 Some(Parent) => Parent,
259
260 None => break, };
262
263 CurrentDir = NextDir;
264 }
265
266 Err(anyhow!(
267 "Could not determine the SideCar base directory. The executable should be built from within the SideCar crate \
268 or from the workspace containing Element/SideCar. Searched up from: {}",
269 CurrentExePath.display()
270 ))
271}
272
273fn GetPlatformMatrix() -> Vec<PlatformTarget> {
276 vec![
277 PlatformTarget {
278 DownloadIdentifier:"win-x64".to_string(),
279
280 ArchiveExtension:"zip".to_string(),
281
282 TauriTargetTriple:"x86_64-pc-windows-msvc".to_string(),
283 },
284 PlatformTarget {
285 DownloadIdentifier:"linux-x64".to_string(),
286
287 ArchiveExtension:"tar.gz".to_string(),
288
289 TauriTargetTriple:"x86_64-unknown-linux-gnu".to_string(),
290 },
291 PlatformTarget {
292 DownloadIdentifier:"linux-arm64".to_string(),
293
294 ArchiveExtension:"tar.gz".to_string(),
295
296 TauriTargetTriple:"aarch64-unknown-linux-gnu".to_string(),
297 },
298 PlatformTarget {
299 DownloadIdentifier:"darwin-x64".to_string(),
300
301 ArchiveExtension:"tar.gz".to_string(),
302
303 TauriTargetTriple:"x86_64-apple-darwin".to_string(),
304 },
305 PlatformTarget {
306 DownloadIdentifier:"darwin-arm64".to_string(),
307
308 ArchiveExtension:"tar.gz".to_string(),
309
310 TauriTargetTriple:"aarch64-apple-darwin".to_string(),
311 },
312 ]
313}
314
315fn GetSidecarsToFetch() -> HashMap<String, Vec<String>> {
318 let mut Sidecars = HashMap::new();
319
320 Sidecars.insert(
321 "NODE".to_string(),
322 vec!["24", "23", "22", "21", "20", "19", "18", "17", "16"]
323 .into_iter()
324 .map(String::from)
325 .collect(),
326 );
327
328 Sidecars
329}
330
331pub const LogEnv:&str = "RUST_LOG";
335
336fn UpdateGitattributes(BaseDirectory:&Path) -> Result<()> {
340 const GITATTRIBUTES_HEADER:&str = r#"################################################################################
341# Git LFS configuration for vendored Tauri Sidecars
342#
343# This file tells Git to use LFS (Large File Storage) for the heavy binary
344# files and modules downloaded by the sidecar vendoring script. This keeps the
345# main repository history small and fast.
346#
347# The `-text` attribute is used to prevent Git from normalizing line endings,
348
349# which is critical for binary files and scripts.
350#
351# This file is automatically managed by the sidecar vendor script.
352################################################################################
353
354# --- Rule Definitions ---"#;
355
356 const GITATTRIBUTES_RULES:&[&str] = &[
357 "**/NODE/**/bin/node filter=lfs diff=lfs merge=lfs -text",
358 "**/NODE/**/node.exe filter=lfs diff=lfs merge=lfs -text",
359 "**/NODE/**/bin/npm filter=lfs diff=lfs merge=lfs -text",
360 "**/NODE/**/bin/npx filter=lfs diff=lfs merge=lfs -text",
361 "**/NODE/**/bin/corepack filter=lfs diff=lfs merge=lfs -text",
362 "**/NODE/**/npm filter=lfs diff=lfs merge=lfs -text",
363 "**/NODE/**/npm.cmd filter=lfs diff=lfs merge=lfs -text",
364 "**/NODE/**/npx filter=lfs diff=lfs merge=lfs -text",
365 "**/NODE/**/npx.cmd filter=lfs diff=lfs merge=lfs -text",
366 "**/NODE/**/corepack filter=lfs diff=lfs merge=lfs -text",
367 "**/NODE/**/corepack.cmd filter=lfs diff=lfs merge=lfs -text",
368 "",
369 "# --- Rules for the SideCar build artifacts ---",
370 "",
371 "Target/debug/*.exe filter=lfs diff=lfs merge=lfs -text",
372 "Target/release/*.exe filter=lfs diff=lfs merge=lfs -text",
373 "",
374 "Target/debug/SideCar filter=lfs diff=lfs merge=lfs -text",
375 "Target/release/SideCar filter=lfs diff=lfs merge=lfs -text",
376 "",
377 "Target/debug/Download filter=lfs diff=lfs merge=lfs -text",
378 "Target/release/Download filter=lfs diff=lfs merge=lfs -text",
379 ];
380
381 let GitattributesPath = BaseDirectory.join(".gitattributes");
382
383 if !GitattributesPath.exists() {
384 info!("Creating .gitattributes file to track binaries with Git LFS.");
385
386 let mut File = File::create(&GitattributesPath)
387 .with_context(|| format!("Failed to create .gitattributes file at {:?}", GitattributesPath))?;
388
389 writeln!(File, "{}", GITATTRIBUTES_HEADER)?;
390
391 for Rule in GITATTRIBUTES_RULES {
392 writeln!(File, "{}", Rule)?;
394 }
395 } else {
396 info!(".gitattributes file found. Verifying LFS rules...");
397
398 let Content = fs::read_to_string(&GitattributesPath)?;
399
400 let MissingRules:Vec<_> = GITATTRIBUTES_RULES
401 .iter()
402
403 .filter(|rule| !rule.is_empty() && !rule.starts_with('#'))
405 .filter(|rule| !Content.contains(*rule))
406 .collect();
407
408 if !MissingRules.is_empty() {
409 info!("Adding {} missing LFS rules to .gitattributes.", MissingRules.len());
410
411 let mut File = fs::OpenOptions::new()
412 .append(true)
413 .open(&GitattributesPath)
414 .with_context(|| format!("Failed to open .gitattributes for appending at {:?}", GitattributesPath))?;
415
416 writeln!(File, "\n\n# --- Rules Automatically Added by Vendor Script ---")?;
417
418 for Rule in MissingRules {
419 writeln!(File, "{}", Rule)?;
420 }
421 } else {
422 info!(".gitattributes is already up to date.");
423 }
424 }
425
426 Ok(())
427}
428
429async fn FetchNodeVersions(Client:&Client) -> Result<Vec<NodeVersionInfo>> {
433 info!("Fetching Node.js version index for resolving versions...");
434
435 let Response = Client
436 .get("https://nodejs.org/dist/index.json")
437 .send()
438 .await
439 .context("Failed to send request to Node.js version index.")?;
440
441 if !Response.status().is_success() {
442 return Err(anyhow!("Received non-success status from Node.js index: {}", Response.status()));
443 }
444
445 let Versions = Response
446 .json::<Vec<NodeVersionInfo>>()
447 .await
448 .context("Failed to parse Node.js version index JSON.")?;
449
450 Ok(Versions)
451}
452
453fn ResolveLatestPatchVersion(MajorVersion:&str, AllVersions:&[NodeVersionInfo]) -> Option<String> {
456 let VersionPrefix = format!("v{}.", MajorVersion);
457
458 AllVersions
459 .iter()
460 .find(|v| v.version.starts_with(&VersionPrefix))
461 .map(|v| v.version.clone())
462}
463
464async fn DownloadFile(Client:&Client, URL:&str, DestinationPath:&Path) -> Result<()> {
466 let mut Response = Client.get(URL).send().await?.error_for_status()?;
467
468 let mut DestinationFile =
469 File::create(DestinationPath).with_context(|| format!("Failed to create file at {:?}", DestinationPath))?;
470
471 while let Some(Chunk) = Response.chunk().await? {
473 DestinationFile.write_all(&Chunk)?;
474 }
475
476 Ok(())
477}
478
479fn ExtractArchive(ArchiveType:&ArchiveType, ArchivePath:&Path, ExtractionDirectory:&Path) -> Result<()> {
483 info!("Performing a full extraction of the archive...");
484
485 match ArchiveType {
486 ArchiveType::Zip => {
487 let File = File::open(ArchivePath)?;
488
489 let mut Archive = zip::ZipArchive::new(File)?;
490
491 Archive.extract(ExtractionDirectory)?;
492 },
493
494 ArchiveType::TarGz => {
495 let File = File::open(ArchivePath)?;
496
497 let Decompressor = flate2::read::GzDecoder::new(File);
498
499 let mut Archive = tar::Archive::new(Decompressor);
500
501 Archive.unpack(ExtractionDirectory)?;
502 },
503 }
504
505 Ok(())
506}
507
508async fn ProcessDownloadTask(Task:DownloadTask, Client:Client, Cache:Arc<Mutex<DownloadCache>>) -> Result<()> {
511 let TempDirectory = Builder::new()
513 .prefix("SideCar-Download-")
514 .tempdir_in(&Task.TempParentDirectory)
515 .context("Failed to create temporary directory.")?;
516
517 let ArchiveName = Task.DownloadURL.split('/').last().unwrap_or("Download.tmp");
518
519 let ArchivePath = TempDirectory.path().join(ArchiveName);
520
521 info!(
522 " [{}/{}] Downloading from: {}",
523 Task.TauriTargetTriple, Task.SidecarName, Task.DownloadURL
524 );
525
526 if let Err(Error) = DownloadFile(&Client, &Task.DownloadURL, &ArchivePath).await {
527 error!(
528 " [{}/{}] Failed to download {}: {}",
529 Task.TauriTargetTriple, Task.SidecarName, ArchiveName, Error
530 );
531
532 return Err(Error.into());
533 }
534
535 info!(" [{}/{}] Extracting archive...", Task.TauriTargetTriple, Task.SidecarName);
536
537 if let Err(Error) = ExtractArchive(&Task.ArchiveType, &ArchivePath, TempDirectory.path()) {
538 error!(
539 " [{}/{}] Failed to extract {}: {}",
540 Task.TauriTargetTriple, Task.SidecarName, ArchiveName, Error
541 );
542
543 return Err(Error.into());
544 }
545
546 let ExtractedPath = TempDirectory.path().join(&Task.ExtractedFolderName);
547
548 if !ExtractedPath.exists() {
549 let ErrorMessage = format!(" Could not find extracted folder: {:?}", ExtractedPath);
550
551 error!("{}", ErrorMessage);
552
553 return Err(anyhow!(ErrorMessage));
554 }
555
556 if Task.DestinationDirectory.exists() {
558 info!(" Removing old version at: {:?}", Task.DestinationDirectory);
559
560 fs::remove_dir_all(&Task.DestinationDirectory)?;
561 }
562
563 if let Some(Parent) = Task.DestinationDirectory.parent() {
565 fs::create_dir_all(Parent)?;
566 }
567
568 info!(" Installing to: {:?}", Task.DestinationDirectory);
569
570 fs::rename(&ExtractedPath, &Task.DestinationDirectory).with_context(|| {
571 format!(
572 "Failed to rename/move extracted directory from {:?} to {:?}",
573 ExtractedPath, Task.DestinationDirectory
574 )
575 })?;
576
577 let CacheKey = format!("{}/{}/{}", Task.TauriTargetTriple, Task.SidecarName, Task.MajorVersion);
579
580 let mut LockedCache = Cache.lock().unwrap();
581
582 LockedCache.Entries.insert(CacheKey, Task.FullVersion.clone());
583
584 info!(
585 " v{} ({}) for '{}' is now up to date.",
586 Task.MajorVersion, Task.FullVersion, Task.TauriTargetTriple
587 );
588
589 Ok(())
590}
591
592pub fn Logger() {
594 let LevelText = env::var(LogEnv).unwrap_or_else(|_| "info".to_string());
595
596 let LogLevel = LevelText.parse::<LevelFilter>().unwrap_or(LevelFilter::Info);
597
598 env_logger::Builder::new()
599 .filter_level(LogLevel)
600 .format(|Buffer, Record| {
601 let LevelStyle = match Record.level() {
602 log::Level::Error => "ERROR".red().bold(),
603
604 log::Level::Warn => "WARN".yellow().bold(),
605
606 log::Level::Info => "INFO".green(),
607
608 log::Level::Debug => "DEBUG".blue(),
609
610 log::Level::Trace => "TRACE".magenta(),
611 };
612
613 writeln!(Buffer, "[{}] [{}]: {}", "Download".red(), LevelStyle, Record.args())
614 })
615 .parse_default_env()
616 .init();
617}
618
619#[tokio::main]
620pub async fn Fn() -> Result<()> {
621 CommonLibrary::Telemetry::Initialize::Fn(CommonLibrary::Telemetry::Tier::Tier::SideCar).await;
624
625 Logger();
626
627 info!("Starting Universal Sidecar vendoring process...");
628
629 let BaseSidecarDirectory = GetBaseSidecarDirectory()?;
631
632 UpdateGitattributes(&BaseSidecarDirectory)?;
634
635 let TempDownloadsDirectory = BaseSidecarDirectory.join("Temporary");
637
638 fs::create_dir_all(&TempDownloadsDirectory)
639 .with_context(|| format!("Failed to create temporary directory at {:?}", TempDownloadsDirectory))?;
640
641 let CachePath = BaseSidecarDirectory.join("Cache.json");
642
643 let Cache = Arc::new(Mutex::new(DownloadCache::Load(&CachePath)));
644
645 let HttpClient = Client::new();
646
647 let PlatformMatrix = GetPlatformMatrix();
648
649 let SidecarsToFetch = GetSidecarsToFetch();
650
651 let NodeVersions = FetchNodeVersions(&HttpClient).await?;
653
654 let mut TasksToRun = Vec::new();
655
656 for Platform in &PlatformMatrix {
659 info!("--- Processing architecture: '{}' ---", Platform.TauriTargetTriple);
660
661 for (SidecarName, MajorVersions) in &SidecarsToFetch {
662 info!(" -> Processing sidecar: '{}'", SidecarName);
663
664 for MajorVersion in MajorVersions {
665 let DestinationDirectory = BaseSidecarDirectory
666 .join(&Platform.TauriTargetTriple)
667 .join(SidecarName)
668 .join(MajorVersion);
669
670 if SidecarName == "NODE" {
672 let FullVersion = match ResolveLatestPatchVersion(MajorVersion, &NodeVersions) {
673 Some(Version) => Version,
674
675 None => {
676 warn!(
677 " Could not resolve a specific version for Node.js v{}. Skipping.",
678 MajorVersion
679 );
680
681 continue;
682 },
683 };
684
685 let CacheKey = format!("{}/{}/{}", &Platform.TauriTargetTriple, SidecarName, MajorVersion);
687
688 let CachedVersion = Cache.lock().unwrap().Entries.get(&CacheKey).cloned();
689
690 if Some(FullVersion.clone()) == CachedVersion {
691 info!(" v{} ({}) is already up to date, skipping.", MajorVersion, FullVersion);
692
693 continue;
694 }
695
696 if CachedVersion.is_some() {
697 info!(
698 " Found newer patch for v{}: {} -> {}. Scheduling update.",
699 MajorVersion,
700 CachedVersion.unwrap(),
701 FullVersion
702 );
703 } else {
704 info!(" Processing v{} (resolved to {})...", MajorVersion, FullVersion);
705 }
706
707 let ArchiveExtension = &Platform.ArchiveExtension;
708
709 let ArchiveName =
710 format!("node-{}-{}.{}", FullVersion, Platform.DownloadIdentifier, ArchiveExtension);
711
712 let DownloadURL = format!("https://nodejs.org/dist/{}/{}", FullVersion, ArchiveName);
713
714 let ExtractedFolderName = format!("node-{}-{}", FullVersion, Platform.DownloadIdentifier);
715
716 let Task = DownloadTask {
717 SidecarName:SidecarName.clone(),
718
719 MajorVersion:MajorVersion.clone(),
720
721 FullVersion,
722
723 DownloadURL,
724
725 TempParentDirectory:TempDownloadsDirectory.clone(),
726
727 DestinationDirectory,
728
729 ArchiveType:if ArchiveExtension == "zip" { ArchiveType::Zip } else { ArchiveType::TarGz },
730
731 ExtractedFolderName,
732
733 TauriTargetTriple:Platform.TauriTargetTriple.clone(),
734 };
735
736 TasksToRun.push(Task);
737 }
738
739 }
742 }
743 }
744
745 if TasksToRun.is_empty() {
747 info!("All sidecar binaries are already up to date.");
748 } else {
749 info!("Found {} tasks to run. Starting concurrent downloads...", TasksToRun.len());
750
751 let NumberOfConcurrentJobs = num_cpus::get().min(8);
753
754 let Results = stream::iter(TasksToRun)
757 .map(|Task| {
758 let Client = HttpClient.clone();
759
760 let Cache = Arc::clone(&Cache);
761
762 tokio::spawn(async move { ProcessDownloadTask(Task, Client, Cache).await })
763 })
764 .buffer_unordered(NumberOfConcurrentJobs)
765 .collect::<Vec<_>>()
766 .await;
767
768 let mut ErrorsEncountered = 0;
770
771 for Result in Results {
772 if let Err(JoinError) = Result {
774 error!("A download task panicked or was cancelled: {}", JoinError);
775
776 ErrorsEncountered += 1;
777 } else if let Ok(Err(AppError)) = Result {
778 error!("A download task failed: {}", AppError);
781
782 ErrorsEncountered += 1;
783 }
784 }
785
786 if ErrorsEncountered > 0 {
787 error!("Completed with {} errors.", ErrorsEncountered);
788 }
789 }
790
791 info!("Saving updated cache...");
793
794 Cache.lock().unwrap().Save(&CachePath)?;
795
796 info!("All sidecar binaries have been successfully processed and organized.");
797
798 Ok(())
799}
800
801fn main() {
803 if let Err(Error) = Fn() {
805 error!("The application encountered a fatal error: {}", Error);
807
808 std::process::exit(1);
809 }
810}
811
812use std::{
814 collections::{BTreeMap, HashMap},
815 env,
816 fs::{self, File},
817 io::Write,
818 path::{Path, PathBuf},
819 sync::{Arc, Mutex},
820};
821
822use anyhow::{Context, Result, anyhow};
823use colored::*;
824use futures::stream::{self, StreamExt};
825use log::{LevelFilter, error, info, warn};
826use reqwest::Client;
827use serde::{Deserialize, Serialize};
828use tempfile::Builder;
829use toml;