Introduction
kiutils-rs is a Rust workspace for lossless KiCad file parsing/editing.
Workspace layers
| Crate | Role |
|---|---|
kiutils_sexpr | Lossless S-expression CST parser/printer |
kiutils_kicad | Typed KiCad document layer |
kiutils-rs (kiutils_rs) | Stable public API for applications |
Design goals
- Lossless by default (
WriteMode::Lossless) - Canonical output when requested (
WriteMode::Canonical) - Forward-compatible unknown token capture (
UnknownNode,UnknownField) - Typed mutation helpers for common edit flows
Version scope
- Primary target: KiCad v10
- Secondary target: KiCad v9
Install
cargo add kiutils-rs
Quickstart
Parse, edit, write
use kiutils_rs::PcbFile;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut doc = PcbFile::read("input.kicad_pcb")?;
doc.set_version(20260101)
.set_generator("kiutils")
.set_generator_version("dev")
.set_title("Demo Board")
.upsert_property("Owner", "Milind")
.remove_property("Obsolete");
doc.write("output.kicad_pcb")?;
Ok(())
}
Build + test
cargo test
cargo test -p kiutils-rs --features serde
cargo test -p kiutils-rs --features parallel
Inspect CLI (typed summary)
cargo run -p kiutils_kicad --bin kiutils-inspect -- \
crates/kiutils_kicad/tests/fixtures/sample.kicad_pcb \
--show-unknown --show-diagnostics --show-canonical
Supported Formats
Current v1 file support in public API:
| File | Type |
|---|---|
.kicad_pcb | PCB |
.kicad_mod | Footprint |
fp-lib-table | Footprint lib table |
sym-lib-table | Symbol lib table |
.kicad_dru | Design rules |
.kicad_pro | Project JSON |
Additional parser support exists in implementation crates (for example schematic/symbol/worksheet), while the stable kiutils-rs public surface is intentionally narrow.
Write modes
| Mode | Behavior |
|---|---|
WriteMode::Lossless | Preserves unrelated formatting/tokens |
WriteMode::Canonical | Emits normalized/canonical representation |
Usage Patterns
This chapter is tuned for automation systems and code generators.
Pattern: safe typed mutation
- Parse with
*_File::read(...) - Update through document setters (
set_*,upsert_*,remove_*) - Write with
write(...)orwrite_mode(...)
Why: setter APIs reconcile AST/CST correctly.
Examples:
ProjectDocument::set_pinned_symbol_libs(...)ProjectDocument::set_pinned_footprint_libs(...)LibTableDocument::upsert_library_uri(...)(updates existing URI only; adds if missing)
Common pitfalls
| Pitfall | What happens | Correct pattern |
|---|---|---|
Mutating with ast_mut() then calling write() | Validation error for non-reconciled state | Use setter/upsert/remove helpers |
| Assuming unknown tokens are dropped | Future syntax might be lost in other libraries | kiutils-rs captures unknowns and round-trips them |
| Forcing canonical output always | Noisy diffs in VCS | Default to lossless; canonical only when required |
Minimal code path
use kiutils_rs::{PcbFile, WriteMode};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut doc = PcbFile::read("input.kicad_pcb")?;
doc.upsert_property("EditedBy", "agent");
doc.write_mode("output.kicad_pcb", WriteMode::Lossless)?;
Ok(())
}
Examples
Real example: PCB round-trip
Snippet included from source so docs stay synced:
fn main() -> Result<(), String> {
let mut args = env::args().skip(1);
let in_path = args.next().map(PathBuf::from).ok_or_else(usage)?;
let out_path = args.next().map(PathBuf::from).ok_or_else(usage)?;
let mut doc = PcbFile::read(&in_path).map_err(|e| e.to_string())?;
doc.set_generator("kiutils")
.set_generator_version("roundtrip-demo")
.set_title("Roundtrip Demo")
.upsert_property("EditedBy", "kiutils_kicad/examples/pcb_roundtrip.rs");
doc.write(&out_path).map_err(|e| e.to_string())?;
let reread = PcbFile::read(&out_path).map_err(|e| e.to_string())?;
println!("input: {}", in_path.display());
println!("output: {}", out_path.display());
println!("version: {:?}", reread.ast().version);
println!("generator: {:?}", reread.ast().generator);
println!("properties: {}", reread.ast().property_count);
println!("unknown_nodes: {}", reread.ast().unknown_nodes.len());
println!("diagnostics: {}", reread.diagnostics().len());
Ok(())
}
Run it:
cargo run -p kiutils_kicad --example pcb_roundtrip -- input.kicad_pcb output.kicad_pcb
Corpus-style examples
cargo run -p kiutils_kicad --example pcb_corpus_roundtrip -- <input_dir> crates/kiutils_kicad/examples/generated/pcbs
cargo run -p kiutils_kicad --example footprint_corpus_roundtrip -- <input_dir> crates/kiutils_kicad/examples/generated/footprints
cargo run -p kiutils_kicad --example schematic_corpus_roundtrip -- <input_dir> crates/kiutils_kicad/examples/generated/schematics
cargo run -p kiutils_kicad --example symbol_corpus_roundtrip -- <input_dir> crates/kiutils_kicad/examples/generated/symbols
cargo run -p kiutils_kicad --example symlib_corpus_roundtrip -- <input_dir> crates/kiutils_kicad/examples/generated/symlib
cargo run -p kiutils_kicad --example dru_corpus_roundtrip -- <input_dir> crates/kiutils_kicad/examples/generated/dru
cargo run -p kiutils_kicad --example worksheet_corpus_roundtrip -- <input_dir> crates/kiutils_kicad/examples/generated/worksheets
Verification
Evidence in repository:
- Integration tests cover fixture-based round-trip behavior across supported document kinds.
- Unknown syntax preservation tested explicitly.
kiutils-inspectJSON/text contract smoke tests present.
Test evidence snippet
#![allow(unused)]
fn main() {
fn pcb_fixture_roundtrip_lossless_and_unknown() {
let src_path = fixture("sample.kicad_pcb");
let src = fs::read_to_string(&src_path).expect("read fixture");
let doc = PcbFile::read(&src_path).expect("parse");
assert_eq!(doc.ast().unknown_nodes.len(), 1);
let out = tmp_file("pcb", "kicad_pcb");
doc.write(&out).expect("write");
let got = fs::read_to_string(&out).expect("read out");
assert_eq!(got, src);
let _ = fs::remove_file(out);
}
}
Local gate
cargo fmt --all
cargo test
cargo clippy --all-targets --all-features -- -D warnings
mdbook build docs
API Reference
Primary API docs:
Use this book for workflows and conventions; use docs.rs for item-level API details.