Skip to main content

SideCar/
Download.rs

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//! ==============================================================================
12//! Universal Sidecar Vendor - Rust Edition
13//!
14//! This program automates downloading and organizing full distributions of
15//! various sidecar runtimes (like Node.js) for a Tauri application. It is a
16//! Rust rewrite of the original shell script, enhanced with modern features.
17//!
18//! Key Features:
19//!   - Asynchronous, Concurrent Downloads: Leverages Tokio to download multiple
20//!     binaries in parallel, significantly speeding up the process.
21//!   - Intelligent Caching: Maintains a `Cache.json` file to track downloaded
22//!     versions. It automatically detects if a newer patch version is available
23//!     for a requested major version and updates the binary.
24//!   - Git LFS Management: Automatically creates or updates the
25//!     `.gitattributes` file to ensure large binaries are tracked by Git LFS.
26//!   - Extensible Design: Easily configured to support new sidecars, versions,
27//!     and platforms.
28//!   - Robust Error Handling: Uses `anyhow` for clear and concise error
29//!     reporting.
30//!   - Preserved File Structure: The final output directory structure remains
31//!     identical to the original script (`Architecture/SidecarName/Version`).
32//!
33//! ==============================================================================
34
35// --- Type Definitions and Structs ---
36
37/// Represents a single platform target for which binaries will be downloaded.
38/// This struct holds all the necessary identifiers for a given platform.
39#[derive(Clone, Debug)]
40struct PlatformTarget {
41	/// The identifier used in the download URL (e.g., "win-x64",
42	/// "linux-arm64").
43	DownloadIdentifier:String,
44
45	/// The file extension of the archive (e.g., "zip", "tar.gz").
46	ArchiveExtension:String,
47
48	/// The official Tauri target triple for this platform (e.g.,
49	/// "x86_64-pc-windows-msvc").
50	TauriTargetTriple:String,
51}
52
53/// Defines the type of archive being handled, which determines the extraction
54/// logic.
55#[derive(Clone, Debug, PartialEq)]
56enum ArchiveType {
57	Zip,
58
59	TarGz,
60}
61
62/// Represents a specific version of Node.js as returned by the official index.
63/// Used for deserializing the JSON response from `nodejs.org`.
64#[derive(Deserialize, Debug)]
65struct NodeVersionInfo {
66	version:String,
67}
68
69/// Contains all the necessary information to perform a single download and
70/// installation task. An instance of this struct is created for each binary
71/// that needs to be downloaded.
72#[derive(Clone, Debug)]
73struct DownloadTask {
74	/// The name of the sidecar (e.g., "NODE").
75	SidecarName:String,
76
77	/// The major version string requested (e.g., "24").
78	MajorVersion:String,
79
80	/// The full, resolved version string (e.g., "v24.0.0").
81	FullVersion:String,
82
83	/// The complete URL to download the archive from.
84	DownloadURL:String,
85
86	/// The directory where temporary folders for this task will be created.
87	TempParentDirectory:PathBuf,
88
89	/// The final destination directory for the extracted binaries.
90	DestinationDirectory:PathBuf,
91
92	/// The type of archive to be downloaded.
93	ArchiveType:ArchiveType,
94
95	/// The name of the root folder inside the archive once extracted.
96	ExtractedFolderName:String,
97
98	/// The Tauri target triple for this download task.
99	TauriTargetTriple:String,
100}
101
102/// Represents the structure of the `Cache.json` file.
103/// It uses a HashMap to map a unique key (representing a specific
104/// sidecar/version/platform) to the full version string that was last
105/// downloaded.
106#[derive(Serialize, Deserialize, Debug, Default)]
107struct DownloadCache {
108	/// The core data structure for the cache.
109	/// Key: A unique string like "x86_64-pc-windows-msvc/NODE/24".
110	/// Value: The full version string, like "v24.0.0".
111	Entries:HashMap<String, String>,
112}
113
114impl DownloadCache {
115	/// Loads the cache from the `Cache.json` file in the base sidecar
116	/// directory. If the file doesn't exist, it returns a new, empty cache.
117	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	/// Saves the current state of the cache to the `Cache.json` file.
150	/// The JSON is pretty-printed with tabs for indentation.
151	/// Entries are sorted alphabetically by key for consistency.
152	fn Save(&self, CachePath:&Path) -> Result<()> {
153		// Create a BTreeMap to sort entries alphabetically by key
154		let SortedEntries:BTreeMap<_, _> = self.Entries.iter().collect();
155
156		// Create a temporary struct to hold the sorted entries for serialization
157		let CacheToSerialize = serde_json::json!({
158
159			"Entries": SortedEntries
160		});
161
162		// Create an in-memory buffer to write the serialized JSON to.
163		let mut Buffer = Vec::new();
164
165		// Create a formatter that uses a tab character for indentation.
166		let Formatter = serde_json::ser::PrettyFormatter::with_indent(b"	");
167
168		// Create a serializer with our custom formatter.
169		let mut Serializer = serde_json::Serializer::with_formatter(&mut Buffer, Formatter);
170
171		// Serialize the sorted cache data into the buffer.
172		CacheToSerialize.serialize(&mut Serializer)?;
173
174		// Write the buffer's contents to the actual file on disk.
175		fs::write(CachePath, &Buffer)
176			.with_context(|| format!("Failed to write tab-formatted cache to {:?}", CachePath))?;
177
178		Ok(())
179	}
180}
181
182// --- Configuration ---
183
184/// Returns the root directory where all sidecars will be stored.
185/// This is determined dynamically by navigating up from the executable's
186/// location and detecting the SideCar project root. It handles both:
187/// - Standalone builds: `.../SideCar/Target/release/`
188/// - Workspace builds: `.../workspace/Target/release/SideCar` (where the
189///   workspace root contains multiple crates including Element/SideCar)
190fn GetBaseSidecarDirectory() -> Result<PathBuf> {
191	// Get the full path to the currently running executable.
192	let CurrentExePath = env::current_exe().context("Failed to get the path of the current executable.")?;
193
194	// Start from the directory containing the executable and walk up the tree.
195	let mut CurrentDir = CurrentExePath
196		.parent()
197		.context("Executable must be in a directory (not the root).")?;
198
199	loop {
200		// Check A: Does Source/Library.rs exist in current directory? → return current
201		// directory
202		let LibraryRsPath = CurrentDir.join("Source").join("Library.rs");
203
204		if LibraryRsPath.exists() {
205			return Ok(CurrentDir.to_path_buf());
206		}
207
208		// Check B: Does a Cargo.toml exist in current directory with package.name =
209		// "SideCar"? → return current directory
210		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								// Verify that Source subdirectory exists as additional confirmation.
219								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		// Check C: Does Element/SideCar/Cargo.toml exist relative to current directory
232		// AND does it have package.name = "SideCar"? → return Element/SideCar
233		// subdirectory path
234		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								// Verify that the Element/SideCar/Source subdirectory exists.
243								let SourceDir = CurrentDir.join("Element").join("SideCar").join("Source");
244
245								if SourceDir.exists() {
246									// Return the full path to the Element/SideCar subdirectory.
247									return Ok(CurrentDir.join("Element").join("SideCar"));
248								}
249							}
250						}
251					}
252				}
253			}
254		}
255
256		// Move up one level.
257		let NextDir = match CurrentDir.parent() {
258			Some(Parent) => Parent,
259
260			None => break, // Reached filesystem root without finding the project
261		};
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
273/// Defines the matrix of platforms to target. Each entry specifies how to
274/// download and identify binaries for a specific architecture.
275fn 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
315/// Defines which sidecars and versions to fetch. This structure makes it
316/// easy to add more sidecars like Deno in the future.
317fn 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
331// --- Helper Functions ---
332
333/// Environment variable for setting the log level.
334pub const LogEnv:&str = "RUST_LOG";
335
336/// Manages the `.gitattributes` file to ensure binaries are tracked by Git LFS.
337/// If the file does not exist, it is created. If it exists, missing rules are
338/// appended.
339fn 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			// This will write a blank line for any empty strings in the array
393			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 out blank lines and comments from the check
404			.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
429// --- Core Logic ---
430
431/// Fetches the official Node.js versions index from nodejs.org.
432async 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
453/// Resolves a major version string (e.g., "22") to the latest full patch
454/// version (e.g., "v22.3.0") using the fetched version index.
455fn 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
464/// Downloads a file from a URL to a specified path.
465async 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	// Stream the download to handle large files without high memory usage.
472	while let Some(Chunk) = Response.chunk().await? {
473		DestinationFile.write_all(&Chunk)?;
474	}
475
476	Ok(())
477}
478
479/// Extracts the contents of a downloaded archive to a target directory.
480/// This function now performs a full extraction to ensure a complete
481/// distribution.
482fn 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
508/// The main asynchronous function for processing a single download task.
509/// This function is designed to be run concurrently for multiple tasks.
510async fn ProcessDownloadTask(Task:DownloadTask, Client:Client, Cache:Arc<Mutex<DownloadCache>>) -> Result<()> {
511	// Create the temporary directory inside the designated "Temporary" subfolder.
512	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 the destination directory already exists, remove it.
557	if Task.DestinationDirectory.exists() {
558		info!("      Removing old version at: {:?}", Task.DestinationDirectory);
559
560		fs::remove_dir_all(&Task.DestinationDirectory)?;
561	}
562
563	// Ensure the parent of the final destination exists.
564	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	// Update the cache with the new version.
578	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
592/// Sets up the global logger for the application.
593pub 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	// [Boot] [Telemetry] Bring up shared dual-pipe (PostHog + OTLP).
622	// No-op in release builds and when `Capture=false`.
623	CommonLibrary::Telemetry::Initialize::Fn(CommonLibrary::Telemetry::Tier::Tier::SideCar).await;
624
625	Logger();
626
627	info!("Starting Universal Sidecar vendoring process...");
628
629	// --- Setup ---
630	let BaseSidecarDirectory = GetBaseSidecarDirectory()?;
631
632	// Manage the .gitattributes file for Git LFS.
633	UpdateGitattributes(&BaseSidecarDirectory)?;
634
635	// Define and create the dedicated directory for temporary downloads.
636	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	// Fetch Node versions once to be used by all tasks.
652	let NodeVersions = FetchNodeVersions(&HttpClient).await?;
653
654	let mut TasksToRun = Vec::new();
655
656	// --- Task Generation Phase (Sequential) ---
657	// First, we determine which downloads are necessary by checking the cache.
658	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				// --- Sidecar-Specific Download Logic ---
671				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					// Check cache to see if we need to download/update.
686					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				// To add Deno, you would add an `else if SidecarName == "DENO"`
740				// block here.
741			}
742		}
743	}
744
745	// --- Concurrent Execution Phase ---
746	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		// Limit to 8 concurrent jobs or num CPUs, whichever is smaller.
752		let NumberOfConcurrentJobs = num_cpus::get().min(8);
753
754		// Spawn a Tokio task for each download.
755		// Run tasks concurrently.
756		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		// Check for any errors that occurred during the concurrent tasks.
769		let mut ErrorsEncountered = 0;
770
771		for Result in Results {
772			// The first result is from tokio::spawn, the second from our function
773			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				// We already logged the error inside `ProcessDownloadTask`, so just count it.
779				// Re-logging here to ensure it's captured at a higher level if needed.
780				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	// --- Finalization ---
792	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
801/// Main executable function.
802fn main() {
803	// We use a block here to handle the Result from Fn.
804	if let Err(Error) = Fn() {
805		// The logger should already be initialized by Fn, so we can use it.
806		error!("The application encountered a fatal error: {}", Error);
807
808		std::process::exit(1);
809	}
810}
811
812// --- Imports ---
813use 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;