mirror of
https://github.com/zhom/banderole.git
synced 2026-06-06 14:33:53 +02:00
refactor: rely on rust compiler for binary generation
This commit is contained in:
Vendored
+1
@@ -23,6 +23,7 @@
|
||||
"Swatinem",
|
||||
"taskkill",
|
||||
"tasklist",
|
||||
"tlsv",
|
||||
"USERPROFILE",
|
||||
"walkdir",
|
||||
"zhom"
|
||||
|
||||
@@ -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
@@ -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(¤t_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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
"#
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
Reference in New Issue
Block a user