feat: adapt artifacts to new packaging and 2.5.6 (#14947)
This commit is contained in:
parent
40a7d1d15b
commit
0f9484dc8a
15 changed files with 422 additions and 556 deletions
2
codex-rs/Cargo.lock
generated
2
codex-rs/Cargo.lock
generated
|
|
@ -1570,11 +1570,13 @@ 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",
|
||||
|
|
|
|||
|
|
@ -19,8 +19,10 @@ which = { workspace = true }
|
|||
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 }
|
||||
|
|
|
|||
|
|
@ -11,10 +11,11 @@ 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 and render commands against a resolved runtime.
|
||||
/// Executes artifact build commands against a resolved runtime.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtifactsClient {
|
||||
runtime_source: RuntimeSource,
|
||||
|
|
@ -54,7 +55,18 @@ impl ArtifactsClient {
|
|||
source,
|
||||
})?;
|
||||
let script_path = staging_dir.path().join("artifact-build.mjs");
|
||||
let wrapped_script = build_wrapped_script(&request.source);
|
||||
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 {
|
||||
|
|
@ -63,44 +75,8 @@ impl ArtifactsClient {
|
|||
})?;
|
||||
|
||||
let mut command = Command::new(js_runtime.executable_path());
|
||||
command
|
||||
.arg(&script_path)
|
||||
.current_dir(&request.cwd)
|
||||
.env("CODEX_ARTIFACT_BUILD_ENTRYPOINT", runtime.build_js_path())
|
||||
.env(
|
||||
"CODEX_ARTIFACT_RENDER_ENTRYPOINT",
|
||||
runtime.render_cli_path(),
|
||||
)
|
||||
.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
|
||||
}
|
||||
|
||||
/// Executes the artifact render CLI against the configured runtime.
|
||||
pub async fn execute_render(
|
||||
&self,
|
||||
request: ArtifactRenderCommandRequest,
|
||||
) -> Result<ArtifactCommandOutput, ArtifactsError> {
|
||||
let runtime = self.resolve_runtime().await?;
|
||||
let js_runtime = runtime.resolve_js_runtime()?;
|
||||
let mut command = Command::new(js_runtime.executable_path());
|
||||
command
|
||||
.arg(runtime.render_cli_path())
|
||||
.args(request.target.to_args())
|
||||
.current_dir(&request.cwd)
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
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");
|
||||
}
|
||||
|
|
@ -132,76 +108,6 @@ pub struct ArtifactBuildRequest {
|
|||
pub env: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
/// Request payload for the artifact render CLI.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ArtifactRenderCommandRequest {
|
||||
pub cwd: PathBuf,
|
||||
pub timeout: Option<Duration>,
|
||||
pub env: BTreeMap<String, String>,
|
||||
pub target: ArtifactRenderTarget,
|
||||
}
|
||||
|
||||
/// Render targets supported by the packaged artifact runtime.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub enum ArtifactRenderTarget {
|
||||
Presentation(PresentationRenderTarget),
|
||||
Spreadsheet(SpreadsheetRenderTarget),
|
||||
}
|
||||
|
||||
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) => {
|
||||
vec![
|
||||
"pptx".to_string(),
|
||||
"render".to_string(),
|
||||
"--in".to_string(),
|
||||
target.input_path.display().to_string(),
|
||||
"--slide".to_string(),
|
||||
target.slide_number.to_string(),
|
||||
"--out".to_string(),
|
||||
target.output_path.display().to_string(),
|
||||
]
|
||||
}
|
||||
Self::Spreadsheet(target) => {
|
||||
let mut args = vec![
|
||||
"xlsx".to_string(),
|
||||
"render".to_string(),
|
||||
"--in".to_string(),
|
||||
target.input_path.display().to_string(),
|
||||
"--sheet".to_string(),
|
||||
target.sheet_name.clone(),
|
||||
"--out".to_string(),
|
||||
target.output_path.display().to_string(),
|
||||
];
|
||||
if let Some(range) = &target.range {
|
||||
args.push("--range".to_string());
|
||||
args.push(range.clone());
|
||||
}
|
||||
args
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Presentation render request parameters.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PresentationRenderTarget {
|
||||
pub input_path: PathBuf,
|
||||
pub output_path: PathBuf,
|
||||
pub slide_number: u32,
|
||||
}
|
||||
|
||||
/// Spreadsheet render request parameters.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SpreadsheetRenderTarget {
|
||||
pub input_path: PathBuf,
|
||||
pub output_path: PathBuf,
|
||||
pub sheet_name: String,
|
||||
pub range: Option<String>,
|
||||
}
|
||||
|
||||
/// Captured stdout, stderr, and exit status from an artifact subprocess.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ArtifactCommandOutput {
|
||||
|
|
@ -232,24 +138,28 @@ pub enum ArtifactsError {
|
|||
TimedOut { timeout: Duration },
|
||||
}
|
||||
|
||||
fn build_wrapped_script(source: &str) -> String {
|
||||
format!(
|
||||
concat!(
|
||||
"import {{ pathToFileURL }} from \"node:url\";\n",
|
||||
"const artifactTool = await import(pathToFileURL(process.env.CODEX_ARTIFACT_BUILD_ENTRYPOINT).href);\n",
|
||||
"globalThis.artifactTool = artifactTool;\n",
|
||||
"globalThis.artifacts = artifactTool;\n",
|
||||
"globalThis.codexArtifacts = artifactTool;\n",
|
||||
"for (const [name, value] of Object.entries(artifactTool)) {{\n",
|
||||
" if (name === \"default\" || Object.prototype.hasOwnProperty.call(globalThis, name)) {{\n",
|
||||
" continue;\n",
|
||||
" }}\n",
|
||||
" globalThis[name] = value;\n",
|
||||
"}}\n\n",
|
||||
"{}\n"
|
||||
),
|
||||
source
|
||||
)
|
||||
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(
|
||||
|
|
|
|||
|
|
@ -5,12 +5,8 @@ mod tests;
|
|||
|
||||
pub use client::ArtifactBuildRequest;
|
||||
pub use client::ArtifactCommandOutput;
|
||||
pub use client::ArtifactRenderCommandRequest;
|
||||
pub use client::ArtifactRenderTarget;
|
||||
pub use client::ArtifactsClient;
|
||||
pub use client::ArtifactsError;
|
||||
pub use client::PresentationRenderTarget;
|
||||
pub use client::SpreadsheetRenderTarget;
|
||||
pub use runtime::ArtifactRuntimeError;
|
||||
pub use runtime::ArtifactRuntimeManager;
|
||||
pub use runtime::ArtifactRuntimeManagerConfig;
|
||||
|
|
@ -19,13 +15,10 @@ 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::ExtractedRuntimeManifest;
|
||||
pub use runtime::InstalledArtifactRuntime;
|
||||
pub use runtime::JsRuntime;
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ pub enum ArtifactRuntimeError {
|
|||
#[source]
|
||||
source: std::io::Error,
|
||||
},
|
||||
#[error("invalid manifest at {path}")]
|
||||
InvalidManifest {
|
||||
#[error("invalid package metadata at {path}")]
|
||||
InvalidPackageMetadata {
|
||||
path: PathBuf,
|
||||
#[source]
|
||||
source: serde_json::Error,
|
||||
|
|
|
|||
|
|
@ -1,15 +1,17 @@
|
|||
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::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,
|
||||
|
|
@ -36,10 +38,7 @@ 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 {
|
||||
|
|
@ -48,19 +47,13 @@ impl InstalledArtifactRuntime {
|
|||
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,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -69,35 +62,16 @@ impl InstalledArtifactRuntime {
|
|||
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 package_metadata = load_package_metadata(&root_dir)?;
|
||||
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,
|
||||
)?;
|
||||
resolve_relative_runtime_path(&root_dir, &package_metadata.build_js_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(),
|
||||
package_metadata.version,
|
||||
platform,
|
||||
manifest,
|
||||
node_path,
|
||||
build_js_path,
|
||||
render_cli_path,
|
||||
))
|
||||
}
|
||||
|
||||
|
|
@ -106,7 +80,7 @@ impl InstalledArtifactRuntime {
|
|||
&self.root_dir
|
||||
}
|
||||
|
||||
/// Returns the runtime version recorded in the extracted manifest.
|
||||
/// Returns the runtime version recorded in `package.json`.
|
||||
pub fn runtime_version(&self) -> &str {
|
||||
&self.runtime_version
|
||||
}
|
||||
|
|
@ -116,33 +90,17 @@ impl InstalledArtifactRuntime {
|
|||
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.
|
||||
/// 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<JsRuntime, ArtifactRuntimeError> {
|
||||
resolve_js_runtime_from_candidates(
|
||||
Some(self.node_path()),
|
||||
system_node_runtime(),
|
||||
system_electron_runtime(),
|
||||
codex_app_runtime_candidates(),
|
||||
|
|
@ -198,3 +156,128 @@ fn verify_required_runtime_path(path: &Path) -> Result<(), ArtifactRuntimeError>
|
|||
source: std::io::Error::new(std::io::ErrorKind::NotFound, "missing runtime file"),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn detect_runtime_root(extraction_root: &Path) -> Result<PathBuf, ArtifactRuntimeError> {
|
||||
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<PackageMetadata, ArtifactRuntimeError> {
|
||||
#[derive(serde::Deserialize)]
|
||||
struct PackageJson {
|
||||
name: String,
|
||||
version: String,
|
||||
exports: PackageExports,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum PackageExports {
|
||||
Main(String),
|
||||
Map(BTreeMap<String, String>),
|
||||
}
|
||||
|
||||
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::<PackageJson>(&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(),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,7 +74,6 @@ pub fn can_manage_artifact_runtime() -> bool {
|
|||
|
||||
pub(crate) fn resolve_machine_js_runtime() -> Option<JsRuntime> {
|
||||
resolve_js_runtime_from_candidates(
|
||||
/*preferred_node_path*/ None,
|
||||
system_node_runtime(),
|
||||
system_electron_runtime(),
|
||||
codex_app_runtime_candidates(),
|
||||
|
|
@ -82,20 +81,15 @@ pub(crate) fn resolve_machine_js_runtime() -> Option<JsRuntime> {
|
|||
}
|
||||
|
||||
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))
|
||||
})
|
||||
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> {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ 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;
|
||||
|
|
@ -79,12 +80,9 @@ impl ArtifactRuntimeReleaseLocator {
|
|||
/// 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}")
|
||||
}
|
||||
},
|
||||
Url::parse(DEFAULT_RELEASE_BASE_URL).unwrap_or_else(|error| {
|
||||
panic!("hard-coded artifact runtime release base URL must be valid: {error}")
|
||||
}),
|
||||
runtime_version,
|
||||
)
|
||||
}
|
||||
|
|
@ -250,4 +248,8 @@ impl ManagedPackage for ArtifactRuntimePackage {
|
|||
) -> Result<Self::Installed, Self::Error> {
|
||||
InstalledArtifactRuntime::load(root_dir, platform)
|
||||
}
|
||||
|
||||
fn detect_extracted_root(&self, extraction_root: &Path) -> Result<PathBuf, Self::Error> {
|
||||
detect_runtime_root(extraction_root)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,25 +13,3 @@ pub struct ReleaseManifest {
|
|||
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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,12 +18,10 @@ 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 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;
|
||||
|
|
|
|||
|
|
@ -1,24 +1,17 @@
|
|||
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 flate2::Compression;
|
||||
use flate2::write::GzEncoder;
|
||||
use pretty_assertions::assert_eq;
|
||||
use sha2::Digest;
|
||||
use sha2::Sha256;
|
||||
|
|
@ -26,11 +19,9 @@ 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 tar::Builder as TarBuilder;
|
||||
use tempfile::TempDir;
|
||||
use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
|
|
@ -71,7 +62,7 @@ fn default_release_locator_uses_openai_codex_github_releases() {
|
|||
#[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 runtime_version = "2.5.6";
|
||||
let platform =
|
||||
ArtifactRuntimePlatform::detect_current().unwrap_or_else(|error| panic!("{error}"));
|
||||
let install_dir = codex_home
|
||||
|
|
@ -79,11 +70,7 @@ fn load_cached_runtime_reads_installed_runtime() {
|
|||
.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")),
|
||||
);
|
||||
write_installed_runtime(&install_dir, runtime_version);
|
||||
|
||||
let runtime = load_cached_runtime(
|
||||
&codex_home.path().join(DEFAULT_CACHE_ROOT_RELATIVE),
|
||||
|
|
@ -93,47 +80,17 @@ fn load_cached_runtime_reads_installed_runtime() {
|
|||
|
||||
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"
|
||||
.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 = "0.1.0";
|
||||
let runtime_version = "2.5.6";
|
||||
let platform =
|
||||
ArtifactRuntimePlatform::detect_current().unwrap_or_else(|error| panic!("{error}"));
|
||||
let install_dir = codex_home
|
||||
|
|
@ -141,12 +98,8 @@ fn load_cached_runtime_requires_build_entrypoint() {
|
|||
.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"))
|
||||
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(
|
||||
|
|
@ -159,9 +112,7 @@ fn load_cached_runtime_requires_build_entrypoint() {
|
|||
error.to_string(),
|
||||
format!(
|
||||
"required runtime file is missing: {}",
|
||||
install_dir
|
||||
.join("artifact-tool/dist/artifact_tool.mjs")
|
||||
.display()
|
||||
install_dir.join("dist/artifact_tool.mjs").display()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
@ -169,7 +120,7 @@ fn load_cached_runtime_requires_build_entrypoint() {
|
|||
#[tokio::test]
|
||||
async fn ensure_installed_downloads_and_extracts_zip_runtime() {
|
||||
let server = MockServer::start().await;
|
||||
let runtime_version = "0.1.0";
|
||||
let runtime_version = "2.5.6";
|
||||
let platform =
|
||||
ArtifactRuntimePlatform::detect_current().unwrap_or_else(|error| panic!("{error}"));
|
||||
let archive_name = format!(
|
||||
|
|
@ -182,7 +133,7 @@ async fn ensure_installed_downloads_and_extracts_zip_runtime() {
|
|||
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()),
|
||||
node_version: None,
|
||||
platforms: BTreeMap::from([(
|
||||
platform.as_str().to_string(),
|
||||
PackageReleaseArchive {
|
||||
|
|
@ -225,28 +176,128 @@ async fn ensure_installed_downloads_and_extracts_zip_runtime() {
|
|||
|
||||
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("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!(
|
||||
runtime.resolve_js_runtime().expect("resolve js runtime"),
|
||||
JsRuntime::node(runtime.node_path().to_path_buf())
|
||||
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 = "0.1.0";
|
||||
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,
|
||||
Some(PathBuf::from("node/bin/node")),
|
||||
);
|
||||
write_installed_runtime(&install_dir, runtime_version);
|
||||
|
||||
let config = ArtifactRuntimeManagerConfig::with_default_release(
|
||||
codex_home.path().to_path_buf(),
|
||||
|
|
@ -265,102 +316,38 @@ fn load_cached_runtime_uses_custom_cache_root() {
|
|||
#[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 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: "console.log('hello');".to_string(),
|
||||
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::from([
|
||||
(
|
||||
"CODEX_TEST_OUTPUT".to_string(),
|
||||
output_path.display().to_string(),
|
||||
),
|
||||
("CUSTOM_ENV".to_string(), "custom-value".to_string()),
|
||||
]),
|
||||
env: BTreeMap::new(),
|
||||
})
|
||||
.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!(output.stderr.trim(), "stderr-ok");
|
||||
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(),
|
||||
]
|
||||
output.stdout.lines().collect::<Vec<_>>(),
|
||||
vec!["undefined", "undefined", "true", "true", "stdout-ok"]
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -369,94 +356,26 @@ fn assert_success(output: &ArtifactCommandOutput) {
|
|||
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"));
|
||||
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("manifest.json"),
|
||||
serde_json::json!(sample_extracted_manifest(runtime_version, node_relative)).to_string(),
|
||||
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("node/bin/node"), "#!/bin/sh\n")
|
||||
.unwrap_or_else(|error| panic!("{error}"));
|
||||
fs::write(
|
||||
install_dir.join("artifact-tool/dist/artifact_tool.mjs"),
|
||||
install_dir.join("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> {
|
||||
|
|
@ -464,34 +383,22 @@ fn build_zip_archive(runtime_version: &str) -> Vec<u8> {
|
|||
{
|
||||
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)
|
||||
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(&manifest)
|
||||
zip.write_all(&package_json)
|
||||
.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")
|
||||
zip.start_file("artifact-runtime/dist/artifact_tool.mjs", options)
|
||||
.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}"));
|
||||
|
|
@ -499,23 +406,48 @@ fn build_zip_archive(runtime_version: &str) -> Vec<u8> {
|
|||
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(),
|
||||
},
|
||||
},
|
||||
fn build_tar_gz_archive(runtime_version: &str) -> Vec<u8> {
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
/// Pinned versions for package-manager-backed installs.
|
||||
pub(crate) const ARTIFACT_RUNTIME: &str = "2.4.0";
|
||||
pub(crate) const ARTIFACT_RUNTIME: &str = "2.5.6";
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ use crate::tools::registry::ToolHandler;
|
|||
use crate::tools::registry::ToolKind;
|
||||
|
||||
const ARTIFACTS_TOOL_NAME: &str = "artifacts";
|
||||
const ARTIFACTS_PRAGMA_PREFIXES: [&str; 2] = ["// codex-artifacts:", "// codex-artifact-tool:"];
|
||||
const ARTIFACT_TOOL_PRAGMA_PREFIX: &str = "// codex-artifact-tool:";
|
||||
const DEFAULT_EXECUTION_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
pub struct ArtifactsHandler;
|
||||
|
|
@ -74,7 +74,7 @@ impl ToolHandler for ArtifactsHandler {
|
|||
ToolPayload::Custom { input } => parse_freeform_args(&input)?,
|
||||
_ => {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"artifacts expects freeform JavaScript input authored against the preloaded @oai/artifact-tool surface".to_string(),
|
||||
"artifacts expects freeform JavaScript input authored against the preloaded @oai/artifact-tool exports".to_string(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
|
@ -123,7 +123,7 @@ impl ToolHandler for ArtifactsHandler {
|
|||
fn parse_freeform_args(input: &str) -> Result<ArtifactsToolArgs, FunctionCallError> {
|
||||
if input.trim().is_empty() {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"artifacts expects raw JavaScript source text (non-empty) authored against the preloaded @oai/artifact-tool surface. Provide JS only, optionally with first-line `// codex-artifacts: timeout_ms=15000` or `// codex-artifact-tool: timeout_ms=15000`."
|
||||
"artifacts expects raw JavaScript source text (non-empty) authored against the preloaded @oai/artifact-tool exports. Provide JS only, optionally with first-line `// codex-artifact-tool: timeout_ms=15000`."
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
|
@ -191,7 +191,7 @@ fn reject_json_or_quoted_source(code: &str) -> Result<(), FunctionCallError> {
|
|||
let trimmed = code.trim();
|
||||
if trimmed.starts_with("```") {
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"artifacts expects raw JavaScript source, not markdown code fences. Resend plain JS only (optional first line `// codex-artifacts: ...` or `// codex-artifact-tool: ...`)."
|
||||
"artifacts expects raw JavaScript source, not markdown code fences. Resend plain JS only (optional first line `// codex-artifact-tool: ...`)."
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
|
|
@ -200,7 +200,7 @@ fn reject_json_or_quoted_source(code: &str) -> Result<(), FunctionCallError> {
|
|||
};
|
||||
match value {
|
||||
JsonValue::Object(_) | JsonValue::String(_) => Err(FunctionCallError::RespondToModel(
|
||||
"artifacts is a freeform tool and expects raw JavaScript source authored against the preloaded @oai/artifact-tool surface. Resend plain JS only (optional first line `// codex-artifacts: ...` or `// codex-artifact-tool: ...`); do not send JSON (`{\"code\":...}`), quoted code, or markdown fences."
|
||||
"artifacts is a freeform tool and expects raw JavaScript source authored against the preloaded @oai/artifact-tool exports. Resend plain JS only (optional first line `// codex-artifact-tool: ...`); do not send JSON (`{\"code\":...}`), quoted code, or markdown fences."
|
||||
.to_string(),
|
||||
)),
|
||||
_ => Ok(()),
|
||||
|
|
@ -208,9 +208,7 @@ fn reject_json_or_quoted_source(code: &str) -> Result<(), FunctionCallError> {
|
|||
}
|
||||
|
||||
fn parse_pragma_prefix(line: &str) -> Option<&str> {
|
||||
ARTIFACTS_PRAGMA_PREFIXES
|
||||
.iter()
|
||||
.find_map(|prefix| line.strip_prefix(prefix))
|
||||
line.strip_prefix(ARTIFACT_TOOL_PRAGMA_PREFIX)
|
||||
}
|
||||
|
||||
fn default_runtime_manager(codex_home: std::path::PathBuf) -> ArtifactRuntimeManager {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
use super::*;
|
||||
use crate::packages::versions;
|
||||
use codex_artifacts::RuntimeEntrypoints;
|
||||
use codex_artifacts::RuntimePathEntry;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
|
|
@ -11,14 +9,6 @@ fn parse_freeform_args_without_pragma() {
|
|||
assert_eq!(args.timeout_ms, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_freeform_args_with_pragma() {
|
||||
let args = parse_freeform_args("// codex-artifacts: timeout_ms=45000\nconsole.log('ok');")
|
||||
.expect("parse args");
|
||||
assert_eq!(args.source, "console.log('ok');");
|
||||
assert_eq!(args.timeout_ms, Some(45_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_freeform_args_with_artifact_tool_pragma() {
|
||||
let args = parse_freeform_args("// codex-artifact-tool: timeout_ms=45000\nconsole.log('ok');")
|
||||
|
|
@ -63,34 +53,25 @@ fn load_cached_runtime_reads_pinned_cache_path() {
|
|||
.join(versions::ARTIFACT_RUNTIME)
|
||||
.join(platform.as_str());
|
||||
std::fs::create_dir_all(&install_dir).expect("create install dir");
|
||||
std::fs::create_dir_all(install_dir.join("dist")).expect("create build entrypoint dir");
|
||||
std::fs::write(
|
||||
install_dir.join("manifest.json"),
|
||||
install_dir.join("package.json"),
|
||||
serde_json::json!({
|
||||
"schema_version": 1,
|
||||
"runtime_version": versions::ARTIFACT_RUNTIME,
|
||||
"node": { "relative_path": "node/bin/node" },
|
||||
"entrypoints": {
|
||||
"build_js": { "relative_path": "artifact-tool/dist/artifact_tool.mjs" },
|
||||
"render_cli": { "relative_path": "granola-render/dist/render_cli.mjs" }
|
||||
"name": "@oai/artifact-tool",
|
||||
"version": versions::ARTIFACT_RUNTIME,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./dist/artifact_tool.mjs"
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.expect("write manifest");
|
||||
std::fs::create_dir_all(install_dir.join("artifact-tool/dist"))
|
||||
.expect("create build entrypoint dir");
|
||||
std::fs::create_dir_all(install_dir.join("granola-render/dist"))
|
||||
.expect("create render entrypoint dir");
|
||||
.expect("write package json");
|
||||
std::fs::write(
|
||||
install_dir.join("artifact-tool/dist/artifact_tool.mjs"),
|
||||
install_dir.join("dist/artifact_tool.mjs"),
|
||||
"export const ok = true;\n",
|
||||
)
|
||||
.expect("write build entrypoint");
|
||||
std::fs::write(
|
||||
install_dir.join("granola-render/dist/render_cli.mjs"),
|
||||
"export const ok = true;\n",
|
||||
)
|
||||
.expect("write render entrypoint");
|
||||
|
||||
let runtime = codex_artifacts::load_cached_runtime(
|
||||
&codex_home
|
||||
|
|
@ -101,15 +82,8 @@ fn load_cached_runtime_reads_pinned_cache_path() {
|
|||
.expect("resolve runtime");
|
||||
assert_eq!(runtime.runtime_version(), versions::ARTIFACT_RUNTIME);
|
||||
assert_eq!(
|
||||
runtime.manifest().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(),
|
||||
},
|
||||
}
|
||||
runtime.build_js_path(),
|
||||
install_dir.join("dist/artifact_tool.mjs")
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2061,7 +2061,7 @@ plain_source: PLAIN_JS_SOURCE
|
|||
|
||||
js_source: JS_SOURCE
|
||||
|
||||
PRAGMA_LINE: /[ \t]*\/\/ codex-artifacts:[^\r\n]*/ | /[ \t]*\/\/ codex-artifact-tool:[^\r\n]*/
|
||||
PRAGMA_LINE: /[ \t]*\/\/ codex-artifact-tool:[^\r\n]*/
|
||||
NEWLINE: /\r?\n/
|
||||
PLAIN_JS_SOURCE: /(?:\s*)(?:[^\s{\"`]|`[^`]|``[^`])[\s\S]*/
|
||||
JS_SOURCE: /(?:\s*)(?:[^\s{\"`]|`[^`]|``[^`])[\s\S]*/
|
||||
|
|
@ -2069,7 +2069,7 @@ JS_SOURCE: /(?:\s*)(?:[^\s{\"`]|`[^`]|``[^`])[\s\S]*/
|
|||
|
||||
ToolSpec::Freeform(FreeformTool {
|
||||
name: "artifacts".to_string(),
|
||||
description: "Runs raw JavaScript against the preinstalled Codex @oai/artifact-tool runtime for creating presentations or spreadsheets. This is plain JavaScript executed by a local Node-compatible runtime with top-level await, not TypeScript: do not use type annotations, `interface`, `type`, or `import type`. Author code the same way you would for `import { Presentation, Workbook, PresentationFile, SpreadsheetFile, FileBlob, ... } from \"@oai/artifact-tool\"`, but omit that import line because the package surface is already preloaded. Named exports are available directly on `globalThis`, and the full module is available as `globalThis.artifactTool` (also aliased as `globalThis.artifacts` and `globalThis.codexArtifacts`). Node built-ins such as `node:fs/promises` may still be imported when needed for saving preview bytes. This is a freeform tool: send raw JavaScript source text, optionally with a first-line pragma like `// codex-artifacts: timeout_ms=15000` or `// codex-artifact-tool: timeout_ms=15000`; do not send JSON/quotes/markdown fences."
|
||||
description: "Runs raw JavaScript against the installed `@oai/artifact-tool` package for creating presentations or spreadsheets. This is plain JavaScript executed by a local Node-compatible runtime with top-level await, not TypeScript: do not use type annotations, `interface`, `type`, or `import type`. Author code the same way you would for `import { Presentation, Workbook, PresentationFile, SpreadsheetFile, FileBlob, ... } from \"@oai/artifact-tool\"`, but omit that import line because the package is preloaded before your code runs. Named exports are copied onto `globalThis`, and the full module namespace is available as `globalThis.artifactTool`. This matches the upstream library-first API: create with `Presentation.create()` / `Workbook.create()`, preview with `presentation.export(...)` or `slide.export(...)`, and save files with `PresentationFile.exportPptx(...)` or `SpreadsheetFile.exportXlsx(...)`. Node built-ins such as `node:fs/promises` may still be imported when needed for saving preview bytes. This is a freeform tool: send raw JavaScript source text, optionally with a first-line pragma like `// codex-artifact-tool: timeout_ms=15000`; do not send JSON/quotes/markdown fences."
|
||||
.to_string(),
|
||||
format: FreeformToolFormat {
|
||||
r#type: "grammar".to_string(),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue