Fix production mode: resolve static/data paths relative to exe location, include static/ in release packages
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -73,6 +73,7 @@ jobs:
|
|||||||
RELEASE_DIR="${{ matrix.asset_name }}-${{ github.ref_name }}"
|
RELEASE_DIR="${{ matrix.asset_name }}-${{ github.ref_name }}"
|
||||||
mkdir -p "$RELEASE_DIR"
|
mkdir -p "$RELEASE_DIR"
|
||||||
cp "backend/target/${{ matrix.target }}/release/ironpad" "$RELEASE_DIR/"
|
cp "backend/target/${{ matrix.target }}/release/ironpad" "$RELEASE_DIR/"
|
||||||
|
cp -r backend/static "$RELEASE_DIR/static"
|
||||||
cp README.md LICENSE "$RELEASE_DIR/"
|
cp README.md LICENSE "$RELEASE_DIR/"
|
||||||
tar czf "$RELEASE_DIR.tar.gz" "$RELEASE_DIR"
|
tar czf "$RELEASE_DIR.tar.gz" "$RELEASE_DIR"
|
||||||
echo "ASSET=$RELEASE_DIR.tar.gz" >> $GITHUB_ENV
|
echo "ASSET=$RELEASE_DIR.tar.gz" >> $GITHUB_ENV
|
||||||
@@ -84,6 +85,7 @@ jobs:
|
|||||||
RELEASE_DIR="${{ matrix.asset_name }}-${{ github.ref_name }}"
|
RELEASE_DIR="${{ matrix.asset_name }}-${{ github.ref_name }}"
|
||||||
mkdir -p "$RELEASE_DIR"
|
mkdir -p "$RELEASE_DIR"
|
||||||
cp "backend/target/${{ matrix.target }}/release/ironpad.exe" "$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/"
|
cp README.md LICENSE "$RELEASE_DIR/"
|
||||||
7z a "$RELEASE_DIR.zip" "$RELEASE_DIR"
|
7z a "$RELEASE_DIR.zip" "$RELEASE_DIR"
|
||||||
echo "ASSET=$RELEASE_DIR.zip" >> $GITHUB_ENV
|
echo "ASSET=$RELEASE_DIR.zip" >> $GITHUB_ENV
|
||||||
|
|||||||
@@ -5,19 +5,39 @@ use std::sync::OnceLock;
|
|||||||
/// Priority: IRONPAD_DATA_DIR env var > auto-detect (production vs development).
|
/// Priority: IRONPAD_DATA_DIR env var > auto-detect (production vs development).
|
||||||
static DATA_DIR: OnceLock<PathBuf> = OnceLock::new();
|
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.
|
/// Initialize the data directory path. Call once at startup.
|
||||||
///
|
///
|
||||||
/// Resolution order:
|
/// Resolution order:
|
||||||
/// 1. `IRONPAD_DATA_DIR` environment variable (if set)
|
/// 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/)
|
/// 3. `../data` (development mode, binary runs from backend/)
|
||||||
pub fn init_data_dir() {
|
pub fn init_data_dir() {
|
||||||
let path = if let Ok(custom) = std::env::var("IRONPAD_DATA_DIR") {
|
let path = if let Ok(custom) = std::env::var("IRONPAD_DATA_DIR") {
|
||||||
tracing::info!("Using custom data directory from IRONPAD_DATA_DIR");
|
tracing::info!("Using custom data directory from IRONPAD_DATA_DIR");
|
||||||
PathBuf::from(custom)
|
PathBuf::from(custom)
|
||||||
} else if Path::new("static/index.html").exists() {
|
} else if is_production() {
|
||||||
// Production mode: data/ is next to the binary
|
// Production mode: data/ is next to the binary
|
||||||
PathBuf::from("data")
|
exe_dir().join("data")
|
||||||
} else {
|
} else {
|
||||||
// Development mode: binary runs from backend/, data/ is one level up
|
// Development mode: binary runs from backend/, data/ is one level up
|
||||||
PathBuf::from("../data")
|
PathBuf::from("../data")
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use std::net::SocketAddr;
|
use std::net::SocketAddr;
|
||||||
use std::path::Path;
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use axum::{routing::get, Router};
|
use axum::{routing::get, Router};
|
||||||
@@ -95,14 +94,19 @@ async fn main() {
|
|||||||
.layer(cors);
|
.layer(cors);
|
||||||
|
|
||||||
// Check for embedded frontend (production mode)
|
// Check for embedded frontend (production mode)
|
||||||
let static_dir = Path::new("static");
|
// Resolve relative to the executable's directory, not the working directory
|
||||||
let has_frontend = static_dir.join("index.html").exists();
|
let has_frontend = config::is_production();
|
||||||
|
|
||||||
if has_frontend {
|
if has_frontend {
|
||||||
// Production mode: serve frontend from static/ and use SPA fallback
|
// Production mode: serve frontend from static/ next to the exe
|
||||||
info!("Production mode: serving frontend from static/");
|
let static_path = config::exe_dir().join("static");
|
||||||
let serve_dir = ServeDir::new("static")
|
let index_path = static_path.join("index.html");
|
||||||
.fallback(tower_http::services::ServeFile::new("static/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);
|
app = app.fallback_service(serve_dir);
|
||||||
} else {
|
} else {
|
||||||
// Development mode: API-only
|
// Development mode: API-only
|
||||||
|
|||||||
Reference in New Issue
Block a user