From 69e194231457f73887bb178a069f73bd9c63a2eb Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Sat, 29 Nov 2025 13:19:03 +0400 Subject: [PATCH] refactr: speed up first execution --- src/bundler.rs | 13 +- src/executable.rs | 14 +-- src/template/build.rs | 18 +-- src/template/crg.toml | 2 +- src/template/src/main.rs | 254 +++++++++++++++++++++++---------------- 5 files changed, 171 insertions(+), 130 deletions(-) diff --git a/src/bundler.rs b/src/bundler.rs index d42be10..1776b8c 100644 --- a/src/bundler.rs +++ b/src/bundler.rs @@ -27,7 +27,7 @@ pub async fn bundle_project( project_path: PathBuf, output_path: Option, custom_name: Option, - no_compression: bool, + _no_compression: bool, ignore_cached_versions: bool, multi: &MultiProgress, ) -> Result<()> { @@ -133,13 +133,10 @@ pub async fn bundle_project( let mut zip_data: Vec = Vec::new(); { let mut zip = ZipWriter::new(std::io::Cursor::new(&mut zip_data)); - let opts: zip::write::FileOptions<'static, ()> = if no_compression { - zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored) - } else { - zip::write::FileOptions::default() - .compression_method(zip::CompressionMethod::Deflated) - .compression_level(Some(8)) - }; + // Always use uncompressed (Stored) for near-instant extraction + // This makes the executable larger but launch time is much faster + let opts: zip::write::FileOptions<'static, ()> = + zip::write::FileOptions::default().compression_method(zip::CompressionMethod::Stored); // Pre-count app files let app_files = count_files_in_dir(&source_dir, true, true); diff --git a/src/executable.rs b/src/executable.rs index 85e2047..aef66b9 100644 --- a/src/executable.rs +++ b/src/executable.rs @@ -31,17 +31,9 @@ pub fn create_self_extracting_executable_with_progress( copy_template_to_build_dir(build_dir)?; - // For improved compression ratio, store an xz-compressed stream of the zip payload. - // The template executable will decompress XZ first, then read the inner zip. - let xz_path = build_dir.join("embedded_data.xz"); - { - use std::io::Cursor; - let mut xz_bytes: Vec = Vec::new(); - let mut reader = Cursor::new(&zip_data); - lzma_rs::xz_compress(&mut reader, &mut xz_bytes) - .context("Failed to XZ-compress embedded payload")?; - fs::write(&xz_path, &xz_bytes).context("Failed to write embedded xz data")?; - } + // Write zip data directly for fast extraction (no XZ compression) + let zip_path = build_dir.join("embedded_data.zip"); + fs::write(&zip_path, &zip_data).context("Failed to write embedded zip data")?; let build_id_path = build_dir.join("build_id.txt"); fs::write(&build_id_path, &build_id).context("Failed to write build ID")?; diff --git a/src/template/build.rs b/src/template/build.rs index 2524c01..5d8e1c2 100644 --- a/src/template/build.rs +++ b/src/template/build.rs @@ -7,24 +7,24 @@ fn main() { let dest_path = Path::new(&out_dir).join("data.rs"); // Check if we have embedded data files - let xz_data_path = Path::new("embedded_data.xz"); + let zip_data_path = Path::new("embedded_data.zip"); let build_id_path = Path::new("build_id.txt"); - if xz_data_path.exists() && build_id_path.exists() { + if zip_data_path.exists() && build_id_path.exists() { // Read the build ID let build_id = fs::read_to_string(build_id_path) .expect("Failed to read build ID"); - // Copy the xz file to the OUT_DIR so include_bytes! can find it - let out_xz_path = Path::new(&out_dir).join("embedded_data.xz"); - fs::copy(xz_data_path, &out_xz_path) + // Copy the zip file to the OUT_DIR so include_bytes! can find it + let out_zip_path = Path::new(&out_dir).join("embedded_data.zip"); + fs::copy(zip_data_path, &out_zip_path) .expect("Failed to copy embedded data to OUT_DIR"); // Generate the data.rs file with embedded data let data_rs_content = format!( r#" -// Generated at build time - contains embedded application data (xz-compressed zip) -const XZ_DATA: &[u8] = include_bytes!("embedded_data.xz"); +// Generated at build time - contains embedded application data (zip) +const ZIP_DATA: &[u8] = include_bytes!("embedded_data.zip"); const BUILD_ID: &str = "{}"; "#, build_id.trim() @@ -36,7 +36,7 @@ const BUILD_ID: &str = "{}"; // Generate placeholder data for template compilation let data_rs_content = r#" // Placeholder data for template compilation -const XZ_DATA: &[u8] = &[]; +const ZIP_DATA: &[u8] = &[]; const BUILD_ID: &str = "template"; "#; @@ -45,6 +45,6 @@ const BUILD_ID: &str = "template"; } // Tell Cargo to rerun this script if the embedded data changes - println!("cargo:rerun-if-changed=embedded_data.xz"); + println!("cargo:rerun-if-changed=embedded_data.zip"); println!("cargo:rerun-if-changed=build_id.txt"); } diff --git a/src/template/crg.toml b/src/template/crg.toml index e35ba96..775864d 100644 --- a/src/template/crg.toml +++ b/src/template/crg.toml @@ -9,8 +9,8 @@ directories = "6" zip = "4" serde_json = "1.0" fs2 = "0.4" -lzma-rs = "0.3" walkdir = "2.4" +rayon = "1.10" [build-dependencies] # No build dependencies needed - data is embedded at compile time diff --git a/src/template/src/main.rs b/src/template/src/main.rs index 4dafb3b..b34adf5 100644 --- a/src/template/src/main.rs +++ b/src/template/src/main.rs @@ -1,13 +1,13 @@ use anyhow::{Context, Result}; +use directories::BaseDirs; +use fs2::FileExt; +use rayon::prelude::*; use std::env; use std::fs; use std::io::Cursor; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; -use std::ffi::OsString; use zip::ZipArchive; -use directories::BaseDirs; -use fs2::FileExt; // These will be replaced during the build process with actual embedded data // The build script will generate a data.rs file with the actual data @@ -15,17 +15,17 @@ include!(concat!(env!("OUT_DIR"), "/data.rs")); fn main() -> Result<()> { let args: Vec = env::args().collect(); - + // Get cache directory let cache_dir = get_cache_dir().context("Failed to determine cache directory")?; let app_dir = cache_dir.join(&BUILD_ID); let ready_file = app_dir.join(".ready"); - + // Check if already extracted and ready if ready_file.exists() && is_extraction_valid(&app_dir)? { return run_app(&app_dir, &args[1..]); } - + // Use file locking to prevent concurrent extraction let lock_file_path = cache_dir.join(format!("{}.lock", BUILD_ID)); let lock_file = fs::OpenOptions::new() @@ -33,34 +33,38 @@ fn main() -> Result<()> { .write(true) .open(&lock_file_path) .with_context(|| format!("Failed to create lock file at {}", lock_file_path.display()))?; - + // Acquire exclusive lock - lock_file.lock_exclusive().context("Failed to acquire extraction lock")?; - + lock_file + .lock_exclusive() + .context("Failed to acquire extraction lock")?; + // Double-check if extraction completed while waiting for lock if ready_file.exists() && is_extraction_valid(&app_dir)? { // Release lock and run lock_file.unlock().ok(); return run_app(&app_dir, &args[1..]); } - + // Extract application if needed extract_application(&app_dir) .with_context(|| format!("Failed to extract application to {}", app_dir.display()))?; - + // Mark as ready fs::write(&ready_file, "ready") .with_context(|| format!("Failed to create ready file at {}", ready_file.display()))?; - + // Release lock - lock_file.unlock().context("Failed to release extraction lock")?; - + lock_file + .unlock() + .context("Failed to release extraction lock")?; + // Run the application run_app(&app_dir, &args[1..]) } fn get_cache_dir() -> Result { - let cache_dir = BaseDirs::new().unwrap().cache_dir().join("banderole"); + let cache_dir = BaseDirs::new().unwrap().cache_dir().join("banderole"); fs::create_dir_all(&cache_dir).context("Failed to create cache directory")?; Ok(cache_dir) } @@ -69,9 +73,7 @@ fn get_node_executable_path(app_dir: &Path) -> PathBuf { let node_dir = app_dir.join("node"); if cfg!(windows) { // Prefer common locations first - let candidates = [ - node_dir.join("node.exe"), - ]; + let candidates = [node_dir.join("node.exe")]; for c in candidates { if c.exists() { return c; @@ -129,24 +131,32 @@ fn is_extraction_valid(app_dir: &Path) -> Result { let node_executable = node_executable .canonicalize() .unwrap_or_else(|_| node_executable.clone()); - + let package_exists = app_package_json.exists(); let node_exists = node_executable.exists(); - + if !package_exists || !node_exists { // Log debugging information for failed validation eprintln!("Extraction validation failed:"); eprintln!(" App directory: {}", app_dir.display()); - eprintln!(" Package.json exists: {} ({})", package_exists, app_package_json.display()); - eprintln!(" Node executable exists: {} ({})", node_exists, node_executable.display()); - + eprintln!( + " Package.json exists: {} ({})", + package_exists, + app_package_json.display() + ); + eprintln!( + " Node executable exists: {} ({})", + node_exists, + node_executable.display() + ); + if let Ok(entries) = fs::read_dir(app_dir) { eprintln!(" App directory contents:"); for entry in entries.flatten() { eprintln!(" - {}", entry.file_name().to_string_lossy()); } } - + if let Ok(entries) = fs::read_dir(app_dir.join("node")) { eprintln!(" Node directory contents:"); for entry in entries.flatten() { @@ -154,118 +164,156 @@ fn is_extraction_valid(app_dir: &Path) -> Result { } } } - + Ok(package_exists && node_exists) } +struct FileEntry { + path: PathBuf, + data: Vec, + #[cfg(unix)] + mode: Option, +} + +struct DirEntry { + path: PathBuf, +} + fn extract_application(app_dir: &Path) -> Result<()> { // Remove existing directory if it exists to ensure clean extraction if app_dir.exists() { fs::remove_dir_all(app_dir).context("Failed to remove existing app directory")?; } - + // Create app directory fs::create_dir_all(app_dir).context("Failed to create app directory")?; - - // Decompress embedded XZ data to get inner ZIP, then extract - let mut tar_buf: Vec = Vec::new(); - { - let mut reader = Cursor::new(XZ_DATA); - lzma_rs::xz_decompress(&mut reader, &mut tar_buf) - .context("Failed to decompress embedded xz data")?; - } - let cursor = Cursor::new(tar_buf); + + // Read ZIP data directly from embedded bytes (no XZ decompression needed) + let cursor = Cursor::new(ZIP_DATA); let mut archive = ZipArchive::new(cursor).context("Failed to open embedded zip archive")?; - + + // First pass: collect all entries from the ZIP archive + let mut files = Vec::new(); + let mut dirs = Vec::new(); + for i in 0..archive.len() { let mut file = archive.by_index(i).context("Failed to read zip entry")?; - - // Get the file name from the zip entry - let file_name = file.name(); - + + // Get the file name from the zip entry (clone to owned String to avoid borrow issues) + let file_name = file.name().to_string(); + // Skip entries with invalid characters or paths if file_name.is_empty() || file_name.contains('\0') { continue; } - + // Determine if this is a directory entry let is_directory = file_name.ends_with('/') || file.is_dir(); - + // Skip empty directory entries that are just the trailing slash if is_directory && (file_name == "/" || file_name.trim_matches('/').is_empty()) { continue; } - + // Remove trailing slash for proper path construction let clean_file_name = if is_directory { - file_name.trim_end_matches('/') + file_name.trim_end_matches('/').to_string() } else { - file_name + file_name.clone() }; - + // Skip if the cleaned name is empty (shouldn't happen but be safe) if clean_file_name.is_empty() { continue; } - + // Use proper path handling instead of string replacement // Split the path by forward slashes and join using PathBuf for proper platform handling - let path_components: Vec<&str> = clean_file_name.split('/').filter(|s| !s.is_empty()).collect(); - + let path_components: Vec<&str> = clean_file_name + .split('/') + .filter(|s| !s.is_empty()) + .collect(); + // Skip if no valid path components if path_components.is_empty() { continue; } - + let mut outpath = app_dir.to_path_buf(); for component in path_components { outpath = outpath.join(component); } - + // Ensure the path is within the app directory (security check) if !outpath.starts_with(app_dir) { continue; } - + if is_directory { - // Directory entry - create the directory - fs::create_dir_all(&outpath) - .with_context(|| format!("Failed to create directory '{}' from zip entry '{}'", outpath.display(), file_name))?; + dirs.push(DirEntry { path: outpath }); } else { - // File entry - create parent directories first, then the file - if let Some(parent) = outpath.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("Failed to create parent directory '{}' for file '{}'", parent.display(), outpath.display()))?; - } - - let mut outfile = fs::File::create(&outpath) - .with_context(|| format!("Failed to create output file '{}' from zip entry '{}'", outpath.display(), file_name))?; - std::io::copy(&mut file, &mut outfile) - .with_context(|| format!("Failed to extract file to {}", outpath.display()))?; - - // Ensure file is fully written before setting permissions - outfile.sync_all().context("Failed to sync file to disk")?; - drop(outfile); // Explicitly close the file - - // Set executable permissions on Unix systems + // Read file data into memory + let mut data = Vec::new(); + std::io::copy(&mut file, &mut data) + .with_context(|| format!("Failed to read file data from {}", file_name))?; + #[cfg(unix)] - { - if let Some(mode) = file.unix_mode() { - use std::os::unix::fs::PermissionsExt; - let permissions = std::fs::Permissions::from_mode(mode); - fs::set_permissions(&outpath, permissions).context("Failed to set permissions")?; - } - } + let mode = file.unix_mode(); + + files.push(FileEntry { + path: outpath, + data, + #[cfg(unix)] + mode, + }); } } - + + // Second pass: create all directories (must be sequential to avoid conflicts) + for dir in dirs { + fs::create_dir_all(&dir.path) + .with_context(|| format!("Failed to create directory '{}'", dir.path.display()))?; + } + + // Third pass: write all files in parallel for maximum speed + files.par_iter().try_for_each(|entry| -> Result<()> { + // Create parent directories first + if let Some(parent) = entry.path.parent() { + fs::create_dir_all(parent).with_context(|| { + format!( + "Failed to create parent directory '{}' for file '{}'", + parent.display(), + entry.path.display() + ) + })?; + } + + // Write file + fs::write(&entry.path, &entry.data) + .with_context(|| format!("Failed to write file to {}", entry.path.display()))?; + + // Set executable permissions on Unix systems + #[cfg(unix)] + { + if let Some(mode) = entry.mode { + use std::os::unix::fs::PermissionsExt; + let permissions = std::fs::Permissions::from_mode(mode); + fs::set_permissions(&entry.path, permissions).with_context(|| { + format!("Failed to set permissions on {}", entry.path.display()) + })?; + } + } + + Ok(()) + })?; + Ok(()) } fn run_app(app_dir: &Path, args: &[String]) -> Result<()> { let app_path = app_dir.join("app"); let node_executable = get_node_executable_path(app_dir); - + // Verify Node.js executable exists and is accessible if !node_executable.exists() { let app_dir_contents = fs::read_dir(&app_dir) @@ -276,7 +324,7 @@ fn run_app(app_dir: &Path, args: &[String]) -> Result<()> { .collect::>() }) .unwrap_or_else(|e| vec![format!("Error reading app dir: {}", e)]); - + let node_dir_contents = fs::read_dir(app_dir.join("node")) .map(|entries| { entries @@ -285,7 +333,7 @@ fn run_app(app_dir: &Path, args: &[String]) -> Result<()> { .collect::>() }) .unwrap_or_else(|e| vec![format!("Error reading node dir: {}", e)]); - + return Err(anyhow::anyhow!( "Node.js executable not found at: {}\nPlatform: {} {}\nApp directory contents: {:?}\nNode directory contents: {:?}", node_executable.display(), @@ -295,44 +343,44 @@ fn run_app(app_dir: &Path, args: &[String]) -> Result<()> { node_dir_contents )); } - + // On Windows, verify the executable is actually executable #[cfg(windows)] { if let Ok(metadata) = fs::metadata(&node_executable) { if !metadata.is_file() { return Err(anyhow::anyhow!( - "Node.js executable path exists but is not a file: {}", + "Node.js executable path exists but is not a file: {}", node_executable.display() )); } } else { return Err(anyhow::anyhow!( - "Cannot read metadata for Node.js executable: {}", + "Cannot read metadata for Node.js executable: {}", node_executable.display() )); } } - + // Verify app directory exists if !app_path.exists() { return Err(anyhow::anyhow!( - "App directory not found at: {}", + "App directory not found at: {}", app_path.display() )); } - + // Change to app directory env::set_current_dir(&app_path) .with_context(|| format!("Failed to change to app directory: {}", app_path.display()))?; - + // Find main script from package.json let main_script = find_main_script(&app_path)?; - + // Build command arguments let mut cmd_args = vec![main_script.clone()]; cmd_args.extend(args.iter().cloned()); - + let mut last_err: Option = None; let max_attempts: u32 = 8; let mut status: Option = None; @@ -370,28 +418,32 @@ fn run_app(app_dir: &Path, args: &[String]) -> Result<()> { } } } - let status = status.ok_or_else(|| last_err.unwrap_or_else(|| anyhow::anyhow!( - "Failed to execute Node.js application after {} attempts", - max_attempts - )))?; - + let status = status.ok_or_else(|| { + last_err.unwrap_or_else(|| { + anyhow::anyhow!( + "Failed to execute Node.js application after {} attempts", + max_attempts + ) + }) + })?; + std::process::exit(status.code().unwrap_or(1)); } fn find_main_script(app_path: &Path) -> Result { let package_json_path = app_path.join("package.json"); - + if package_json_path.exists() { - let package_content = fs::read_to_string(&package_json_path) - .context("Failed to read package.json")?; - + let package_content = + fs::read_to_string(&package_json_path).context("Failed to read package.json")?; + if let Ok(package_json) = serde_json::from_str::(&package_content) { if let Some(main) = package_json["main"].as_str() { return Ok(main.to_string()); } } } - + // Default to index.js Ok("index.js".to_string()) }