Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 80 additions & 5 deletions apps/bootstrap-installer/src-tauri/src/bootstrap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,11 @@ pub async fn launch_hermes_desktop(

tracing::info!(?exe_path, "launching Hermes desktop");

// Detach from us — the installer is about to exit.
let mut cmd = tokio::process::Command::new(&exe_path);
cmd.current_dir(exe_path.parent().unwrap_or(&install_root));
// Detach from us — the installer is about to exit. On macOS launch the
// bundle through LaunchServices instead of exec'ing Contents/MacOS/Hermes
// directly; this matches user double-click/open behavior and avoids cwd /
// quarantine oddities after a self-update rebuild.
let mut cmd = desktop_launch_command(&exe_path, &install_root);
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
Expand Down Expand Up @@ -232,6 +234,24 @@ pub(crate) fn resolve_hermes_desktop_exe(install_root: &std::path::Path) -> Opti
None
}

pub(crate) fn resolve_hermes_desktop_app(install_root: &std::path::Path) -> Option<PathBuf> {
let exe = resolve_hermes_desktop_exe(install_root)?;
#[cfg(target_os = "macos")]
{
// .../Hermes.app/Contents/MacOS/Hermes -> .../Hermes.app
let app = exe.parent()?.parent()?.parent()?.to_path_buf();
if app.extension().and_then(|e| e.to_str()) == Some("app") && app.is_dir() {
return Some(app);
}
}
#[cfg(not(target_os = "macos"))]
{
return Some(exe);
}
#[allow(unreachable_code)]
None
}

/// True when a prior install completed (bootstrap-complete marker present) AND a
/// launchable desktop app exists on disk. Used by the installer's launcher fast
/// path so a bare re-open just opens Hermes instead of re-running setup.
Expand All @@ -247,8 +267,7 @@ pub(crate) fn spawn_installed_desktop(install_root: &std::path::Path) -> std::io
let exe = resolve_hermes_desktop_exe(install_root).ok_or_else(|| {
std::io::Error::new(std::io::ErrorKind::NotFound, "no built Hermes desktop app")
})?;
let mut cmd = std::process::Command::new(&exe);
cmd.current_dir(exe.parent().unwrap_or(install_root));
let mut cmd = desktop_launch_command_std(&exe, install_root);
#[cfg(target_os = "windows")]
{
use std::os::windows::process::CommandExt;
Expand All @@ -261,6 +280,62 @@ pub(crate) fn spawn_installed_desktop(install_root: &std::path::Path) -> std::io
cmd.spawn().map(|_child| ())
}

#[cfg(target_os = "macos")]
pub(crate) fn open_macos_app_detached(app_bundle: &std::path::Path) -> std::io::Result<()> {
let mut cmd = std::process::Command::new("/usr/bin/open");
cmd.arg(app_bundle);
cmd.current_dir(crate::paths::hermes_home());
cmd.spawn().map(|_child| ())
}

#[cfg(target_os = "macos")]
fn app_bundle_for_exe(exe: &std::path::Path) -> Option<PathBuf> {
let app = exe.parent()?.parent()?.parent()?.to_path_buf();
if app.extension().and_then(|e| e.to_str()) == Some("app") && app.is_dir() {
Some(app)
} else {
None
}
}

fn desktop_launch_command(
exe_path: &std::path::Path,
install_root: &std::path::Path,
) -> tokio::process::Command {
#[cfg(target_os = "macos")]
{
if let Some(app_bundle) = app_bundle_for_exe(exe_path) {
let mut cmd = tokio::process::Command::new("/usr/bin/open");
cmd.arg(app_bundle);
cmd.current_dir(crate::paths::hermes_home());
return cmd;
}
}

let mut cmd = tokio::process::Command::new(exe_path);
cmd.current_dir(exe_path.parent().unwrap_or(install_root));
cmd
}

fn desktop_launch_command_std(
exe_path: &std::path::Path,
install_root: &std::path::Path,
) -> std::process::Command {
#[cfg(target_os = "macos")]
{
if let Some(app_bundle) = app_bundle_for_exe(exe_path) {
let mut cmd = std::process::Command::new("/usr/bin/open");
cmd.arg(app_bundle);
cmd.current_dir(crate::paths::hermes_home());
return cmd;
}
}

let mut cmd = std::process::Command::new(exe_path);
cmd.current_dir(exe_path.parent().unwrap_or(install_root));
cmd
}

// ---------------------------------------------------------------------------
// Bootstrap implementation
// ---------------------------------------------------------------------------
Expand Down
25 changes: 25 additions & 0 deletions apps/bootstrap-installer/src-tauri/src/powershell.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,14 @@ pub async fn run_script(
) -> Result<ScriptResult> {
let mut cmd = build_command(script_path, args);

// The installer can be launched from a .app bundle that is later replaced
// during self-update. Pin child scripts to a stable directory so bash/zsh
// never starts from a deleted cwd and emits getcwd/job-working-directory
// errors at the end of an otherwise successful install.
if let Some(cwd) = stable_script_cwd(script_path, hermes_home_override) {
cmd.current_dir(cwd);
}

if let Some(home) = hermes_home_override {
cmd.env("HERMES_HOME", home);
}
Expand Down Expand Up @@ -146,6 +154,16 @@ pub async fn run_script(
})
}

fn stable_script_cwd<'a>(script_path: &'a Path, hermes_home_override: Option<&'a str>) -> Option<&'a Path> {
if let Some(home) = hermes_home_override {
let path = Path::new(home);
if path.is_dir() {
return Some(path);
}
}
script_path.parent().filter(|p| p.is_dir())
}

async fn recv_cancel(rx: &mut Option<CancelRx>) {
match rx {
Some(r) => {
Expand Down Expand Up @@ -264,4 +282,11 @@ info line
assert!(parse_stage_result("just banner\n").is_none());
assert!(parse_manifest("just banner\n").is_none());
}

#[test]
fn stable_script_cwd_prefers_existing_hermes_home() {
let script = Path::new("/tmp/install.sh");
let cwd = stable_script_cwd(script, Some("/"));
assert_eq!(cwd, Some(Path::new("/")));
}
}
Loading
Loading