From d70f0d6baf1bf5037527f917438f5d41398fe406 Mon Sep 17 00:00:00 2001 From: Akhil Velagapudi Date: Wed, 18 Feb 2026 20:28:08 -0500 Subject: [PATCH] Fix publish layout sync false positives --- CHANGELOG.md | 4 +++ crates/pcb-layout/src/lib.rs | 46 ++++++++++++++++++++++++++++++++- crates/pcb/src/release.rs | 50 +++++++++++++++++++++++++++++++++++- 3 files changed, 98 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f77ef602..813b64e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/crates/pcb-layout/src/lib.rs b/crates/pcb-layout/src/lib.rs index e5115c20..66840954 100644 --- a/crates/pcb-layout/src/lib.rs +++ b/crates/pcb-layout/src/lib.rs @@ -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 @@ -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, diff --git a/crates/pcb/src/release.rs b/crates/pcb/src/release.rs index 52e2bf8c..938325cc 100644 --- a/crates/pcb/src/release.rs +++ b/crates/pcb/src/release.rs @@ -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: {}", @@ -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(()) + } +}