mirror of
https://github.com/zhom/banderole.git
synced 2026-06-06 06:23:53 +02:00
refactr: speed up first execution
This commit is contained in:
+5
-8
@@ -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
@@ -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")?;
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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
@@ -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())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user