refactr: speed up first execution

This commit is contained in:
zhom
2025-11-29 13:19:03 +04:00
parent ce1cd6d018
commit 69e1942314
5 changed files with 171 additions and 130 deletions
+5 -8
View File
@@ -27,7 +27,7 @@ pub async fn bundle_project(
project_path: PathBuf,
output_path: Option<PathBuf>,
custom_name: Option<String>,
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<u8> = 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);
+3 -11
View File
@@ -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<u8> = 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")?;
+9 -9
View File
@@ -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");
}
+1 -1
View File
@@ -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
+153 -101
View File
@@ -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<String> = 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<PathBuf> {
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<bool> {
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<bool> {
}
}
}
Ok(package_exists && node_exists)
}
struct FileEntry {
path: PathBuf,
data: Vec<u8>,
#[cfg(unix)]
mode: Option<u32>,
}
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<u8> = 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::<Vec<_>>()
})
.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::<Vec<_>>()
})
.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<anyhow::Error> = None;
let max_attempts: u32 = 8;
let mut status: Option<std::process::ExitStatus> = 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<String> {
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::<serde_json::Value>(&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())
}