feat(app-tauri): packager les assets web en ressource et les résoudre depuis le resource dir (#68)
L'AppImage ne pouvait pas servir d'assets web : `resolve_web_root` ne connaissait que `IDEA_WEB_ROOT`, le dossier de l'exécutable et le cwd — aucun ne pointe vers un bundle packagé. - `bundle.resources` embarque les assets sous `web/`, `beforeBuildCommand` déclenche le build du frontend. - Le resource dir Tauri descend de `lib.rs` jusqu'à `EmbeddedServerController` via `AppState::build_with_resource_dir`, en gardant les constructeurs historiques comme façade. - `resolve_web_root` insère le candidat packagé juste après `IDEA_WEB_ROOT`, qui garde donc la priorité pour le développement. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -162,6 +162,7 @@ impl FsServerExposureSettingsStore {
|
|||||||
/// Desktop-owned embedded server manager.
|
/// Desktop-owned embedded server manager.
|
||||||
pub struct EmbeddedServerController {
|
pub struct EmbeddedServerController {
|
||||||
app_data_dir: PathBuf,
|
app_data_dir: PathBuf,
|
||||||
|
resource_dir: Option<PathBuf>,
|
||||||
store: FsServerExposureSettingsStore,
|
store: FsServerExposureSettingsStore,
|
||||||
inner: Mutex<EmbeddedServerInner>,
|
inner: Mutex<EmbeddedServerInner>,
|
||||||
}
|
}
|
||||||
@ -170,9 +171,16 @@ impl EmbeddedServerController {
|
|||||||
/// Creates the controller.
|
/// Creates the controller.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn new(app_data_dir: PathBuf) -> Self {
|
pub fn new(app_data_dir: PathBuf) -> Self {
|
||||||
|
Self::with_resource_dir(app_data_dir, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates the controller with the Tauri bundle resource directory.
|
||||||
|
#[must_use]
|
||||||
|
pub fn with_resource_dir(app_data_dir: PathBuf, resource_dir: Option<PathBuf>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
store: FsServerExposureSettingsStore::new(app_data_dir.clone()),
|
store: FsServerExposureSettingsStore::new(app_data_dir.clone()),
|
||||||
app_data_dir,
|
app_data_dir,
|
||||||
|
resource_dir,
|
||||||
inner: Mutex::new(EmbeddedServerInner::default()),
|
inner: Mutex::new(EmbeddedServerInner::default()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -237,7 +245,7 @@ impl EmbeddedServerController {
|
|||||||
let config = match server_config_from_settings(
|
let config = match server_config_from_settings(
|
||||||
&settings,
|
&settings,
|
||||||
self.app_data_dir.clone(),
|
self.app_data_dir.clone(),
|
||||||
resolve_web_root()?,
|
resolve_web_root(self.resource_dir.as_deref())?,
|
||||||
) {
|
) {
|
||||||
Ok(config) => config,
|
Ok(config) => config,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
@ -487,11 +495,25 @@ fn parse_ip(value: &str, field: &str) -> Result<IpAddr, ErrorDto> {
|
|||||||
.map_err(|_| invalid_error(format!("{field} must be an IP address")))
|
.map_err(|_| invalid_error(format!("{field} must be an IP address")))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_web_root() -> Result<PathBuf, ErrorDto> {
|
fn resolve_web_root(resource_dir: Option<&Path>) -> Result<PathBuf, ErrorDto> {
|
||||||
|
let explicit_web_root = std::env::var_os("IDEA_WEB_ROOT").map(PathBuf::from);
|
||||||
|
let candidates = web_root_candidates(explicit_web_root, resource_dir);
|
||||||
|
first_web_root(candidates).ok_or_else(|| {
|
||||||
|
invalid_error("web assets not found: build frontend/dist or set IDEA_WEB_ROOT")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn web_root_candidates(
|
||||||
|
explicit_web_root: Option<PathBuf>,
|
||||||
|
resource_dir: Option<&Path>,
|
||||||
|
) -> Vec<PathBuf> {
|
||||||
let mut candidates = Vec::new();
|
let mut candidates = Vec::new();
|
||||||
if let Some(path) = std::env::var_os("IDEA_WEB_ROOT").map(PathBuf::from) {
|
if let Some(path) = explicit_web_root {
|
||||||
candidates.push(path);
|
candidates.push(path);
|
||||||
}
|
}
|
||||||
|
if let Some(resource_dir) = resource_dir {
|
||||||
|
candidates.push(resource_dir.join("web"));
|
||||||
|
}
|
||||||
if let Ok(exe) = std::env::current_exe() {
|
if let Ok(exe) = std::env::current_exe() {
|
||||||
if let Some(dir) = exe.parent() {
|
if let Some(dir) = exe.parent() {
|
||||||
candidates.push(dir.join("web"));
|
candidates.push(dir.join("web"));
|
||||||
@ -501,12 +523,13 @@ fn resolve_web_root() -> Result<PathBuf, ErrorDto> {
|
|||||||
if let Ok(cwd) = std::env::current_dir() {
|
if let Ok(cwd) = std::env::current_dir() {
|
||||||
candidates.push(cwd.join("frontend").join("dist"));
|
candidates.push(cwd.join("frontend").join("dist"));
|
||||||
}
|
}
|
||||||
|
candidates
|
||||||
|
}
|
||||||
|
|
||||||
|
fn first_web_root(candidates: impl IntoIterator<Item = PathBuf>) -> Option<PathBuf> {
|
||||||
candidates
|
candidates
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|candidate| candidate.join("index.html").is_file())
|
.find(|candidate| candidate.join("index.html").is_file())
|
||||||
.ok_or_else(|| {
|
|
||||||
invalid_error("web assets not found: build frontend/dist or set IDEA_WEB_ROOT")
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn write_atomic_user_only(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
|
fn write_atomic_user_only(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
|
||||||
@ -588,6 +611,36 @@ mod tests {
|
|||||||
web_root
|
web_root
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn web_root_candidates_keep_packaged_resource_before_exe_fallbacks() {
|
||||||
|
let explicit = tmp_app_data().join("explicit-web-root");
|
||||||
|
let resource_dir = tmp_app_data().join("resources");
|
||||||
|
|
||||||
|
let candidates = web_root_candidates(Some(explicit.clone()), Some(&resource_dir));
|
||||||
|
|
||||||
|
assert_eq!(candidates[0], explicit);
|
||||||
|
assert_eq!(candidates[1], resource_dir.join("web"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn first_web_root_uses_first_candidate_with_index_html() {
|
||||||
|
let missing = tmp_app_data().join("missing-web-root");
|
||||||
|
let resource_dir = tmp_app_data().join("resources");
|
||||||
|
let resource_web = resource_dir.join("web");
|
||||||
|
std::fs::create_dir_all(&resource_web).unwrap();
|
||||||
|
std::fs::write(
|
||||||
|
resource_web.join("index.html"),
|
||||||
|
"<!doctype html><title>Packaged</title>",
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let later = tmp_web_root();
|
||||||
|
|
||||||
|
let resolved = first_web_root([missing, resource_web.clone(), later])
|
||||||
|
.expect("web root should resolve");
|
||||||
|
|
||||||
|
assert_eq!(resolved, resource_web);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn non_loopback_remote_requires_trusted_proxy() {
|
fn non_loopback_remote_requires_trusted_proxy() {
|
||||||
let settings = ServerExposureSettingsDto {
|
let settings = ServerExposureSettingsDto {
|
||||||
|
|||||||
@ -96,13 +96,14 @@ pub fn run() {
|
|||||||
.path()
|
.path()
|
||||||
.app_data_dir()
|
.app_data_dir()
|
||||||
.expect("failed to resolve the app data directory");
|
.expect("failed to resolve the app data directory");
|
||||||
|
let resource_dir = app.path().resource_dir().ok();
|
||||||
// Point the orchestrator's best-effort diagnostics at a persistent file
|
// Point the orchestrator's best-effort diagnostics at a persistent file
|
||||||
// (`<app-data>/logs/idea.log`) so inter-agent rendezvous beacons survive a
|
// (`<app-data>/logs/idea.log`) so inter-agent rendezvous beacons survive a
|
||||||
// click-launched AppImage (whose stderr is otherwise discarded). Best-effort:
|
// click-launched AppImage (whose stderr is otherwise discarded). Best-effort:
|
||||||
// if the file can't be opened the beacons simply stay on stderr.
|
// if the file can't be opened the beacons simply stay on stderr.
|
||||||
application::diag::set_log_path(app_data_dir.join("logs").join("idea.log"));
|
application::diag::set_log_path(app_data_dir.join("logs").join("idea.log"));
|
||||||
application::diag!("[startup] IdeA launched; diagnostics log armed");
|
application::diag!("[startup] IdeA launched; diagnostics log armed");
|
||||||
let app_state = AppState::build(app_data_dir);
|
let app_state = AppState::build_with_resource_dir(app_data_dir, resource_dir);
|
||||||
|
|
||||||
// Wire the domain event bus → Tauri events relay.
|
// Wire the domain event bus → Tauri events relay.
|
||||||
events::spawn_relay(app.handle().clone(), &app_state.event_bus);
|
events::spawn_relay(app.handle().clone(), &app_state.event_bus);
|
||||||
|
|||||||
@ -51,6 +51,13 @@ impl AppState {
|
|||||||
/// into the backend core.
|
/// into the backend core.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn build(app_data_dir: PathBuf) -> Self {
|
pub fn build(app_data_dir: PathBuf) -> Self {
|
||||||
|
Self::build_with_resource_dir(app_data_dir, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the shared backend core with the Tauri bundle resource directory
|
||||||
|
/// available to desktop-only adapters.
|
||||||
|
#[must_use]
|
||||||
|
pub fn build_with_resource_dir(app_data_dir: PathBuf, resource_dir: Option<PathBuf>) -> Self {
|
||||||
let core = Arc::new(BackendCore::build(app_data_dir.clone()));
|
let core = Arc::new(BackendCore::build(app_data_dir.clone()));
|
||||||
core.ticket_tool_binder
|
core.ticket_tool_binder
|
||||||
.bind(Arc::new(AppTicketToolProvider {
|
.bind(Arc::new(AppTicketToolProvider {
|
||||||
@ -69,7 +76,10 @@ impl AppState {
|
|||||||
core,
|
core,
|
||||||
pty_bridge: Arc::new(PtyBridge::new()),
|
pty_bridge: Arc::new(PtyBridge::new()),
|
||||||
chat_bridge: Arc::new(ChatBridge::new()),
|
chat_bridge: Arc::new(ChatBridge::new()),
|
||||||
embedded_server: Arc::new(EmbeddedServerController::new(app_data_dir)),
|
embedded_server: Arc::new(EmbeddedServerController::with_resource_dir(
|
||||||
|
app_data_dir,
|
||||||
|
resource_dir,
|
||||||
|
)),
|
||||||
focused_project: Mutex::new(None),
|
focused_project: Mutex::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,7 +6,8 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"frontendDist": "../../frontend/dist",
|
"frontendDist": "../../frontend/dist",
|
||||||
"devUrl": "http://localhost:5173",
|
"devUrl": "http://localhost:5173",
|
||||||
"beforeDevCommand": "npm --prefix ../frontend run dev"
|
"beforeDevCommand": "npm --prefix ../frontend run dev",
|
||||||
|
"beforeBuildCommand": "npm --prefix ../frontend run build"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
@ -24,6 +25,9 @@
|
|||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": ["appimage", "nsis"],
|
"targets": ["appimage", "nsis"],
|
||||||
"icon": ["icons/icon.png"]
|
"icon": ["icons/icon.png"],
|
||||||
|
"resources": {
|
||||||
|
"../../frontend/dist": "web"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user