CLI: render --out is relative to the working directory (#300)

`effectcraft-cli render --out proj/out/d.mp4 --project proj/x.ecproj` wrote
`proj/proj/out/d.mp4`: the Render Queue resolves a relative output against its own base, while
`--project` and every other CLI path are relative to the working directory. The CLI now makes
`--out` absolute against the working directory before queueing it, so the file lands where the
shell user expects; an absolute path is unchanged, and `rendered[].output` in the JSON result is
the absolute path. Render Queue outputs set inside a project (and `render --queue`) resolve as
before: tokens expanded, then `export.defaultOutputFolder`, the project's folder, the working
directory.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
This commit is contained in:
Oldrich333andClaude Sonnet 5.5 committed 2026-10-08 20:32:51 +00:00
1 parent d60cb71e29
commit a560830b64
3 files changed
+59

No files matched your search

+9
View File
@@ -15,6 +15,7 @@
//! [--jpeg-quality N] [--bitrate KBPS] [--prores proxy|lt|standard|hq|4444|4444xq] [--audio auto|on|off]
//! [--profile main|main10] [--level auto|4.1] [--rate-control bitrate|quality] [--video-quality 1-100]
//! [--keyint FRAMES] [--webm-codec vp9|av1] [--audio-bitrate KBPS] [--opus-app audio|voice]
//! (a relative --out is relative to the working directory; --queue outputs follow the project's own settings)
//! effectcraft-cli render F.ecproj --queue render the project's Render Queue
//! effectcraft-cli bench [--comp C] [--time S] [--scale K] [--n N] [--play N] [--gpu [--adv3d]] render timings
//! (--gpu: CPU vs GPU ms/frame for every comp at Full and Half)
@@ -100,6 +101,7 @@ const USAGE: &str = "usage: effectcraft-cli <info|commands|exec|run|props|get|se
[--keyint FRAMES] [--webm-codec vp9|av1] [--audio-bitrate KBPS] [--opus-app audio|voice] | --queue
(formats h264|hevc|av1|prores|webm|png|jpeg|tiff|exr|gif|wav|aiff;
--profile..--keyint: HEVC / AV1, --audio-bitrate/--opus-app: WebM Opus)
a relative --out is relative to the working directory
bench [--comp C] [--time S] [--scale K] [--n N] [--play N] [--gpu [--adv3d]] per-layer/effect render timings;
--play N renders N consecutive frames with/without the layer cache;
--gpu compares CPU and GPU ms/frame for every comp at Full and Half
@@ -587,6 +589,12 @@ fn with_saved(v: Value, saved: Option<String>) -> Value {
}
}
/// A relative `--out` is relative to the working directory, like `--project` and every other CLI
/// path. The Render Queue alone would resolve it against the project's folder.
fn from_cwd(path: &str) -> Result<String, Failure> {
std::path::absolute(path).map(|p| p.to_string_lossy().into_owned()).map_err(|e| Failure::Error(format!("render: cannot resolve --out {path}: {e}")))
}
/// `render`: queue `--comp` (or the active comp) with the given settings unless `--queue`, then
/// render the queue with a progress line on stderr. Fails if any item fails.
fn render(args: &Args, json_out: bool) -> Result<(), Failure> {
@@ -602,6 +610,7 @@ fn render(args: &Args, json_out: bool) -> Result<(), Failure> {
let err = |e: effectcraft_engine::EngineError| Failure::Error(e.to_string());
if !args.flag("--queue") {
let Some(out) = args.opt("--out") else { return usage_err("render: --out FILE is required (or --queue)") };
let out = &from_cwd(out)?;
let mut p = json!({"output": out});
if let Some(c) = args.opt("--comp") {
p["comp"] = json!(c);
+49
View File
@@ -247,3 +247,52 @@ fn script_file_and_eval() {
let err = String::from_utf8_lossy(&out.stderr);
assert!(err.contains(".jsxbin scripts are not supported: run the .jsx source") && !err.contains("SyntaxError"), "{err}");
}
/// `render --out` (#300): a relative path is relative to the working directory like `--project`,
/// not to the project's folder (which doubled a path that already named it).
#[test]
fn render_out_is_relative_to_the_working_directory() {
let root = tmp("render-out");
let _ = std::fs::remove_dir_all(&root);
let (proj, elsewhere) = (root.join("proj"), root.join("elsewhere"));
std::fs::create_dir_all(&proj).unwrap();
std::fs::create_dir_all(&elsewhere).unwrap();
let project = proj.join("x.ecproj");
let p = project.to_str().unwrap();
ok_json(&[
"run",
"comp.new",
r#"{"name":"T","width":64,"height":36,"duration":1}"#,
"layer.newSolid",
r#"{"width":64,"height":36}"#,
"--empty",
"--save-as",
p,
]);
assert!(project.exists());
let render = |cwd: &std::path::Path, project: &str, out: &str| {
let o = bin()
.current_dir(cwd)
.args(["render", "--project", project, "--format", "gif", "--start", "0", "--end", "0.1", "--resolution", "quarter", "--out", out, "--json"])
.output()
.unwrap();
assert!(o.status.success(), "{out}: {}", String::from_utf8_lossy(&o.stderr));
let v: Value = serde_json::from_slice(&o.stdout).unwrap();
PathBuf::from(v["rendered"][0]["output"].as_str().unwrap())
};
// A path that already names the project folder is not doubled.
let done = render(&root, "proj/x.ecproj", "proj/out/d.gif");
assert_eq!(done, root.join("proj/out/d.gif"));
assert!(done.exists() && !proj.join("proj").exists());
// Another working directory: the file lands there, not beside the project.
let done = render(&elsewhere, project.to_str().unwrap(), "out/card.gif");
assert_eq!(done, elsewhere.join("out/card.gif"));
assert!(done.exists() && !proj.join("out/card.gif").exists());
// `./f.gif` too.
assert_eq!(render(&elsewhere, project.to_str().unwrap(), "./f.gif"), elsewhere.join("./f.gif"));
// An absolute path is unchanged.
let abs = root.join("abs.gif");
assert_eq!(render(&elsewhere, project.to_str().unwrap(), abs.to_str().unwrap()), abs);
assert!(abs.exists());
let _ = std::fs::remove_dir_all(&root);
}