Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ and this project adheres to Semantic Versioning (https://semver.org/spec/v2.0.0.

## [Unreleased]

### Fixed

- Reduced `layout.sync` false positives in publish/check flows by normalizing `.kicad_pro` newline writes and ignoring trailing whitespace-only drift when comparing synced layout files.

## [0.3.43] - 2026-02-18

### Added
Expand Down
46 changes: 45 additions & 1 deletion crates/pcb-layout/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ fn files_differ(original_path: &Path, generated_path: &Path, what: &str) -> anyh
.with_context(|| format!("Failed to read {what} file: {}", original_path.display()))?;
let generated_bytes = fs::read(generated_path)
.with_context(|| format!("Failed to read {what} file: {}", generated_path.display()))?;
Ok(original_bytes != generated_bytes)
Ok(original_bytes.trim_ascii_end() != generated_bytes.trim_ascii_end())
}

/// Apply moved() path renames to a PCB file
Expand Down Expand Up @@ -815,6 +815,50 @@ pub mod utils {
}
}

#[cfg(test)]
mod diff_tests {
use super::*;

#[test]
fn files_differ_ignores_trailing_whitespace() -> anyhow::Result<()> {
let temp = tempfile::tempdir()?;
let original = temp.path().join("original.kicad_pcb");
let generated = temp.path().join("generated.kicad_pcb");

fs::write(&original, "(kicad_pcb (version 20240101))\n")?;
fs::write(&generated, "(kicad_pcb (version 20240101))\r\n\t")?;

assert!(!files_differ(&original, &generated, "PCB")?);
Ok(())
}

#[test]
fn files_differ_detects_leading_whitespace_changes() -> anyhow::Result<()> {
let temp = tempfile::tempdir()?;
let original = temp.path().join("original.kicad_pcb");
let generated = temp.path().join("generated.kicad_pcb");

fs::write(&original, "(kicad_pcb (version 20240101))\n")?;
fs::write(&generated, " (kicad_pcb (version 20240101))\n")?;

assert!(files_differ(&original, &generated, "PCB")?);
Ok(())
}

#[test]
fn files_differ_detects_internal_whitespace_changes() -> anyhow::Result<()> {
let temp = tempfile::tempdir()?;
let original = temp.path().join("original.kicad_pro");
let generated = temp.path().join("generated.kicad_pro");

fs::write(&original, r#"{"a":1,"b":2}"#)?;
fs::write(&generated, r#"{"a":1, "b":2}"#)?;

assert!(files_differ(&original, &generated, "project")?);
Ok(())
}
}

/// Build netclass assignments from net impedance properties
fn build_netclass_assignments(
schematic: &Schematic,
Expand Down
50 changes: 49 additions & 1 deletion crates/pcb/src/release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -749,7 +749,8 @@ fn update_kicad_pro_text_variables(
);

// Write back to file with pretty formatting
let updated_content = serde_json::to_string_pretty(&project)?;
let mut updated_content = serde_json::to_string_pretty(&project)?;
updated_content.push('\n');
fs::write(kicad_pro_path, updated_content).with_context(|| {
format!(
"Failed to write updated .kicad_pro file: {}",
Expand Down Expand Up @@ -1530,3 +1531,50 @@ fn run_kicad_drc(info: &ReleaseInfo, spinner: &Spinner) -> Result<()> {

Ok(())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn update_kicad_pro_text_variables_writes_trailing_newline() -> Result<()> {
let temp_dir = tempfile::tempdir()?;
let kicad_pro_path = temp_dir.path().join("layout.kicad_pro");

fs::write(
&kicad_pro_path,
r#"{
"text_variables": {}
}"#,
)?;

update_kicad_pro_text_variables(&kicad_pro_path, "1.2.3", "abcdef0", "Demo Board")?;

let content = fs::read_to_string(&kicad_pro_path)?;
assert!(
content.ends_with('\n'),
"expected .kicad_pro to end with newline"
);

let project: serde_json::Value = serde_json::from_str(&content)?;
let vars = project
.get("text_variables")
.and_then(|v| v.as_object())
.expect("text_variables should exist");

assert_eq!(
vars.get("PCB_VERSION").and_then(|v| v.as_str()),
Some("1.2.3")
);
assert_eq!(
vars.get("PCB_GIT_HASH").and_then(|v| v.as_str()),
Some("abcdef0")
);
assert_eq!(
vars.get("PCB_NAME").and_then(|v| v.as_str()),
Some("Demo Board")
);

Ok(())
}
}