From 6dcac41d5375d70dc9606c790c68b63260fb2635 Mon Sep 17 00:00:00 2001 From: jif-oai Date: Thu, 26 Mar 2026 14:28:59 +0000 Subject: [PATCH] chore: drop artifacts lib (#15864) --- codex-rs/Cargo.lock | 21 - codex-rs/Cargo.toml | 4 +- codex-rs/artifacts/BUILD.bazel | 6 - codex-rs/artifacts/Cargo.toml | 28 -- codex-rs/artifacts/README.md | 36 -- codex-rs/artifacts/src/client.rs | 229 ---------- codex-rs/artifacts/src/lib.rs | 24 - codex-rs/artifacts/src/runtime/error.rs | 28 -- codex-rs/artifacts/src/runtime/installed.rs | 283 ------------ codex-rs/artifacts/src/runtime/js_runtime.rs | 171 ------- codex-rs/artifacts/src/runtime/manager.rs | 255 ----------- codex-rs/artifacts/src/runtime/manifest.rs | 15 - codex-rs/artifacts/src/runtime/mod.rs | 28 -- codex-rs/artifacts/src/tests.rs | 453 ------------------- 14 files changed, 1 insertion(+), 1580 deletions(-) delete mode 100644 codex-rs/artifacts/BUILD.bazel delete mode 100644 codex-rs/artifacts/Cargo.toml delete mode 100644 codex-rs/artifacts/README.md delete mode 100644 codex-rs/artifacts/src/client.rs delete mode 100644 codex-rs/artifacts/src/lib.rs delete mode 100644 codex-rs/artifacts/src/runtime/error.rs delete mode 100644 codex-rs/artifacts/src/runtime/installed.rs delete mode 100644 codex-rs/artifacts/src/runtime/js_runtime.rs delete mode 100644 codex-rs/artifacts/src/runtime/manager.rs delete mode 100644 codex-rs/artifacts/src/runtime/manifest.rs delete mode 100644 codex-rs/artifacts/src/runtime/mod.rs delete mode 100644 codex-rs/artifacts/src/tests.rs diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 14d45c1dd..a2b1450e8 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1551,27 +1551,6 @@ dependencies = [ "tokio", ] -[[package]] -name = "codex-artifacts" -version = "0.0.0" -dependencies = [ - "codex-package-manager", - "flate2", - "pretty_assertions", - "reqwest", - "serde", - "serde_json", - "sha2", - "tar", - "tempfile", - "thiserror 2.0.18", - "tokio", - "url", - "which 8.0.0", - "wiremock", - "zip", -] - [[package]] name = "codex-async-utils" version = "0.0.0" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index 4b2d918fb..8d047d923 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -83,7 +83,6 @@ members = [ "test-macros", "package-manager", "plugin", - "artifacts", ] resolver = "2" @@ -102,7 +101,6 @@ app_test_support = { path = "app-server/tests/common" } codex-ansi-escape = { path = "ansi-escape" } codex-analytics = { path = "analytics" } codex-api = { path = "codex-api" } -codex-artifacts = { path = "artifacts" } codex-code-mode = { path = "code-mode" } codex-package-manager = { path = "package-manager" } codex-app-server = { path = "app-server" } @@ -397,7 +395,7 @@ unwrap_used = "deny" ignored = [ "icu_provider", "openssl-sys", - "codex-artifacts", + "codex-package-manager", "codex-utils-readiness", "codex-utils-template", "codex-v8-poc", diff --git a/codex-rs/artifacts/BUILD.bazel b/codex-rs/artifacts/BUILD.bazel deleted file mode 100644 index 300e9b0fc..000000000 --- a/codex-rs/artifacts/BUILD.bazel +++ /dev/null @@ -1,6 +0,0 @@ -load("//:defs.bzl", "codex_rust_crate") - -codex_rust_crate( - name = "artifacts", - crate_name = "codex_artifacts", -) diff --git a/codex-rs/artifacts/Cargo.toml b/codex-rs/artifacts/Cargo.toml deleted file mode 100644 index 0c5bbfc25..000000000 --- a/codex-rs/artifacts/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "codex-artifacts" -version.workspace = true -edition.workspace = true -license.workspace = true - -[dependencies] -codex-package-manager = { workspace = true } -reqwest = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -tempfile = { workspace = true } -thiserror = { workspace = true } -tokio = { workspace = true, features = ["fs", "io-util", "process", "time"] } -url = { workspace = true } -which = { workspace = true } - -[lints] -workspace = true - -[dev-dependencies] -flate2 = { workspace = true } -pretty_assertions = { workspace = true } -sha2 = { workspace = true } -tar = { workspace = true } -tokio = { workspace = true, features = ["fs", "io-util", "macros", "process", "rt", "rt-multi-thread", "time"] } -wiremock = { workspace = true } -zip = { workspace = true } diff --git a/codex-rs/artifacts/README.md b/codex-rs/artifacts/README.md deleted file mode 100644 index dae83f3e8..000000000 --- a/codex-rs/artifacts/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# codex-artifacts - -Runtime and process-management helpers for Codex artifact generation. - -This crate has two main responsibilities: - -- locating, validating, and optionally downloading the pinned artifact runtime -- spawning the artifact build or render command against that runtime - -## Module layout - -- `src/client.rs` - Runs build and render commands once a runtime has been resolved. -- `src/runtime/manager.rs` - Defines the release locator and the package-manager-backed runtime installer. -- `src/runtime/installed.rs` - Loads an extracted runtime from disk and validates its manifest and entrypoints. -- `src/runtime/js_runtime.rs` - Chooses the JavaScript executable to use for artifact execution. -- `src/runtime/manifest.rs` - Manifest types for release metadata and extracted runtimes. -- `src/runtime/error.rs` - Public runtime-loading and installation errors. -- `src/tests.rs` - Crate-level tests that exercise the public API and integration seams. - -## Public API - -- `ArtifactRuntimeManager` - Resolves or installs a runtime package into `~/.codex/packages/artifacts/...`. -- `load_cached_runtime` - Reads a previously installed runtime from a caller-provided cache root without attempting a download. -- `is_js_runtime_available` - Checks whether artifact execution is possible with either a cached runtime or a host JS runtime. -- `ArtifactsClient` - Executes artifact build or render requests using either a managed or preinstalled runtime. diff --git a/codex-rs/artifacts/src/client.rs b/codex-rs/artifacts/src/client.rs deleted file mode 100644 index d0a10ed12..000000000 --- a/codex-rs/artifacts/src/client.rs +++ /dev/null @@ -1,229 +0,0 @@ -use crate::ArtifactRuntimeError; -use crate::ArtifactRuntimeManager; -use crate::InstalledArtifactRuntime; -use std::collections::BTreeMap; -use std::path::PathBuf; -use std::process::Stdio; -use std::time::Duration; -use tempfile::TempDir; -use thiserror::Error; -use tokio::fs; -use tokio::io::AsyncReadExt; -use tokio::process::Command; -use tokio::time::timeout; -use url::Url; - -const DEFAULT_EXECUTION_TIMEOUT: Duration = Duration::from_secs(30); - -/// Executes artifact build commands against a resolved runtime. -#[derive(Clone, Debug)] -pub struct ArtifactsClient { - runtime_source: RuntimeSource, -} - -#[derive(Clone, Debug)] -#[allow(clippy::large_enum_variant)] -enum RuntimeSource { - Managed(ArtifactRuntimeManager), - Installed(InstalledArtifactRuntime), -} - -impl ArtifactsClient { - /// Creates a client that lazily resolves or downloads the runtime on demand. - pub fn from_runtime_manager(runtime_manager: ArtifactRuntimeManager) -> Self { - Self { - runtime_source: RuntimeSource::Managed(runtime_manager), - } - } - - /// Creates a client pinned to an already loaded runtime. - pub fn from_installed_runtime(runtime: InstalledArtifactRuntime) -> Self { - Self { - runtime_source: RuntimeSource::Installed(runtime), - } - } - - /// Executes artifact-building JavaScript against the configured runtime. - pub async fn execute_build( - &self, - request: ArtifactBuildRequest, - ) -> Result { - let runtime = self.resolve_runtime().await?; - let js_runtime = runtime.resolve_js_runtime()?; - let staging_dir = TempDir::new().map_err(|source| ArtifactsError::Io { - context: "failed to create build staging directory".to_string(), - source, - })?; - let script_path = staging_dir.path().join("artifact-build.mjs"); - let build_entrypoint_url = - Url::from_file_path(runtime.build_js_path()).map_err(|()| ArtifactsError::Io { - context: format!( - "failed to convert artifact build entrypoint to a file URL: {}", - runtime.build_js_path().display() - ), - source: std::io::Error::new( - std::io::ErrorKind::InvalidInput, - "invalid artifact build entrypoint path", - ), - })?; - let wrapped_script = build_wrapped_script(&build_entrypoint_url, &request.source); - fs::write(&script_path, wrapped_script) - .await - .map_err(|source| ArtifactsError::Io { - context: format!("failed to write {}", script_path.display()), - source, - })?; - - let mut command = Command::new(js_runtime.executable_path()); - command.arg(&script_path).current_dir(&request.cwd); - command.stdout(Stdio::piped()).stderr(Stdio::piped()); - if js_runtime.requires_electron_run_as_node() { - command.env("ELECTRON_RUN_AS_NODE", "1"); - } - for (key, value) in &request.env { - command.env(key, value); - } - - run_command( - command, - request.timeout.unwrap_or(DEFAULT_EXECUTION_TIMEOUT), - ) - .await - } - - async fn resolve_runtime(&self) -> Result { - match &self.runtime_source { - RuntimeSource::Installed(runtime) => Ok(runtime.clone()), - RuntimeSource::Managed(manager) => manager.ensure_installed().await.map_err(Into::into), - } - } -} - -/// Request payload for the artifact build command. -#[derive(Clone, Debug, Default)] -pub struct ArtifactBuildRequest { - pub source: String, - pub cwd: PathBuf, - pub timeout: Option, - pub env: BTreeMap, -} - -/// Captured stdout, stderr, and exit status from an artifact subprocess. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ArtifactCommandOutput { - pub exit_code: Option, - pub stdout: String, - pub stderr: String, -} - -impl ArtifactCommandOutput { - /// Returns whether the subprocess exited successfully. - pub fn success(&self) -> bool { - self.exit_code == Some(0) - } -} - -/// Errors raised while spawning or awaiting artifact subprocesses. -#[derive(Debug, Error)] -pub enum ArtifactsError { - #[error(transparent)] - Runtime(#[from] ArtifactRuntimeError), - #[error("{context}")] - Io { - context: String, - #[source] - source: std::io::Error, - }, - #[error("artifact command timed out after {timeout:?}")] - TimedOut { timeout: Duration }, -} - -fn build_wrapped_script(build_entrypoint_url: &Url, source: &str) -> String { - let mut wrapped = String::new(); - wrapped.push_str("const artifactTool = await import("); - wrapped.push_str( - &serde_json::to_string(build_entrypoint_url.as_str()).unwrap_or_else(|error| { - panic!("artifact build entrypoint URL must serialize: {error}") - }), - ); - wrapped.push_str(");\n"); - wrapped.push_str( - r#"globalThis.artifactTool = artifactTool; -for (const [name, value] of Object.entries(artifactTool)) { - if (name === "default" || Object.prototype.hasOwnProperty.call(globalThis, name)) { - continue; - } - globalThis[name] = value; -} -"#, - ); - wrapped.push_str(source); - wrapped.push('\n'); - wrapped -} - -async fn run_command( - mut command: Command, - execution_timeout: Duration, -) -> Result { - let mut child = command.spawn().map_err(|source| ArtifactsError::Io { - context: "failed to spawn artifact command".to_string(), - source, - })?; - let mut stdout = child.stdout.take().ok_or_else(|| ArtifactsError::Io { - context: "artifact command stdout was not captured".to_string(), - source: std::io::Error::other("missing stdout pipe"), - })?; - let mut stderr = child.stderr.take().ok_or_else(|| ArtifactsError::Io { - context: "artifact command stderr was not captured".to_string(), - source: std::io::Error::other("missing stderr pipe"), - })?; - let stdout_task = tokio::spawn(async move { - let mut bytes = Vec::new(); - stdout.read_to_end(&mut bytes).await.map(|_| bytes) - }); - let stderr_task = tokio::spawn(async move { - let mut bytes = Vec::new(); - stderr.read_to_end(&mut bytes).await.map(|_| bytes) - }); - - let status = match timeout(execution_timeout, child.wait()).await { - Ok(result) => result.map_err(|source| ArtifactsError::Io { - context: "failed while waiting for artifact command".to_string(), - source, - })?, - Err(_) => { - let _ = child.kill().await; - let _ = child.wait().await; - return Err(ArtifactsError::TimedOut { - timeout: execution_timeout, - }); - } - }; - let stdout_bytes = stdout_task - .await - .map_err(|source| ArtifactsError::Io { - context: "failed to join stdout reader".to_string(), - source: std::io::Error::other(source.to_string()), - })? - .map_err(|source| ArtifactsError::Io { - context: "failed to read artifact command stdout".to_string(), - source, - })?; - let stderr_bytes = stderr_task - .await - .map_err(|source| ArtifactsError::Io { - context: "failed to join stderr reader".to_string(), - source: std::io::Error::other(source.to_string()), - })? - .map_err(|source| ArtifactsError::Io { - context: "failed to read artifact command stderr".to_string(), - source, - })?; - - Ok(ArtifactCommandOutput { - exit_code: status.code(), - stdout: String::from_utf8_lossy(&stdout_bytes).into_owned(), - stderr: String::from_utf8_lossy(&stderr_bytes).into_owned(), - }) -} diff --git a/codex-rs/artifacts/src/lib.rs b/codex-rs/artifacts/src/lib.rs deleted file mode 100644 index 812c3db85..000000000 --- a/codex-rs/artifacts/src/lib.rs +++ /dev/null @@ -1,24 +0,0 @@ -mod client; -mod runtime; -#[cfg(all(test, not(windows)))] -mod tests; - -pub use client::ArtifactBuildRequest; -pub use client::ArtifactCommandOutput; -pub use client::ArtifactsClient; -pub use client::ArtifactsError; -pub use runtime::ArtifactRuntimeError; -pub use runtime::ArtifactRuntimeManager; -pub use runtime::ArtifactRuntimeManagerConfig; -pub use runtime::ArtifactRuntimePlatform; -pub use runtime::ArtifactRuntimeReleaseLocator; -pub use runtime::DEFAULT_CACHE_ROOT_RELATIVE; -pub use runtime::DEFAULT_RELEASE_BASE_URL; -pub use runtime::DEFAULT_RELEASE_TAG_PREFIX; -pub use runtime::InstalledArtifactRuntime; -pub use runtime::JsRuntime; -pub use runtime::JsRuntimeKind; -pub use runtime::ReleaseManifest; -pub use runtime::can_manage_artifact_runtime; -pub use runtime::is_js_runtime_available; -pub use runtime::load_cached_runtime; diff --git a/codex-rs/artifacts/src/runtime/error.rs b/codex-rs/artifacts/src/runtime/error.rs deleted file mode 100644 index 9a7090d46..000000000 --- a/codex-rs/artifacts/src/runtime/error.rs +++ /dev/null @@ -1,28 +0,0 @@ -use codex_package_manager::PackageManagerError; -use std::path::PathBuf; -use thiserror::Error; - -/// Errors raised while locating, validating, or installing an artifact runtime. -#[derive(Debug, Error)] -pub enum ArtifactRuntimeError { - #[error(transparent)] - PackageManager(#[from] PackageManagerError), - #[error("{context}")] - Io { - context: String, - #[source] - source: std::io::Error, - }, - #[error("invalid package metadata at {path}")] - InvalidPackageMetadata { - path: PathBuf, - #[source] - source: serde_json::Error, - }, - #[error("runtime path `{0}` is invalid")] - InvalidRuntimePath(String), - #[error( - "no compatible JavaScript runtime found for artifact runtime at {root_dir}; install Node or the Codex desktop app" - )] - MissingJsRuntime { root_dir: PathBuf }, -} diff --git a/codex-rs/artifacts/src/runtime/installed.rs b/codex-rs/artifacts/src/runtime/installed.rs deleted file mode 100644 index 76e3c6d13..000000000 --- a/codex-rs/artifacts/src/runtime/installed.rs +++ /dev/null @@ -1,283 +0,0 @@ -use super::ArtifactRuntimeError; -use super::ArtifactRuntimePlatform; -use super::JsRuntime; -use super::codex_app_runtime_candidates; -use super::resolve_js_runtime_from_candidates; -use super::system_electron_runtime; -use super::system_node_runtime; -use std::collections::BTreeMap; -use std::path::Component; -use std::path::Path; -use std::path::PathBuf; - -const ARTIFACT_TOOL_PACKAGE_NAME: &str = "@oai/artifact-tool"; - -/// Loads a previously installed runtime from a caller-provided cache root. -pub fn load_cached_runtime( - cache_root: &Path, - runtime_version: &str, -) -> Result { - let platform = ArtifactRuntimePlatform::detect_current()?; - let install_dir = cached_runtime_install_dir(cache_root, runtime_version, platform); - if !install_dir.exists() { - return Err(ArtifactRuntimeError::Io { - context: format!( - "artifact runtime {runtime_version} is not installed at {}", - install_dir.display() - ), - source: std::io::Error::new(std::io::ErrorKind::NotFound, "missing artifact runtime"), - }); - } - - InstalledArtifactRuntime::load(install_dir, platform) -} - -/// A validated runtime installation extracted into the local package cache. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct InstalledArtifactRuntime { - root_dir: PathBuf, - runtime_version: String, - platform: ArtifactRuntimePlatform, - build_js_path: PathBuf, -} - -impl InstalledArtifactRuntime { - /// Creates an installed-runtime value from prevalidated paths. - pub fn new( - root_dir: PathBuf, - runtime_version: String, - platform: ArtifactRuntimePlatform, - build_js_path: PathBuf, - ) -> Self { - Self { - root_dir, - runtime_version, - platform, - build_js_path, - } - } - - /// Loads and validates an extracted runtime directory. - pub fn load( - root_dir: PathBuf, - platform: ArtifactRuntimePlatform, - ) -> Result { - let package_metadata = load_package_metadata(&root_dir)?; - let build_js_path = - resolve_relative_runtime_path(&root_dir, &package_metadata.build_js_relative_path)?; - verify_required_runtime_path(&build_js_path)?; - - Ok(Self::new( - root_dir, - package_metadata.version, - platform, - build_js_path, - )) - } - - /// Returns the extracted runtime root directory. - pub fn root_dir(&self) -> &Path { - &self.root_dir - } - - /// Returns the runtime version recorded in `package.json`. - pub fn runtime_version(&self) -> &str { - &self.runtime_version - } - - /// Returns the platform this runtime was installed for. - pub fn platform(&self) -> ArtifactRuntimePlatform { - self.platform - } - - /// Returns the artifact build entrypoint path. - pub fn build_js_path(&self) -> &Path { - &self.build_js_path - } - - /// Resolves the best executable to use for artifact commands. - /// - /// Preference order is a machine Node install, then Electron from the - /// machine or a Codex desktop app bundle. - pub fn resolve_js_runtime(&self) -> Result { - resolve_js_runtime_from_candidates( - system_node_runtime(), - system_electron_runtime(), - codex_app_runtime_candidates(), - ) - .ok_or_else(|| ArtifactRuntimeError::MissingJsRuntime { - root_dir: self.root_dir.clone(), - }) - } -} - -pub(crate) fn cached_runtime_install_dir( - cache_root: &Path, - runtime_version: &str, - platform: ArtifactRuntimePlatform, -) -> PathBuf { - cache_root.join(runtime_version).join(platform.as_str()) -} - -pub(crate) fn default_cached_runtime_root(codex_home: &Path) -> PathBuf { - codex_home.join(super::DEFAULT_CACHE_ROOT_RELATIVE) -} - -fn resolve_relative_runtime_path( - root_dir: &Path, - relative_path: &str, -) -> Result { - let relative = Path::new(relative_path); - if relative.as_os_str().is_empty() || relative.is_absolute() { - return Err(ArtifactRuntimeError::InvalidRuntimePath( - relative_path.to_string(), - )); - } - if relative.components().any(|component| { - matches!( - component, - Component::ParentDir | Component::Prefix(_) | Component::RootDir - ) - }) { - return Err(ArtifactRuntimeError::InvalidRuntimePath( - relative_path.to_string(), - )); - } - Ok(root_dir.join(relative)) -} - -fn verify_required_runtime_path(path: &Path) -> Result<(), ArtifactRuntimeError> { - if path.is_file() { - return Ok(()); - } - - Err(ArtifactRuntimeError::Io { - context: format!("required runtime file is missing: {}", path.display()), - source: std::io::Error::new(std::io::ErrorKind::NotFound, "missing runtime file"), - }) -} - -pub(crate) fn detect_runtime_root(extraction_root: &Path) -> Result { - if is_runtime_root(extraction_root) { - 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| ArtifactRuntimeError::Io { - context: format!("failed to read {}", extraction_root.display()), - source, - })? { - let entry = entry.map_err(|source| ArtifactRuntimeError::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 is_runtime_root(candidate) { - return Ok(candidate.clone()); - } - } - - Err(ArtifactRuntimeError::Io { - context: format!( - "failed to detect artifact runtime root under {}", - extraction_root.display() - ), - source: std::io::Error::new( - std::io::ErrorKind::NotFound, - "missing artifact runtime root", - ), - }) -} - -fn is_runtime_root(root_dir: &Path) -> bool { - let Ok(package_metadata) = load_package_metadata(root_dir) else { - return false; - }; - let Ok(build_js_path) = - resolve_relative_runtime_path(root_dir, &package_metadata.build_js_relative_path) - else { - return false; - }; - - build_js_path.is_file() -} - -struct PackageMetadata { - version: String, - build_js_relative_path: String, -} - -fn load_package_metadata(root_dir: &Path) -> Result { - #[derive(serde::Deserialize)] - struct PackageJson { - name: String, - version: String, - exports: PackageExports, - } - - #[derive(serde::Deserialize)] - #[serde(untagged)] - enum PackageExports { - Main(String), - Map(BTreeMap), - } - - impl PackageExports { - fn build_entrypoint(&self) -> Option<&str> { - match self { - Self::Main(path) => Some(path), - Self::Map(exports) => exports.get(".").map(String::as_str), - } - } - } - - let package_json_path = root_dir.join("package.json"); - let package_json_bytes = - std::fs::read(&package_json_path).map_err(|source| ArtifactRuntimeError::Io { - context: format!("failed to read {}", package_json_path.display()), - source, - })?; - let package_json = - serde_json::from_slice::(&package_json_bytes).map_err(|source| { - ArtifactRuntimeError::InvalidPackageMetadata { - path: package_json_path.clone(), - source, - } - })?; - - if package_json.name != ARTIFACT_TOOL_PACKAGE_NAME { - return Err(ArtifactRuntimeError::Io { - context: format!( - "unsupported artifact runtime package at {}; expected name `{ARTIFACT_TOOL_PACKAGE_NAME}`, got `{}`", - package_json_path.display(), - package_json.name - ), - source: std::io::Error::new( - std::io::ErrorKind::InvalidData, - "unsupported package name", - ), - }); - } - - let Some(build_js_relative_path) = package_json.exports.build_entrypoint() else { - return Err(ArtifactRuntimeError::Io { - context: format!( - "unsupported artifact runtime package at {}; expected `exports[\".\"]` to point at the JS entrypoint", - package_json_path.display() - ), - source: std::io::Error::new(std::io::ErrorKind::InvalidData, "missing package export"), - }); - }; - - Ok(PackageMetadata { - version: package_json.version, - build_js_relative_path: build_js_relative_path.trim_start_matches("./").to_string(), - }) -} diff --git a/codex-rs/artifacts/src/runtime/js_runtime.rs b/codex-rs/artifacts/src/runtime/js_runtime.rs deleted file mode 100644 index 228747e47..000000000 --- a/codex-rs/artifacts/src/runtime/js_runtime.rs +++ /dev/null @@ -1,171 +0,0 @@ -use crate::ArtifactRuntimePlatform; -use crate::runtime::default_cached_runtime_root; -use crate::runtime::load_cached_runtime; -use std::path::Path; -use std::path::PathBuf; -use which::which; - -const CODEX_APP_PRODUCT_NAMES: [&str; 6] = [ - "Codex", - "Codex (Dev)", - "Codex (Agent)", - "Codex (Nightly)", - "Codex (Alpha)", - "Codex (Beta)", -]; - -/// The JavaScript runtime used to execute the artifact tool. -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum JsRuntimeKind { - Node, - Electron, -} - -/// A discovered JavaScript executable and the way it should be invoked. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct JsRuntime { - executable_path: PathBuf, - kind: JsRuntimeKind, -} - -impl JsRuntime { - pub(crate) fn node(executable_path: PathBuf) -> Self { - Self { - executable_path, - kind: JsRuntimeKind::Node, - } - } - - pub(crate) fn electron(executable_path: PathBuf) -> Self { - Self { - executable_path, - kind: JsRuntimeKind::Electron, - } - } - - /// Returns the executable to spawn for artifact commands. - pub fn executable_path(&self) -> &Path { - &self.executable_path - } - - /// Returns whether the command must set `ELECTRON_RUN_AS_NODE=1`. - pub fn requires_electron_run_as_node(&self) -> bool { - self.kind == JsRuntimeKind::Electron - } -} - -/// Returns `true` when artifact execution can find both runtime assets and a JS executable. -pub fn is_js_runtime_available(codex_home: &Path, runtime_version: &str) -> bool { - load_cached_runtime(&default_cached_runtime_root(codex_home), runtime_version) - .ok() - .and_then(|runtime| runtime.resolve_js_runtime().ok()) - .or_else(resolve_machine_js_runtime) - .is_some() -} - -/// Returns `true` when this machine can use the managed artifact runtime flow. -/// -/// This is a platform capability check, not a cache or binary availability check. -/// Callers that rely on `ArtifactRuntimeManager::ensure_installed()` should use this -/// to decide whether the feature can be exposed on the current machine. -pub fn can_manage_artifact_runtime() -> bool { - ArtifactRuntimePlatform::detect_current().is_ok() -} - -pub(crate) fn resolve_machine_js_runtime() -> Option { - resolve_js_runtime_from_candidates( - system_node_runtime(), - system_electron_runtime(), - codex_app_runtime_candidates(), - ) -} - -pub(crate) fn resolve_js_runtime_from_candidates( - node_runtime: Option, - electron_runtime: Option, - codex_app_candidates: Vec, -) -> Option { - node_runtime.or(electron_runtime).or_else(|| { - codex_app_candidates - .into_iter() - .find_map(|candidate| electron_runtime_from_path(&candidate)) - }) -} - -pub(crate) fn system_node_runtime() -> Option { - which("node") - .ok() - .and_then(|path| node_runtime_from_path(&path)) -} - -pub(crate) fn system_electron_runtime() -> Option { - which("electron") - .ok() - .and_then(|path| electron_runtime_from_path(&path)) -} - -pub(crate) fn node_runtime_from_path(path: &Path) -> Option { - path.is_file().then(|| JsRuntime::node(path.to_path_buf())) -} - -pub(crate) fn electron_runtime_from_path(path: &Path) -> Option { - path.is_file() - .then(|| JsRuntime::electron(path.to_path_buf())) -} - -pub(crate) fn codex_app_runtime_candidates() -> Vec { - match std::env::consts::OS { - "macos" => { - let mut roots = vec![PathBuf::from("/Applications")]; - if let Some(home) = std::env::var_os("HOME") { - roots.push(PathBuf::from(home).join("Applications")); - } - - roots - .into_iter() - .flat_map(|root| { - CODEX_APP_PRODUCT_NAMES - .into_iter() - .map(move |product_name| { - root.join(format!("{product_name}.app")) - .join("Contents") - .join("MacOS") - .join(product_name) - }) - }) - .collect() - } - "windows" => { - let mut roots = Vec::new(); - if let Some(local_app_data) = std::env::var_os("LOCALAPPDATA") { - roots.push(PathBuf::from(local_app_data).join("Programs")); - } - if let Some(program_files) = std::env::var_os("ProgramFiles") { - roots.push(PathBuf::from(program_files)); - } - if let Some(program_files_x86) = std::env::var_os("ProgramFiles(x86)") { - roots.push(PathBuf::from(program_files_x86)); - } - - roots - .into_iter() - .flat_map(|root| { - CODEX_APP_PRODUCT_NAMES - .into_iter() - .map(move |product_name| { - root.join(product_name).join(format!("{product_name}.exe")) - }) - }) - .collect() - } - "linux" => [PathBuf::from("/opt"), PathBuf::from("/usr/lib")] - .into_iter() - .flat_map(|root| { - CODEX_APP_PRODUCT_NAMES - .into_iter() - .map(move |product_name| root.join(product_name).join(product_name)) - }) - .collect(), - _ => Vec::new(), - } -} diff --git a/codex-rs/artifacts/src/runtime/manager.rs b/codex-rs/artifacts/src/runtime/manager.rs deleted file mode 100644 index b0a1c60ef..000000000 --- a/codex-rs/artifacts/src/runtime/manager.rs +++ /dev/null @@ -1,255 +0,0 @@ -use super::ArtifactRuntimeError; -use super::ArtifactRuntimePlatform; -use super::InstalledArtifactRuntime; -use super::ReleaseManifest; -use super::detect_runtime_root; -use codex_package_manager::ManagedPackage; -use codex_package_manager::PackageManager; -use codex_package_manager::PackageManagerConfig; -use codex_package_manager::PackageManagerError; -use codex_package_manager::PackageReleaseArchive; -use reqwest::Client; -use std::path::Path; -use std::path::PathBuf; -use url::Url; - -/// Release tag prefix used for artifact runtime assets. -pub const DEFAULT_RELEASE_TAG_PREFIX: &str = "artifact-runtime-v"; - -/// Relative cache root for installed artifact runtimes under `codex_home`. -pub const DEFAULT_CACHE_ROOT_RELATIVE: &str = "packages/artifacts"; - -/// Base URL used by default when downloading runtime assets from GitHub releases. -pub const DEFAULT_RELEASE_BASE_URL: &str = "https://github.com/openai/codex/releases/download/"; - -/// Describes where a particular artifact runtime release can be downloaded from. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ArtifactRuntimeReleaseLocator { - base_url: Url, - runtime_version: String, - release_tag_prefix: String, -} - -impl ArtifactRuntimeReleaseLocator { - /// Creates a locator for a runtime version under a release base URL. - pub fn new(base_url: Url, runtime_version: impl Into) -> Self { - Self { - base_url, - runtime_version: runtime_version.into(), - release_tag_prefix: DEFAULT_RELEASE_TAG_PREFIX.to_string(), - } - } - - /// Overrides the release-tag prefix used when constructing asset names. - pub fn with_tag_prefix(mut self, release_tag_prefix: impl Into) -> Self { - self.release_tag_prefix = release_tag_prefix.into(); - self - } - - /// Returns the release asset base URL. - pub fn base_url(&self) -> &Url { - &self.base_url - } - - /// Returns the expected runtime version. - pub fn runtime_version(&self) -> &str { - &self.runtime_version - } - - /// Returns the full release tag for the runtime version. - pub fn release_tag(&self) -> String { - format!("{}{}", self.release_tag_prefix, self.runtime_version) - } - - /// Returns the expected manifest filename for the release. - pub fn manifest_file_name(&self) -> String { - format!("{}-manifest.json", self.release_tag()) - } - - /// Returns the manifest URL for this runtime release. - pub fn manifest_url(&self) -> Result { - self.base_url - .join(&format!( - "{}/{}", - self.release_tag(), - self.manifest_file_name() - )) - .map_err(PackageManagerError::InvalidBaseUrl) - } - - /// Returns the default GitHub-release locator for a runtime version. - pub fn default(runtime_version: impl Into) -> Self { - Self::new( - Url::parse(DEFAULT_RELEASE_BASE_URL).unwrap_or_else(|error| { - panic!("hard-coded artifact runtime release base URL must be valid: {error}") - }), - runtime_version, - ) - } -} - -/// Configuration for resolving artifact runtimes under a Codex home directory. -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ArtifactRuntimeManagerConfig { - package_manager: PackageManagerConfig, - release: ArtifactRuntimeReleaseLocator, -} - -impl ArtifactRuntimeManagerConfig { - /// Creates a runtime-manager config from a Codex home and explicit release locator. - pub fn new(codex_home: PathBuf, release: ArtifactRuntimeReleaseLocator) -> Self { - Self { - package_manager: PackageManagerConfig::new( - codex_home, - ArtifactRuntimePackage::new(release.clone()), - ), - release, - } - } - - /// Creates a runtime-manager config that downloads from the default GitHub release location. - pub fn with_default_release(codex_home: PathBuf, runtime_version: impl Into) -> Self { - Self::new( - codex_home, - ArtifactRuntimeReleaseLocator::default(runtime_version), - ) - } - - /// Overrides the runtime cache root. - pub fn with_cache_root(mut self, cache_root: PathBuf) -> Self { - self.package_manager = self.package_manager.with_cache_root(cache_root); - self - } - - /// Returns the runtime cache root. - pub fn cache_root(&self) -> PathBuf { - self.package_manager.cache_root() - } - - /// Returns the release locator used by this config. - pub fn release(&self) -> &ArtifactRuntimeReleaseLocator { - &self.release - } -} - -/// Package-manager-backed artifact runtime resolver and installer. -#[derive(Clone, Debug)] -pub struct ArtifactRuntimeManager { - package_manager: PackageManager, - config: ArtifactRuntimeManagerConfig, -} - -impl ArtifactRuntimeManager { - /// Creates a runtime manager using the default `reqwest` client. - pub fn new(config: ArtifactRuntimeManagerConfig) -> Self { - let package_manager = PackageManager::new(config.package_manager.clone()); - Self { - package_manager, - config, - } - } - - /// Creates a runtime manager with a caller-provided HTTP client. - pub fn with_client(config: ArtifactRuntimeManagerConfig, client: Client) -> Self { - let package_manager = PackageManager::with_client(config.package_manager.clone(), client); - Self { - package_manager, - config, - } - } - - /// Returns the manager configuration. - pub fn config(&self) -> &ArtifactRuntimeManagerConfig { - &self.config - } - - /// Returns the installed runtime if it is already present and valid. - pub async fn resolve_cached( - &self, - ) -> Result, ArtifactRuntimeError> { - self.package_manager.resolve_cached().await - } - - /// Returns the installed runtime, downloading and caching it if necessary. - pub async fn ensure_installed(&self) -> Result { - self.package_manager.ensure_installed().await - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -struct ArtifactRuntimePackage { - release: ArtifactRuntimeReleaseLocator, -} - -impl ArtifactRuntimePackage { - fn new(release: ArtifactRuntimeReleaseLocator) -> Self { - Self { release } - } -} - -impl ManagedPackage for ArtifactRuntimePackage { - type Error = ArtifactRuntimeError; - type Installed = InstalledArtifactRuntime; - type ReleaseManifest = ReleaseManifest; - - fn default_cache_root_relative(&self) -> &str { - DEFAULT_CACHE_ROOT_RELATIVE - } - - fn version(&self) -> &str { - self.release.runtime_version() - } - - fn manifest_url(&self) -> Result { - self.release.manifest_url() - } - - fn archive_url(&self, archive: &PackageReleaseArchive) -> Result { - self.release - .base_url() - .join(&format!( - "{}/{}", - self.release.release_tag(), - archive.archive - )) - .map_err(PackageManagerError::InvalidBaseUrl) - } - - fn release_version<'a>(&self, manifest: &'a Self::ReleaseManifest) -> &'a str { - &manifest.runtime_version - } - - fn platform_archive( - &self, - manifest: &Self::ReleaseManifest, - platform: ArtifactRuntimePlatform, - ) -> Result { - manifest - .platforms - .get(platform.as_str()) - .cloned() - .ok_or_else(|| { - PackageManagerError::MissingPlatform(platform.as_str().to_string()).into() - }) - } - - fn install_dir(&self, cache_root: &Path, platform: ArtifactRuntimePlatform) -> PathBuf { - cache_root.join(self.version()).join(platform.as_str()) - } - - fn installed_version<'a>(&self, package: &'a Self::Installed) -> &'a str { - package.runtime_version() - } - - fn load_installed( - &self, - root_dir: PathBuf, - platform: ArtifactRuntimePlatform, - ) -> Result { - InstalledArtifactRuntime::load(root_dir, platform) - } - - fn detect_extracted_root(&self, extraction_root: &Path) -> Result { - detect_runtime_root(extraction_root) - } -} diff --git a/codex-rs/artifacts/src/runtime/manifest.rs b/codex-rs/artifacts/src/runtime/manifest.rs deleted file mode 100644 index ad02afa89..000000000 --- a/codex-rs/artifacts/src/runtime/manifest.rs +++ /dev/null @@ -1,15 +0,0 @@ -use codex_package_manager::PackageReleaseArchive; -use serde::Deserialize; -use serde::Serialize; -use std::collections::BTreeMap; - -/// Release metadata published alongside the packaged artifact runtime. -#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] -pub struct ReleaseManifest { - pub schema_version: u32, - pub runtime_version: String, - pub release_tag: String, - #[serde(default)] - pub node_version: Option, - pub platforms: BTreeMap, -} diff --git a/codex-rs/artifacts/src/runtime/mod.rs b/codex-rs/artifacts/src/runtime/mod.rs deleted file mode 100644 index 41fd1a48f..000000000 --- a/codex-rs/artifacts/src/runtime/mod.rs +++ /dev/null @@ -1,28 +0,0 @@ -mod error; -mod installed; -mod js_runtime; -mod manager; -mod manifest; - -pub use codex_package_manager::PackagePlatform as ArtifactRuntimePlatform; -pub use error::ArtifactRuntimeError; -pub use installed::InstalledArtifactRuntime; -pub use installed::load_cached_runtime; -pub use js_runtime::JsRuntime; -pub use js_runtime::JsRuntimeKind; -pub use js_runtime::can_manage_artifact_runtime; -pub use js_runtime::is_js_runtime_available; -pub use manager::ArtifactRuntimeManager; -pub use manager::ArtifactRuntimeManagerConfig; -pub use manager::ArtifactRuntimeReleaseLocator; -pub use manager::DEFAULT_CACHE_ROOT_RELATIVE; -pub use manager::DEFAULT_RELEASE_BASE_URL; -pub use manager::DEFAULT_RELEASE_TAG_PREFIX; -pub use manifest::ReleaseManifest; - -pub(crate) use installed::default_cached_runtime_root; -pub(crate) use installed::detect_runtime_root; -pub(crate) use js_runtime::codex_app_runtime_candidates; -pub(crate) use js_runtime::resolve_js_runtime_from_candidates; -pub(crate) use js_runtime::system_electron_runtime; -pub(crate) use js_runtime::system_node_runtime; diff --git a/codex-rs/artifacts/src/tests.rs b/codex-rs/artifacts/src/tests.rs deleted file mode 100644 index 3db8a0bcc..000000000 --- a/codex-rs/artifacts/src/tests.rs +++ /dev/null @@ -1,453 +0,0 @@ -use crate::ArtifactBuildRequest; -use crate::ArtifactCommandOutput; -use crate::ArtifactRuntimeManager; -use crate::ArtifactRuntimeManagerConfig; -use crate::ArtifactRuntimePlatform; -use crate::ArtifactRuntimeReleaseLocator; -use crate::ArtifactsClient; -use crate::DEFAULT_CACHE_ROOT_RELATIVE; -use crate::ReleaseManifest; -use crate::load_cached_runtime; -use codex_package_manager::ArchiveFormat; -use codex_package_manager::PackageReleaseArchive; -use flate2::Compression; -use flate2::write::GzEncoder; -use pretty_assertions::assert_eq; -use sha2::Digest; -use sha2::Sha256; -use std::collections::BTreeMap; -use std::fs; -use std::io::Cursor; -use std::io::Write; -use std::path::Path; -use std::time::Duration; -use tar::Builder as TarBuilder; -use tempfile::TempDir; -use wiremock::Mock; -use wiremock::MockServer; -use wiremock::ResponseTemplate; -use wiremock::matchers::method; -use wiremock::matchers::path; -use zip::ZipWriter; -use zip::write::SimpleFileOptions; - -#[test] -fn release_locator_builds_manifest_url() { - let locator = ArtifactRuntimeReleaseLocator::new( - url::Url::parse("https://example.test/releases/").unwrap_or_else(|error| panic!("{error}")), - "0.1.0", - ); - let url = locator - .manifest_url() - .unwrap_or_else(|error| panic!("{error}")); - assert_eq!( - url.as_str(), - "https://example.test/releases/artifact-runtime-v0.1.0/artifact-runtime-v0.1.0-manifest.json" - ); -} - -#[test] -fn default_release_locator_uses_openai_codex_github_releases() { - let locator = ArtifactRuntimeReleaseLocator::default("0.1.0"); - let url = locator - .manifest_url() - .unwrap_or_else(|error| panic!("{error}")); - - assert_eq!( - url.as_str(), - "https://github.com/openai/codex/releases/download/artifact-runtime-v0.1.0/artifact-runtime-v0.1.0-manifest.json" - ); -} - -#[test] -fn load_cached_runtime_reads_installed_runtime() { - let codex_home = TempDir::new().unwrap_or_else(|error| panic!("{error}")); - let runtime_version = "2.5.6"; - let platform = - ArtifactRuntimePlatform::detect_current().unwrap_or_else(|error| panic!("{error}")); - let install_dir = codex_home - .path() - .join(DEFAULT_CACHE_ROOT_RELATIVE) - .join(runtime_version) - .join(platform.as_str()); - write_installed_runtime(&install_dir, runtime_version); - - let runtime = load_cached_runtime( - &codex_home.path().join(DEFAULT_CACHE_ROOT_RELATIVE), - runtime_version, - ) - .unwrap_or_else(|error| panic!("{error}")); - - assert_eq!(runtime.runtime_version(), runtime_version); - assert_eq!(runtime.platform(), platform); - assert!( - runtime - .build_js_path() - .ends_with(Path::new("dist/artifact_tool.mjs")) - ); -} - -#[test] -fn load_cached_runtime_requires_build_entrypoint() { - let codex_home = TempDir::new().unwrap_or_else(|error| panic!("{error}")); - let runtime_version = "2.5.6"; - let platform = - ArtifactRuntimePlatform::detect_current().unwrap_or_else(|error| panic!("{error}")); - let install_dir = codex_home - .path() - .join(DEFAULT_CACHE_ROOT_RELATIVE) - .join(runtime_version) - .join(platform.as_str()); - write_installed_runtime(&install_dir, runtime_version); - fs::remove_file(install_dir.join("dist/artifact_tool.mjs")) - .unwrap_or_else(|error| panic!("{error}")); - - let error = load_cached_runtime( - &codex_home.path().join(DEFAULT_CACHE_ROOT_RELATIVE), - runtime_version, - ) - .unwrap_err(); - - assert_eq!( - error.to_string(), - format!( - "required runtime file is missing: {}", - install_dir.join("dist/artifact_tool.mjs").display() - ) - ); -} - -#[tokio::test] -async fn ensure_installed_downloads_and_extracts_zip_runtime() { - let server = MockServer::start().await; - let runtime_version = "2.5.6"; - let platform = - ArtifactRuntimePlatform::detect_current().unwrap_or_else(|error| panic!("{error}")); - let archive_name = format!( - "artifact-runtime-v{runtime_version}-{}.zip", - platform.as_str() - ); - let archive_bytes = build_zip_archive(runtime_version); - let archive_sha = format!("{:x}", Sha256::digest(&archive_bytes)); - let manifest = ReleaseManifest { - schema_version: 1, - runtime_version: runtime_version.to_string(), - release_tag: format!("artifact-runtime-v{runtime_version}"), - node_version: None, - platforms: BTreeMap::from([( - platform.as_str().to_string(), - PackageReleaseArchive { - archive: archive_name.clone(), - sha256: archive_sha, - format: ArchiveFormat::Zip, - size_bytes: Some(archive_bytes.len() as u64), - }, - )]), - }; - Mock::given(method("GET")) - .and(path(format!( - "/artifact-runtime-v{runtime_version}/artifact-runtime-v{runtime_version}-manifest.json" - ))) - .respond_with(ResponseTemplate::new(200).set_body_json(&manifest)) - .mount(&server) - .await; - Mock::given(method("GET")) - .and(path(format!( - "/artifact-runtime-v{runtime_version}/{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 locator = ArtifactRuntimeReleaseLocator::new( - url::Url::parse(&format!("{}/", server.uri())).unwrap_or_else(|error| panic!("{error}")), - runtime_version, - ); - let manager = ArtifactRuntimeManager::new(ArtifactRuntimeManagerConfig::new( - codex_home.path().to_path_buf(), - locator, - )); - - let runtime = manager - .ensure_installed() - .await - .unwrap_or_else(|error| panic!("{error}")); - - assert_eq!(runtime.runtime_version(), runtime_version); - assert_eq!(runtime.platform(), platform); - assert!( - runtime - .build_js_path() - .ends_with(Path::new("dist/artifact_tool.mjs")) - ); -} - -#[test] -fn load_cached_runtime_requires_package_export() { - let codex_home = TempDir::new().unwrap_or_else(|error| panic!("{error}")); - let runtime_version = "2.5.6"; - let platform = - ArtifactRuntimePlatform::detect_current().unwrap_or_else(|error| panic!("{error}")); - let install_dir = codex_home - .path() - .join(DEFAULT_CACHE_ROOT_RELATIVE) - .join(runtime_version) - .join(platform.as_str()); - write_installed_runtime(&install_dir, runtime_version); - fs::write( - install_dir.join("package.json"), - serde_json::json!({ - "name": "@oai/artifact-tool", - "version": runtime_version, - "type": "module", - }) - .to_string(), - ) - .unwrap_or_else(|error| panic!("{error}")); - - let error = load_cached_runtime( - &codex_home.path().join(DEFAULT_CACHE_ROOT_RELATIVE), - runtime_version, - ) - .unwrap_err(); - - assert_eq!( - error.to_string(), - format!( - "invalid package metadata at {}", - install_dir.join("package.json").display() - ) - ); -} - -#[tokio::test] -async fn ensure_installed_downloads_and_extracts_tar_gz_runtime() { - let server = MockServer::start().await; - let runtime_version = "2.5.6"; - let platform = - ArtifactRuntimePlatform::detect_current().unwrap_or_else(|error| panic!("{error}")); - let archive_name = format!( - "artifact-runtime-v{runtime_version}-{}.tar.gz", - platform.as_str() - ); - let archive_bytes = build_tar_gz_archive(runtime_version); - let archive_sha = format!("{:x}", Sha256::digest(&archive_bytes)); - let manifest = ReleaseManifest { - schema_version: 1, - runtime_version: runtime_version.to_string(), - release_tag: format!("artifact-runtime-v{runtime_version}"), - node_version: None, - platforms: BTreeMap::from([( - platform.as_str().to_string(), - PackageReleaseArchive { - archive: archive_name.clone(), - sha256: archive_sha, - format: ArchiveFormat::TarGz, - size_bytes: Some(archive_bytes.len() as u64), - }, - )]), - }; - Mock::given(method("GET")) - .and(path(format!( - "/artifact-runtime-v{runtime_version}/artifact-runtime-v{runtime_version}-manifest.json" - ))) - .respond_with(ResponseTemplate::new(200).set_body_json(&manifest)) - .mount(&server) - .await; - Mock::given(method("GET")) - .and(path(format!( - "/artifact-runtime-v{runtime_version}/{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 locator = ArtifactRuntimeReleaseLocator::new( - url::Url::parse(&format!("{}/", server.uri())).unwrap_or_else(|error| panic!("{error}")), - runtime_version, - ); - let manager = ArtifactRuntimeManager::new(ArtifactRuntimeManagerConfig::new( - codex_home.path().to_path_buf(), - locator, - )); - - let runtime = manager - .ensure_installed() - .await - .unwrap_or_else(|error| panic!("{error}")); - - assert_eq!(runtime.runtime_version(), runtime_version); - assert_eq!(runtime.platform(), platform); - assert!( - runtime - .build_js_path() - .ends_with(Path::new("dist/artifact_tool.mjs")) - ); -} - -#[test] -fn load_cached_runtime_uses_custom_cache_root() { - let codex_home = TempDir::new().unwrap_or_else(|error| panic!("{error}")); - let runtime_version = "2.5.6"; - let custom_cache_root = codex_home.path().join("runtime-cache"); - let platform = - ArtifactRuntimePlatform::detect_current().unwrap_or_else(|error| panic!("{error}")); - let install_dir = custom_cache_root - .join(runtime_version) - .join(platform.as_str()); - write_installed_runtime(&install_dir, runtime_version); - - let config = ArtifactRuntimeManagerConfig::with_default_release( - codex_home.path().to_path_buf(), - runtime_version, - ) - .with_cache_root(custom_cache_root); - - let runtime = load_cached_runtime(&config.cache_root(), runtime_version) - .unwrap_or_else(|error| panic!("{error}")); - - assert_eq!(runtime.runtime_version(), runtime_version); - assert_eq!(runtime.platform(), platform); -} - -#[tokio::test] -#[cfg(unix)] -async fn artifacts_client_execute_build_writes_wrapped_script_and_env() { - let temp = TempDir::new().unwrap_or_else(|error| panic!("{error}")); - let runtime_root = temp.path().join("runtime"); - write_installed_runtime(&runtime_root, "2.5.6"); - let runtime = crate::InstalledArtifactRuntime::load( - runtime_root, - ArtifactRuntimePlatform::detect_current().unwrap_or_else(|error| panic!("{error}")), - ) - .unwrap_or_else(|error| panic!("{error}")); - let client = ArtifactsClient::from_installed_runtime(runtime); - - let output = client - .execute_build(ArtifactBuildRequest { - source: concat!( - "console.log(typeof artifacts);\n", - "console.log(typeof codexArtifacts);\n", - "console.log(artifactTool.ok);\n", - "console.log(ok);\n", - "console.error('stderr-ok');\n", - "console.log('stdout-ok');\n" - ) - .to_string(), - cwd: temp.path().to_path_buf(), - timeout: Some(Duration::from_secs(5)), - env: BTreeMap::new(), - }) - .await - .unwrap_or_else(|error| panic!("{error}")); - - assert_success(&output); - assert_eq!(output.stderr.trim(), "stderr-ok"); - assert_eq!( - output.stdout.lines().collect::>(), - vec!["undefined", "undefined", "true", "true", "stdout-ok"] - ); -} - -fn assert_success(output: &ArtifactCommandOutput) { - assert!(output.success()); - assert_eq!(output.exit_code, Some(0)); -} - -fn write_installed_runtime(install_dir: &Path, runtime_version: &str) { - fs::create_dir_all(install_dir.join("dist")).unwrap_or_else(|error| panic!("{error}")); - fs::write( - install_dir.join("package.json"), - serde_json::json!({ - "name": "@oai/artifact-tool", - "version": runtime_version, - "type": "module", - "exports": { - ".": "./dist/artifact_tool.mjs", - } - }) - .to_string(), - ) - .unwrap_or_else(|error| panic!("{error}")); - fs::write( - install_dir.join("dist/artifact_tool.mjs"), - "export const ok = true;\n", - ) - .unwrap_or_else(|error| panic!("{error}")); -} - -fn build_zip_archive(runtime_version: &str) -> Vec { - let mut bytes = Cursor::new(Vec::new()); - { - let mut zip = ZipWriter::new(&mut bytes); - let options = SimpleFileOptions::default(); - let package_json = serde_json::json!({ - "name": "@oai/artifact-tool", - "version": runtime_version, - "type": "module", - "exports": { - ".": "./dist/artifact_tool.mjs", - } - }) - .to_string() - .into_bytes(); - zip.start_file("artifact-runtime/package.json", options) - .unwrap_or_else(|error| panic!("{error}")); - zip.write_all(&package_json) - .unwrap_or_else(|error| panic!("{error}")); - zip.start_file("artifact-runtime/dist/artifact_tool.mjs", options) - .unwrap_or_else(|error| panic!("{error}")); - zip.write_all(b"export const ok = true;\n") - .unwrap_or_else(|error| panic!("{error}")); - zip.finish().unwrap_or_else(|error| panic!("{error}")); - } - bytes.into_inner() -} - -fn build_tar_gz_archive(runtime_version: &str) -> Vec { - let mut bytes = Vec::new(); - { - let encoder = GzEncoder::new(&mut bytes, Compression::default()); - let mut archive = TarBuilder::new(encoder); - - let package_json = serde_json::json!({ - "name": "@oai/artifact-tool", - "version": runtime_version, - "type": "module", - "exports": { - ".": "./dist/artifact_tool.mjs", - } - }) - .to_string() - .into_bytes(); - let mut package_header = tar::Header::new_gnu(); - package_header.set_mode(0o644); - package_header.set_size(package_json.len() as u64); - package_header.set_cksum(); - archive - .append_data( - &mut package_header, - "package/package.json", - package_json.as_slice(), - ) - .unwrap_or_else(|error| panic!("{error}")); - - let build_js = b"export const ok = true;\n"; - let mut build_header = tar::Header::new_gnu(); - build_header.set_mode(0o644); - build_header.set_size(build_js.len() as u64); - build_header.set_cksum(); - archive - .append_data( - &mut build_header, - "package/dist/artifact_tool.mjs", - &build_js[..], - ) - .unwrap_or_else(|error| panic!("{error}")); - - archive.finish().unwrap_or_else(|error| panic!("{error}")); - } - bytes -}