Files
codex/codex-rs/package-manager/src/lib.rs
2026-03-05 11:57:13 +00:00

1088 lines
38 KiB
Rust

use fd_lock::RwLock as FileRwLock;
use flate2::read::GzDecoder;
use reqwest::Client;
use serde::de::DeserializeOwned;
use sha2::Digest;
use sha2::Sha256;
use std::fs::File;
use std::fs::OpenOptions;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
use std::time::Duration;
use tar::Archive;
use tempfile::tempdir_in;
use thiserror::Error;
use tokio::fs;
use tokio::time::sleep;
use url::Url;
use zip::ZipArchive;
const INSTALL_LOCK_POLL_INTERVAL: Duration = Duration::from_millis(50);
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PackageManagerConfig<P> {
codex_home: PathBuf,
package: P,
cache_root: Option<PathBuf>,
}
impl<P> PackageManagerConfig<P> {
pub fn new(codex_home: PathBuf, package: P) -> Self {
Self {
codex_home,
package,
cache_root: None,
}
}
pub fn with_cache_root(mut self, cache_root: PathBuf) -> Self {
self.cache_root = Some(cache_root);
self
}
pub fn codex_home(&self) -> &Path {
&self.codex_home
}
pub fn package(&self) -> &P {
&self.package
}
}
impl<P: ManagedPackage> PackageManagerConfig<P> {
pub fn cache_root(&self) -> PathBuf {
self.cache_root.clone().unwrap_or_else(|| {
self.codex_home.join(
self.package
.default_cache_root_relative()
.replace('/', std::path::MAIN_SEPARATOR_STR),
)
})
}
}
#[derive(Clone, Debug)]
pub struct PackageManager<P> {
client: Client,
config: PackageManagerConfig<P>,
}
impl<P> PackageManager<P> {
pub fn new(config: PackageManagerConfig<P>) -> Self {
Self {
client: Client::new(),
config,
}
}
pub fn with_client(config: PackageManagerConfig<P>, client: Client) -> Self {
Self { client, config }
}
pub fn config(&self) -> &PackageManagerConfig<P> {
&self.config
}
}
impl<P: ManagedPackage> PackageManager<P> {
pub async fn resolve_cached(&self) -> Result<Option<P::Installed>, P::Error> {
let platform = PackagePlatform::detect_current().map_err(P::Error::from)?;
let install_dir = self
.config
.package()
.install_dir(&self.config.cache_root(), platform);
self.resolve_cached_at(platform, install_dir).await
}
async fn resolve_cached_at(
&self,
platform: PackagePlatform,
install_dir: PathBuf,
) -> Result<Option<P::Installed>, P::Error> {
if !fs::try_exists(&install_dir)
.await
.map_err(|source| PackageManagerError::Io {
context: format!("failed to read {}", install_dir.display()),
source,
})
.map_err(P::Error::from)?
{
return Ok(None);
}
let package = match self.config.package().load_installed(install_dir, platform) {
Ok(package) => package,
Err(_) => return Ok(None),
};
if self.config.package().installed_version(&package) != self.config.package().version() {
return Ok(None);
}
Ok(Some(package))
}
pub async fn ensure_installed(&self) -> Result<P::Installed, P::Error> {
if let Some(package) = self.resolve_cached().await? {
return Ok(package);
}
let platform = PackagePlatform::detect_current().map_err(P::Error::from)?;
let cache_root = self.config.cache_root();
let install_dir = self.config.package().install_dir(&cache_root, platform);
if let Some(package) = self
.resolve_cached_at(platform, install_dir.clone())
.await?
{
return Ok(package);
}
if let Some(parent) = install_dir.parent() {
fs::create_dir_all(parent)
.await
.map_err(|source| PackageManagerError::Io {
context: format!("failed to create {}", parent.display()),
source,
})
.map_err(P::Error::from)?;
}
let lock_path = install_dir.with_extension("lock");
let lock_file = OpenOptions::new()
.create(true)
.read(true)
.write(true)
.truncate(false)
.open(&lock_path)
.map_err(|source| PackageManagerError::Io {
context: format!("failed to open {}", lock_path.display()),
source,
})
.map_err(P::Error::from)?;
let mut install_lock = FileRwLock::new(lock_file);
let _install_guard = loop {
match install_lock.try_write() {
Ok(guard) => break guard,
Err(source) if source.kind() == std::io::ErrorKind::WouldBlock => {
sleep(INSTALL_LOCK_POLL_INTERVAL).await;
}
Err(source) => {
return Err(PackageManagerError::Io {
context: format!("failed to lock {}", lock_path.display()),
source,
}
.into());
}
}
};
if let Some(package) = self
.resolve_cached_at(platform, install_dir.clone())
.await?
{
return Ok(package);
}
let manifest = self.fetch_release_manifest().await?;
if self.config.package().release_version(&manifest) != self.config.package().version() {
return Err(PackageManagerError::UnexpectedPackageVersion {
expected: self.config.package().version().to_string(),
actual: self.config.package().release_version(&manifest).to_string(),
}
.into());
}
fs::create_dir_all(&cache_root)
.await
.map_err(|source| PackageManagerError::Io {
context: format!("failed to create {}", cache_root.display()),
source,
})
.map_err(P::Error::from)?;
let staging_root = cache_root.join(".staging");
fs::create_dir_all(&staging_root)
.await
.map_err(|source| PackageManagerError::Io {
context: format!("failed to create {}", staging_root.display()),
source,
})
.map_err(P::Error::from)?;
let platform_archive = self
.config
.package()
.platform_archive(&manifest, platform)?;
let archive_url = self
.config
.package()
.archive_url(&platform_archive)
.map_err(P::Error::from)?;
let archive_bytes = self.download_bytes(&archive_url).await?;
verify_sha256(&archive_bytes, &platform_archive.sha256).map_err(P::Error::from)?;
let staging_dir = tempdir_in(&staging_root)
.map_err(|source| PackageManagerError::Io {
context: format!(
"failed to create staging directory in {}",
staging_root.display()
),
source,
})
.map_err(P::Error::from)?;
let archive_path = staging_dir.path().join(&platform_archive.archive);
fs::write(&archive_path, &archive_bytes)
.await
.map_err(|source| PackageManagerError::Io {
context: format!("failed to write {}", archive_path.display()),
source,
})
.map_err(P::Error::from)?;
let extraction_root = staging_dir.path().join("extract");
fs::create_dir_all(&extraction_root)
.await
.map_err(|source| PackageManagerError::Io {
context: format!("failed to create {}", extraction_root.display()),
source,
})
.map_err(P::Error::from)?;
extract_archive(&archive_path, &extraction_root, platform_archive.format)
.map_err(P::Error::from)?;
let extracted_root = self
.config
.package()
.detect_extracted_root(&extraction_root)?;
let package = self
.config
.package()
.load_installed(extracted_root.clone(), platform)?;
if self.config.package().installed_version(&package) != self.config.package().version() {
return Err(PackageManagerError::UnexpectedPackageVersion {
expected: self.config.package().version().to_string(),
actual: self
.config
.package()
.installed_version(&package)
.to_string(),
}
.into());
}
if let Some(parent) = install_dir.parent() {
fs::create_dir_all(parent)
.await
.map_err(|source| PackageManagerError::Io {
context: format!("failed to create {}", parent.display()),
source,
})
.map_err(P::Error::from)?;
}
let mut replaced_install_dir = None;
if fs::try_exists(&install_dir)
.await
.map_err(|source| PackageManagerError::Io {
context: format!("failed to read {}", install_dir.display()),
source,
})
.map_err(P::Error::from)?
{
let install_name = install_dir.file_name().ok_or_else(|| {
PackageManagerError::ArchiveExtraction(format!(
"install path `{}` has no terminal component",
install_dir.display()
))
})?;
let install_name = install_name.to_string_lossy();
let mut suffix = 0u32;
loop {
let quarantined_path = install_dir.with_file_name(format!(
".{install_name}.replaced-{}-{suffix}",
std::process::id()
));
match fs::rename(&install_dir, &quarantined_path).await {
Ok(()) => {
replaced_install_dir = Some(quarantined_path);
break;
}
Err(source) if source.kind() == std::io::ErrorKind::AlreadyExists => {
suffix += 1;
}
Err(source) => {
return Err(PackageManagerError::Io {
context: format!(
"failed to quarantine {} to {}",
install_dir.display(),
quarantined_path.display()
),
source,
}
.into());
}
}
}
}
match fs::rename(&extracted_root, &install_dir).await {
Ok(()) => {}
Err(source)
if matches!(
source.kind(),
std::io::ErrorKind::AlreadyExists | std::io::ErrorKind::DirectoryNotEmpty
) =>
{
if let Some(package) = self
.resolve_cached_at(platform, install_dir.clone())
.await?
{
return Ok(package);
}
return Err(PackageManagerError::Io {
context: format!(
"failed to move {} to {}",
extracted_root.display(),
install_dir.display()
),
source,
}
.into());
}
Err(source) => {
return Err(PackageManagerError::Io {
context: format!(
"failed to move {} to {}",
extracted_root.display(),
install_dir.display()
),
source,
}
.into());
}
}
if let Some(replaced_install_dir) = replaced_install_dir {
let _ = fs::remove_dir_all(replaced_install_dir).await;
}
self.config.package().load_installed(install_dir, platform)
}
async fn fetch_release_manifest(&self) -> Result<P::ReleaseManifest, P::Error> {
let manifest_url = self
.config
.package()
.manifest_url()
.map_err(P::Error::from)?;
let response = self
.client
.get(manifest_url.clone())
.send()
.await
.map_err(|source| PackageManagerError::Http {
context: format!("failed to fetch {manifest_url}"),
source,
})
.map_err(P::Error::from)?
.error_for_status()
.map_err(|source| PackageManagerError::Http {
context: format!("manifest request failed for {manifest_url}"),
source,
})
.map_err(P::Error::from)?;
response
.json::<P::ReleaseManifest>()
.await
.map_err(|source| PackageManagerError::Http {
context: format!("failed to decode manifest from {manifest_url}"),
source,
})
.map_err(P::Error::from)
}
async fn download_bytes(&self, url: &Url) -> Result<Vec<u8>, P::Error> {
let response = self
.client
.get(url.clone())
.send()
.await
.map_err(|source| PackageManagerError::Http {
context: format!("failed to download {url}"),
source,
})
.map_err(P::Error::from)?
.error_for_status()
.map_err(|source| PackageManagerError::Http {
context: format!("archive request failed for {url}"),
source,
})
.map_err(P::Error::from)?;
let bytes = response
.bytes()
.await
.map_err(|source| PackageManagerError::Http {
context: format!("failed to read response body for {url}"),
source,
})
.map_err(P::Error::from)?;
Ok(bytes.to_vec())
}
}
pub trait ManagedPackage: Clone {
type Error: From<PackageManagerError>;
type Installed: Clone;
type ReleaseManifest: DeserializeOwned;
fn default_cache_root_relative(&self) -> &str;
fn version(&self) -> &str;
fn manifest_url(&self) -> Result<Url, PackageManagerError>;
fn archive_url(&self, archive: &PackageReleaseArchive) -> Result<Url, PackageManagerError>;
fn release_version<'a>(&self, manifest: &'a Self::ReleaseManifest) -> &'a str;
fn platform_archive(
&self,
manifest: &Self::ReleaseManifest,
platform: PackagePlatform,
) -> Result<PackageReleaseArchive, Self::Error>;
fn install_dir(&self, cache_root: &Path, platform: PackagePlatform) -> PathBuf;
fn installed_version<'a>(&self, package: &'a Self::Installed) -> &'a str;
fn load_installed(
&self,
root_dir: PathBuf,
platform: PackagePlatform,
) -> Result<Self::Installed, Self::Error>;
fn detect_extracted_root(&self, extraction_root: &Path) -> Result<PathBuf, Self::Error> {
detect_single_package_root(extraction_root).map_err(Self::Error::from)
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum PackagePlatform {
DarwinArm64,
DarwinX64,
LinuxArm64,
LinuxX64,
WindowsArm64,
WindowsX64,
}
impl PackagePlatform {
pub fn detect_current() -> Result<Self, PackageManagerError> {
match (std::env::consts::OS, std::env::consts::ARCH) {
("macos", "aarch64") | ("macos", "arm64") => Ok(Self::DarwinArm64),
("macos", "x86_64") => Ok(Self::DarwinX64),
("linux", "aarch64") | ("linux", "arm64") => Ok(Self::LinuxArm64),
("linux", "x86_64") => Ok(Self::LinuxX64),
("windows", "aarch64") | ("windows", "arm64") => Ok(Self::WindowsArm64),
("windows", "x86_64") => Ok(Self::WindowsX64),
(os, arch) => Err(PackageManagerError::UnsupportedPlatform {
os: os.to_string(),
arch: arch.to_string(),
}),
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::DarwinArm64 => "darwin-arm64",
Self::DarwinX64 => "darwin-x64",
Self::LinuxArm64 => "linux-arm64",
Self::LinuxX64 => "linux-x64",
Self::WindowsArm64 => "windows-arm64",
Self::WindowsX64 => "windows-x64",
}
}
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
pub struct PackageReleaseArchive {
pub archive: String,
pub sha256: String,
pub format: ArchiveFormat,
pub size_bytes: Option<u64>,
}
#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize, PartialEq, Eq)]
pub enum ArchiveFormat {
#[serde(rename = "zip")]
Zip,
#[serde(rename = "tar.gz")]
TarGz,
}
#[derive(Debug, Error)]
pub enum PackageManagerError {
#[error("unsupported platform: {os}-{arch}")]
UnsupportedPlatform { os: String, arch: String },
#[error("invalid release base url")]
InvalidBaseUrl(#[source] url::ParseError),
#[error("{context}")]
Http {
context: String,
#[source]
source: reqwest::Error,
},
#[error("{context}")]
Io {
context: String,
#[source]
source: std::io::Error,
},
#[error("missing platform entry `{0}` in release manifest")]
MissingPlatform(String),
#[error("unexpected package version: expected `{expected}`, got `{actual}`")]
UnexpectedPackageVersion { expected: String, actual: String },
#[error("checksum mismatch: expected `{expected}`, got `{actual}`")]
ChecksumMismatch { expected: String, actual: String },
#[error("archive extraction failed: {0}")]
ArchiveExtraction(String),
#[error("archive did not contain a package root with manifest.json under {0}")]
MissingPackageRoot(PathBuf),
}
pub fn detect_single_package_root(extraction_root: &Path) -> Result<PathBuf, PackageManagerError> {
let direct_manifest = extraction_root.join("manifest.json");
if direct_manifest.exists() {
return Ok(extraction_root.to_path_buf());
}
let mut directory_candidates = Vec::new();
for entry in std::fs::read_dir(extraction_root).map_err(|source| PackageManagerError::Io {
context: format!("failed to read {}", extraction_root.display()),
source,
})? {
let entry = entry.map_err(|source| PackageManagerError::Io {
context: format!("failed to read entry in {}", extraction_root.display()),
source,
})?;
let path = entry.path();
if path.is_dir() {
directory_candidates.push(path);
}
}
if directory_candidates.len() == 1 {
let candidate = &directory_candidates[0];
if candidate.join("manifest.json").exists() {
return Ok(candidate.clone());
}
}
Err(PackageManagerError::MissingPackageRoot(
extraction_root.to_path_buf(),
))
}
fn verify_sha256(bytes: &[u8], expected: &str) -> Result<(), PackageManagerError> {
let actual = format!("{:x}", Sha256::digest(bytes));
if actual == expected.to_ascii_lowercase() {
return Ok(());
}
Err(PackageManagerError::ChecksumMismatch {
expected: expected.to_string(),
actual,
})
}
fn extract_archive(
archive_path: &Path,
destination: &Path,
format: ArchiveFormat,
) -> Result<(), PackageManagerError> {
match format {
ArchiveFormat::Zip => extract_zip_archive(archive_path, destination),
ArchiveFormat::TarGz => extract_tar_gz_archive(archive_path, destination),
}
}
fn extract_zip_archive(archive_path: &Path, destination: &Path) -> Result<(), PackageManagerError> {
let file = File::open(archive_path).map_err(|source| PackageManagerError::Io {
context: format!("failed to open {}", archive_path.display()),
source,
})?;
let mut archive = ZipArchive::new(file)
.map_err(|error| PackageManagerError::ArchiveExtraction(error.to_string()))?;
for index in 0..archive.len() {
let mut entry = archive
.by_index(index)
.map_err(|error| PackageManagerError::ArchiveExtraction(error.to_string()))?;
let Some(relative_path) = entry.enclosed_name() else {
return Err(PackageManagerError::ArchiveExtraction(format!(
"zip entry `{}` escapes extraction root",
entry.name()
)));
};
let output_path = destination.join(relative_path);
if entry.is_dir() {
std::fs::create_dir_all(&output_path).map_err(|source| PackageManagerError::Io {
context: format!("failed to create {}", output_path.display()),
source,
})?;
continue;
}
if let Some(parent) = output_path.parent() {
std::fs::create_dir_all(parent).map_err(|source| PackageManagerError::Io {
context: format!("failed to create {}", parent.display()),
source,
})?;
}
let mut output = File::create(&output_path).map_err(|source| PackageManagerError::Io {
context: format!("failed to create {}", output_path.display()),
source,
})?;
std::io::copy(&mut entry, &mut output).map_err(|source| PackageManagerError::Io {
context: format!("failed to write {}", output_path.display()),
source,
})?;
apply_zip_permissions(&entry, &output_path)?;
}
Ok(())
}
#[cfg(unix)]
fn apply_zip_permissions(
entry: &zip::read::ZipFile<'_>,
output_path: &Path,
) -> Result<(), PackageManagerError> {
let Some(mode) = entry.unix_mode() else {
return Ok(());
};
std::fs::set_permissions(output_path, std::fs::Permissions::from_mode(mode)).map_err(|source| {
PackageManagerError::Io {
context: format!("failed to set permissions on {}", output_path.display()),
source,
}
})
}
#[cfg(not(unix))]
fn apply_zip_permissions(
_entry: &zip::read::ZipFile<'_>,
_output_path: &Path,
) -> Result<(), PackageManagerError> {
Ok(())
}
fn extract_tar_gz_archive(
archive_path: &Path,
destination: &Path,
) -> Result<(), PackageManagerError> {
let file = File::open(archive_path).map_err(|source| PackageManagerError::Io {
context: format!("failed to open {}", archive_path.display()),
source,
})?;
let decoder = GzDecoder::new(file);
let mut archive = Archive::new(decoder);
for entry in archive
.entries()
.map_err(|error| PackageManagerError::ArchiveExtraction(error.to_string()))?
{
let mut entry =
entry.map_err(|error| PackageManagerError::ArchiveExtraction(error.to_string()))?;
let path = entry
.path()
.map_err(|error| PackageManagerError::ArchiveExtraction(error.to_string()))?;
let output_path = safe_extract_path(destination, path.as_ref())?;
if let Some(parent) = output_path.parent() {
std::fs::create_dir_all(parent).map_err(|source| PackageManagerError::Io {
context: format!("failed to create {}", parent.display()),
source,
})?;
}
entry
.unpack(&output_path)
.map_err(|error| PackageManagerError::ArchiveExtraction(error.to_string()))?;
}
Ok(())
}
fn safe_extract_path(root: &Path, relative_path: &Path) -> Result<PathBuf, PackageManagerError> {
let mut clean_relative = PathBuf::new();
for component in relative_path.components() {
match component {
Component::Normal(segment) => clean_relative.push(segment),
Component::CurDir => {}
Component::ParentDir | Component::RootDir | Component::Prefix(_) => {
return Err(PackageManagerError::ArchiveExtraction(format!(
"entry `{}` escapes extraction root",
relative_path.display()
)));
}
}
}
if clean_relative.as_os_str().is_empty() {
return Err(PackageManagerError::ArchiveExtraction(
"archive entry had an empty path".to_string(),
));
}
Ok(root.join(clean_relative))
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
use serde::Deserialize;
use std::collections::BTreeMap;
use std::io::Cursor;
use std::io::Write;
use std::sync::Arc;
use tempfile::TempDir;
use tokio::sync::Barrier;
use wiremock::Mock;
use wiremock::MockServer;
use wiremock::ResponseTemplate;
use wiremock::matchers::method;
use wiremock::matchers::path;
use zip::ZipWriter;
use zip::write::SimpleFileOptions;
#[derive(Clone, Debug)]
struct TestPackage {
base_url: Url,
version: String,
}
#[derive(Clone, Debug, Deserialize)]
struct TestReleaseManifest {
package_version: String,
platforms: BTreeMap<String, PackageReleaseArchive>,
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct TestInstalledPackage {
version: String,
platform: PackagePlatform,
root_dir: PathBuf,
}
impl ManagedPackage for TestPackage {
type Error = PackageManagerError;
type Installed = TestInstalledPackage;
type ReleaseManifest = TestReleaseManifest;
fn default_cache_root_relative(&self) -> &str {
"packages/test-package"
}
fn version(&self) -> &str {
&self.version
}
fn manifest_url(&self) -> Result<Url, PackageManagerError> {
self.base_url
.join(&format!("test-package-v{}-manifest.json", self.version))
.map_err(PackageManagerError::InvalidBaseUrl)
}
fn archive_url(&self, archive: &PackageReleaseArchive) -> Result<Url, PackageManagerError> {
self.base_url
.join(&archive.archive)
.map_err(PackageManagerError::InvalidBaseUrl)
}
fn release_version<'a>(&self, manifest: &'a Self::ReleaseManifest) -> &'a str {
&manifest.package_version
}
fn platform_archive(
&self,
manifest: &Self::ReleaseManifest,
platform: PackagePlatform,
) -> Result<PackageReleaseArchive, Self::Error> {
manifest
.platforms
.get(platform.as_str())
.cloned()
.ok_or_else(|| PackageManagerError::MissingPlatform(platform.as_str().to_string()))
}
fn install_dir(&self, cache_root: &Path, platform: PackagePlatform) -> PathBuf {
cache_root.join(self.version()).join(platform.as_str())
}
fn installed_version<'a>(&self, package: &'a Self::Installed) -> &'a str {
&package.version
}
fn load_installed(
&self,
root_dir: PathBuf,
platform: PackagePlatform,
) -> Result<Self::Installed, Self::Error> {
let version =
std::fs::read_to_string(root_dir.join("manifest.json")).map_err(|source| {
PackageManagerError::Io {
context: format!(
"failed to read {}",
root_dir.join("manifest.json").display()
),
source,
}
})?;
Ok(TestInstalledPackage {
version: version.trim().to_string(),
platform,
root_dir,
})
}
}
#[tokio::test]
async fn ensure_installed_downloads_and_extracts_zip_package() {
let server = MockServer::start().await;
let version = "0.1.0";
let platform = PackagePlatform::detect_current().unwrap_or_else(|error| panic!("{error}"));
let archive_name = format!("test-package-v{version}-{}.zip", platform.as_str());
let archive_bytes = build_zip_archive(version);
let archive_sha = format!("{:x}", Sha256::digest(&archive_bytes));
let manifest = serde_json::json!({
"package_version": version,
"platforms": {
platform.as_str(): {
"archive": archive_name,
"sha256": archive_sha,
"format": "zip",
"size_bytes": archive_bytes.len(),
}
}
});
Mock::given(method("GET"))
.and(path(format!("/test-package-v{version}-manifest.json")))
.respond_with(ResponseTemplate::new(200).set_body_json(&manifest))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path(format!("/{archive_name}")))
.respond_with(ResponseTemplate::new(200).set_body_bytes(archive_bytes))
.mount(&server)
.await;
let codex_home = TempDir::new().unwrap_or_else(|error| panic!("{error}"));
let package = TestPackage {
base_url: Url::parse(&format!("{}/", server.uri()))
.unwrap_or_else(|error| panic!("{error}")),
version: version.to_string(),
};
let manager = PackageManager::new(PackageManagerConfig::new(
codex_home.path().to_path_buf(),
package,
));
let installed = manager
.ensure_installed()
.await
.unwrap_or_else(|error| panic!("{error}"));
assert_eq!(
installed,
TestInstalledPackage {
version: version.to_string(),
platform,
root_dir: codex_home
.path()
.join("packages")
.join("test-package")
.join(version)
.join(platform.as_str()),
}
);
#[cfg(unix)]
{
let executable_mode = std::fs::metadata(installed.root_dir.join("bin/tool"))
.unwrap_or_else(|error| panic!("{error}"))
.permissions()
.mode();
assert_eq!(executable_mode & 0o111, 0o111);
}
}
#[tokio::test]
async fn ensure_installed_replaces_invalid_cached_install() {
let server = MockServer::start().await;
let version = "0.1.0";
let platform = PackagePlatform::detect_current().unwrap_or_else(|error| panic!("{error}"));
let archive_name = format!("test-package-v{version}-{}.zip", platform.as_str());
let archive_bytes = build_zip_archive(version);
let archive_sha = format!("{:x}", Sha256::digest(&archive_bytes));
let manifest = serde_json::json!({
"package_version": version,
"platforms": {
platform.as_str(): {
"archive": archive_name,
"sha256": archive_sha,
"format": "zip",
"size_bytes": archive_bytes.len(),
}
}
});
Mock::given(method("GET"))
.and(path(format!("/test-package-v{version}-manifest.json")))
.respond_with(ResponseTemplate::new(200).set_body_json(&manifest))
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path(format!("/{archive_name}")))
.respond_with(ResponseTemplate::new(200).set_body_bytes(archive_bytes))
.mount(&server)
.await;
let codex_home = TempDir::new().unwrap_or_else(|error| panic!("{error}"));
let install_dir = codex_home
.path()
.join("packages")
.join("test-package")
.join(version)
.join(platform.as_str());
std::fs::create_dir_all(&install_dir).unwrap_or_else(|error| panic!("{error}"));
std::fs::write(install_dir.join("broken.txt"), "stale")
.unwrap_or_else(|error| panic!("{error}"));
let manager = PackageManager::new(PackageManagerConfig::new(
codex_home.path().to_path_buf(),
TestPackage {
base_url: Url::parse(&format!("{}/", server.uri()))
.unwrap_or_else(|error| panic!("{error}")),
version: version.to_string(),
},
));
let installed = manager
.ensure_installed()
.await
.unwrap_or_else(|error| panic!("{error}"));
assert_eq!(installed.version, version);
assert!(installed.root_dir.join("manifest.json").exists());
assert!(!installed.root_dir.join("broken.txt").exists());
}
#[tokio::test]
async fn ensure_installed_serializes_concurrent_installs() {
let server = MockServer::start().await;
let version = "0.1.0";
let platform = PackagePlatform::detect_current().unwrap_or_else(|error| panic!("{error}"));
let archive_name = format!("test-package-v{version}-{}.zip", platform.as_str());
let archive_bytes = build_zip_archive(version);
let archive_sha = format!("{:x}", Sha256::digest(&archive_bytes));
let manifest = serde_json::json!({
"package_version": version,
"platforms": {
platform.as_str(): {
"archive": archive_name,
"sha256": archive_sha,
"format": "zip",
"size_bytes": archive_bytes.len(),
}
}
});
Mock::given(method("GET"))
.and(path(format!("/test-package-v{version}-manifest.json")))
.respond_with(ResponseTemplate::new(200).set_body_json(&manifest))
.expect(1)
.mount(&server)
.await;
Mock::given(method("GET"))
.and(path(format!("/{archive_name}")))
.respond_with(ResponseTemplate::new(200).set_body_bytes(archive_bytes))
.expect(1)
.mount(&server)
.await;
let codex_home = TempDir::new().unwrap_or_else(|error| panic!("{error}"));
let config = PackageManagerConfig::new(
codex_home.path().to_path_buf(),
TestPackage {
base_url: Url::parse(&format!("{}/", server.uri()))
.unwrap_or_else(|error| panic!("{error}")),
version: version.to_string(),
},
);
let manager_one = PackageManager::new(config.clone());
let manager_two = PackageManager::new(config);
let barrier = Arc::new(Barrier::new(2));
let barrier_one = Arc::clone(&barrier);
let barrier_two = Arc::clone(&barrier);
let (first, second) = tokio::join!(
async {
barrier_one.wait().await;
manager_one.ensure_installed().await
},
async {
barrier_two.wait().await;
manager_two.ensure_installed().await
}
);
let first = first.unwrap_or_else(|error| panic!("{error}"));
let second = second.unwrap_or_else(|error| panic!("{error}"));
assert_eq!(first, second);
}
#[test]
fn tar_gz_extraction_supports_default_package_root_detection() {
let temp = TempDir::new().unwrap_or_else(|error| panic!("{error}"));
let archive_path = temp.path().join("package.tar.gz");
let extraction_root = temp.path().join("extract");
std::fs::create_dir_all(&extraction_root).unwrap_or_else(|error| panic!("{error}"));
write_tar_gz_archive(&archive_path, "0.2.0");
extract_archive(&archive_path, &extraction_root, ArchiveFormat::TarGz)
.unwrap_or_else(|error| panic!("{error}"));
let package_root =
detect_single_package_root(&extraction_root).unwrap_or_else(|error| panic!("{error}"));
assert!(package_root.join("manifest.json").exists());
}
fn build_zip_archive(version: &str) -> Vec<u8> {
let mut bytes = Cursor::new(Vec::new());
{
let mut zip = ZipWriter::new(&mut bytes);
let options = SimpleFileOptions::default();
zip.start_file("test-package/manifest.json", options)
.unwrap_or_else(|error| panic!("{error}"));
zip.write_all(version.as_bytes())
.unwrap_or_else(|error| panic!("{error}"));
zip.start_file("test-package/bin/tool", options.unix_permissions(0o755))
.unwrap_or_else(|error| panic!("{error}"));
zip.write_all(b"#!/bin/sh\n")
.unwrap_or_else(|error| panic!("{error}"));
zip.finish().unwrap_or_else(|error| panic!("{error}"));
}
bytes.into_inner()
}
fn write_tar_gz_archive(archive_path: &Path, version: &str) {
let file = File::create(archive_path).unwrap_or_else(|error| panic!("{error}"));
let encoder = flate2::write::GzEncoder::new(file, flate2::Compression::default());
let mut builder = tar::Builder::new(encoder);
append_tar_file(
&mut builder,
"test-package/manifest.json",
version.as_bytes(),
);
builder.finish().unwrap_or_else(|error| panic!("{error}"));
}
fn append_tar_file(
builder: &mut tar::Builder<flate2::write::GzEncoder<File>>,
path: &str,
contents: &[u8],
) {
let mut header = tar::Header::new_gnu();
header.set_size(contents.len() as u64);
header.set_mode(0o755);
header.set_cksum();
builder
.append_data(&mut header, path, contents)
.unwrap_or_else(|error| panic!("{error}"));
}
}