mirror of
https://github.com/zhom/banderole.git
synced 2026-06-06 14:33:53 +02:00
refactor: extract executable template into its own file
This commit is contained in:
+73
-403
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,277 @@
|
||||
use crate::platform::Platform;
|
||||
use anyhow::{Context, Result};
|
||||
use base64::Engine as _;
|
||||
use std::fs;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
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();
|
||||
|
||||
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 Unix-compatible self-extracting executable
|
||||
fn create_unix_executable(out: &Path, zip_data: Vec<u8>, build_id: &str) -> Result<()> {
|
||||
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/{}"
|
||||
READY_FILE="$APP_DIR/.ready"
|
||||
|
||||
run_app() {{
|
||||
cd "$APP_DIR/app" || exit 1
|
||||
|
||||
# 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")
|
||||
|
||||
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__
|
||||
"#,
|
||||
build_id
|
||||
);
|
||||
|
||||
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")?;
|
||||
|
||||
let mut perms = file.metadata()?.permissions();
|
||||
perms.set_mode(0o755);
|
||||
fs::set_permissions(out, perms)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a Windows-compatible self-extracting executable
|
||||
fn create_windows_executable(out: &Path, zip_data: Vec<u8>, build_id: &str) -> Result<()> {
|
||||
let mut file = fs::File::create(out).context("Failed to create output executable")?;
|
||||
|
||||
let script = format!(
|
||||
r#"@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
set "CACHE_DIR=%LOCALAPPDATA%\banderole"
|
||||
set "APP_DIR=!CACHE_DIR!\{}"
|
||||
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"
|
||||
|
||||
:run_app
|
||||
cd /d "!APP_DIR!\app"
|
||||
|
||||
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
|
||||
|
||||
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!\%RANDOM%-%TIME:~6,5%.queue"
|
||||
echo %RANDOM% > "!QUEUE_ENTRY!"
|
||||
|
||||
:acquire_lock
|
||||
if not exist "!LOCK_FILE!" (
|
||||
mkdir "!LOCK_FILE!" 2>nul
|
||||
if !errorlevel! equ 0 (
|
||||
echo !RANDOM! > "!EXTRACTION_PID_FILE!"
|
||||
goto extract
|
||||
)
|
||||
)
|
||||
:wait_for_ready
|
||||
if exist "!READY_FILE!" (
|
||||
:wait_queue_turn
|
||||
if not exist "!QUEUE_ENTRY!" goto run_app
|
||||
if exist "!READY_FILE!" goto run_app
|
||||
|
||||
timeout /t 1 /nobreak >nul 2>&1
|
||||
goto wait_queue_turn
|
||||
)
|
||||
|
||||
if exist "!EXTRACTION_PID_FILE!" (
|
||||
timeout /t 1 /nobreak >nul 2>&1
|
||||
goto wait_for_ready
|
||||
) else (
|
||||
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
|
||||
goto run_app
|
||||
)
|
||||
|
||||
if not exist "!CACHE_DIR!" mkdir "!CACHE_DIR!"
|
||||
if not exist "!APP_DIR!" mkdir "!APP_DIR!"
|
||||
|
||||
set "TEMP_ZIP=%TEMP%\banderole-bundle-%RANDOM%.zip"
|
||||
powershell -NoProfile -Command "$content = Get-Content '%~f0' -Raw; $dataStart = $content.IndexOf('__DATA__') + 8; $data = $content.Substring($dataStart).Trim(); [System.IO.File]::WriteAllBytes('%TEMP_ZIP%', [System.Convert]::FromBase64String($data))"
|
||||
|
||||
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
|
||||
|
||||
__DATA__
|
||||
"#,
|
||||
build_id
|
||||
);
|
||||
|
||||
file.write_all(script.as_bytes())?;
|
||||
|
||||
// Append base64-encoded zip data
|
||||
let encoded = base64::engine::general_purpose::STANDARD.encode(&zip_data);
|
||||
file.write_all(encoded.as_bytes())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user