refactor: rely on rust compiler for binary generation

This commit is contained in:
zhom
2025-07-28 15:45:25 +04:00
parent 0c3b1cad19
commit b09a96ce46
8 changed files with 521 additions and 342 deletions
+1
View File
@@ -23,6 +23,7 @@
"Swatinem",
"taskkill",
"tasklist",
"tlsv",
"USERPROFILE",
"walkdir",
"zhom"
+45
View File
@@ -0,0 +1,45 @@
use anyhow::{Context, Result};
use std::fs;
use std::path::Path;
/// Embedded template files
pub struct EmbeddedTemplate {
pub cargo_toml: &'static str,
pub build_rs: &'static str,
pub main_rs: &'static str,
}
impl EmbeddedTemplate {
/// Get the embedded template files
pub fn new() -> Self {
Self {
cargo_toml: include_str!("../template/Cargo.toml"),
build_rs: include_str!("../template/build.rs"),
main_rs: include_str!("../template/src/main.rs"),
}
}
/// Write the template files to a build directory
pub fn write_to_dir(&self, build_dir: &Path) -> Result<()> {
// Create the src directory
let src_dir = build_dir.join("src");
fs::create_dir_all(&src_dir).context("Failed to create src directory")?;
// Write Cargo.toml
let cargo_toml_path = build_dir.join("Cargo.toml");
fs::write(&cargo_toml_path, self.cargo_toml)
.context("Failed to write Cargo.toml")?;
// Write build.rs
let build_rs_path = build_dir.join("build.rs");
fs::write(&build_rs_path, self.build_rs)
.context("Failed to write build.rs")?;
// Write src/main.rs
let main_rs_path = src_dir.join("main.rs");
fs::write(&main_rs_path, self.main_rs)
.context("Failed to write src/main.rs")?;
Ok(())
}
}
+135 -342
View File
@@ -1,365 +1,158 @@
use crate::platform::Platform;
use anyhow::{Context, Result};
use base64::Engine as _;
use std::fs;
use std::io::Write;
use std::path::Path;
use std::process::Command;
use tempfile::TempDir;
use uuid::Uuid;
/// Create a self-extracting executable for the current platform
pub fn create_self_extracting_executable(
out: &Path,
zip_data: Vec<u8>,
_app_name: &str,
) -> Result<()> {
let build_id = Uuid::new_v4();
use crate::embedded_template::EmbeddedTemplate;
use crate::platform::Platform;
use crate::rust_toolchain::RustToolchain;
if Platform::current().is_windows() {
create_windows_executable(out, zip_data, &build_id.to_string())
} else {
create_unix_executable(out, zip_data, &build_id.to_string())
/// Create a cross-platform Rust executable with embedded data
pub fn create_self_extracting_executable(
output_path: &Path,
zip_data: Vec<u8>,
app_name: &str,
) -> Result<()> {
// Check if Rust toolchain is available
if let Err(e) = RustToolchain::check_availability() {
eprintln!("\nError: {}", e);
eprintln!("{}", RustToolchain::get_installation_instructions());
return Err(e);
}
let build_id = Uuid::new_v4().to_string();
// Create temporary directory for building
let temp_dir = TempDir::new().context("Failed to create temporary directory")?;
let build_dir = temp_dir.path();
// Copy template to build directory
copy_template_to_build_dir(build_dir)?;
// Write embedded data
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")?;
// Update Cargo.toml with app name
update_cargo_toml(build_dir, app_name)?;
// Build the executable
build_executable(build_dir, output_path, app_name)?;
Ok(())
}
/// Create a Unix-compatible self-extracting executable
fn create_unix_executable(out: &Path, zip_data: Vec<u8>, build_id: &str) -> Result<()> {
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
let mut file = fs::File::create(out).context("Failed to create output executable")?;
let script = format!(
r#"#!/bin/bash
set -e
if [ -n "$XDG_CACHE_HOME" ]; then
CACHE_DIR="$XDG_CACHE_HOME/banderole"
elif [ -n "$HOME" ]; then
CACHE_DIR="$HOME/.cache/banderole"
else
CACHE_DIR="/tmp/banderole-cache"
fi
APP_DIR="$CACHE_DIR/{build_id}"
READY_FILE="$APP_DIR/.ready"
run_app() {{
cd "$APP_DIR/app" || exit 1
fn copy_template_to_build_dir(build_dir: &Path) -> Result<()> {
// Use embedded template files instead of filesystem copy
let template = EmbeddedTemplate::new();
template.write_to_dir(build_dir)
.context("Failed to write embedded template files to build directory")?;
# Find main script from package.json
MAIN_SCRIPT=$("$APP_DIR/node/bin/node" -e "try {{ console.log(require('./package.json').main || 'index.js'); }} catch(e) {{ console.log('index.js'); }}" 2>/dev/null || echo "index.js")
Ok(())
}
fn update_cargo_toml(build_dir: &Path, app_name: &str) -> Result<()> {
let cargo_toml_path = build_dir.join("Cargo.toml");
let cargo_content = fs::read_to_string(&cargo_toml_path)
.context("Failed to read Cargo.toml")?;
if [ -f "$MAIN_SCRIPT" ]; then
exec "$APP_DIR/node/bin/node" "$MAIN_SCRIPT" "$@"
else
echo "Error: Main script '$MAIN_SCRIPT' not found" >&2
exit 1
fi
}}
if [ -f "$READY_FILE" ] && [ -f "$APP_DIR/app/package.json" ] && [ -x "$APP_DIR/node/bin/node" ]; then
run_app "$@"
fi
mkdir -p "$CACHE_DIR" 2>/dev/null || true
ATTEMPTS=0
MAX_ATTEMPTS=30
while [ $ATTEMPTS -lt $MAX_ATTEMPTS ]; do
if mkdir "$APP_DIR" 2>/dev/null; then
TEMP_ZIP=$(mktemp) || exit 1
trap 'rm -f "$TEMP_ZIP"' EXIT
awk '/^__DATA__$/{{p=1;next}} p{{print}}' "$0" | base64 -d > "$TEMP_ZIP"
if [ ! -s "$TEMP_ZIP" ]; then
echo "Error: Failed to extract bundle data" >&2
rm -rf "$APP_DIR" 2>/dev/null || true
exit 1
fi
if ! unzip -q "$TEMP_ZIP" -d "$APP_DIR" 2>/dev/null; then
echo "Error: Failed to extract bundle" >&2
rm -rf "$APP_DIR" 2>/dev/null || true
exit 1
fi
if [ ! -f "$APP_DIR/app/package.json" ] || [ ! -x "$APP_DIR/node/bin/node" ]; then
echo "Error: Bundle extraction incomplete" >&2
rm -rf "$APP_DIR" 2>/dev/null || true
exit 1
fi
touch "$READY_FILE"
run_app "$@"
else
if [ -f "$READY_FILE" ] && [ -f "$APP_DIR/app/package.json" ] && [ -x "$APP_DIR/node/bin/node" ]; then
run_app "$@"
fi
ATTEMPTS=$((ATTEMPTS + 1))
if [ $ATTEMPTS -lt $MAX_ATTEMPTS ]; then
DELAY=$(awk "BEGIN {{print int(100 * (2 ^ (($ATTEMPTS - 1) / 3)) + 0.5)}}" 2>/dev/null || echo 100)
if [ "$DELAY" -gt 1000 ]; then
DELAY=1000
fi
sleep $(awk "BEGIN {{print $DELAY / 1000}}" 2>/dev/null || echo 0.1)
fi
fi
done
# If we've exhausted retries, exit with error
echo "Error: Failed to extract or run application after $MAX_ATTEMPTS attempts" >&2
exit 1
__DATA__
"#
// Replace the package name
let updated_content = cargo_content.replace(
r#"name = "banderole-app""#,
&format!(r#"name = "{}""#, sanitize_package_name(app_name))
);
fs::write(&cargo_toml_path, updated_content)
.context("Failed to write updated Cargo.toml")?;
Ok(())
}
file.write_all(script.as_bytes())?;
let encoded = base64::engine::general_purpose::STANDARD.encode(&zip_data);
file.write_all(encoded.as_bytes())?;
file.write_all(b"\n")?;
fn sanitize_package_name(name: &str) -> String {
// Rust package names must be valid identifiers
name.chars()
.map(|c| if c.is_alphanumeric() || c == '_' || c == '-' { c } else { '-' })
.collect::<String>()
.trim_start_matches(|c: char| c.is_numeric() || c == '-')
.to_string()
}
fn build_executable(build_dir: &Path, output_path: &Path, app_name: &str) -> Result<()> {
let current_platform = Platform::current();
let target_triple = get_target_triple(&current_platform);
// Ensure we have the target installed
install_rust_target(&target_triple)?;
// Build the executable
let mut cmd = Command::new("cargo");
cmd.current_dir(build_dir)
.args(&["build", "--release", "--target", &target_triple]);
let output = cmd.output().context("Failed to execute cargo build")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("Cargo build failed:\n{}", stderr);
}
// Get the sanitized package name to find the correct executable
let package_name = sanitize_package_name(app_name);
let executable_name = if current_platform.is_windows() {
format!("{}.exe", package_name)
} else {
package_name
};
let built_executable = build_dir
.join("target")
.join(&target_triple)
.join("release")
.join(executable_name);
if !built_executable.exists() {
anyhow::bail!("Built executable not found at {}", built_executable.display());
}
// Ensure output directory exists
if let Some(parent) = output_path.parent() {
fs::create_dir_all(parent).context("Failed to create output directory")?;
}
fs::copy(&built_executable, output_path)
.context("Failed to copy built executable to output path")?;
// Set executable permissions on Unix systems
#[cfg(unix)]
{
let mut perms = file.metadata()?.permissions();
use std::os::unix::fs::PermissionsExt;
let mut perms = fs::metadata(output_path)?.permissions();
perms.set_mode(0o755);
fs::set_permissions(out, perms)?;
fs::set_permissions(output_path, perms)?;
}
Ok(())
}
/// Create a Windows-compatible self-extracting executable
fn create_windows_executable(out: &Path, zip_data: Vec<u8>, build_id: &str) -> Result<()> {
let bat_path = out.with_extension("bat");
let mut file = fs::File::create(&bat_path).context("Failed to create output batch file")?;
let script = format!(
r#"@echo off
setlocal enabledelayedexpansion
set "CACHE_DIR=%LOCALAPPDATA%\banderole"
set "APP_DIR=!CACHE_DIR!\{build_id}"
set "LOCK_FILE=!APP_DIR!\.lock"
set "READY_FILE=!APP_DIR!\.ready"
set "QUEUE_DIR=!APP_DIR!\.queue"
set "EXTRACTION_PID_FILE=!APP_DIR!\.extraction.pid"
rem Get current process ID with better uniqueness
set "CURRENT_PID=%RANDOM%-%TIME:~6,5%"
for /f "tokens=2 delims=," %%i in ('tasklist /fi "imagename eq cmd.exe" /fo csv 2^>nul ^| find /v "ImageName" 2^>nul') do (
set "CURRENT_PID=%%~i-%RANDOM%"
goto :got_pid
)
:got_pid
if "!CURRENT_PID!"=="" set "CURRENT_PID=%RANDOM%-%TIME:~6,5%"
rem Check if app is already extracted and ready
if exist "!READY_FILE!" if exist "!APP_DIR!\app\package.json" if exist "!APP_DIR!\node\node.exe" (
goto run_app
)
if not exist "!QUEUE_DIR!" mkdir "!QUEUE_DIR!" 2>nul
set "QUEUE_ENTRY=!QUEUE_DIR!\!CURRENT_PID!.queue"
echo !CURRENT_PID! > "!QUEUE_ENTRY!"
set /a WAIT_COUNT=0
set /a MAX_WAIT=120
:acquire_lock
call :cleanup_stale_locks
if not exist "!LOCK_FILE!" (
mkdir "!LOCK_FILE!" 2>nul
if !errorlevel! equ 0 (
echo !CURRENT_PID! > "!EXTRACTION_PID_FILE!"
goto extract
)
)
:wait_for_ready
if exist "!READY_FILE!" (
if exist "!APP_DIR!\app\package.json" if exist "!APP_DIR!\node\node.exe" (
call :cleanup_queue
goto run_app
)
)
set /a WAIT_COUNT+=1
if !WAIT_COUNT! geq !MAX_WAIT! (
echo Error: Timeout waiting for extraction to complete >&2
call :cleanup_queue
exit /b 1
)
if exist "!EXTRACTION_PID_FILE!" (
timeout /t 1 /nobreak >nul 2>&1
goto wait_for_ready
) else (
timeout /t 1 /nobreak >nul 2>&1
goto acquire_lock
)
:extract
if exist "!READY_FILE!" if exist "!APP_DIR!\app\package.json" if exist "!APP_DIR!\node\node.exe" (
call :cleanup_queue
rmdir "!LOCK_FILE!" 2>nul
del "!EXTRACTION_PID_FILE!" 2>nul
goto run_app
)
if not exist "!CACHE_DIR!" mkdir "!CACHE_DIR!"
if not exist "!APP_DIR!" mkdir "!APP_DIR!"
rem Create unique temp file name to avoid conflicts
set "TEMP_ZIP=%TEMP%\banderole-!CURRENT_PID!.zip"
set "TEMP_SCRIPT=%TEMP%\banderole-extract-!CURRENT_PID!.ps1"
rem Create PowerShell script file to avoid command line length issues
echo $scriptPath = "%~f0" > "!TEMP_SCRIPT!"
echo $content = [System.IO.File]::ReadAllText($scriptPath, [System.Text.Encoding]::UTF8) >> "!TEMP_SCRIPT!"
echo $dataMarker = '__DATA__' >> "!TEMP_SCRIPT!"
echo $dataStart = $content.IndexOf($dataMarker) >> "!TEMP_SCRIPT!"
echo if ($dataStart -eq -1) {{ Write-Error 'Data marker not found'; exit 1 }} >> "!TEMP_SCRIPT!"
echo $dataStart += $dataMarker.Length >> "!TEMP_SCRIPT!"
echo $rawData = $content.Substring($dataStart) >> "!TEMP_SCRIPT!"
echo $data = ($rawData -split '[\r\n]+' ^| Where-Object {{ $_.Trim() -ne '' }}) -join '' >> "!TEMP_SCRIPT!"
echo if ([string]::IsNullOrWhiteSpace($data)) {{ Write-Error 'No data found after marker'; exit 1 }} >> "!TEMP_SCRIPT!"
echo try {{ >> "!TEMP_SCRIPT!"
echo [System.IO.File]::WriteAllBytes("%TEMP%\banderole-!CURRENT_PID!.zip", [System.Convert]::FromBase64String($data)) >> "!TEMP_SCRIPT!"
echo Write-Output "%TEMP%\banderole-!CURRENT_PID!.zip" >> "!TEMP_SCRIPT!"
echo }} catch {{ >> "!TEMP_SCRIPT!"
echo Write-Error ("Base64 decode failed: " + $_.Exception.Message) >> "!TEMP_SCRIPT!"
echo exit 1 >> "!TEMP_SCRIPT!"
echo }} >> "!TEMP_SCRIPT!"
powershell -NoProfile -ExecutionPolicy Bypass -File "!TEMP_SCRIPT!" > "%TEMP%\temp_zip_path.txt" 2>&1
set "EXTRACT_EXIT_CODE=!errorlevel!"
del "!TEMP_SCRIPT!" 2>nul
if !EXTRACT_EXIT_CODE! neq 0 (
type "%TEMP%\temp_zip_path.txt" >>&2
del "%TEMP%\temp_zip_path.txt" 2>nul
echo Error: Failed to extract bundle data >&2
call :cleanup_queue
rmdir "!LOCK_FILE!" 2>nul
del "!EXTRACTION_PID_FILE!" 2>nul
exit /b 1
)
set /p TEMP_ZIP=<"%TEMP%\temp_zip_path.txt"
del "%TEMP%\temp_zip_path.txt" 2>nul
if not exist "!TEMP_ZIP!" (
echo Error: Failed to extract bundle data >&2
call :cleanup_queue
rmdir "!LOCK_FILE!" 2>nul
del "!EXTRACTION_PID_FILE!" 2>nul
exit /b 1
)
powershell -NoProfile -Command "try {{ Expand-Archive -Path '!TEMP_ZIP!' -DestinationPath '!APP_DIR!' -Force }} catch {{ Write-Error $_.Exception.Message; exit 1 }}"
set "EXTRACT_RESULT=!errorlevel!"
del "!TEMP_ZIP!" 2>nul
if !EXTRACT_RESULT! neq 0 (
echo Error: Failed to extract bundle >&2
call :cleanup_queue
rmdir /s /q "!APP_DIR!" 2>nul
rmdir "!LOCK_FILE!" 2>nul
del "!EXTRACTION_PID_FILE!" 2>nul
exit /b 1
)
if not exist "!APP_DIR!\app\package.json" (
echo Error: Bundle extraction incomplete >&2
call :cleanup_queue
rmdir /s /q "!APP_DIR!" 2>nul
rmdir "!LOCK_FILE!" 2>nul
del "!EXTRACTION_PID_FILE!" 2>nul
exit /b 1
)
if not exist "!APP_DIR!\node\node.exe" (
echo Error: Node.js executable not found >&2
call :cleanup_queue
rmdir /s /q "!APP_DIR!" 2>nul
rmdir "!LOCK_FILE!" 2>nul
del "!EXTRACTION_PID_FILE!" 2>nul
exit /b 1
)
echo ready > "!READY_FILE!"
if exist "!QUEUE_DIR!" (
for %%f in ("!QUEUE_DIR!\*.queue") do (
if exist "%%f" del "%%f" 2>nul
)
)
del "!EXTRACTION_PID_FILE!" 2>nul
call :cleanup_queue
rmdir "!LOCK_FILE!" 2>nul
goto run_app
:run_app
cd /d "!APP_DIR!\app" || exit /b 1
for /f "delims=" %%i in ('"!APP_DIR!\node\node.exe" -e "try {{ console.log(require('./package.json').main ^|^| 'index.js'); }} catch(e) {{ console.log('index.js'); }}" 2^>nul') do set "MAIN_SCRIPT=%%i"
if "!MAIN_SCRIPT!"=="" set "MAIN_SCRIPT=index.js"
if exist "!MAIN_SCRIPT!" (
"!APP_DIR!\node\node.exe" "!MAIN_SCRIPT!" %*
exit /b !errorlevel!
) else (
echo Error: Main script '!MAIN_SCRIPT!' not found >&2
exit /b 1
)
:cleanup_queue
if defined QUEUE_ENTRY if exist "!QUEUE_ENTRY!" (
del "!QUEUE_ENTRY!" 2>nul
)
exit /b
:cleanup_stale_locks
if exist "!LOCK_FILE!" (
for /f %%i in ('powershell -NoProfile -Command "if (Test-Path '!LOCK_FILE!') {{ $age = (Get-Date) - (Get-Item '!LOCK_FILE!').CreationTime; if ($age.TotalSeconds -gt 300) {{ Write-Output 'stale' }} }}"') do (
if "%%i"=="stale" (
rmdir "!LOCK_FILE!" 2>nul
del "!EXTRACTION_PID_FILE!" 2>nul
)
)
)
exit /b
"#
);
file.write_all(script.as_bytes())?;
// Add data marker with proper line ending
file.write_all(b"\r\n__DATA__\r\n")?;
// Append base64-encoded zip data with proper line breaks for Windows
let encoded = base64::engine::general_purpose::STANDARD.encode(&zip_data);
// Split into 76-character lines as per Base64 standard
let lines: Vec<&str> = encoded
.as_bytes()
.chunks(76)
.map(|chunk| std::str::from_utf8(chunk).unwrap())
.collect();
for line in lines {
file.write_all(line.as_bytes())?;
file.write_all(b"\r\n")?;
fn get_target_triple(platform: &Platform) -> String {
match platform {
Platform::MacosX64 => "x86_64-apple-darwin".to_string(),
Platform::MacosArm64 => "aarch64-apple-darwin".to_string(),
Platform::LinuxX64 => "x86_64-unknown-linux-gnu".to_string(),
Platform::LinuxArm64 => "aarch64-unknown-linux-gnu".to_string(),
Platform::WindowsX64 => "x86_64-pc-windows-msvc".to_string(),
Platform::WindowsArm64 => "aarch64-pc-windows-msvc".to_string(),
}
Ok(())
}
fn install_rust_target(target: &str) -> Result<()> {
RustToolchain::ensure_target_installed(target)
}
+2
View File
@@ -1,8 +1,10 @@
mod bundler;
mod embedded_template;
mod executable;
mod node_downloader;
mod node_version_manager;
mod platform;
mod rust_toolchain;
use clap::{Parser, Subcommand};
use std::path::PathBuf;
+114
View File
@@ -0,0 +1,114 @@
use anyhow::{Context, Result};
use std::process::Command;
/// Manages Rust toolchain requirements and installation
pub struct RustToolchain;
impl RustToolchain {
/// Check if Rust toolchain is available and properly configured
pub fn check_availability() -> Result<()> {
// Check if rustc is available
let rustc_output = Command::new("rustc")
.arg("--version")
.output();
match rustc_output {
Ok(output) if output.status.success() => {
let version = String::from_utf8_lossy(&output.stdout);
println!("Found Rust compiler: {}", version.trim());
}
_ => {
return Err(anyhow::anyhow!(
"Rust compiler (rustc) not found. Please install Rust from https://rustup.rs/"
));
}
}
// Check if cargo is available
let cargo_output = Command::new("cargo")
.arg("--version")
.output();
match cargo_output {
Ok(output) if output.status.success() => {
let version = String::from_utf8_lossy(&output.stdout);
println!("Found Cargo: {}", version.trim());
}
_ => {
return Err(anyhow::anyhow!(
"Cargo not found. Please install Rust from https://rustup.rs/"
));
}
}
// Check if rustup is available (for target management)
let rustup_output = Command::new("rustup")
.arg("--version")
.output();
match rustup_output {
Ok(output) if output.status.success() => {
let version = String::from_utf8_lossy(&output.stdout);
println!("Found rustup: {}", version.trim());
}
_ => {
return Err(anyhow::anyhow!(
"rustup not found. Please install Rust from https://rustup.rs/"
));
}
}
Ok(())
}
/// Install a Rust target if not already installed
pub fn ensure_target_installed(target: &str) -> Result<()> {
// Check if target is already installed
let output = Command::new("rustup")
.args(&["target", "list", "--installed"])
.output()
.context("Failed to check installed targets")?;
let installed_targets = String::from_utf8_lossy(&output.stdout);
if !installed_targets.contains(target) {
println!("Installing Rust target: {}", target);
let install_output = Command::new("rustup")
.args(&["target", "add", target])
.output()
.context("Failed to install Rust target")?;
if !install_output.status.success() {
let stderr = String::from_utf8_lossy(&install_output.stderr);
anyhow::bail!("Failed to install target {}:\n{}", target, stderr);
}
println!("Successfully installed target: {}", target);
} else {
println!("Target {} is already installed", target);
}
Ok(())
}
/// Get helpful installation instructions for the user
pub fn get_installation_instructions() -> String {
format!(
r#"
Rust toolchain is required to build portable executables.
To install Rust:
1. Visit https://rustup.rs/
2. Follow the installation instructions for your platform
3. Restart your terminal/command prompt
4. Verify installation with: rustc --version
For automated installation:
- Linux/macOS: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
- Windows: Download and run rustup-init.exe from https://rustup.rs/
After installation, you can use banderole to create portable Node.js executables
without requiring users to have Node.js or Rust installed.
"#
)
}
}
+21
View File
@@ -0,0 +1,21 @@
[package]
name = "banderole-app"
version = "1.0.0"
edition = "2021"
[dependencies]
anyhow = "1.0"
directories = "6"
zip = "4"
serde_json = "1.0"
[build-dependencies]
# No build dependencies needed - data is embedded at compile time
# Optimize for size and performance
[profile.release]
opt-level = "z" # Optimize for size
lto = true # Enable Link Time Optimization
codegen-units = 1 # Reduce number of codegen units to increase optimizations
panic = "abort" # Abort on panic (smaller binary)
strip = true # Strip symbols from binary
+50
View File
@@ -0,0 +1,50 @@
use std::env;
use std::fs;
use std::path::Path;
fn main() {
let out_dir = env::var("OUT_DIR").unwrap();
let dest_path = Path::new(&out_dir).join("data.rs");
// Check if we have embedded data files
let zip_data_path = Path::new("embedded_data.zip");
let build_id_path = Path::new("build_id.txt");
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 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
const ZIP_DATA: &[u8] = include_bytes!("embedded_data.zip");
const BUILD_ID: &str = "{}";
"#,
build_id.trim()
);
fs::write(&dest_path, data_rs_content)
.expect("Failed to write data.rs");
} else {
// Generate placeholder data for template compilation
let data_rs_content = r#"
// Placeholder data for template compilation
const ZIP_DATA: &[u8] = &[];
const BUILD_ID: &str = "template";
"#;
fs::write(&dest_path, data_rs_content)
.expect("Failed to write placeholder data.rs");
}
// Tell Cargo to rerun this script if the embedded data changes
println!("cargo:rerun-if-changed=embedded_data.zip");
println!("cargo:rerun-if-changed=build_id.txt");
}
+153
View File
@@ -0,0 +1,153 @@
use anyhow::{Context, Result};
use std::env;
use std::fs;
use std::io::Cursor;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
use zip::ZipArchive;
// 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
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()?;
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..]);
}
// Extract application if needed
extract_application(&app_dir)?;
// Mark as ready
fs::write(&ready_file, "ready")?;
// Run the application
run_app(&app_dir, &args[1..])
}
fn get_cache_dir() -> Result<PathBuf> {
let cache_dir = if let Some(xdg_cache) = env::var_os("XDG_CACHE_HOME") {
PathBuf::from(xdg_cache).join("banderole")
} else if let Some(home) = env::var_os("HOME") {
PathBuf::from(home).join(".cache").join("banderole")
} else if cfg!(windows) {
if let Some(appdata) = env::var_os("LOCALAPPDATA") {
PathBuf::from(appdata).join("banderole")
} else if let Some(temp) = env::var_os("TEMP") {
PathBuf::from(temp).join("banderole-cache")
} else {
PathBuf::from("C:\\temp\\banderole-cache")
}
} else {
PathBuf::from("/tmp/banderole-cache")
};
fs::create_dir_all(&cache_dir).context("Failed to create cache directory")?;
Ok(cache_dir)
}
fn is_extraction_valid(app_dir: &Path) -> Result<bool> {
let app_package_json = app_dir.join("app").join("package.json");
let node_executable = if cfg!(windows) {
app_dir.join("node").join("node.exe")
} else {
app_dir.join("node").join("bin").join("node")
};
Ok(app_package_json.exists() && node_executable.exists())
}
fn extract_application(app_dir: &Path) -> Result<()> {
// Create app directory
fs::create_dir_all(app_dir).context("Failed to create app directory")?;
// Extract embedded zip data
let cursor = Cursor::new(ZIP_DATA);
let mut archive = ZipArchive::new(cursor).context("Failed to open embedded zip archive")?;
for i in 0..archive.len() {
let mut file = archive.by_index(i).context("Failed to read zip entry")?;
let outpath = app_dir.join(file.name());
if file.name().ends_with('/') {
// Directory
fs::create_dir_all(&outpath).context("Failed to create directory")?;
} else {
// File
if let Some(parent) = outpath.parent() {
fs::create_dir_all(parent).context("Failed to create parent directory")?;
}
let mut outfile = fs::File::create(&outpath).context("Failed to create output file")?;
std::io::copy(&mut file, &mut outfile).context("Failed to extract file")?;
// Set executable permissions on Unix systems
#[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")?;
}
}
}
}
Ok(())
}
fn run_app(app_dir: &Path, args: &[String]) -> Result<()> {
let app_path = app_dir.join("app");
let node_executable = if cfg!(windows) {
app_dir.join("node").join("node.exe")
} else {
app_dir.join("node").join("bin").join("node")
};
// Change to app directory
env::set_current_dir(&app_path).context("Failed to change to app directory")?;
// Find main script from package.json
let main_script = find_main_script(&app_path)?;
// Build command arguments
let mut cmd_args = vec![main_script];
cmd_args.extend(args.iter().cloned());
// Execute Node.js application
let status = Command::new(&node_executable)
.args(&cmd_args)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.context("Failed to execute Node.js application")?;
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")?;
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())
}