Codex Handbook
cli/src/desktop_app/mac.rs 348 lines
use anyhow::Context as _;use std::ffi::CString;use std::path::Path;use std::path::PathBuf;use tempfile::Builder;use tokio::process::Command;const CODEX_DMG_URL_ARM64: &str = "https://persistent.oaistatic.com/codex-app-prod/Codex.dmg";const CODEX_DMG_URL_X64: &str =    "https://persistent.oaistatic.com/codex-app-prod/Codex-latest-x64.dmg";pub async fn run_mac_app_open_or_install(    workspace: PathBuf,    download_url_override: Option<String>,) -> anyhow::Result<()> {    if let Some(app_path) = find_existing_codex_app_path() {        eprintln!(            "Opening Codex Desktop at {app_path}...",            app_path = app_path.display()        );        open_codex_app(&app_path, &workspace).await?;        return Ok(());    }    eprintln!("Codex Desktop not found; downloading installer...");    let download_url = download_url_override.unwrap_or_else(|| {        let default_url = if is_apple_silicon_mac() {            CODEX_DMG_URL_ARM64        } else {            CODEX_DMG_URL_X64        };        default_url.to_string()    });    let installed_app = download_and_install_codex_to_user_applications(&download_url)        .await        .context("failed to download/install Codex Desktop")?;    eprintln!(        "Launching Codex Desktop from {installed_app}...",        installed_app = installed_app.display()    );    open_codex_app(&installed_app, &workspace).await?;    Ok(())}fn is_apple_silicon_mac() -> bool {    fn macos_sysctl_flag(name: &str) -> Option<bool> {        let name = CString::new(name).ok()?;        let mut value: libc::c_int = 0;        let mut size = std::mem::size_of_val(&value);        let result = unsafe {            libc::sysctlbyname(                name.as_ptr(),                (&mut value as *mut libc::c_int).cast::<libc::c_void>(),                &mut size,                std::ptr::null_mut(),                0,            )        };        (result == 0).then_some(value != 0)    }    std::env::consts::ARCH == "aarch64"        || macos_sysctl_flag("sysctl.proc_translated").unwrap_or(false)        || macos_sysctl_flag("hw.optional.arm64").unwrap_or(false)}fn find_existing_codex_app_path() -> Option<PathBuf> {    candidate_codex_app_paths()        .into_iter()        .find(|candidate| candidate.is_dir())}fn candidate_codex_app_paths() -> Vec<PathBuf> {    let mut paths = vec![PathBuf::from("/Applications/Codex.app")];    if let Some(home) = std::env::var_os("HOME") {        paths.push(PathBuf::from(home).join("Applications").join("Codex.app"));    }    paths}async fn open_codex_app(app_path: &Path, workspace: &Path) -> anyhow::Result<()> {    eprintln!(        "Opening workspace {workspace}...",        workspace = workspace.display()    );    let url = codex_new_thread_url(workspace);    let status = Command::new("open")        .arg("-a")        .arg(app_path)        .arg(&url)        .status()        .await        .context("failed to invoke `open`")?;    if status.success() {        return Ok(());    }    anyhow::bail!(        "`open -a {app_path} {url}` exited with {status}",        app_path = app_path.display(),        url = url    );}fn codex_new_thread_url(workspace: &Path) -> String {    let workspace = workspace.as_os_str().to_string_lossy();    let mut serializer = url::form_urlencoded::Serializer::new(String::new());    serializer.append_pair("path", workspace.as_ref());    let query = serializer.finish();    format!("codex://threads/new?{query}")}async fn download_and_install_codex_to_user_applications(dmg_url: &str) -> anyhow::Result<PathBuf> {    let temp_dir = Builder::new()        .prefix("codex-app-installer-")        .tempdir()        .context("failed to create temp dir")?;    let tmp_root = temp_dir.path().to_path_buf();    let _temp_dir = temp_dir;    let dmg_path = tmp_root.join("Codex.dmg");    download_dmg(dmg_url, &dmg_path).await?;    eprintln!("Mounting Codex Desktop installer...");    let mount_point = mount_dmg(&dmg_path).await?;    eprintln!(        "Installer mounted at {mount_point}.",        mount_point = mount_point.display()    );    let result = async {        let app_in_volume = find_codex_app_in_mount(&mount_point)            .context("failed to locate Codex.app in mounted dmg")?;        install_codex_app_bundle(&app_in_volume).await    }    .await;    let detach_result = detach_dmg(&mount_point).await;    if let Err(err) = detach_result {        eprintln!(            "warning: failed to detach dmg at {mount_point}: {err}",            mount_point = mount_point.display()        );    }    result}async fn install_codex_app_bundle(app_in_volume: &Path) -> anyhow::Result<PathBuf> {    for applications_dir in candidate_applications_dirs()? {        eprintln!(            "Installing Codex Desktop into {applications_dir}...",            applications_dir = applications_dir.display()        );        std::fs::create_dir_all(&applications_dir).with_context(|| {            format!(                "failed to create applications dir {applications_dir}",                applications_dir = applications_dir.display()            )        })?;        let dest_app = applications_dir.join("Codex.app");        if dest_app.is_dir() {            return Ok(dest_app);        }        match copy_app_bundle(app_in_volume, &dest_app).await {            Ok(()) => return Ok(dest_app),            Err(err) => {                eprintln!(                    "warning: failed to install Codex.app to {applications_dir}: {err}",                    applications_dir = applications_dir.display()                );            }        }    }    anyhow::bail!("failed to install Codex.app to any applications directory");}fn candidate_applications_dirs() -> anyhow::Result<Vec<PathBuf>> {    let mut dirs = vec![PathBuf::from("/Applications")];    dirs.push(user_applications_dir()?);    Ok(dirs)}async fn download_dmg(url: &str, dest: &Path) -> anyhow::Result<()> {    eprintln!("Downloading installer...");    let status = Command::new("curl")        .arg("-fL")        .arg("--retry")        .arg("3")        .arg("--retry-delay")        .arg("1")        .arg("-o")        .arg(dest)        .arg(url)        .status()        .await        .context("failed to invoke `curl`")?;    if status.success() {        return Ok(());    }    anyhow::bail!("curl download failed with {status}");}async fn mount_dmg(dmg_path: &Path) -> anyhow::Result<PathBuf> {    let output = Command::new("hdiutil")        .arg("attach")        .arg("-nobrowse")        .arg("-readonly")        .arg(dmg_path)        .output()        .await        .context("failed to invoke `hdiutil attach`")?;    if !output.status.success() {        anyhow::bail!(            "`hdiutil attach` failed with {status}: {stderr}",            status = output.status,            stderr = String::from_utf8_lossy(&output.stderr)        );    }    let stdout = String::from_utf8_lossy(&output.stdout);    parse_hdiutil_attach_mount_point(&stdout)        .map(PathBuf::from)        .with_context(|| format!("failed to parse mount point from hdiutil output:\n{stdout}"))}async fn detach_dmg(mount_point: &Path) -> anyhow::Result<()> {    let status = Command::new("hdiutil")        .arg("detach")        .arg(mount_point)        .status()        .await        .context("failed to invoke `hdiutil detach`")?;    if status.success() {        return Ok(());    }    anyhow::bail!("hdiutil detach failed with {status}");}fn find_codex_app_in_mount(mount_point: &Path) -> anyhow::Result<PathBuf> {    let direct = mount_point.join("Codex.app");    if direct.is_dir() {        return Ok(direct);    }    for entry in std::fs::read_dir(mount_point).with_context(|| {        format!(            "failed to read {mount_point}",            mount_point = mount_point.display()        )    })? {        let entry = entry.context("failed to read mount directory entry")?;        let path = entry.path();        if path.extension().is_some_and(|ext| ext == "app") && path.is_dir() {            return Ok(path);        }    }    anyhow::bail!(        "no .app bundle found at {mount_point}",        mount_point = mount_point.display()    );}async fn copy_app_bundle(src_app: &Path, dest_app: &Path) -> anyhow::Result<()> {    let status = Command::new("ditto")        .arg(src_app)        .arg(dest_app)        .status()        .await        .context("failed to invoke `ditto`")?;    if status.success() {        return Ok(());    }    anyhow::bail!("ditto copy failed with {status}");}fn user_applications_dir() -> anyhow::Result<PathBuf> {    let home = std::env::var_os("HOME").context("HOME is not set")?;    Ok(PathBuf::from(home).join("Applications"))}fn parse_hdiutil_attach_mount_point(output: &str) -> Option<String> {    output.lines().find_map(|line| {        if !line.contains("/Volumes/") {            return None;        }        if let Some((_, mount)) = line.rsplit_once('\t') {            return Some(mount.trim().to_string());        }        line.split_whitespace()            .find(|field| field.starts_with("/Volumes/"))            .map(str::to_string)    })}#[cfg(test)]mod tests {    use super::codex_new_thread_url;    use super::parse_hdiutil_attach_mount_point;    use pretty_assertions::assert_eq;    use std::path::Path;    #[test]    fn parses_mount_point_from_tab_separated_hdiutil_output() {        let output = "/dev/disk2s1\tApple_HFS\tCodex\t/Volumes/Codex\n";        assert_eq!(            parse_hdiutil_attach_mount_point(output).as_deref(),            Some("/Volumes/Codex")        );    }    #[test]    fn parses_mount_point_with_spaces() {        let output = "/dev/disk2s1\tApple_HFS\tCodex Installer\t/Volumes/Codex Installer\n";        assert_eq!(            parse_hdiutil_attach_mount_point(output).as_deref(),            Some("/Volumes/Codex Installer")        );    }    #[test]    fn codex_new_thread_url_encodes_workspace_path() {        let url = url::Url::parse(&codex_new_thread_url(Path::new("/tmp/codex workspace/#1")))            .expect("deep link should parse");        assert_eq!(            (                url.scheme().to_string(),                url.host_str().map(str::to_string),                url.path().to_string(),                url.query_pairs().into_owned().collect::<Vec<_>>(),            ),            (                "codex".to_string(),                Some("threads".to_string()),                "/new".to_string(),                vec![("path".to_string(), "/tmp/codex workspace/#1".to_string())],            )        );    }}