parent
0cc6835416
commit
5e92f4af12
12 changed files with 1300 additions and 962 deletions
36
codex-rs/artifacts/README.md
Normal file
36
codex-rs/artifacts/README.md
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# 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.
|
||||
|
|
@ -14,30 +14,35 @@ use tokio::time::timeout;
|
|||
|
||||
const DEFAULT_EXECUTION_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
/// Executes artifact build and render 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,
|
||||
|
|
@ -82,6 +87,7 @@ impl ArtifactsClient {
|
|||
.await
|
||||
}
|
||||
|
||||
/// Executes the artifact render CLI against the configured runtime.
|
||||
pub async fn execute_render(
|
||||
&self,
|
||||
request: ArtifactRenderCommandRequest,
|
||||
|
|
@ -117,6 +123,7 @@ impl ArtifactsClient {
|
|||
}
|
||||
}
|
||||
|
||||
/// Request payload for the artifact build command.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct ArtifactBuildRequest {
|
||||
pub source: String,
|
||||
|
|
@ -125,6 +132,7 @@ pub struct ArtifactBuildRequest {
|
|||
pub env: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
/// Request payload for the artifact render CLI.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtifactRenderCommandRequest {
|
||||
pub cwd: PathBuf,
|
||||
|
|
@ -133,6 +141,7 @@ pub struct ArtifactRenderCommandRequest {
|
|||
pub target: ArtifactRenderTarget,
|
||||
}
|
||||
|
||||
/// Render targets supported by the packaged artifact runtime.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ArtifactRenderTarget {
|
||||
Presentation(PresentationRenderTarget),
|
||||
|
|
@ -140,6 +149,7 @@ pub enum ArtifactRenderTarget {
|
|||
}
|
||||
|
||||
impl ArtifactRenderTarget {
|
||||
/// Converts a render target to the CLI args expected by `render_cli.mjs`.
|
||||
pub fn to_args(&self) -> Vec<String> {
|
||||
match self {
|
||||
Self::Presentation(target) => {
|
||||
|
|
@ -175,6 +185,7 @@ impl ArtifactRenderTarget {
|
|||
}
|
||||
}
|
||||
|
||||
/// Presentation render request parameters.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PresentationRenderTarget {
|
||||
pub input_path: PathBuf,
|
||||
|
|
@ -182,6 +193,7 @@ pub struct PresentationRenderTarget {
|
|||
pub slide_number: u32,
|
||||
}
|
||||
|
||||
/// Spreadsheet render request parameters.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SpreadsheetRenderTarget {
|
||||
pub input_path: PathBuf,
|
||||
|
|
@ -190,6 +202,7 @@ pub struct SpreadsheetRenderTarget {
|
|||
pub range: Option<String>,
|
||||
}
|
||||
|
||||
/// Captured stdout, stderr, and exit status from an artifact subprocess.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ArtifactCommandOutput {
|
||||
pub exit_code: Option<i32>,
|
||||
|
|
@ -198,11 +211,13 @@ pub struct ArtifactCommandOutput {
|
|||
}
|
||||
|
||||
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)]
|
||||
|
|
@ -302,153 +317,3 @@ async fn run_command(
|
|||
stderr: String::from_utf8_lossy(&stderr_bytes).into_owned(),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[cfg(unix)]
|
||||
use crate::ArtifactRuntimePlatform;
|
||||
#[cfg(unix)]
|
||||
use crate::ExtractedRuntimeManifest;
|
||||
#[cfg(unix)]
|
||||
use crate::RuntimeEntrypoints;
|
||||
#[cfg(unix)]
|
||||
use crate::RuntimePathEntry;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
#[test]
|
||||
fn wrapped_build_script_exposes_artifact_tool_surface() {
|
||||
let wrapped = build_wrapped_script("console.log(Object.keys(artifactTool).length);");
|
||||
assert!(wrapped.contains("const artifactTool = await import("));
|
||||
assert!(wrapped.contains("globalThis.artifactTool = artifactTool;"));
|
||||
assert!(wrapped.contains("globalThis.artifacts = artifactTool;"));
|
||||
assert!(wrapped.contains("globalThis.codexArtifacts = artifactTool;"));
|
||||
assert!(wrapped.contains("Object.entries(artifactTool)"));
|
||||
assert!(wrapped.contains("globalThis[name] = value;"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn presentation_render_target_builds_expected_args() {
|
||||
let args = ArtifactRenderTarget::Presentation(PresentationRenderTarget {
|
||||
input_path: PathBuf::from("deck.pptx"),
|
||||
output_path: PathBuf::from("slide.png"),
|
||||
slide_number: 2,
|
||||
})
|
||||
.to_args();
|
||||
|
||||
assert_eq!(
|
||||
args,
|
||||
vec![
|
||||
"pptx",
|
||||
"render",
|
||||
"--in",
|
||||
"deck.pptx",
|
||||
"--slide",
|
||||
"2",
|
||||
"--out",
|
||||
"slide.png"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spreadsheet_render_target_builds_expected_args() {
|
||||
let args = ArtifactRenderTarget::Spreadsheet(SpreadsheetRenderTarget {
|
||||
input_path: PathBuf::from("book.xlsx"),
|
||||
output_path: PathBuf::from("sheet.png"),
|
||||
sheet_name: "Summary".to_string(),
|
||||
range: Some("A1:C3".to_string()),
|
||||
})
|
||||
.to_args();
|
||||
|
||||
assert_eq!(
|
||||
args,
|
||||
vec![
|
||||
"xlsx",
|
||||
"render",
|
||||
"--in",
|
||||
"book.xlsx",
|
||||
"--sheet",
|
||||
"Summary",
|
||||
"--out",
|
||||
"sheet.png",
|
||||
"--range",
|
||||
"A1:C3"
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test]
|
||||
async fn execute_build_invokes_runtime_node_with_expected_environment() {
|
||||
let temp = TempDir::new().unwrap_or_else(|error| panic!("{error}"));
|
||||
let cwd = temp.path().join("cwd");
|
||||
fs::create_dir_all(&cwd)
|
||||
.await
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
let log_path = temp.path().join("build.log");
|
||||
let fake_node = temp.path().join("fake-node.sh");
|
||||
let build_entrypoint = temp.path().join("artifact_tool.mjs");
|
||||
let render_entrypoint = temp.path().join("render_cli.mjs");
|
||||
fs::write(
|
||||
&fake_node,
|
||||
format!(
|
||||
"#!/bin/sh\nprintf '%s\\n' \"$1\" > \"{}\"\nprintf '%s\\n' \"$CODEX_ARTIFACT_BUILD_ENTRYPOINT\" >> \"{}\"\n",
|
||||
log_path.display(),
|
||||
log_path.display()
|
||||
),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
std::fs::set_permissions(&fake_node, std::fs::Permissions::from_mode(0o755))
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
|
||||
let runtime = InstalledArtifactRuntime::new(
|
||||
temp.path().join("runtime"),
|
||||
"0.1.0".to_string(),
|
||||
ArtifactRuntimePlatform::LinuxX64,
|
||||
sample_manifest("0.1.0"),
|
||||
fake_node.clone(),
|
||||
build_entrypoint.clone(),
|
||||
render_entrypoint,
|
||||
);
|
||||
let client = ArtifactsClient::from_installed_runtime(runtime);
|
||||
|
||||
let output = client
|
||||
.execute_build(ArtifactBuildRequest {
|
||||
source: "console.log('hello');".to_string(),
|
||||
cwd: cwd.clone(),
|
||||
timeout: Some(Duration::from_secs(5)),
|
||||
env: BTreeMap::new(),
|
||||
})
|
||||
.await
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
|
||||
assert!(output.success());
|
||||
let logged = fs::read_to_string(&log_path)
|
||||
.await
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
assert!(logged.contains("artifact-build.mjs"));
|
||||
assert!(logged.contains(&build_entrypoint.display().to_string()));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn sample_manifest(runtime_version: &str) -> ExtractedRuntimeManifest {
|
||||
ExtractedRuntimeManifest {
|
||||
schema_version: 1,
|
||||
runtime_version: runtime_version.to_string(),
|
||||
node: RuntimePathEntry {
|
||||
relative_path: "node/bin/node".to_string(),
|
||||
},
|
||||
entrypoints: RuntimeEntrypoints {
|
||||
build_js: RuntimePathEntry {
|
||||
relative_path: "artifact-tool/dist/artifact_tool.mjs".to_string(),
|
||||
},
|
||||
render_cli: RuntimePathEntry {
|
||||
relative_path: "granola-render/dist/cli.mjs".to_string(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
mod client;
|
||||
mod runtime;
|
||||
#[cfg(all(test, not(windows)))]
|
||||
mod tests;
|
||||
|
||||
pub use client::ArtifactBuildRequest;
|
||||
pub use client::ArtifactCommandOutput;
|
||||
|
|
@ -24,5 +26,6 @@ pub use runtime::JsRuntimeKind;
|
|||
pub use runtime::ReleaseManifest;
|
||||
pub use runtime::RuntimeEntrypoints;
|
||||
pub use runtime::RuntimePathEntry;
|
||||
pub use runtime::can_manage_artifact_runtime;
|
||||
pub use runtime::is_js_runtime_available;
|
||||
pub use runtime::load_cached_runtime;
|
||||
|
|
|
|||
|
|
@ -1,810 +0,0 @@
|
|||
use codex_package_manager::ManagedPackage;
|
||||
use codex_package_manager::PackageManager;
|
||||
use codex_package_manager::PackageManagerConfig;
|
||||
use codex_package_manager::PackageManagerError;
|
||||
pub use codex_package_manager::PackagePlatform as ArtifactRuntimePlatform;
|
||||
use codex_package_manager::PackageReleaseArchive;
|
||||
use reqwest::Client;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::Component;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use thiserror::Error;
|
||||
use url::Url;
|
||||
use which::which;
|
||||
|
||||
pub const DEFAULT_RELEASE_TAG_PREFIX: &str = "artifact-runtime-v";
|
||||
pub const DEFAULT_CACHE_ROOT_RELATIVE: &str = "packages/artifacts";
|
||||
pub const DEFAULT_RELEASE_BASE_URL: &str = "https://github.com/openai/codex/releases/download/";
|
||||
const CODEX_APP_PRODUCT_NAMES: [&str; 6] = [
|
||||
"Codex",
|
||||
"Codex (Dev)",
|
||||
"Codex (Agent)",
|
||||
"Codex (Nightly)",
|
||||
"Codex (Alpha)",
|
||||
"Codex (Beta)",
|
||||
];
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum JsRuntimeKind {
|
||||
Node,
|
||||
Electron,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct JsRuntime {
|
||||
executable_path: PathBuf,
|
||||
kind: JsRuntimeKind,
|
||||
}
|
||||
|
||||
impl JsRuntime {
|
||||
fn node(executable_path: PathBuf) -> Self {
|
||||
Self {
|
||||
executable_path,
|
||||
kind: JsRuntimeKind::Node,
|
||||
}
|
||||
}
|
||||
|
||||
fn electron(executable_path: PathBuf) -> Self {
|
||||
Self {
|
||||
executable_path,
|
||||
kind: JsRuntimeKind::Electron,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn executable_path(&self) -> &Path {
|
||||
&self.executable_path
|
||||
}
|
||||
|
||||
pub fn requires_electron_run_as_node(&self) -> bool {
|
||||
self.kind == JsRuntimeKind::Electron
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_js_runtime_available(codex_home: &Path, runtime_version: &str) -> bool {
|
||||
load_cached_runtime(codex_home, runtime_version)
|
||||
.ok()
|
||||
.and_then(|runtime| runtime.resolve_js_runtime().ok())
|
||||
.or_else(resolve_machine_js_runtime)
|
||||
.is_some()
|
||||
}
|
||||
|
||||
pub fn load_cached_runtime(
|
||||
codex_home: &Path,
|
||||
runtime_version: &str,
|
||||
) -> Result<InstalledArtifactRuntime, ArtifactRuntimeError> {
|
||||
let platform = ArtifactRuntimePlatform::detect_current()?;
|
||||
let install_dir = cached_runtime_install_dir(codex_home, 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)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ArtifactRuntimeReleaseLocator {
|
||||
base_url: Url,
|
||||
runtime_version: String,
|
||||
release_tag_prefix: String,
|
||||
}
|
||||
|
||||
impl ArtifactRuntimeReleaseLocator {
|
||||
pub fn new(base_url: Url, runtime_version: impl Into<String>) -> Self {
|
||||
Self {
|
||||
base_url,
|
||||
runtime_version: runtime_version.into(),
|
||||
release_tag_prefix: DEFAULT_RELEASE_TAG_PREFIX.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_tag_prefix(mut self, release_tag_prefix: impl Into<String>) -> Self {
|
||||
self.release_tag_prefix = release_tag_prefix.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn base_url(&self) -> &Url {
|
||||
&self.base_url
|
||||
}
|
||||
|
||||
pub fn runtime_version(&self) -> &str {
|
||||
&self.runtime_version
|
||||
}
|
||||
|
||||
pub fn release_tag(&self) -> String {
|
||||
format!("{}{}", self.release_tag_prefix, self.runtime_version)
|
||||
}
|
||||
|
||||
pub fn manifest_file_name(&self) -> String {
|
||||
format!("{}-manifest.json", self.release_tag())
|
||||
}
|
||||
|
||||
pub fn manifest_url(&self) -> Result<Url, PackageManagerError> {
|
||||
self.base_url
|
||||
.join(&format!(
|
||||
"{}/{}",
|
||||
self.release_tag(),
|
||||
self.manifest_file_name()
|
||||
))
|
||||
.map_err(PackageManagerError::InvalidBaseUrl)
|
||||
}
|
||||
|
||||
pub fn default(runtime_version: impl Into<String>) -> Self {
|
||||
Self::new(
|
||||
match Url::parse(DEFAULT_RELEASE_BASE_URL) {
|
||||
Ok(url) => url,
|
||||
Err(error) => {
|
||||
panic!("hard-coded artifact runtime release base URL must be valid: {error}")
|
||||
}
|
||||
},
|
||||
runtime_version,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ArtifactRuntimeManagerConfig {
|
||||
package_manager: PackageManagerConfig<ArtifactRuntimePackage>,
|
||||
}
|
||||
|
||||
impl ArtifactRuntimeManagerConfig {
|
||||
pub fn new(codex_home: PathBuf, release: ArtifactRuntimeReleaseLocator) -> Self {
|
||||
Self {
|
||||
package_manager: PackageManagerConfig::new(
|
||||
codex_home,
|
||||
ArtifactRuntimePackage::new(release),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_default_release(codex_home: PathBuf, runtime_version: impl Into<String>) -> Self {
|
||||
Self::new(
|
||||
codex_home,
|
||||
ArtifactRuntimeReleaseLocator::default(runtime_version),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn with_cache_root(mut self, cache_root: PathBuf) -> Self {
|
||||
self.package_manager = self.package_manager.with_cache_root(cache_root);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn cache_root(&self) -> PathBuf {
|
||||
self.package_manager.cache_root()
|
||||
}
|
||||
|
||||
pub fn release(&self) -> &ArtifactRuntimeReleaseLocator {
|
||||
&self.package_manager.package().release
|
||||
}
|
||||
|
||||
pub fn codex_home(&self) -> &Path {
|
||||
self.package_manager.codex_home()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtifactRuntimeManager {
|
||||
package_manager: PackageManager<ArtifactRuntimePackage>,
|
||||
config: ArtifactRuntimeManagerConfig,
|
||||
}
|
||||
|
||||
impl ArtifactRuntimeManager {
|
||||
pub fn new(config: ArtifactRuntimeManagerConfig) -> Self {
|
||||
let package_manager = PackageManager::new(config.package_manager.clone());
|
||||
Self {
|
||||
package_manager,
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_client(config: ArtifactRuntimeManagerConfig, client: Client) -> Self {
|
||||
let package_manager = PackageManager::with_client(config.package_manager.clone(), client);
|
||||
Self {
|
||||
package_manager,
|
||||
config,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn config(&self) -> &ArtifactRuntimeManagerConfig {
|
||||
&self.config
|
||||
}
|
||||
|
||||
pub async fn resolve_cached(
|
||||
&self,
|
||||
) -> Result<Option<InstalledArtifactRuntime>, ArtifactRuntimeError> {
|
||||
self.package_manager.resolve_cached().await
|
||||
}
|
||||
|
||||
pub async fn ensure_installed(&self) -> Result<InstalledArtifactRuntime, ArtifactRuntimeError> {
|
||||
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<Url, PackageManagerError> {
|
||||
self.release.manifest_url()
|
||||
}
|
||||
|
||||
fn archive_url(&self, archive: &PackageReleaseArchive) -> Result<Url, PackageManagerError> {
|
||||
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<PackageReleaseArchive, Self::Error> {
|
||||
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<Self::Installed, Self::Error> {
|
||||
InstalledArtifactRuntime::load(root_dir, platform)
|
||||
}
|
||||
}
|
||||
|
||||
#[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<String>,
|
||||
pub platforms: BTreeMap<String, PackageReleaseArchive>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct ExtractedRuntimeManifest {
|
||||
pub schema_version: u32,
|
||||
pub runtime_version: String,
|
||||
pub node: RuntimePathEntry,
|
||||
pub entrypoints: RuntimeEntrypoints,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct RuntimePathEntry {
|
||||
pub relative_path: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct RuntimeEntrypoints {
|
||||
pub build_js: RuntimePathEntry,
|
||||
pub render_cli: RuntimePathEntry,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct InstalledArtifactRuntime {
|
||||
root_dir: PathBuf,
|
||||
runtime_version: String,
|
||||
platform: ArtifactRuntimePlatform,
|
||||
manifest: ExtractedRuntimeManifest,
|
||||
node_path: PathBuf,
|
||||
build_js_path: PathBuf,
|
||||
render_cli_path: PathBuf,
|
||||
}
|
||||
|
||||
impl InstalledArtifactRuntime {
|
||||
pub fn new(
|
||||
root_dir: PathBuf,
|
||||
runtime_version: String,
|
||||
platform: ArtifactRuntimePlatform,
|
||||
manifest: ExtractedRuntimeManifest,
|
||||
node_path: PathBuf,
|
||||
build_js_path: PathBuf,
|
||||
render_cli_path: PathBuf,
|
||||
) -> Self {
|
||||
Self {
|
||||
root_dir,
|
||||
runtime_version,
|
||||
platform,
|
||||
manifest,
|
||||
node_path,
|
||||
build_js_path,
|
||||
render_cli_path,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load(
|
||||
root_dir: PathBuf,
|
||||
platform: ArtifactRuntimePlatform,
|
||||
) -> Result<Self, ArtifactRuntimeError> {
|
||||
let manifest_path = root_dir.join("manifest.json");
|
||||
let manifest_bytes =
|
||||
std::fs::read(&manifest_path).map_err(|source| ArtifactRuntimeError::Io {
|
||||
context: format!("failed to read {}", manifest_path.display()),
|
||||
source,
|
||||
})?;
|
||||
let manifest = serde_json::from_slice::<ExtractedRuntimeManifest>(&manifest_bytes)
|
||||
.map_err(|source| ArtifactRuntimeError::InvalidManifest {
|
||||
path: manifest_path,
|
||||
source,
|
||||
})?;
|
||||
let node_path = resolve_relative_runtime_path(&root_dir, &manifest.node.relative_path)?;
|
||||
let build_js_path =
|
||||
resolve_relative_runtime_path(&root_dir, &manifest.entrypoints.build_js.relative_path)?;
|
||||
let render_cli_path = resolve_relative_runtime_path(
|
||||
&root_dir,
|
||||
&manifest.entrypoints.render_cli.relative_path,
|
||||
)?;
|
||||
verify_required_runtime_path(&build_js_path)?;
|
||||
verify_required_runtime_path(&render_cli_path)?;
|
||||
|
||||
Ok(Self::new(
|
||||
root_dir,
|
||||
manifest.runtime_version.clone(),
|
||||
platform,
|
||||
manifest,
|
||||
node_path,
|
||||
build_js_path,
|
||||
render_cli_path,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn root_dir(&self) -> &Path {
|
||||
&self.root_dir
|
||||
}
|
||||
|
||||
pub fn runtime_version(&self) -> &str {
|
||||
&self.runtime_version
|
||||
}
|
||||
|
||||
pub fn platform(&self) -> ArtifactRuntimePlatform {
|
||||
self.platform
|
||||
}
|
||||
|
||||
pub fn manifest(&self) -> &ExtractedRuntimeManifest {
|
||||
&self.manifest
|
||||
}
|
||||
|
||||
pub fn node_path(&self) -> &Path {
|
||||
&self.node_path
|
||||
}
|
||||
|
||||
pub fn build_js_path(&self) -> &Path {
|
||||
&self.build_js_path
|
||||
}
|
||||
|
||||
pub fn render_cli_path(&self) -> &Path {
|
||||
&self.render_cli_path
|
||||
}
|
||||
|
||||
pub fn resolve_js_runtime(&self) -> Result<JsRuntime, ArtifactRuntimeError> {
|
||||
resolve_js_runtime_from_candidates(
|
||||
Some(self.node_path()),
|
||||
system_node_runtime(),
|
||||
system_electron_runtime(),
|
||||
codex_app_runtime_candidates(),
|
||||
)
|
||||
.ok_or_else(|| ArtifactRuntimeError::MissingJsRuntime {
|
||||
root_dir: self.root_dir.clone(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum ArtifactRuntimeError {
|
||||
#[error(transparent)]
|
||||
PackageManager(#[from] PackageManagerError),
|
||||
#[error("{context}")]
|
||||
Io {
|
||||
context: String,
|
||||
#[source]
|
||||
source: std::io::Error,
|
||||
},
|
||||
#[error("invalid manifest at {path}")]
|
||||
InvalidManifest {
|
||||
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 },
|
||||
}
|
||||
|
||||
fn cached_runtime_install_dir(
|
||||
codex_home: &Path,
|
||||
runtime_version: &str,
|
||||
platform: ArtifactRuntimePlatform,
|
||||
) -> PathBuf {
|
||||
codex_home
|
||||
.join(DEFAULT_CACHE_ROOT_RELATIVE)
|
||||
.join(runtime_version)
|
||||
.join(platform.as_str())
|
||||
}
|
||||
|
||||
fn resolve_machine_js_runtime() -> Option<JsRuntime> {
|
||||
resolve_js_runtime_from_candidates(
|
||||
None,
|
||||
system_node_runtime(),
|
||||
system_electron_runtime(),
|
||||
codex_app_runtime_candidates(),
|
||||
)
|
||||
}
|
||||
|
||||
fn resolve_js_runtime_from_candidates(
|
||||
preferred_node_path: Option<&Path>,
|
||||
node_runtime: Option<JsRuntime>,
|
||||
electron_runtime: Option<JsRuntime>,
|
||||
codex_app_candidates: Vec<PathBuf>,
|
||||
) -> Option<JsRuntime> {
|
||||
preferred_node_path
|
||||
.and_then(node_runtime_from_path)
|
||||
.or(node_runtime)
|
||||
.or(electron_runtime)
|
||||
.or_else(|| {
|
||||
codex_app_candidates
|
||||
.into_iter()
|
||||
.find_map(|candidate| electron_runtime_from_path(&candidate))
|
||||
})
|
||||
}
|
||||
|
||||
fn system_node_runtime() -> Option<JsRuntime> {
|
||||
which("node")
|
||||
.ok()
|
||||
.and_then(|path| node_runtime_from_path(&path))
|
||||
}
|
||||
|
||||
fn system_electron_runtime() -> Option<JsRuntime> {
|
||||
which("electron")
|
||||
.ok()
|
||||
.and_then(|path| electron_runtime_from_path(&path))
|
||||
}
|
||||
|
||||
fn node_runtime_from_path(path: &Path) -> Option<JsRuntime> {
|
||||
path.is_file().then(|| JsRuntime::node(path.to_path_buf()))
|
||||
}
|
||||
|
||||
fn electron_runtime_from_path(path: &Path) -> Option<JsRuntime> {
|
||||
path.is_file()
|
||||
.then(|| JsRuntime::electron(path.to_path_buf()))
|
||||
}
|
||||
|
||||
fn codex_app_runtime_candidates() -> Vec<PathBuf> {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_relative_runtime_path(
|
||||
root_dir: &Path,
|
||||
relative_path: &str,
|
||||
) -> Result<PathBuf, ArtifactRuntimeError> {
|
||||
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"),
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use sha2::Digest;
|
||||
use sha2::Sha256;
|
||||
use std::io::Cursor;
|
||||
use std::io::Write;
|
||||
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::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"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ensure_installed_downloads_and_extracts_zip_runtime() {
|
||||
let server = MockServer::start().await;
|
||||
let runtime_version = "0.1.0";
|
||||
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: Some("22.0.0".to_string()),
|
||||
platforms: BTreeMap::from([(
|
||||
platform.as_str().to_string(),
|
||||
PackageReleaseArchive {
|
||||
archive: archive_name.clone(),
|
||||
sha256: archive_sha,
|
||||
format: codex_package_manager::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::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.node_path().ends_with(Path::new("node/bin/node")));
|
||||
assert!(
|
||||
runtime
|
||||
.build_js_path()
|
||||
.ends_with(Path::new("artifact-tool/dist/artifact_tool.mjs"))
|
||||
);
|
||||
assert!(
|
||||
runtime
|
||||
.render_cli_path()
|
||||
.ends_with(Path::new("granola-render/dist/render_cli.mjs"))
|
||||
);
|
||||
assert_eq!(
|
||||
runtime.resolve_js_runtime().expect("resolve js runtime"),
|
||||
JsRuntime::node(runtime.node_path().to_path_buf())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolve_js_runtime_uses_codex_app_electron_candidate() {
|
||||
let temp_dir = TempDir::new().unwrap_or_else(|error| panic!("{error}"));
|
||||
let electron_path = temp_dir.path().join("Codex");
|
||||
let missing_node = temp_dir.path().join("missing-node");
|
||||
std::fs::write(&electron_path, "#!/bin/sh\n").unwrap_or_else(|error| panic!("{error}"));
|
||||
|
||||
let runtime = resolve_js_runtime_from_candidates(
|
||||
Some(missing_node.as_path()),
|
||||
None,
|
||||
None,
|
||||
vec![electron_path.clone()],
|
||||
)
|
||||
.expect("resolve js runtime");
|
||||
|
||||
assert_eq!(runtime, JsRuntime::electron(electron_path));
|
||||
assert!(runtime.requires_electron_run_as_node());
|
||||
}
|
||||
|
||||
fn build_zip_archive(runtime_version: &str) -> Vec<u8> {
|
||||
let mut bytes = Cursor::new(Vec::new());
|
||||
{
|
||||
let mut zip = ZipWriter::new(&mut bytes);
|
||||
let options = SimpleFileOptions::default();
|
||||
let manifest = serde_json::to_vec(&sample_extracted_manifest(runtime_version))
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
zip.start_file("artifact-runtime/manifest.json", options)
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
zip.write_all(&manifest)
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
zip.start_file(
|
||||
"artifact-runtime/node/bin/node",
|
||||
options.unix_permissions(0o755),
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
zip.write_all(b"#!/bin/sh\n")
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
zip.start_file(
|
||||
"artifact-runtime/artifact-tool/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.start_file(
|
||||
"artifact-runtime/granola-render/dist/render_cli.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 sample_extracted_manifest(runtime_version: &str) -> ExtractedRuntimeManifest {
|
||||
ExtractedRuntimeManifest {
|
||||
schema_version: 1,
|
||||
runtime_version: runtime_version.to_string(),
|
||||
node: RuntimePathEntry {
|
||||
relative_path: "node/bin/node".to_string(),
|
||||
},
|
||||
entrypoints: RuntimeEntrypoints {
|
||||
build_js: RuntimePathEntry {
|
||||
relative_path: "artifact-tool/dist/artifact_tool.mjs".to_string(),
|
||||
},
|
||||
render_cli: RuntimePathEntry {
|
||||
relative_path: "granola-render/dist/render_cli.mjs".to_string(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
28
codex-rs/artifacts/src/runtime/error.rs
Normal file
28
codex-rs/artifacts/src/runtime/error.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
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 manifest at {path}")]
|
||||
InvalidManifest {
|
||||
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 },
|
||||
}
|
||||
200
codex-rs/artifacts/src/runtime/installed.rs
Normal file
200
codex-rs/artifacts/src/runtime/installed.rs
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
use super::ArtifactRuntimeError;
|
||||
use super::ArtifactRuntimePlatform;
|
||||
use super::ExtractedRuntimeManifest;
|
||||
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::path::Component;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
|
||||
/// Loads a previously installed runtime from a caller-provided cache root.
|
||||
pub fn load_cached_runtime(
|
||||
cache_root: &Path,
|
||||
runtime_version: &str,
|
||||
) -> Result<InstalledArtifactRuntime, ArtifactRuntimeError> {
|
||||
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,
|
||||
manifest: ExtractedRuntimeManifest,
|
||||
node_path: PathBuf,
|
||||
build_js_path: PathBuf,
|
||||
render_cli_path: PathBuf,
|
||||
}
|
||||
|
||||
impl InstalledArtifactRuntime {
|
||||
/// Creates an installed-runtime value from prevalidated paths.
|
||||
pub fn new(
|
||||
root_dir: PathBuf,
|
||||
runtime_version: String,
|
||||
platform: ArtifactRuntimePlatform,
|
||||
manifest: ExtractedRuntimeManifest,
|
||||
node_path: PathBuf,
|
||||
build_js_path: PathBuf,
|
||||
render_cli_path: PathBuf,
|
||||
) -> Self {
|
||||
Self {
|
||||
root_dir,
|
||||
runtime_version,
|
||||
platform,
|
||||
manifest,
|
||||
node_path,
|
||||
build_js_path,
|
||||
render_cli_path,
|
||||
}
|
||||
}
|
||||
|
||||
/// Loads and validates an extracted runtime directory.
|
||||
pub fn load(
|
||||
root_dir: PathBuf,
|
||||
platform: ArtifactRuntimePlatform,
|
||||
) -> Result<Self, ArtifactRuntimeError> {
|
||||
let manifest_path = root_dir.join("manifest.json");
|
||||
let manifest_bytes =
|
||||
std::fs::read(&manifest_path).map_err(|source| ArtifactRuntimeError::Io {
|
||||
context: format!("failed to read {}", manifest_path.display()),
|
||||
source,
|
||||
})?;
|
||||
let manifest = serde_json::from_slice::<ExtractedRuntimeManifest>(&manifest_bytes)
|
||||
.map_err(|source| ArtifactRuntimeError::InvalidManifest {
|
||||
path: manifest_path,
|
||||
source,
|
||||
})?;
|
||||
let node_path = resolve_relative_runtime_path(&root_dir, &manifest.node.relative_path)?;
|
||||
let build_js_path =
|
||||
resolve_relative_runtime_path(&root_dir, &manifest.entrypoints.build_js.relative_path)?;
|
||||
let render_cli_path = resolve_relative_runtime_path(
|
||||
&root_dir,
|
||||
&manifest.entrypoints.render_cli.relative_path,
|
||||
)?;
|
||||
verify_required_runtime_path(&build_js_path)?;
|
||||
verify_required_runtime_path(&render_cli_path)?;
|
||||
|
||||
Ok(Self::new(
|
||||
root_dir,
|
||||
manifest.runtime_version.clone(),
|
||||
platform,
|
||||
manifest,
|
||||
node_path,
|
||||
build_js_path,
|
||||
render_cli_path,
|
||||
))
|
||||
}
|
||||
|
||||
/// Returns the extracted runtime root directory.
|
||||
pub fn root_dir(&self) -> &Path {
|
||||
&self.root_dir
|
||||
}
|
||||
|
||||
/// Returns the runtime version recorded in the extracted manifest.
|
||||
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 parsed extracted-runtime manifest.
|
||||
pub fn manifest(&self) -> &ExtractedRuntimeManifest {
|
||||
&self.manifest
|
||||
}
|
||||
|
||||
/// Returns the bundled Node executable path advertised by the runtime manifest.
|
||||
pub fn node_path(&self) -> &Path {
|
||||
&self.node_path
|
||||
}
|
||||
|
||||
/// Returns the artifact build entrypoint path.
|
||||
pub fn build_js_path(&self) -> &Path {
|
||||
&self.build_js_path
|
||||
}
|
||||
|
||||
/// Returns the artifact render CLI entrypoint path.
|
||||
pub fn render_cli_path(&self) -> &Path {
|
||||
&self.render_cli_path
|
||||
}
|
||||
|
||||
/// Resolves the best executable to use for artifact commands.
|
||||
///
|
||||
/// Preference order is the bundled Node path, then a machine Node install,
|
||||
/// then Electron from the machine or a Codex desktop app bundle.
|
||||
pub fn resolve_js_runtime(&self) -> Result<JsRuntime, ArtifactRuntimeError> {
|
||||
resolve_js_runtime_from_candidates(
|
||||
Some(self.node_path()),
|
||||
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<PathBuf, ArtifactRuntimeError> {
|
||||
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"),
|
||||
})
|
||||
}
|
||||
177
codex-rs/artifacts/src/runtime/js_runtime.rs
Normal file
177
codex-rs/artifacts/src/runtime/js_runtime.rs
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
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<JsRuntime> {
|
||||
resolve_js_runtime_from_candidates(
|
||||
None,
|
||||
system_node_runtime(),
|
||||
system_electron_runtime(),
|
||||
codex_app_runtime_candidates(),
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_js_runtime_from_candidates(
|
||||
preferred_node_path: Option<&Path>,
|
||||
node_runtime: Option<JsRuntime>,
|
||||
electron_runtime: Option<JsRuntime>,
|
||||
codex_app_candidates: Vec<PathBuf>,
|
||||
) -> Option<JsRuntime> {
|
||||
preferred_node_path
|
||||
.and_then(node_runtime_from_path)
|
||||
.or(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<JsRuntime> {
|
||||
which("node")
|
||||
.ok()
|
||||
.and_then(|path| node_runtime_from_path(&path))
|
||||
}
|
||||
|
||||
pub(crate) fn system_electron_runtime() -> Option<JsRuntime> {
|
||||
which("electron")
|
||||
.ok()
|
||||
.and_then(|path| electron_runtime_from_path(&path))
|
||||
}
|
||||
|
||||
pub(crate) fn node_runtime_from_path(path: &Path) -> Option<JsRuntime> {
|
||||
path.is_file().then(|| JsRuntime::node(path.to_path_buf()))
|
||||
}
|
||||
|
||||
pub(crate) fn electron_runtime_from_path(path: &Path) -> Option<JsRuntime> {
|
||||
path.is_file()
|
||||
.then(|| JsRuntime::electron(path.to_path_buf()))
|
||||
}
|
||||
|
||||
pub(crate) fn codex_app_runtime_candidates() -> Vec<PathBuf> {
|
||||
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(),
|
||||
}
|
||||
}
|
||||
253
codex-rs/artifacts/src/runtime/manager.rs
Normal file
253
codex-rs/artifacts/src/runtime/manager.rs
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
use super::ArtifactRuntimeError;
|
||||
use super::ArtifactRuntimePlatform;
|
||||
use super::InstalledArtifactRuntime;
|
||||
use super::ReleaseManifest;
|
||||
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<String>) -> 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<String>) -> 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<Url, PackageManagerError> {
|
||||
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<String>) -> Self {
|
||||
Self::new(
|
||||
match Url::parse(DEFAULT_RELEASE_BASE_URL) {
|
||||
Ok(url) => url,
|
||||
Err(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<ArtifactRuntimePackage>,
|
||||
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<String>) -> 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<ArtifactRuntimePackage>,
|
||||
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<Option<InstalledArtifactRuntime>, ArtifactRuntimeError> {
|
||||
self.package_manager.resolve_cached().await
|
||||
}
|
||||
|
||||
/// Returns the installed runtime, downloading and caching it if necessary.
|
||||
pub async fn ensure_installed(&self) -> Result<InstalledArtifactRuntime, ArtifactRuntimeError> {
|
||||
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<Url, PackageManagerError> {
|
||||
self.release.manifest_url()
|
||||
}
|
||||
|
||||
fn archive_url(&self, archive: &PackageReleaseArchive) -> Result<Url, PackageManagerError> {
|
||||
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<PackageReleaseArchive, Self::Error> {
|
||||
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<Self::Installed, Self::Error> {
|
||||
InstalledArtifactRuntime::load(root_dir, platform)
|
||||
}
|
||||
}
|
||||
37
codex-rs/artifacts/src/runtime/manifest.rs
Normal file
37
codex-rs/artifacts/src/runtime/manifest.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
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<String>,
|
||||
pub platforms: BTreeMap<String, PackageReleaseArchive>,
|
||||
}
|
||||
|
||||
/// Manifest shipped inside the extracted runtime payload.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct ExtractedRuntimeManifest {
|
||||
pub schema_version: u32,
|
||||
pub runtime_version: String,
|
||||
pub node: RuntimePathEntry,
|
||||
pub entrypoints: RuntimeEntrypoints,
|
||||
}
|
||||
|
||||
/// A relative path entry inside an extracted runtime manifest.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct RuntimePathEntry {
|
||||
pub relative_path: String,
|
||||
}
|
||||
|
||||
/// Entrypoints required to build and render artifacts.
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)]
|
||||
pub struct RuntimeEntrypoints {
|
||||
pub build_js: RuntimePathEntry,
|
||||
pub render_cli: RuntimePathEntry,
|
||||
}
|
||||
30
codex-rs/artifacts/src/runtime/mod.rs
Normal file
30
codex-rs/artifacts/src/runtime/mod.rs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
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::ExtractedRuntimeManifest;
|
||||
pub use manifest::ReleaseManifest;
|
||||
pub use manifest::RuntimeEntrypoints;
|
||||
pub use manifest::RuntimePathEntry;
|
||||
|
||||
pub(crate) use installed::default_cached_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;
|
||||
521
codex-rs/artifacts/src/tests.rs
Normal file
521
codex-rs/artifacts/src/tests.rs
Normal file
|
|
@ -0,0 +1,521 @@
|
|||
use crate::ArtifactBuildRequest;
|
||||
use crate::ArtifactCommandOutput;
|
||||
use crate::ArtifactRenderCommandRequest;
|
||||
use crate::ArtifactRenderTarget;
|
||||
use crate::ArtifactRuntimeManager;
|
||||
use crate::ArtifactRuntimeManagerConfig;
|
||||
use crate::ArtifactRuntimePlatform;
|
||||
use crate::ArtifactRuntimeReleaseLocator;
|
||||
use crate::ArtifactsClient;
|
||||
use crate::DEFAULT_CACHE_ROOT_RELATIVE;
|
||||
use crate::ExtractedRuntimeManifest;
|
||||
use crate::InstalledArtifactRuntime;
|
||||
use crate::JsRuntime;
|
||||
use crate::PresentationRenderTarget;
|
||||
use crate::ReleaseManifest;
|
||||
use crate::RuntimeEntrypoints;
|
||||
use crate::RuntimePathEntry;
|
||||
use crate::SpreadsheetRenderTarget;
|
||||
use crate::load_cached_runtime;
|
||||
use codex_package_manager::ArchiveFormat;
|
||||
use codex_package_manager::PackageReleaseArchive;
|
||||
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;
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
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 = "0.1.0";
|
||||
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,
|
||||
Some(PathBuf::from("node/bin/node")),
|
||||
);
|
||||
|
||||
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.node_path().ends_with(Path::new("node/bin/node")));
|
||||
assert!(
|
||||
runtime
|
||||
.build_js_path()
|
||||
.ends_with(Path::new("artifact-tool/dist/artifact_tool.mjs"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_cached_runtime_rejects_parent_relative_paths() {
|
||||
let codex_home = TempDir::new().unwrap_or_else(|error| panic!("{error}"));
|
||||
let runtime_version = "0.1.0";
|
||||
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,
|
||||
Some(PathBuf::from("../node/bin/node")),
|
||||
);
|
||||
|
||||
let error = load_cached_runtime(
|
||||
&codex_home.path().join(DEFAULT_CACHE_ROOT_RELATIVE),
|
||||
runtime_version,
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
"runtime path `../node/bin/node` is invalid"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_cached_runtime_requires_build_entrypoint() {
|
||||
let codex_home = TempDir::new().unwrap_or_else(|error| panic!("{error}"));
|
||||
let runtime_version = "0.1.0";
|
||||
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,
|
||||
Some(PathBuf::from("node/bin/node")),
|
||||
);
|
||||
fs::remove_file(install_dir.join("artifact-tool/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("artifact-tool/dist/artifact_tool.mjs")
|
||||
.display()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ensure_installed_downloads_and_extracts_zip_runtime() {
|
||||
let server = MockServer::start().await;
|
||||
let runtime_version = "0.1.0";
|
||||
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: Some("22.0.0".to_string()),
|
||||
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.node_path().ends_with(Path::new("node/bin/node")));
|
||||
assert_eq!(
|
||||
runtime.resolve_js_runtime().expect("resolve js runtime"),
|
||||
JsRuntime::node(runtime.node_path().to_path_buf())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_cached_runtime_uses_custom_cache_root() {
|
||||
let codex_home = TempDir::new().unwrap_or_else(|error| panic!("{error}"));
|
||||
let runtime_version = "0.1.0";
|
||||
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,
|
||||
Some(PathBuf::from("node/bin/node")),
|
||||
);
|
||||
|
||||
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 output_path = temp.path().join("build-output.txt");
|
||||
let wrapped_script_path = temp.path().join("wrapped-script.mjs");
|
||||
let runtime = fake_installed_runtime(temp.path(), &output_path, &wrapped_script_path);
|
||||
let client = ArtifactsClient::from_installed_runtime(runtime);
|
||||
|
||||
let output = client
|
||||
.execute_build(ArtifactBuildRequest {
|
||||
source: "console.log('hello');".to_string(),
|
||||
cwd: temp.path().to_path_buf(),
|
||||
timeout: Some(Duration::from_secs(5)),
|
||||
env: BTreeMap::from([
|
||||
(
|
||||
"CODEX_TEST_OUTPUT".to_string(),
|
||||
output_path.display().to_string(),
|
||||
),
|
||||
("CUSTOM_ENV".to_string(), "custom-value".to_string()),
|
||||
]),
|
||||
})
|
||||
.await
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
|
||||
assert_success(&output);
|
||||
let command_log = fs::read_to_string(&output_path).unwrap_or_else(|error| panic!("{error}"));
|
||||
assert!(command_log.contains("arg0="));
|
||||
assert!(command_log.contains("CODEX_ARTIFACT_BUILD_ENTRYPOINT="));
|
||||
assert!(command_log.contains("CODEX_ARTIFACT_RENDER_ENTRYPOINT="));
|
||||
assert!(command_log.contains("CUSTOM_ENV=custom-value"));
|
||||
|
||||
let wrapped_script =
|
||||
fs::read_to_string(wrapped_script_path).unwrap_or_else(|error| panic!("{error}"));
|
||||
assert!(wrapped_script.contains("globalThis.artifacts = artifactTool;"));
|
||||
assert!(wrapped_script.contains("globalThis.codexArtifacts = artifactTool;"));
|
||||
assert!(wrapped_script.contains("console.log('hello');"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
#[cfg(unix)]
|
||||
async fn artifacts_client_execute_render_passes_expected_args() {
|
||||
let temp = TempDir::new().unwrap_or_else(|error| panic!("{error}"));
|
||||
let output_path = temp.path().join("render-output.txt");
|
||||
let wrapped_script_path = temp.path().join("unused-script-copy.mjs");
|
||||
let runtime = fake_installed_runtime(temp.path(), &output_path, &wrapped_script_path);
|
||||
let client = ArtifactsClient::from_installed_runtime(runtime.clone());
|
||||
let render_output = temp.path().join("slide.png");
|
||||
|
||||
let output = client
|
||||
.execute_render(ArtifactRenderCommandRequest {
|
||||
cwd: temp.path().to_path_buf(),
|
||||
timeout: Some(Duration::from_secs(5)),
|
||||
env: BTreeMap::from([(
|
||||
"CODEX_TEST_OUTPUT".to_string(),
|
||||
output_path.display().to_string(),
|
||||
)]),
|
||||
target: ArtifactRenderTarget::Presentation(PresentationRenderTarget {
|
||||
input_path: temp.path().join("deck.pptx"),
|
||||
output_path: render_output.clone(),
|
||||
slide_number: 3,
|
||||
}),
|
||||
})
|
||||
.await
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
|
||||
assert_success(&output);
|
||||
let command_log = fs::read_to_string(&output_path).unwrap_or_else(|error| panic!("{error}"));
|
||||
assert!(command_log.contains(&format!("arg0={}", runtime.render_cli_path().display())));
|
||||
assert!(command_log.contains("arg1=pptx"));
|
||||
assert!(command_log.contains("arg2=render"));
|
||||
assert!(command_log.contains("arg5=--slide"));
|
||||
assert!(command_log.contains("arg6=3"));
|
||||
assert!(command_log.contains("arg7=--out"));
|
||||
assert!(command_log.contains(&format!("arg8={}", render_output.display())));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spreadsheet_render_target_to_args_includes_optional_range() {
|
||||
let target = ArtifactRenderTarget::Spreadsheet(SpreadsheetRenderTarget {
|
||||
input_path: PathBuf::from("/tmp/input.xlsx"),
|
||||
output_path: PathBuf::from("/tmp/output.png"),
|
||||
sheet_name: "Summary".to_string(),
|
||||
range: Some("A1:C8".to_string()),
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
target.to_args(),
|
||||
vec![
|
||||
"xlsx".to_string(),
|
||||
"render".to_string(),
|
||||
"--in".to_string(),
|
||||
"/tmp/input.xlsx".to_string(),
|
||||
"--sheet".to_string(),
|
||||
"Summary".to_string(),
|
||||
"--out".to_string(),
|
||||
"/tmp/output.png".to_string(),
|
||||
"--range".to_string(),
|
||||
"A1:C8".to_string(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_success(output: &ArtifactCommandOutput) {
|
||||
assert!(output.success());
|
||||
assert_eq!(output.exit_code, Some(0));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn fake_installed_runtime(
|
||||
root: &Path,
|
||||
output_path: &Path,
|
||||
wrapped_script_path: &Path,
|
||||
) -> InstalledArtifactRuntime {
|
||||
let runtime_root = root.join("runtime");
|
||||
write_installed_runtime(&runtime_root, "0.1.0", Some(PathBuf::from("node/bin/node")));
|
||||
write_fake_node_script(
|
||||
&runtime_root.join("node/bin/node"),
|
||||
output_path,
|
||||
wrapped_script_path,
|
||||
);
|
||||
InstalledArtifactRuntime::load(
|
||||
runtime_root,
|
||||
ArtifactRuntimePlatform::detect_current().unwrap_or_else(|error| panic!("{error}")),
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("{error}"))
|
||||
}
|
||||
|
||||
fn write_installed_runtime(
|
||||
install_dir: &Path,
|
||||
runtime_version: &str,
|
||||
node_relative: Option<PathBuf>,
|
||||
) {
|
||||
fs::create_dir_all(install_dir.join("node/bin")).unwrap_or_else(|error| panic!("{error}"));
|
||||
fs::create_dir_all(install_dir.join("artifact-tool/dist"))
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
fs::create_dir_all(install_dir.join("granola-render/dist"))
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
let node_relative = node_relative.unwrap_or_else(|| PathBuf::from("node/bin/node"));
|
||||
fs::write(
|
||||
install_dir.join("manifest.json"),
|
||||
serde_json::json!(sample_extracted_manifest(runtime_version, node_relative)).to_string(),
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
fs::write(install_dir.join("node/bin/node"), "#!/bin/sh\n")
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
fs::write(
|
||||
install_dir.join("artifact-tool/dist/artifact_tool.mjs"),
|
||||
"export const ok = true;\n",
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
fs::write(
|
||||
install_dir.join("granola-render/dist/render_cli.mjs"),
|
||||
"export const ok = true;\n",
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn write_fake_node_script(script_path: &Path, output_path: &Path, wrapped_script_path: &Path) {
|
||||
fs::write(
|
||||
script_path,
|
||||
format!(
|
||||
concat!(
|
||||
"#!/bin/sh\n",
|
||||
"printf 'arg0=%s\\n' \"$1\" > \"{}\"\n",
|
||||
"cp \"$1\" \"{}\"\n",
|
||||
"shift\n",
|
||||
"i=1\n",
|
||||
"for arg in \"$@\"; do\n",
|
||||
" printf 'arg%s=%s\\n' \"$i\" \"$arg\" >> \"{}\"\n",
|
||||
" i=$((i + 1))\n",
|
||||
"done\n",
|
||||
"printf 'CODEX_ARTIFACT_BUILD_ENTRYPOINT=%s\\n' \"$CODEX_ARTIFACT_BUILD_ENTRYPOINT\" >> \"{}\"\n",
|
||||
"printf 'CODEX_ARTIFACT_RENDER_ENTRYPOINT=%s\\n' \"$CODEX_ARTIFACT_RENDER_ENTRYPOINT\" >> \"{}\"\n",
|
||||
"printf 'CUSTOM_ENV=%s\\n' \"$CUSTOM_ENV\" >> \"{}\"\n",
|
||||
"echo stdout-ok\n",
|
||||
"echo stderr-ok >&2\n"
|
||||
),
|
||||
output_path.display(),
|
||||
wrapped_script_path.display(),
|
||||
output_path.display(),
|
||||
output_path.display(),
|
||||
output_path.display(),
|
||||
output_path.display(),
|
||||
),
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let mut permissions = fs::metadata(script_path)
|
||||
.unwrap_or_else(|error| panic!("{error}"))
|
||||
.permissions();
|
||||
permissions.set_mode(0o755);
|
||||
fs::set_permissions(script_path, permissions).unwrap_or_else(|error| panic!("{error}"));
|
||||
}
|
||||
}
|
||||
|
||||
fn build_zip_archive(runtime_version: &str) -> Vec<u8> {
|
||||
let mut bytes = Cursor::new(Vec::new());
|
||||
{
|
||||
let mut zip = ZipWriter::new(&mut bytes);
|
||||
let options = SimpleFileOptions::default();
|
||||
let manifest = serde_json::to_vec(&sample_extracted_manifest(
|
||||
runtime_version,
|
||||
PathBuf::from("node/bin/node"),
|
||||
))
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
zip.start_file("artifact-runtime/manifest.json", options)
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
zip.write_all(&manifest)
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
zip.start_file(
|
||||
"artifact-runtime/node/bin/node",
|
||||
options.unix_permissions(0o755),
|
||||
)
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
zip.write_all(b"#!/bin/sh\n")
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
zip.start_file(
|
||||
"artifact-runtime/artifact-tool/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.start_file(
|
||||
"artifact-runtime/granola-render/dist/render_cli.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 sample_extracted_manifest(
|
||||
runtime_version: &str,
|
||||
node_relative: PathBuf,
|
||||
) -> ExtractedRuntimeManifest {
|
||||
ExtractedRuntimeManifest {
|
||||
schema_version: 1,
|
||||
runtime_version: runtime_version.to_string(),
|
||||
node: RuntimePathEntry {
|
||||
relative_path: node_relative.display().to_string(),
|
||||
},
|
||||
entrypoints: RuntimeEntrypoints {
|
||||
build_js: RuntimePathEntry {
|
||||
relative_path: "artifact-tool/dist/artifact_tool.mjs".to_string(),
|
||||
},
|
||||
render_cli: RuntimePathEntry {
|
||||
relative_path: "granola-render/dist/render_cli.mjs".to_string(),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,3 @@
|
|||
#![doc = include_str!("../README.md")]
|
||||
|
||||
mod archive;
|
||||
mod config;
|
||||
mod error;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue