Download codex-rs/cli/src/desktop_app/mac.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 17.1 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/cli/src/desktop_app/mac.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/cli/src/desktop_app/mac.rs
-
curl -L -o mac.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/cli/src/desktop_app/mac.rs
17.1 kB
| 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_BUNDLE_IDENTIFIER: &str = "com.openai.codex"; | |
| const OPENAI_APPLE_TEAM_IDENTIFIER: &str = "2DC432GLL2"; | |
| 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(&codex_app_search_dirs()) { | |
| eprintln!( | |
| "Opening Desktop app at {app_path}...", | |
| app_path = app_path.display() | |
| ); | |
| open_codex_app(&app_path, &workspace).await?; | |
| return Ok(()); | |
| } | |
| eprintln!("Desktop app 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 Desktop app")?; | |
| eprintln!( | |
| "Launching Desktop app 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(applications_dirs: &[PathBuf]) -> Option<PathBuf> { | |
| applications_dirs | |
| .iter() | |
| .flat_map(|dir| ["ChatGPT.app", "Codex.app"].map(|app_name| dir.join(app_name))) | |
| .find(|candidate| is_codex_app_bundle(candidate)) | |
| } | |
| fn codex_app_search_dirs() -> Vec<PathBuf> { | |
| let mut paths = vec![PathBuf::from("/Applications")]; | |
| if let Some(home) = std::env::var_os("HOME") { | |
| paths.push(PathBuf::from(home).join("Applications")); | |
| } | |
| paths | |
| } | |
| fn is_codex_app_bundle(app_path: &Path) -> bool { | |
| if !app_path.is_dir() { | |
| return false; | |
| } | |
| std::process::Command::new("/usr/bin/plutil") | |
| .arg("-extract") | |
| .arg("CFBundleIdentifier") | |
| .arg("raw") | |
| .arg("-o") | |
| .arg("-") | |
| .arg(app_path.join("Contents/Info.plist")) | |
| .output() | |
| .is_ok_and(|output| { | |
| output.status.success() | |
| && String::from_utf8_lossy(&output.stdout).trim() == CODEX_BUNDLE_IDENTIFIER | |
| }) | |
| } | |
| async fn open_codex_app(app_path: &Path, workspace: &Path) -> anyhow::Result<()> { | |
| verify_codex_app_bundle(app_path).await?; | |
| 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 | |
| ); | |
| } | |
| async fn verify_codex_app_bundle(app_path: &Path) -> anyhow::Result<()> { | |
| let requirement = format!( | |
| "identifier \"{CODEX_BUNDLE_IDENTIFIER}\" and anchor apple generic and certificate leaf[subject.OU] = \"{OPENAI_APPLE_TEAM_IDENTIFIER}\"" | |
| ); | |
| let output = Command::new("/usr/bin/codesign") | |
| .args(["--verify", "--deep", "--strict"]) | |
| .arg(format!("-R={requirement}")) | |
| .arg(app_path) | |
| .output() | |
| .await | |
| .context("failed to verify Desktop app signature")?; | |
| if output.status.success() { | |
| return Ok(()); | |
| } | |
| anyhow::bail!( | |
| "Desktop app at {} failed OpenAI signature verification (team {OPENAI_APPLE_TEAM_IDENTIFIER}, bundle {CODEX_BUNDLE_IDENTIFIER}): {}", | |
| app_path.display(), | |
| String::from_utf8_lossy(&output.stderr).trim() | |
| ); | |
| } | |
| 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 Desktop app 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")?; | |
| verify_codex_app_bundle(&app_in_volume) | |
| .await | |
| .context("refusing to install an unverified Desktop app")?; | |
| 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 Desktop app 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) | |
| }) | |
| } | |
| mod tests { | |
| use super::codex_new_thread_url; | |
| use super::find_existing_codex_app_path; | |
| use super::open_codex_app; | |
| use super::parse_hdiutil_attach_mount_point; | |
| use super::verify_codex_app_bundle; | |
| use pretty_assertions::assert_eq; | |
| use std::fs; | |
| use std::path::Path; | |
| fn write_app_bundle(app_path: &Path, bundle_identifier: &str) { | |
| let contents_path = app_path.join("Contents"); | |
| fs::create_dir_all(&contents_path).expect("create app bundle"); | |
| fs::write( | |
| contents_path.join("Info.plist"), | |
| format!( | |
| r#"<?xml version="1.0"?><plist version="1.0"><dict><key>CFBundleIdentifier</key><string>{bundle_identifier}</string></dict></plist>"# | |
| ), | |
| ) | |
| .expect("write Info.plist"); | |
| } | |
| fn finds_chatgpt_app_with_codex_bundle_identifier() { | |
| let temp_dir = tempfile::tempdir().expect("create temp dir"); | |
| let app_path = temp_dir.path().join("ChatGPT.app"); | |
| write_app_bundle(&app_path, "com.openai.codex"); | |
| assert_eq!( | |
| find_existing_codex_app_path(&[temp_dir.path().to_path_buf()]), | |
| Some(app_path) | |
| ); | |
| } | |
| fn ignores_classic_chatgpt_app() { | |
| let temp_dir = tempfile::tempdir().expect("create temp dir"); | |
| write_app_bundle(&temp_dir.path().join("ChatGPT.app"), "com.openai.chat"); | |
| let codex_app_path = temp_dir.path().join("Codex.app"); | |
| write_app_bundle(&codex_app_path, "com.openai.codex"); | |
| assert_eq!( | |
| find_existing_codex_app_path(&[temp_dir.path().to_path_buf()]), | |
| Some(codex_app_path) | |
| ); | |
| } | |
| async fn rejects_unsigned_app_with_codex_bundle_identifier() { | |
| let temp_dir = tempfile::tempdir().expect("create temp dir"); | |
| let app_path = temp_dir.path().join("Codex.app"); | |
| write_app_bundle(&app_path, "com.openai.codex"); | |
| let err = verify_codex_app_bundle(&app_path) | |
| .await | |
| .expect_err("unsigned app should not satisfy the OpenAI signing requirement"); | |
| assert!( | |
| err.to_string() | |
| .contains("failed OpenAI signature verification"), | |
| "unexpected verification error: {err}" | |
| ); | |
| } | |
| async fn rejects_valid_signature_without_openai_signing_identity() { | |
| let temp_dir = tempfile::tempdir().expect("create temp dir"); | |
| let app_path = temp_dir.path().join("Codex.app"); | |
| let contents_path = app_path.join("Contents"); | |
| let executable_path = contents_path.join("MacOS/Codex"); | |
| fs::create_dir_all(executable_path.parent().expect("executable parent")) | |
| .expect("create executable directory"); | |
| fs::copy("/usr/bin/true", &executable_path).expect("copy executable into app bundle"); | |
| fs::write( | |
| contents_path.join("Info.plist"), | |
| r#"<?xml version="1.0"?><plist version="1.0"><dict><key>CFBundleIdentifier</key><string>com.openai.codex</string><key>CFBundleExecutable</key><string>Codex</string></dict></plist>"#, | |
| ) | |
| .expect("write Info.plist"); | |
| let sign_status = std::process::Command::new("/usr/bin/codesign") | |
| .args(["--force", "--sign", "-"]) | |
| .arg(&app_path) | |
| .status() | |
| .expect("sign app bundle"); | |
| assert!(sign_status.success(), "failed to ad-hoc sign app bundle"); | |
| let verify_status = std::process::Command::new("/usr/bin/codesign") | |
| .args(["--verify", "--deep", "--strict"]) | |
| .arg(&app_path) | |
| .status() | |
| .expect("verify app bundle signature"); | |
| assert!(verify_status.success(), "ad-hoc signature should be valid"); | |
| let err = verify_codex_app_bundle(&app_path) | |
| .await | |
| .expect_err("valid signature without the OpenAI team should not be trusted"); | |
| assert!( | |
| err.to_string() | |
| .contains("failed OpenAI signature verification"), | |
| "unexpected verification error: {err}" | |
| ); | |
| } | |
| async fn refuses_to_launch_unsigned_existing_codex_app() { | |
| let temp_dir = tempfile::tempdir().expect("create temp dir"); | |
| let app_path = temp_dir.path().join("Codex.app"); | |
| write_app_bundle(&app_path, "com.openai.codex"); | |
| let err = open_codex_app(&app_path, temp_dir.path()) | |
| .await | |
| .expect_err("unsigned existing app should not be launched"); | |
| assert!( | |
| err.to_string() | |
| .contains("failed OpenAI signature verification"), | |
| "unexpected launch error: {err}" | |
| ); | |
| } | |
| 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") | |
| ); | |
| } | |
| 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") | |
| ); | |
| } | |
| 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())], | |
| ) | |
| ); | |
| } | |
| } | |