use std::{
env, fs as std_fs,
path::{Path, PathBuf},
};
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use thiserror::Error;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BundledBinarySpec<'a> {
pub bundle_root: &'a Path,
pub version: &'a str,
pub platform: Option<&'a str>,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct BundledBinary {
pub binary_path: PathBuf,
pub platform: String,
pub version: String,
}
#[derive(Debug, Error)]
pub enum BundledBinaryError {
#[error("bundled Codex version cannot be empty")]
EmptyVersion,
#[error("bundled Codex platform label cannot be empty")]
EmptyPlatform,
#[error("bundle root `{bundle_root}` does not exist or is unreadable")]
BundleRootUnreadable {
bundle_root: PathBuf,
#[source]
source: std::io::Error,
},
#[error("bundle root `{bundle_root}` is not a directory")]
BundleRootNotDirectory { bundle_root: PathBuf },
#[error("bundle platform directory `{platform_dir}` for `{platform}` does not exist or is unreadable")]
PlatformUnreadable {
platform: String,
platform_dir: PathBuf,
#[source]
source: std::io::Error,
},
#[error("bundle platform directory `{platform_dir}` for `{platform}` is not a directory")]
PlatformNotDirectory {
platform: String,
platform_dir: PathBuf,
},
#[error(
"bundle version directory `{version_dir}` for `{version}` does not exist or is unreadable"
)]
VersionUnreadable {
version: String,
version_dir: PathBuf,
#[source]
source: std::io::Error,
},
#[error("bundle version directory `{version_dir}` for `{version}` is not a directory")]
VersionNotDirectory {
version: String,
version_dir: PathBuf,
},
#[error("bundled Codex binary `{binary}` is missing or unreadable")]
BinaryUnreadable {
binary: PathBuf,
#[source]
source: std::io::Error,
},
#[error("bundled Codex binary `{binary}` is not a file")]
BinaryNotFile { binary: PathBuf },
#[error("bundled Codex binary `{binary}` is not executable")]
BinaryNotExecutable { binary: PathBuf },
#[error("failed to canonicalize bundled Codex binary `{path}`: {source}")]
Canonicalize {
path: PathBuf,
#[source]
source: std::io::Error,
},
}
pub fn resolve_bundled_binary(
spec: BundledBinarySpec<'_>,
) -> Result<BundledBinary, BundledBinaryError> {
let platform = match spec.platform {
Some(label) => {
super::normalize_non_empty(label).ok_or(BundledBinaryError::EmptyPlatform)?
}
None => default_bundled_platform_label(),
};
let version =
super::normalize_non_empty(spec.version).ok_or(BundledBinaryError::EmptyVersion)?;
require_directory(
spec.bundle_root,
|source| BundledBinaryError::BundleRootUnreadable {
bundle_root: spec.bundle_root.to_path_buf(),
source,
},
|| BundledBinaryError::BundleRootNotDirectory {
bundle_root: spec.bundle_root.to_path_buf(),
},
)?;
let platform_dir = spec.bundle_root.join(&platform);
require_directory(
&platform_dir,
|source| BundledBinaryError::PlatformUnreadable {
platform: platform.clone(),
platform_dir: platform_dir.clone(),
source,
},
|| BundledBinaryError::PlatformNotDirectory {
platform: platform.clone(),
platform_dir: platform_dir.clone(),
},
)?;
let version_dir = platform_dir.join(&version);
require_directory(
&version_dir,
|source| BundledBinaryError::VersionUnreadable {
version: version.clone(),
version_dir: version_dir.clone(),
source,
},
|| BundledBinaryError::VersionNotDirectory {
version: version.clone(),
version_dir: version_dir.clone(),
},
)?;
let binary_path = version_dir.join(bundled_binary_filename(&platform));
let metadata =
std_fs::metadata(&binary_path).map_err(|source| BundledBinaryError::BinaryUnreadable {
binary: binary_path.clone(),
source,
})?;
if !metadata.is_file() {
return Err(BundledBinaryError::BinaryNotFile {
binary: binary_path.clone(),
});
}
ensure_executable(&metadata, &binary_path)?;
let canonical =
std_fs::canonicalize(&binary_path).map_err(|source| BundledBinaryError::Canonicalize {
path: binary_path.clone(),
source,
})?;
Ok(BundledBinary {
binary_path: canonical,
platform,
version,
})
}
pub fn default_bundled_platform_label() -> String {
let os = match env::consts::OS {
"macos" => "darwin",
other => other,
};
let arch = match env::consts::ARCH {
"x86_64" => "x64",
"aarch64" => "arm64",
other => other,
};
format!("{os}-{arch}")
}
fn require_directory(
path: &Path,
on_read_error: impl FnOnce(std::io::Error) -> BundledBinaryError,
on_wrong_type: impl FnOnce() -> BundledBinaryError,
) -> Result<(), BundledBinaryError> {
let metadata = std_fs::metadata(path).map_err(on_read_error)?;
if !metadata.is_dir() {
return Err(on_wrong_type());
}
Ok(())
}
fn ensure_executable(metadata: &std_fs::Metadata, binary: &Path) -> Result<(), BundledBinaryError> {
if binary_is_executable(metadata) {
return Ok(());
}
Err(BundledBinaryError::BinaryNotExecutable {
binary: binary.to_path_buf(),
})
}
fn binary_is_executable(metadata: &std_fs::Metadata) -> bool {
#[cfg(unix)]
{
metadata.permissions().mode() & 0o111 != 0
}
#[cfg(not(unix))]
{
true
}
}
pub(super) fn bundled_binary_filename(platform: &str) -> &'static str {
if platform.to_ascii_lowercase().contains("windows") {
"codex.exe"
} else {
"codex"
}
}