@@ -2,15 +2,13 @@ use anyhow::{Context, Result as AnyhowResult};
22use log:: { debug, info} ;
33use pcb_sch:: { AttributeValue , InstanceKind , Schematic , ATTR_LAYOUT_PATH } ;
44use pcb_zen_core:: diagnostics:: Diagnostic ;
5- use pcb_zen_core:: lang:: stackup:: {
6- ApproxEq , BoardConfig , BoardConfigError , NetClass , Stackup , StackupError , THICKNESS_EPS ,
7- } ;
5+ use pcb_zen_core:: lang:: stackup:: { BoardConfig , BoardConfigError , NetClass , Stackup , StackupError } ;
86use rust_decimal:: prelude:: ToPrimitive ;
97use serde:: { Deserialize , Serialize } ;
108use starlark:: errors:: EvalSeverity ;
119use std:: collections:: { HashMap , HashSet } ;
1210use std:: fs;
13- use std:: io:: Write ;
11+ use std:: io:: { BufWriter , Write } ;
1412use std:: path:: { Path , PathBuf } ;
1513use tempfile:: TempDir ;
1614use thiserror:: Error ;
@@ -601,7 +599,7 @@ pub fn process_layout(
601599 // Apply board config (stackup + netclass patterns)
602600 if let Some ( ref config) = board_config {
603601 if let Some ( ref stackup) = config. stackup {
604- patch_stackup_if_needed ( & paths. pcb , stackup) ?;
602+ patch_stackup ( & paths. pcb , stackup) ?;
605603 }
606604
607605 let assignments = build_netclass_assignments ( schematic, config. netclasses ( ) ) ;
@@ -940,189 +938,155 @@ fn patch_netclass_patterns(
940938 Ok ( ( ) )
941939}
942940
943- /// Apply stackup configuration if it differs from existing PCB file
944- fn patch_stackup_if_needed ( pcb_path : & Path , zen_stackup : & Stackup ) -> Result < ( ) , LayoutError > {
945- // Read current PCB file
941+ /// Apply and normalize stackup-related sections in a PCB file.
942+ fn patch_stackup ( pcb_path : & Path , zen_stackup : & Stackup ) -> Result < ( ) , LayoutError > {
946943 let pcb_content = fs:: read_to_string ( pcb_path) . map_err ( |e| {
947944 LayoutError :: StackupPatchingError ( format ! ( "Failed to read PCB file: {}" , e) )
948945 } ) ?;
949946
950- // Parse existing stackup from PCB file
951- let existing_stackup = Stackup :: from_kicad_pcb ( & pcb_content) ?;
947+ let mut board = pcb_sexpr:: parse ( & pcb_content) . map_err ( |e| {
948+ LayoutError :: StackupPatchingError ( format ! ( "Failed to parse PCB file: {}" , e) )
949+ } ) ?;
952950
953- // Compare stackups - only patch if they're different
954- let needs_update = match existing_stackup {
955- Some ( existing) => {
956- let equivalent = zen_stackup. approx_eq ( & existing, THICKNESS_EPS ) ;
957- if !equivalent {
958- debug ! ( "Zen stackup: {:?}" , zen_stackup) ;
959- debug ! ( "Existing stackup: {:?}" , existing) ;
960- }
961- !equivalent
962- }
963- None => {
964- debug ! ( "No existing stackup found in PCB file" ) ;
965- true // No existing stackup, so we need to add it
966- }
967- } ;
951+ let layers = zen_stackup. generate_layers_expr ( 4 ) ;
952+ let stackup = zen_stackup. generate_stackup_expr ( ) ;
968953
969- if !needs_update {
970- debug ! ( "Stackup configuration matches, skipping update" ) ;
971- return Ok ( ( ) ) ;
972- }
954+ apply_stackup_sections ( & mut board , layers , stackup ) ? ;
955+
956+ let updated_content =
957+ pcb_sexpr :: formatter :: format_tree ( & board , pcb_sexpr :: formatter :: FormatMode :: Normal ) ;
973958
974959 info ! ( "Updating stackup configuration in {}" , pcb_path. display( ) ) ;
960+ write_text_atomic_buffered ( pcb_path, & updated_content) . map_err ( |e| {
961+ LayoutError :: StackupPatchingError ( format ! (
962+ "Failed to write updated PCB file {}: {}" ,
963+ pcb_path. display( ) ,
964+ e
965+ ) )
966+ } ) ?;
967+ info ! ( "Successfully updated stackup configuration" ) ;
975968
976- // Generate new S-expressions (using default user layers)
977- let layers_sexpr = zen_stackup. generate_layers_sexpr ( 4 ) ;
978- let stackup_sexpr = zen_stackup. generate_stackup_sexpr ( ) ;
969+ Ok ( ( ) )
970+ }
979971
980- // Use surgical string replacement to avoid parsing issues with hex numbers
981- let mut updated_content = pcb_content;
982- updated_content = replace_section_in_pcb_content ( & updated_content, "layers" , & layers_sexpr) ?;
983- updated_content = replace_section_in_pcb_content ( & updated_content, "stackup" , & stackup_sexpr) ?;
972+ fn write_text_atomic_buffered ( path : & Path , text : & str ) -> std:: io:: Result < ( ) > {
973+ let tmp_path = path. with_extension ( "kicad_pcb.tmp" ) ;
974+ let mut writer = BufWriter :: new ( fs:: File :: create ( & tmp_path) ?) ;
975+ writer. write_all ( text. as_bytes ( ) ) ?;
976+ writer. flush ( ) ?;
977+ fs:: rename ( & tmp_path, path) ?;
978+ Ok ( ( ) )
979+ }
984980
985- // Write updated content back to file
986- fs:: write ( pcb_path, updated_content) . map_err ( |e| {
987- LayoutError :: StackupPatchingError ( format ! ( "Failed to write updated PCB file: {}" , e) )
981+ fn apply_stackup_sections (
982+ board : & mut pcb_sexpr:: Sexpr ,
983+ layers : pcb_sexpr:: Sexpr ,
984+ stackup : pcb_sexpr:: Sexpr ,
985+ ) -> Result < ( ) , LayoutError > {
986+ let root_items = board. as_list_mut ( ) . ok_or_else ( || {
987+ LayoutError :: StackupPatchingError ( "PCB root is not an S-expression list" . to_string ( ) )
988988 } ) ?;
989989
990- info ! ( "Successfully updated stackup configuration" ) ;
991- Ok ( ( ) )
992- }
990+ if root_items. first ( ) . and_then ( pcb_sexpr:: Sexpr :: as_sym) != Some ( "kicad_pcb" ) {
991+ return Err ( LayoutError :: StackupPatchingError (
992+ "PCB root must start with (kicad_pcb ...)" . to_string ( ) ,
993+ ) ) ;
994+ }
993995
994- /// Replace a section in KiCad PCB content using careful string matching
995- fn replace_section_in_pcb_content (
996- content : & str ,
997- section_name : & str ,
998- new_section : & str ,
999- ) -> Result < String , LayoutError > {
1000- // Find the section by parsing just enough to locate it
1001- let section_start = find_section_start ( content, section_name) ?;
1002-
1003- if let Some ( start_pos) = section_start {
1004- let end_pos = find_matching_paren ( content, start_pos) ?;
1005-
1006- // Replace the section with the new content
1007- let mut result = String :: with_capacity ( content. len ( ) + new_section. len ( ) ) ;
1008- result. push_str ( & content[ ..start_pos] ) ;
1009- result. push_str ( new_section) ;
1010- result. push_str ( & content[ end_pos + 1 ..] ) ;
1011- Ok ( result)
996+ set_or_insert_named_list ( root_items, "layers" , layers, Some ( "general" ) ) ;
997+
998+ if let Some ( setup_idx) = find_named_list_index ( root_items, "setup" ) {
999+ let setup_items = root_items[ setup_idx] . as_list_mut ( ) . ok_or_else ( || {
1000+ LayoutError :: StackupPatchingError ( "setup section is not a list" . to_string ( ) )
1001+ } ) ?;
1002+ set_or_insert_named_list ( setup_items, "stackup" , stackup, None ) ;
10121003 } else {
1013- // Section doesn't exist, need to add it
1014- add_section_to_pcb_content ( content, section_name, new_section)
1004+ root_items. push ( pcb_sexpr:: Sexpr :: list ( vec ! [
1005+ pcb_sexpr:: Sexpr :: symbol( "setup" ) ,
1006+ stackup,
1007+ ] ) ) ;
10151008 }
1016- }
1017-
1018- /// Find the start position of a section in PCB content
1019- fn find_section_start ( content : & str , section_name : & str ) -> Result < Option < usize > , LayoutError > {
1020- let pattern = format ! ( "({}" , section_name) ;
1021- let mut pos = 0 ;
10221009
1023- while let Some ( found ) = content [ pos.. ] . find ( & pattern ) {
1024- let abs_pos = pos + found ;
1010+ Ok ( ( ) )
1011+ }
10251012
1026- // Check if this is a word boundary (not part of a larger identifier)
1027- let next_char_pos = abs_pos + pattern. len ( ) ;
1028- if next_char_pos < content. len ( ) {
1029- let next_char = content. chars ( ) . nth ( next_char_pos) . unwrap ( ) ;
1030- if next_char. is_whitespace ( ) || next_char == '\n' || next_char == '\t' {
1031- return Ok ( Some ( abs_pos) ) ;
1032- }
1033- } else {
1034- return Ok ( Some ( abs_pos) ) ;
1035- }
1013+ fn find_named_list_index ( items : & [ pcb_sexpr:: Sexpr ] , name : & str ) -> Option < usize > {
1014+ items. iter ( ) . position ( |item| {
1015+ item. as_list ( )
1016+ . and_then ( |list| list. first ( ) )
1017+ . and_then ( pcb_sexpr:: Sexpr :: as_sym)
1018+ == Some ( name)
1019+ } )
1020+ }
10361021
1037- pos = abs_pos + 1 ;
1022+ fn set_or_insert_named_list (
1023+ parent : & mut Vec < pcb_sexpr:: Sexpr > ,
1024+ name : & str ,
1025+ replacement : pcb_sexpr:: Sexpr ,
1026+ insert_after : Option < & str > ,
1027+ ) {
1028+ if let Some ( idx) = find_named_list_index ( parent, name) {
1029+ parent[ idx] = replacement;
1030+ return ;
10381031 }
10391032
1040- Ok ( None )
1033+ let insert_idx = insert_after
1034+ . and_then ( |anchor| find_named_list_index ( parent, anchor) . map ( |idx| idx + 1 ) )
1035+ . unwrap_or ( parent. len ( ) ) ;
1036+
1037+ parent. insert ( insert_idx, replacement) ;
10411038}
10421039
1043- /// Find the matching closing parenthesis for an opening parenthesis
1044- fn find_matching_paren ( content : & str , start_pos : usize ) -> Result < usize , LayoutError > {
1045- let mut depth = 0 ;
1046- let mut in_string = false ;
1047- let mut escaped = false ;
1040+ #[ cfg( test) ]
1041+ mod tests {
1042+ use super :: apply_stackup_sections;
1043+
1044+ #[ test]
1045+ fn apply_stackup_sections_replaces_existing_layers_and_stackup ( ) {
1046+ let mut board = pcb_sexpr:: parse (
1047+ r#"(kicad_pcb
1048+ (general (thickness 1.6))
1049+ (layers (0 "F.Cu" signal))
1050+ (setup (stackup (old yes)) (other 1))
1051+ )"# ,
1052+ )
1053+ . unwrap ( ) ;
10481054
1049- let chars: Vec < char > = content. chars ( ) . collect ( ) ;
1055+ let new_layers =
1056+ pcb_sexpr:: parse ( r#"(layers (0 "F.Cu" signal) (2 "B.Cu" signal))"# ) . unwrap ( ) ;
1057+ let new_stackup = pcb_sexpr:: parse ( r#"(stackup (layer "F.Cu" (type "copper")))"# ) . unwrap ( ) ;
10501058
1051- for ( i, & ch) in chars. iter ( ) . enumerate ( ) . skip ( start_pos) {
1052- if escaped {
1053- escaped = false ;
1054- continue ;
1055- }
1059+ apply_stackup_sections ( & mut board, new_layers, new_stackup) . unwrap ( ) ;
10561060
1057- match ch {
1058- '\\' if in_string => escaped = true ,
1059- '"' => in_string = !in_string,
1060- '(' if !in_string => depth += 1 ,
1061- ')' if !in_string => {
1062- depth -= 1 ;
1063- if depth == 0 {
1064- return Ok ( i) ;
1065- }
1066- }
1067- _ => { }
1068- }
1061+ let formatted =
1062+ pcb_sexpr:: formatter:: format_tree ( & board, pcb_sexpr:: formatter:: FormatMode :: Normal ) ;
1063+ assert ! ( formatted. contains( r#"(2 "B.Cu" signal)"# ) ) ;
1064+ assert ! ( formatted. contains( r#"(layer "F.Cu""# ) ) ;
1065+ assert ! ( formatted. contains( r#"(type "copper")"# ) ) ;
1066+ assert ! ( !formatted. contains( "(old yes)" ) ) ;
10691067 }
10701068
1071- Err ( LayoutError :: StackupPatchingError (
1072- "Could not find matching closing parenthesis" . to_string ( ) ,
1073- ) )
1074- }
1069+ #[ test]
1070+ fn apply_stackup_sections_inserts_layers_and_setup_when_missing ( ) {
1071+ let mut board =
1072+ pcb_sexpr:: parse ( r#"(kicad_pcb (version 20240101) (general (thickness 1.6)))"# )
1073+ . unwrap ( ) ;
10751074
1076- /// Add a new section to PCB content
1077- fn add_section_to_pcb_content (
1078- content : & str ,
1079- section_name : & str ,
1080- new_section : & str ,
1081- ) -> Result < String , LayoutError > {
1082- match section_name {
1083- "layers" => {
1084- // Add after general section
1085- if let Some ( general_start) = find_section_start ( content, "general" ) ? {
1086- let general_end = find_matching_paren ( content, general_start) ?;
1087- let insert_pos = general_end + 1 ;
1088-
1089- let mut result = String :: with_capacity ( content. len ( ) + new_section. len ( ) + 10 ) ;
1090- result. push_str ( & content[ ..insert_pos] ) ;
1091- result. push ( '\n' ) ;
1092- result. push ( '\t' ) ;
1093- result. push_str ( new_section) ;
1094- result. push_str ( & content[ insert_pos..] ) ;
1095- Ok ( result)
1096- } else {
1097- Err ( LayoutError :: StackupPatchingError (
1098- "Could not find general section for layers insertion" . to_string ( ) ,
1099- ) )
1100- }
1101- }
1102- "stackup" => {
1103- // Add within setup section
1104- if let Some ( setup_start) = find_section_start ( content, "setup" ) ? {
1105- let setup_end = find_matching_paren ( content, setup_start) ?;
1106- let insert_pos = setup_end; // Before closing paren
1107-
1108- let mut result = String :: with_capacity ( content. len ( ) + new_section. len ( ) + 20 ) ;
1109- result. push_str ( & content[ ..insert_pos] ) ;
1110- result. push ( '\n' ) ;
1111- result. push_str ( "\t \t " ) ;
1112- result. push_str ( new_section) ;
1113- result. push ( '\n' ) ;
1114- result. push ( '\t' ) ;
1115- result. push_str ( & content[ insert_pos..] ) ;
1116- Ok ( result)
1117- } else {
1118- Err ( LayoutError :: StackupPatchingError (
1119- "Could not find setup section for stackup insertion" . to_string ( ) ,
1120- ) )
1121- }
1122- }
1123- _ => Err ( LayoutError :: StackupPatchingError ( format ! (
1124- "Unknown section type: {}" ,
1125- section_name
1126- ) ) ) ,
1075+ let new_layers =
1076+ pcb_sexpr:: parse ( r#"(layers (0 "F.Cu" signal) (2 "B.Cu" signal))"# ) . unwrap ( ) ;
1077+ let new_stackup = pcb_sexpr:: parse ( r#"(stackup (layer "F.Cu" (type "copper")))"# ) . unwrap ( ) ;
1078+
1079+ apply_stackup_sections ( & mut board, new_layers, new_stackup) . unwrap ( ) ;
1080+
1081+ let formatted =
1082+ pcb_sexpr:: formatter:: format_tree ( & board, pcb_sexpr:: formatter:: FormatMode :: Normal ) ;
1083+ assert ! ( formatted. contains( "\n \t (layers\n " ) ) ;
1084+ assert ! ( formatted. contains( "\n \t (setup\n " ) ) ;
1085+ assert ! ( formatted. contains( r#"(layer "F.Cu""# ) ) ;
1086+ assert ! ( formatted. contains( r#"(type "copper")"# ) ) ;
1087+
1088+ let general_pos = formatted. find ( "\n \t (general" ) . unwrap ( ) ;
1089+ let layers_pos = formatted. find ( "\n \t (layers" ) . unwrap ( ) ;
1090+ assert ! ( layers_pos > general_pos) ;
11271091 }
11281092}
0 commit comments