use std::fs;
use std::path::Path;
use std::path::PathBuf;
use std::process::ExitCode;
use clap::Args;
use clap::Parser;
use clap::Subcommand;
use ignore::gitignore::Gitignore;
use ignore::gitignore::GitignoreBuilder;
use kcl_lib::ExecOutcome;
use kcl_lib::ExecutorContext;
use kcl_lib::ExecutorSettings;
use kcl_lib::KclError;
use kcl_lib::KclErrorWithOutputs;
use kcl_lib::Program;
use kcl_lib::TypedPath;
use kcl_lib::exec::RetryConfig;
use kcl_lib::exec::execute_with_retries;
use kcl_lib::pre_execute_transpile;
#[cfg(not(target_arch = "wasm32"))]
use kcl_lib::test_server;
use kcl_lib::tooling::image_comparison::DEFAULT_MIN_SIMILARITY;
use kcl_lib::tooling::image_comparison::compare_images;
use kcl_lib::tooling::render_artifacts::RenderArtifactKind;
use kcl_lib::tooling::render_artifacts::render_artifact_path;
use kcl_lib::transpile_all_old_sketches_to_new;
use walkdir::DirEntry;
use walkdir::WalkDir;
#[derive(Parser, Debug)]
#[command(
version = clap::crate_version!(),
author = clap::crate_authors!("\n"),
arg_required_else_help = true,
subcommand_required = true
)]
struct Cli {
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand, Debug)]
enum Command {
Convert(OptionalOutputCommandArgs),
Run(RequiredOutputCommandArgs),
Render(RequiredOutputCommandArgs),
Compare(RequiredOutputCommandArgs),
}
#[derive(Args, Debug)]
struct CommonArgs {
#[arg(value_parser = parse_input_path)]
input: PathBuf,
#[arg(short = 'j', long)]
json: bool,
#[arg(short, long)]
quiet: bool,
}
#[derive(Args, Debug)]
struct BatchArgs {
#[arg(short, long)]
recursive: bool,
#[arg(short, long, requires = "recursive")]
ignore_file: Option<PathBuf>,
#[arg(short, long, requires = "recursive")]
keep_going: bool,
#[arg(long, requires = "recursive")]
report_file: Option<PathBuf>,
}
#[derive(Args, Debug)]
struct OptionalOutputCommandArgs {
#[command(flatten)]
common: CommonArgs,
#[command(flatten)]
batch: BatchArgs,
#[arg(short, long, requires = "recursive", required_if_eq("recursive", "true"))]
out_dir: Option<PathBuf>,
}
#[derive(Args, Debug)]
struct RequiredOutputCommandArgs {
#[command(flatten)]
common: CommonArgs,
#[command(flatten)]
batch: BatchArgs,
#[arg(short, long)]
out_dir: PathBuf,
}
impl OptionalOutputCommandArgs {
fn input(&self) -> &Path {
&self.common.input
}
fn json(&self) -> bool {
self.common.json
}
fn quiet(&self) -> bool {
self.common.quiet
}
fn recursive(&self) -> bool {
self.batch.recursive
}
fn ignore_file(&self) -> Option<&Path> {
self.batch.ignore_file.as_deref()
}
fn keep_going(&self) -> bool {
self.batch.keep_going
}
fn report_file(&self) -> Option<&Path> {
self.batch.report_file.as_deref()
}
}
impl RequiredOutputCommandArgs {
fn input(&self) -> &Path {
&self.common.input
}
fn json(&self) -> bool {
self.common.json
}
fn quiet(&self) -> bool {
self.common.quiet
}
fn recursive(&self) -> bool {
self.batch.recursive
}
fn ignore_file(&self) -> Option<&Path> {
self.batch.ignore_file.as_deref()
}
fn keep_going(&self) -> bool {
self.batch.keep_going
}
fn report_file(&self) -> Option<&Path> {
self.batch.report_file.as_deref()
}
fn out_dir(&self) -> &Path {
&self.out_dir
}
}
fn not_implemented() -> ExitCode {
eprintln!("Not implemented yet!");
ExitCode::FAILURE
}
#[tokio::main]
async fn main() -> ExitCode {
let cli = Cli::parse();
let result = match cli.command {
Command::Convert(args) => convert(args).await,
Command::Run(args) => run(args).await,
Command::Render(args) => render(args).await,
Command::Compare(args) => compare(args).await,
};
match result {
Ok(code) => code,
Err(err) => {
eprintln!("{err}");
ExitCode::FAILURE
}
}
}
async fn convert(args: OptionalOutputCommandArgs) -> Result<ExitCode, std::io::Error> {
if args.json() {
return Ok(not_implemented());
}
if args.recursive() {
convert_recursive(args).await
} else {
convert_single(args).await
}
}
async fn convert_single(args: OptionalOutputCommandArgs) -> Result<ExitCode, std::io::Error> {
let input = normalize_input_path(args.input().to_path_buf())?;
let output_file_name = input
.file_name()
.ok_or_else(|| std::io::Error::other(format!("Input path `{}` has no filename", input.display())))?;
if let Some(out_dir) = args.out_dir {
let out_path = out_dir.join(output_file_name);
transpile_to_output(&input, &out_path, true)
.await
.map_err(std::io::Error::other)?;
} else {
let new_source = transpile_source_file(&input, true)
.await
.map_err(std::io::Error::other)?;
println!("{new_source}");
}
Ok(ExitCode::SUCCESS)
}
async fn convert_recursive(args: OptionalOutputCommandArgs) -> Result<ExitCode, std::io::Error> {
let Some(ref out_dir) = args.out_dir else {
unreachable!("clap enforces --out-dir with --recursive")
};
let root = args.input();
let mut report = ConvertReport::default();
for entry in collect_recursive_entries(root, args.ignore_file())? {
let file = entry.source_path;
let rel_path = entry.relative_path;
report.processed += 1;
match transpile_to_output(&file, &out_dir.join(&rel_path), !args.keep_going()).await {
Ok(_) => {
report.succeeded += 1;
if !args.quiet() {
eprintln!("Converted: {}", rel_path.display());
}
}
Err(err) => {
report.failures.push(ConvertFailure {
path: rel_path.clone(),
error: err.clone(),
});
if !args.quiet() {
eprintln!("Convert failed: {}: {}", rel_path.display(), err);
}
if !args.keep_going() {
break;
}
}
}
}
let report_text = render_convert_report(&report);
write_report(&report_text, args.quiet(), args.report_file())?;
let exit_code = if report.failures.is_empty() {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
};
Ok(exit_code)
}
async fn run(args: RequiredOutputCommandArgs) -> Result<ExitCode, std::io::Error> {
if args.json() {
return Ok(not_implemented());
}
if args.recursive() {
return run_recursive(args).await;
}
run_single(args).await
}
async fn run_single(args: RequiredOutputCommandArgs) -> Result<ExitCode, std::io::Error> {
let input = normalize_input_path(args.input().to_path_buf())?;
fs::create_dir_all(args.out_dir())?;
let output_kcl_path = args.out_dir().join(
input
.file_name()
.ok_or_else(|| std::io::Error::other(format!("Input path `{}` has no filename", input.display())))?,
);
let transpiled = match transpile_to_output(&input, &output_kcl_path, true).await {
Ok(source) => source,
Err(err) => {
fs::write(args.out_dir().join("run-log.txt"), format!("convert_failed\n{err}\n"))?;
return Ok(ExitCode::FAILURE);
}
};
match execute_source_with_input_settings(&transpiled, &input).await {
Ok(()) => {
fs::write(args.out_dir().join("run-log.txt"), "success\n")?;
Ok(ExitCode::SUCCESS)
}
Err(err) => {
fs::write(args.out_dir().join("run-log.txt"), format!("run_failed\n{err}\n"))?;
Ok(ExitCode::FAILURE)
}
}
}
async fn run_recursive(args: RequiredOutputCommandArgs) -> Result<ExitCode, std::io::Error> {
let root = args.input();
let mut report = RunReport::default();
for entry in collect_recursive_entries(root, args.ignore_file())? {
let file = entry.source_path;
let rel_path = entry.relative_path;
report.processed += 1;
let out_kcl_path = args.out_dir().join(&rel_path);
let log_path = args
.out_dir()
.join(rel_path.parent().unwrap_or_else(|| Path::new("")))
.join(run_log_file_name(&file)?);
let transpiled = match transpile_to_output(&file, &out_kcl_path, !args.keep_going()).await {
Ok(source) => source,
Err(err) => {
if let Some(parent) = log_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&log_path, format!("convert_failed\n{err}\n"))?;
report.convert_failures.push(RunFailure {
path: rel_path.clone(),
error: err.clone(),
});
if !args.quiet() {
eprintln!("Convert failed: {}: {}", rel_path.display(), err);
}
if !args.keep_going() {
break;
}
continue;
}
};
match execute_source_with_input_settings(&transpiled, &file).await {
Ok(()) => {
fs::write(&log_path, "success\n")?;
report.succeeded += 1;
if !args.quiet() {
eprintln!("Ran: {}", rel_path.display());
}
}
Err(err) => {
fs::write(&log_path, format!("run_failed\n{err}\n"))?;
report.run_failures.push(RunFailure {
path: rel_path.clone(),
error: err.clone(),
});
if !args.quiet() {
eprintln!("Run failed: {}: {}", rel_path.display(), err);
}
if !args.keep_going() {
break;
}
}
}
}
let report_text = render_run_report(&report);
write_report(&report_text, args.quiet(), args.report_file())?;
let exit_code = if report.convert_failures.is_empty() && report.run_failures.is_empty() {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
};
Ok(exit_code)
}
async fn render(args: RequiredOutputCommandArgs) -> Result<ExitCode, std::io::Error> {
if args.json() {
return Ok(not_implemented());
}
if args.recursive() {
return render_recursive(args).await;
}
render_single(args).await
}
async fn render_single(args: RequiredOutputCommandArgs) -> Result<ExitCode, std::io::Error> {
let input = normalize_input_path(args.input().to_path_buf())?;
fs::create_dir_all(args.out_dir())?;
let output_kcl_path = args.out_dir().join(
input
.file_name()
.ok_or_else(|| std::io::Error::other(format!("Input path `{}` has no filename", input.display())))?,
);
let transpiled = match transpile_to_output(&input, &output_kcl_path, true).await {
Ok(source) => source,
Err(err) => {
fs::write(
args.out_dir().join("render-log.txt"),
format!("convert_failed\n{err}\n"),
)?;
return Ok(ExitCode::FAILURE);
}
};
let rendered = match render_source_with_input_settings(&transpiled, &input).await {
Ok(image) => image,
Err(err) => {
fs::write(args.out_dir().join("render-log.txt"), format!("render_failed\n{err}\n"))?;
return Ok(ExitCode::FAILURE);
}
};
let image_path = render_artifact_path(args.out_dir(), None, RenderArtifactKind::RenderedModel);
if let Err(err) = write_png(&image_path, &rendered) {
fs::write(
args.out_dir().join("render-log.txt"),
format!("render_failed\nFailed to write `{}`: {err}\n", image_path.display()),
)?;
return Ok(ExitCode::FAILURE);
}
fs::write(args.out_dir().join("render-log.txt"), "success\n")?;
Ok(ExitCode::SUCCESS)
}
async fn render_recursive(args: RequiredOutputCommandArgs) -> Result<ExitCode, std::io::Error> {
let root = args.input();
let mut report = RenderReport::default();
for entry in collect_recursive_entries(root, args.ignore_file())? {
let file = entry.source_path;
let rel_path = entry.relative_path;
report.processed += 1;
let out_kcl_path = args.out_dir().join(&rel_path);
let out_dir = args.out_dir().join(rel_path.parent().unwrap_or_else(|| Path::new("")));
let log_path = out_dir.join(render_log_file_name(&file)?);
let transpiled = match transpile_to_output(&file, &out_kcl_path, !args.keep_going()).await {
Ok(source) => source,
Err(err) => {
if let Some(parent) = log_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&log_path, format!("convert_failed\n{err}\n"))?;
report.convert_failures.push(RenderFailure {
path: rel_path.clone(),
error: err.clone(),
});
if !args.quiet() {
eprintln!("Convert failed: {}: {}", rel_path.display(), err);
}
if !args.keep_going() {
break;
}
continue;
}
};
let rendered = match render_source_with_input_settings(&transpiled, &file).await {
Ok(image) => image,
Err(err) => {
fs::write(&log_path, format!("render_failed\n{err}\n"))?;
report.render_failures.push(RenderFailure {
path: rel_path.clone(),
error: err.clone(),
});
if !args.quiet() {
eprintln!("Render failed: {}: {}", rel_path.display(), err);
}
if !args.keep_going() {
break;
}
continue;
}
};
let stem = file_stem_string(&file)?;
let image_path = render_artifact_path(&out_dir, Some(&stem), RenderArtifactKind::RenderedModel);
match write_png(&image_path, &rendered) {
Ok(()) => {
fs::write(&log_path, "success\n")?;
report.succeeded += 1;
if !args.quiet() {
eprintln!("Rendered: {}", rel_path.display());
}
}
Err(err) => {
fs::write(
&log_path,
format!("render_failed\nFailed to write `{}`: {err}\n", image_path.display()),
)?;
report.render_failures.push(RenderFailure {
path: rel_path.clone(),
error: format!("Failed to write `{}`: {err}", image_path.display()),
});
if !args.quiet() {
eprintln!(
"Render failed: {}: Failed to write `{}`: {}",
rel_path.display(),
image_path.display(),
err
);
}
if !args.keep_going() {
break;
}
}
}
}
let report_text = render_render_report(&report);
write_report(&report_text, args.quiet(), args.report_file())?;
let exit_code = if report.convert_failures.is_empty() && report.render_failures.is_empty() {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
};
Ok(exit_code)
}
async fn compare(args: RequiredOutputCommandArgs) -> Result<ExitCode, std::io::Error> {
if args.json() {
return Ok(not_implemented());
}
if args.recursive() {
return compare_recursive(args).await;
}
compare_single(args).await
}
async fn compare_single(args: RequiredOutputCommandArgs) -> Result<ExitCode, std::io::Error> {
let input = normalize_input_path(args.input().to_path_buf())?;
fs::create_dir_all(args.out_dir())?;
let output_kcl_path = args.out_dir().join(
input
.file_name()
.ok_or_else(|| std::io::Error::other(format!("Input path `{}` has no filename", input.display())))?,
);
let v1_render = match render_input_file(&input).await {
Ok(image) => image,
Err(err) => {
fs::write(
args.out_dir().join("compare-log.txt"),
format!("render_v1_failed\n{err}\n"),
)?;
return Ok(ExitCode::FAILURE);
}
};
let transpiled = match transpile_to_output(&input, &output_kcl_path, true).await {
Ok(source) => source,
Err(err) => {
fs::write(
args.out_dir().join("compare-log.txt"),
format!("convert_failed\n{err}\n"),
)?;
return Ok(ExitCode::FAILURE);
}
};
let v2_render = match render_source_with_input_settings(&transpiled, &input).await {
Ok(image) => image,
Err(err) => {
fs::write(
args.out_dir().join("compare-log.txt"),
format!("render_v2_failed\n{err}\n"),
)?;
return Ok(ExitCode::FAILURE);
}
};
let v1_path = render_artifact_path(args.out_dir(), None, RenderArtifactKind::V1RenderedModel);
if let Err(err) = write_png(&v1_path, &v1_render) {
fs::write(
args.out_dir().join("compare-log.txt"),
format!("render_v1_failed\nFailed to write `{}`: {err}\n", v1_path.display()),
)?;
return Ok(ExitCode::FAILURE);
}
let v2_path = render_artifact_path(args.out_dir(), None, RenderArtifactKind::V2RenderedModel);
if let Err(err) = write_png(&v2_path, &v2_render) {
fs::write(
args.out_dir().join("compare-log.txt"),
format!("render_v2_failed\nFailed to write `{}`: {err}\n", v2_path.display()),
)?;
return Ok(ExitCode::FAILURE);
}
let comparison = match compare_images(&v1_render, &v2_render) {
Ok(comparison) => comparison,
Err(err) => {
fs::write(
args.out_dir().join("compare-log.txt"),
format!("compare_failed\n{err}\n"),
)?;
return Ok(ExitCode::FAILURE);
}
};
let diff_path = render_artifact_path(args.out_dir(), None, RenderArtifactKind::Diff);
if let Err(err) = write_png(&diff_path, &comparison.diff_image) {
fs::write(
args.out_dir().join("compare-log.txt"),
format!("compare_failed\nFailed to write `{}`: {err}\n", diff_path.display()),
)?;
return Ok(ExitCode::FAILURE);
}
let compare_log = format!(
"{}\nsimilarity={:.6}\ndifference={:.6}\n",
if comparison.similarity >= DEFAULT_MIN_SIMILARITY {
"success"
} else {
"compare_failed"
},
comparison.similarity,
comparison.difference
);
fs::write(args.out_dir().join("compare-log.txt"), compare_log)?;
Ok(if comparison.similarity >= DEFAULT_MIN_SIMILARITY {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
})
}
async fn compare_recursive(args: RequiredOutputCommandArgs) -> Result<ExitCode, std::io::Error> {
let root = args.input();
let mut report = CompareReport::default();
for entry in collect_recursive_entries(root, args.ignore_file())? {
let file = entry.source_path;
let rel_path = entry.relative_path;
report.processed += 1;
let out_kcl_path = args.out_dir().join(&rel_path);
let out_dir = args.out_dir().join(rel_path.parent().unwrap_or_else(|| Path::new("")));
let log_path = out_dir.join(compare_log_file_name(&file)?);
let stem = file_stem_string(&file)?;
let v1_render = match render_input_file(&file).await {
Ok(image) => image,
Err(err) => {
if let Some(parent) = log_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&log_path, format!("render_v1_failed\n{err}\n"))?;
report.v1_render_failures.push(CompareFailure {
path: rel_path.clone(),
error: err.clone(),
});
if !args.quiet() {
eprintln!("V1 render failed: {}: {}", rel_path.display(), err);
}
if !args.keep_going() {
break;
}
continue;
}
};
let transpiled = match transpile_to_output(&file, &out_kcl_path, !args.keep_going()).await {
Ok(source) => source,
Err(err) => {
if let Some(parent) = log_path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(&log_path, format!("convert_failed\n{err}\n"))?;
report.convert_failures.push(CompareFailure {
path: rel_path.clone(),
error: err.clone(),
});
if !args.quiet() {
eprintln!("Convert failed: {}: {}", rel_path.display(), err);
}
if !args.keep_going() {
break;
}
continue;
}
};
let v2_render = match render_source_with_input_settings(&transpiled, &file).await {
Ok(image) => image,
Err(err) => {
fs::write(&log_path, format!("render_v2_failed\n{err}\n"))?;
report.v2_render_failures.push(CompareFailure {
path: rel_path.clone(),
error: err.clone(),
});
if !args.quiet() {
eprintln!("V2 render failed: {}: {}", rel_path.display(), err);
}
if !args.keep_going() {
break;
}
continue;
}
};
let v1_path = render_artifact_path(&out_dir, Some(&stem), RenderArtifactKind::V1RenderedModel);
if let Err(err) = write_png(&v1_path, &v1_render) {
let error = format!("Failed to write `{}`: {err}", v1_path.display());
fs::write(&log_path, format!("render_v1_failed\n{error}\n"))?;
report.v1_render_failures.push(CompareFailure {
path: rel_path.clone(),
error: error.clone(),
});
if !args.quiet() {
eprintln!("V1 render failed: {}: {}", rel_path.display(), error);
}
if !args.keep_going() {
break;
}
continue;
}
let v2_path = render_artifact_path(&out_dir, Some(&stem), RenderArtifactKind::V2RenderedModel);
if let Err(err) = write_png(&v2_path, &v2_render) {
let error = format!("Failed to write `{}`: {err}", v2_path.display());
fs::write(&log_path, format!("render_v2_failed\n{error}\n"))?;
report.v2_render_failures.push(CompareFailure {
path: rel_path.clone(),
error: error.clone(),
});
if !args.quiet() {
eprintln!("V2 render failed: {}: {}", rel_path.display(), error);
}
if !args.keep_going() {
break;
}
continue;
}
let comparison = match compare_images(&v1_render, &v2_render) {
Ok(comparison) => comparison,
Err(err) => {
fs::write(&log_path, format!("compare_failed\n{err}\n"))?;
report.compare_failures.push(CompareFailure {
path: rel_path.clone(),
error: err.to_string(),
});
if !args.quiet() {
eprintln!("Compare failed: {}: {}", rel_path.display(), err);
}
if !args.keep_going() {
break;
}
continue;
}
};
let diff_path = render_artifact_path(&out_dir, Some(&stem), RenderArtifactKind::Diff);
if let Err(err) = write_png(&diff_path, &comparison.diff_image) {
let error = format!("Failed to write `{}`: {err}", diff_path.display());
fs::write(&log_path, format!("compare_failed\n{error}\n"))?;
report.compare_failures.push(CompareFailure {
path: rel_path.clone(),
error: error.clone(),
});
if !args.quiet() {
eprintln!("Compare failed: {}: {}", rel_path.display(), error);
}
if !args.keep_going() {
break;
}
continue;
}
let compare_log = format!(
"{}\nsimilarity={:.6}\ndifference={:.6}\n",
if comparison.similarity >= DEFAULT_MIN_SIMILARITY {
"success"
} else {
"compare_failed"
},
comparison.similarity,
comparison.difference
);
fs::write(&log_path, compare_log)?;
if comparison.similarity >= DEFAULT_MIN_SIMILARITY {
report.succeeded += 1;
if !args.quiet() {
eprintln!("Compared: {}", rel_path.display());
}
} else {
let error = format!(
"similarity {:.6} below threshold {:.2}",
comparison.similarity, DEFAULT_MIN_SIMILARITY
);
report.compare_failures.push(CompareFailure {
path: rel_path.clone(),
error: error.clone(),
});
if !args.quiet() {
eprintln!("Compare failed: {}: {}", rel_path.display(), error);
}
if !args.keep_going() {
break;
}
}
}
let report_text = render_compare_report(&report);
write_report(&report_text, args.quiet(), args.report_file())?;
let exit_code = if report.convert_failures.is_empty()
&& report.v1_render_failures.is_empty()
&& report.v2_render_failures.is_empty()
&& report.compare_failures.is_empty()
{
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
};
Ok(exit_code)
}
fn normalize_input_path(mut path: PathBuf) -> Result<PathBuf, std::io::Error> {
if let Some(ext) = path.extension() {
if !ext.eq_ignore_ascii_case("kcl") {
return Err(std::io::Error::other("input file must have .kcl extension"));
}
} else {
path = path.join("main.kcl");
}
Ok(path)
}
fn parse_input_path(input: &str) -> Result<PathBuf, String> {
let path = PathBuf::from(input);
if let Some(ext) = path.extension()
&& !ext.eq_ignore_ascii_case("kcl")
{
return Err("input file must have .kcl extension".to_owned());
}
Ok(path)
}
fn write_transpiled_output(path: &Path, source: &str) -> Result<(), std::io::Error> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
fs::write(path, source)
}
fn write_report(report_text: &str, quiet: bool, report_file: Option<&Path>) -> Result<(), std::io::Error> {
if !quiet {
eprintln!("{report_text}");
}
if let Some(report_file) = report_file {
if let Some(parent) = report_file.parent() {
fs::create_dir_all(parent)?;
}
fs::write(report_file, report_text)?;
}
Ok(())
}
struct RecursiveEntry {
source_path: PathBuf,
relative_path: PathBuf,
}
fn collect_recursive_entries(root: &Path, ignore_file: Option<&Path>) -> Result<Vec<RecursiveEntry>, std::io::Error> {
let matcher = build_ignore_matcher(root, ignore_file)?;
let files = collect_kcl_files(root, &matcher)?;
files
.into_iter()
.map(|source_path| {
let relative_path = relative_output_path(root, &source_path)?;
Ok(RecursiveEntry {
source_path,
relative_path,
})
})
.collect()
}
async fn transpile_to_output(input_path: &Path, output_path: &Path, fail_fast: bool) -> Result<String, String> {
let new_source = transpile_source_file(input_path, fail_fast).await?;
write_transpiled_output(output_path, &new_source)
.map_err(|err| format!("Failed to write `{}`: {err}", output_path.display()))?;
Ok(new_source)
}
async fn transpile_source_file(path: &Path, fail_fast: bool) -> Result<String, String> {
let text = fs::read_to_string(path).map_err(|err| format!("Failed to read `{}`: {err}", path.display()))?;
let (program, errs) =
Program::parse(&text).map_err(|err| format!("Failed to parse `{}`: {err:#?}", path.display()))?;
if !errs.is_empty() {
let parse_errors = errs
.into_iter()
.map(|err| format!("{err:#?}"))
.collect::<Vec<_>>()
.join("\n");
return Err(format!("Parse errors in `{}`:\n{parse_errors}", path.display()));
}
let mut program = program.ok_or_else(|| format!("Failed to parse `{}`", path.display()))?;
let settings = executor_settings_for_input(path);
let result = async {
pre_execute_transpile(&mut program)?;
let exec_outcome = execute_with_retries(&RetryConfig::default(), || execute(program.clone(), settings.clone()))
.await
.map_err(|e| e.error)?;
transpile_all_old_sketches_to_new(&exec_outcome, &mut program, fail_fast)
}
.await;
result.map_err(|err| format!("Execution error for `{}`: {err:#?}", path.display()))?;
Ok(program.recast())
}
async fn execute_source_with_input_settings(source: &str, input_path: &Path) -> Result<(), String> {
let program =
Program::parse_no_errs(source).map_err(|err| format!("Failed to parse transpiled source: {err:#?}"))?;
let settings = executor_settings_for_input(input_path);
execute_with_retries(&RetryConfig::default(), || execute(program.clone(), settings.clone()))
.await
.map(|_| ())
.map_err(|err| format!("Execution error for `{}`: {:#?}", input_path.display(), err.error))
}
async fn render_source_with_input_settings(source: &str, input_path: &Path) -> Result<image::DynamicImage, String> {
test_server::execute_and_snapshot(source, Some(input_path.to_path_buf()))
.await
.map_err(|err| format!("Render error for `{}`: {err:#?}", input_path.display()))
}
async fn render_input_file(input_path: &Path) -> Result<image::DynamicImage, String> {
let source =
fs::read_to_string(input_path).map_err(|err| format!("Failed to read `{}`: {err}", input_path.display()))?;
render_source_with_input_settings(&source, input_path).await
}
fn executor_settings_for_input(path: &Path) -> ExecutorSettings {
let project_directory_string = path.parent().map(|p| p.to_string_lossy());
let project_directory = project_directory_string.map(|p| TypedPath::from(p.as_ref()));
ExecutorSettings {
project_directory,
..Default::default()
}
}
async fn execute(program: Program, settings: ExecutorSettings) -> Result<ExecOutcome, KclErrorWithOutputs> {
let ctx = ExecutorContext::new_with_client(settings, None, None)
.await
.map_err(|err| KclErrorWithOutputs::no_outputs(KclError::internal(format!("{err:?}"))))?;
let result = ctx.run_with_caching(program).await;
ctx.close().await;
result
}
#[derive(Debug, Default)]
struct ConvertReport {
processed: usize,
succeeded: usize,
failures: Vec<ConvertFailure>,
}
#[derive(Debug)]
struct ConvertFailure {
path: PathBuf,
error: String,
}
#[derive(Debug, Default)]
struct RunReport {
processed: usize,
succeeded: usize,
convert_failures: Vec<RunFailure>,
run_failures: Vec<RunFailure>,
}
#[derive(Debug)]
struct RunFailure {
path: PathBuf,
error: String,
}
#[derive(Debug, Default)]
struct RenderReport {
processed: usize,
succeeded: usize,
convert_failures: Vec<RenderFailure>,
render_failures: Vec<RenderFailure>,
}
#[derive(Debug)]
struct RenderFailure {
path: PathBuf,
error: String,
}
#[derive(Debug, Default)]
struct CompareReport {
processed: usize,
succeeded: usize,
convert_failures: Vec<CompareFailure>,
v1_render_failures: Vec<CompareFailure>,
v2_render_failures: Vec<CompareFailure>,
compare_failures: Vec<CompareFailure>,
}
#[derive(Debug)]
struct CompareFailure {
path: PathBuf,
error: String,
}
fn render_convert_report(report: &ConvertReport) -> String {
let mut output = String::new();
output.push_str(&format!("Processed: {}\n", report.processed));
output.push_str(&format!("Succeeded: {}\n", report.succeeded));
output.push_str(&format!("Convert failed: {}\n", report.failures.len()));
if !report.failures.is_empty() {
output.push_str("\nConvert failed\n");
for failure in &report.failures {
output.push_str(&format!("- {}: {}\n", failure.path.display(), failure.error));
}
}
output
}
fn render_run_report(report: &RunReport) -> String {
let mut output = String::new();
output.push_str(&format!("Processed: {}\n", report.processed));
output.push_str(&format!("Succeeded: {}\n", report.succeeded));
output.push_str(&format!("Convert failed: {}\n", report.convert_failures.len()));
output.push_str(&format!("Run failed: {}\n", report.run_failures.len()));
if !report.convert_failures.is_empty() {
output.push_str("\nConvert failed\n");
for failure in &report.convert_failures {
output.push_str(&format!("- {}: {}\n", failure.path.display(), failure.error));
}
}
if !report.run_failures.is_empty() {
output.push_str("\nRun failed\n");
for failure in &report.run_failures {
output.push_str(&format!("- {}: {}\n", failure.path.display(), failure.error));
}
}
output
}
fn render_render_report(report: &RenderReport) -> String {
let mut output = String::new();
output.push_str(&format!("Processed: {}\n", report.processed));
output.push_str(&format!("Succeeded: {}\n", report.succeeded));
output.push_str(&format!("Convert failed: {}\n", report.convert_failures.len()));
output.push_str(&format!("Render failed: {}\n", report.render_failures.len()));
if !report.convert_failures.is_empty() {
output.push_str("\nConvert failed\n");
for failure in &report.convert_failures {
output.push_str(&format!("- {}: {}\n", failure.path.display(), failure.error));
}
}
if !report.render_failures.is_empty() {
output.push_str("\nRender failed\n");
for failure in &report.render_failures {
output.push_str(&format!("- {}: {}\n", failure.path.display(), failure.error));
}
}
output
}
fn render_compare_report(report: &CompareReport) -> String {
let mut output = String::new();
output.push_str(&format!("Processed: {}\n", report.processed));
output.push_str(&format!("Succeeded: {}\n", report.succeeded));
output.push_str(&format!("Convert failed: {}\n", report.convert_failures.len()));
output.push_str(&format!("V1 render failed: {}\n", report.v1_render_failures.len()));
output.push_str(&format!("V2 render failed: {}\n", report.v2_render_failures.len()));
output.push_str(&format!("Compare failed: {}\n", report.compare_failures.len()));
if !report.convert_failures.is_empty() {
output.push_str("\nConvert failed\n");
for failure in &report.convert_failures {
output.push_str(&format!("- {}: {}\n", failure.path.display(), failure.error));
}
}
if !report.v1_render_failures.is_empty() {
output.push_str("\nV1 render failed\n");
for failure in &report.v1_render_failures {
output.push_str(&format!("- {}: {}\n", failure.path.display(), failure.error));
}
}
if !report.v2_render_failures.is_empty() {
output.push_str("\nV2 render failed\n");
for failure in &report.v2_render_failures {
output.push_str(&format!("- {}: {}\n", failure.path.display(), failure.error));
}
}
if !report.compare_failures.is_empty() {
output.push_str("\nCompare failed\n");
for failure in &report.compare_failures {
output.push_str(&format!("- {}: {}\n", failure.path.display(), failure.error));
}
}
output
}
fn build_ignore_matcher(root: &Path, ignore_file: Option<&Path>) -> Result<Gitignore, std::io::Error> {
let mut builder = GitignoreBuilder::new(root);
if let Some(ignore_file) = ignore_file {
builder.add(ignore_file);
}
builder
.build()
.map_err(|err| std::io::Error::other(format!("Failed to build ignore matcher: {err}")))
}
fn collect_kcl_files(root: &Path, matcher: &Gitignore) -> Result<Vec<PathBuf>, std::io::Error> {
let mut files = Vec::new();
let walker = WalkDir::new(root)
.into_iter()
.filter_entry(|entry| !is_ignored(root, matcher, entry));
for entry in walker {
let entry =
entry.map_err(|err| std::io::Error::other(format!("Failed to walk `{}`: {err}", root.display())))?;
if entry.file_type().is_file()
&& entry
.path()
.extension()
.is_some_and(|ext| ext.eq_ignore_ascii_case("kcl"))
{
files.push(entry.into_path());
}
}
files.sort();
Ok(files)
}
fn is_ignored(root: &Path, matcher: &Gitignore, entry: &DirEntry) -> bool {
let Ok(relative_path) = entry.path().strip_prefix(root) else {
return false;
};
if relative_path.as_os_str().is_empty() {
return false;
}
matcher
.matched_path_or_any_parents(relative_path, entry.file_type().is_dir())
.is_ignore()
}
fn relative_output_path(root: &Path, file: &Path) -> Result<PathBuf, std::io::Error> {
if root.is_file() {
return file
.file_name()
.map(PathBuf::from)
.ok_or_else(|| std::io::Error::other(format!("File `{}` has no filename", file.display())));
}
file.strip_prefix(root).map(PathBuf::from).map_err(|err| {
std::io::Error::other(format!(
"Failed to compute relative path for `{}`: {err}",
file.display()
))
})
}
fn run_log_file_name(file: &Path) -> Result<PathBuf, std::io::Error> {
let stem = file_stem_string(file)?;
Ok(PathBuf::from(format!("{stem}-run-log.txt")))
}
fn render_log_file_name(file: &Path) -> Result<PathBuf, std::io::Error> {
let stem = file_stem_string(file)?;
Ok(PathBuf::from(format!("{stem}-render-log.txt")))
}
fn compare_log_file_name(file: &Path) -> Result<PathBuf, std::io::Error> {
let stem = file_stem_string(file)?;
Ok(PathBuf::from(format!("{stem}-compare-log.txt")))
}
fn file_stem_string(file: &Path) -> Result<String, std::io::Error> {
let stem = file
.file_stem()
.ok_or_else(|| std::io::Error::other(format!("File `{}` has no stem", file.display())))?;
Ok(stem.to_string_lossy().into_owned())
}
fn write_png(path: &Path, image: &image::DynamicImage) -> Result<(), std::io::Error> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
image
.save_with_format(path, image::ImageFormat::Png)
.map_err(std::io::Error::other)
}