use std::path::{Path, PathBuf};
use miette::Diagnostic;
use thiserror::Error;
use crate::commands::check::{self, CheckError};
use crate::commands::target::resolve_entry;
use crate::config::FusibleConfig;
use crate::tscircuit::{Runner, RunnerError};
#[derive(Debug, Error, Diagnostic)]
pub enum ExportError {
#[error(transparent)]
#[diagnostic(transparent)]
Check(#[from] CheckError),
#[error(transparent)]
#[diagnostic(transparent)]
Runner(#[from] RunnerError),
#[error("failed to export manufacturing files: {0}")]
#[diagnostic(code(fusible::export::io_error))]
Io(#[from] std::io::Error),
#[error("unknown export preset \"{preset}\"")]
#[diagnostic(
code(fusible::export::unknown_preset),
help("valid presets are: `fab` (gerbers + json), `cad` (kicad + step), `docs` (svg + pdf), or `all`")
)]
UnknownPreset { preset: String },
}
pub fn run(
file: Option<PathBuf>,
preset: Option<&str>,
format: &str,
output: Option<PathBuf>,
skip_lint: bool,
) -> Result<(), ExportError> {
if let Some(preset_name) = preset {
validate_preset(preset_name)?;
}
let entry = resolve_entry(file.clone()).unwrap_or_else(|_| PathBuf::from("index.tsx"));
if !skip_lint {
println!("🔍 Verifying electrical rules before export (fuse check)...");
check::run(file, None, None)?;
println!("⚡ Fuse intact — proceeding with artifact export.\n");
} else {
println!("⚠️ Skipping electrical rule verification (--skip-lint requested).\n");
}
let runner = Runner::detect()?;
let board_title = resolve_board_title();
if let Some(preset_name) = preset {
let circuit_json = if preset_name == "fab" || preset_name == "all" {
load_or_build_circuit_json(&entry, &runner)?
} else {
String::new()
};
run_preset(
&runner,
&entry,
&circuit_json,
&board_title,
preset_name,
output.as_deref(),
)
} else if format == "gerbers" || format == "gerber" {
let circuit_json = load_or_build_circuit_json(&entry, &runner)?;
let dest = output.unwrap_or_else(|| PathBuf::from("dist/gerbers.zip"));
if let Some(parent) = dest.parent() {
std::fs::create_dir_all(parent)?;
}
println!(
"📦 Generating native RS-274X gerbers to {}...",
dest.display()
);
let files = crate::gerber::generate_gerber_files(&circuit_json, &board_title);
let zip_bytes = crate::gerber::create_gerber_zip(&files)?;
std::fs::write(&dest, zip_bytes)?;
println!("✅ Successfully exported gerbers to {}!", dest.display());
Ok(())
} else if format == "kicad_project" || format == "kicad" {
let dest = output.unwrap_or_else(|| PathBuf::from("dist/cad/kicad"));
std::fs::create_dir_all(&dest)?;
let tmp_zip = dest.join(".kicad_export.zip");
println!("📦 Exporting KiCad project to {}...", dest.display());
runner.export(&entry, "kicad_zip", Some(&tmp_zip))?;
extract_zip_archive(&tmp_zip, &dest);
let _ = std::fs::remove_file(&tmp_zip);
println!(
"✅ Successfully exported KiCad project to {}!",
dest.display()
);
Ok(())
} else {
println!(
"📦 Exporting \"{}\" to format: {}...",
entry.display(),
format
);
runner.export(&entry, format, output.as_deref())?;
println!("✅ Successfully exported {format}!");
Ok(())
}
}
fn resolve_board_title() -> String {
FusibleConfig::find_and_load(Path::new("."))
.ok()
.flatten()
.and_then(|(_, cfg)| cfg.name)
.unwrap_or_else(|| "Hardware Board".to_string())
}
fn load_or_build_circuit_json(entry: &Path, runner: &Runner) -> Result<String, ExportError> {
if entry.extension().and_then(|e| e.to_str()) == Some("json") {
std::fs::read_to_string(entry).map_err(|e| {
CheckError::Read(check::ReadError {
path: entry.display().to_string(),
source: e,
})
.into()
})
} else {
println!(
"⚙️ Evaluating \"{}\" for native export...",
entry.display()
);
let json_path = runner.build(entry, None)?;
std::fs::read_to_string(&json_path).map_err(|e| {
CheckError::Read(check::ReadError {
path: json_path.display().to_string(),
source: e,
})
.into()
})
}
}
fn extract_zip_archive(zip_path: &Path, target_dir: &Path) {
let mut cmd = std::process::Command::new("unzip");
cmd.args(["-o", "-q"])
.arg(zip_path)
.arg("-d")
.arg(target_dir);
let _ = cmd.status();
}
fn export_fab(
_runner: &Runner,
_entry: &Path,
circuit_json: &str,
board_title: &str,
out: Option<&Path>,
) -> Result<(), ExportError> {
let base = out
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("dist/fab"));
std::fs::create_dir_all(&base)?;
println!("📦 Running fabrication export preset (fab)...");
println!(" • Exporting RS-274X gerbers & drill files via native Rust engine...");
crate::gerber::export_fab_bundle(circuit_json, board_title, &base)?;
println!(
"✅ Fabrication artifacts generated in \"{}\" (gerbers.zip, circuit.json)",
base.display()
);
Ok(())
}
fn export_cad(runner: &Runner, entry: &Path, out: Option<&Path>) -> Result<(), ExportError> {
let base = out
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("dist/cad"));
std::fs::create_dir_all(&base).ok();
println!("📦 Running CAD export preset (cad)...");
let kicad = base.join("kicad.zip");
println!(" • Exporting KiCad project -> {}", kicad.display());
runner.export(entry, "kicad_zip", Some(&kicad))?;
let kicad_dir = base.join("kicad");
std::fs::create_dir_all(&kicad_dir).ok();
extract_zip_archive(&kicad, &kicad_dir);
println!(
" • KiCad project files extracted -> {}",
kicad_dir.display()
);
let step = base.join("board.step");
println!(" • Exporting 3D STEP -> {}", step.display());
runner.export(entry, "step", Some(&step))?;
println!("✅ CAD artifacts generated in \"{}\"", base.display());
Ok(())
}
fn export_docs(runner: &Runner, entry: &Path, out: Option<&Path>) -> Result<(), ExportError> {
let base = out
.map(PathBuf::from)
.unwrap_or_else(|| PathBuf::from("dist/docs"));
std::fs::create_dir_all(&base).ok();
println!("📦 Running documentation export preset (docs)...");
let sch = base.join("schematic.svg");
println!(" • Exporting schematic SVG -> {}", sch.display());
runner.export(entry, "schematic-svg", Some(&sch))?;
let pcb = base.join("pcb.svg");
println!(" • Exporting PCB layout SVG -> {}", pcb.display());
runner.export(entry, "pcb-svg", Some(&pcb))?;
let pdf = base.join("schematic.pdf");
println!(" • Exporting schematic PDF -> {}", pdf.display());
runner.export(entry, "schematic-pdf", Some(&pdf))?;
println!(
"✅ Documentation artifacts generated in \"{}\"",
base.display()
);
Ok(())
}
fn validate_preset(preset: &str) -> Result<(), ExportError> {
match preset {
"fab" | "cad" | "docs" | "all" => Ok(()),
other => Err(ExportError::UnknownPreset {
preset: other.to_string(),
}),
}
}
fn run_preset(
runner: &Runner,
entry: &Path,
circuit_json: &str,
board_title: &str,
preset: &str,
custom_output: Option<&Path>,
) -> Result<(), ExportError> {
validate_preset(preset)?;
match preset {
"fab" => export_fab(runner, entry, circuit_json, board_title, custom_output),
"cad" => export_cad(runner, entry, custom_output),
"docs" => export_docs(runner, entry, custom_output),
"all" => {
export_fab(runner, entry, circuit_json, board_title, custom_output)?;
println!();
export_cad(runner, entry, custom_output)?;
println!();
export_docs(runner, entry, custom_output)?;
Ok(())
}
_ => unreachable!(),
}
}