Fix production mode: resolve static/data paths relative to exe location, include static/ in release packages
Some checks failed
Release / Build Linux (x86_64) (push) Has been cancelled
Release / Build macOS (x86_64) (push) Has been cancelled
Release / Build Windows (x86_64) (push) Has been cancelled
Release / Create Release (push) Has been cancelled

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
skepsismusic
2026-02-06 00:40:45 +01:00
parent cd1cb27d49
commit f5bdf461ba
3 changed files with 36 additions and 10 deletions

View File

@@ -73,6 +73,7 @@ jobs:
RELEASE_DIR="${{ matrix.asset_name }}-${{ github.ref_name }}"
mkdir -p "$RELEASE_DIR"
cp "backend/target/${{ matrix.target }}/release/ironpad" "$RELEASE_DIR/"
cp -r backend/static "$RELEASE_DIR/static"
cp README.md LICENSE "$RELEASE_DIR/"
tar czf "$RELEASE_DIR.tar.gz" "$RELEASE_DIR"
echo "ASSET=$RELEASE_DIR.tar.gz" >> $GITHUB_ENV
@@ -84,6 +85,7 @@ jobs:
RELEASE_DIR="${{ matrix.asset_name }}-${{ github.ref_name }}"
mkdir -p "$RELEASE_DIR"
cp "backend/target/${{ matrix.target }}/release/ironpad.exe" "$RELEASE_DIR/"
cp -r backend/static "$RELEASE_DIR/static"
cp README.md LICENSE "$RELEASE_DIR/"
7z a "$RELEASE_DIR.zip" "$RELEASE_DIR"
echo "ASSET=$RELEASE_DIR.zip" >> $GITHUB_ENV

View File

@@ -5,19 +5,39 @@ use std::sync::OnceLock;
/// Priority: IRONPAD_DATA_DIR env var > auto-detect (production vs development).
static DATA_DIR: OnceLock<PathBuf> = OnceLock::new();
/// Directory where the executable lives.
/// Used to resolve `static/` and `data/` in production mode.
static EXE_DIR: OnceLock<PathBuf> = OnceLock::new();
/// Get the directory containing the executable.
/// Falls back to "." if detection fails.
pub fn exe_dir() -> &'static Path {
EXE_DIR.get_or_init(|| {
std::env::current_exe()
.ok()
.and_then(|p| p.parent().map(|p| p.to_path_buf()))
.unwrap_or_else(|| PathBuf::from("."))
})
}
/// Check if we're in production mode (static/index.html exists next to the binary).
pub fn is_production() -> bool {
exe_dir().join("static").join("index.html").exists()
}
/// Initialize the data directory path. Call once at startup.
///
/// Resolution order:
/// 1. `IRONPAD_DATA_DIR` environment variable (if set)
/// 2. `./data` if `static/index.html` exists (production mode)
/// 2. `{exe_dir}/data` if `{exe_dir}/static/index.html` exists (production mode)
/// 3. `../data` (development mode, binary runs from backend/)
pub fn init_data_dir() {
let path = if let Ok(custom) = std::env::var("IRONPAD_DATA_DIR") {
tracing::info!("Using custom data directory from IRONPAD_DATA_DIR");
PathBuf::from(custom)
} else if Path::new("static/index.html").exists() {
} else if is_production() {
// Production mode: data/ is next to the binary
PathBuf::from("data")
exe_dir().join("data")
} else {
// Development mode: binary runs from backend/, data/ is one level up
PathBuf::from("../data")

View File

@@ -1,5 +1,4 @@
use std::net::SocketAddr;
use std::path::Path;
use std::sync::Arc;
use axum::{routing::get, Router};
@@ -95,14 +94,19 @@ async fn main() {
.layer(cors);
// Check for embedded frontend (production mode)
let static_dir = Path::new("static");
let has_frontend = static_dir.join("index.html").exists();
// Resolve relative to the executable's directory, not the working directory
let has_frontend = config::is_production();
if has_frontend {
// Production mode: serve frontend from static/ and use SPA fallback
info!("Production mode: serving frontend from static/");
let serve_dir = ServeDir::new("static")
.fallback(tower_http::services::ServeFile::new("static/index.html"));
// Production mode: serve frontend from static/ next to the exe
let static_path = config::exe_dir().join("static");
let index_path = static_path.join("index.html");
info!(
"Production mode: serving frontend from {}",
static_path.display()
);
let serve_dir =
ServeDir::new(&static_path).fallback(tower_http::services::ServeFile::new(index_path));
app = app.fallback_service(serve_dir);
} else {
// Development mode: API-only