@@ -3,6 +3,8 @@ use clap::Args;
33use colored:: Colorize ;
44use indicatif:: ProgressBar ;
55use inquire:: { Select , Text } ;
6+ use pcb_sexpr:: formatter:: { prettify, FormatMode } ;
7+ use pcb_sexpr:: PatchSet ;
68use pcb_zen_core:: config:: find_workspace_root;
79use regex:: Regex ;
810use reqwest:: blocking:: Client ;
@@ -438,11 +440,11 @@ fn embed_step_into_footprint_file(
438440
439441 let embedded_content = embed_step_in_footprint ( footprint_content, step_bytes, step_filename) ?;
440442
441- // Normalize line endings and write to temporary file
443+ // Normalize line endings, format as KiCad S-expression, then write atomically.
442444 let normalized_content = embedded_content. replace ( "\r \n " , "\n " ) ;
445+ let formatted_content = format_kicad_sexpr_source ( & normalized_content, footprint_path) ?;
443446 let temp_path = footprint_path. with_extension ( "kicad_mod.tmp" ) ;
444- fs:: write ( & temp_path, normalized_content)
445- . context ( "Failed to write temporary footprint file" ) ?;
447+ fs:: write ( & temp_path, formatted_content) . context ( "Failed to write temporary footprint file" ) ?;
446448
447449 // Atomic rename to replace original
448450 fs:: rename ( & temp_path, footprint_path) . context ( "Failed to rename temporary footprint file" ) ?;
@@ -705,13 +707,7 @@ pub fn add_component_to_workspace(
705707 }
706708
707709 // Finalize: embed STEP, generate .zen file
708- finalize_component (
709- & component_dir,
710- part_number,
711- manufacturer,
712- has_footprint,
713- has_datasheet,
714- ) ?;
710+ finalize_component ( & component_dir, part_number, manufacturer) ?;
715711
716712 Ok ( AddComponentResult {
717713 component_path : zen_file,
@@ -737,48 +733,118 @@ fn component_dir_path(workspace_root: &Path, manufacturer: Option<&str>, mpn: &s
737733}
738734
739735/// Embed STEP into footprint (if both exist) and generate .zen file
740- fn finalize_component (
741- component_dir : & Path ,
742- mpn : & str ,
743- manufacturer : Option < & str > ,
744- has_footprint : bool ,
745- has_datasheet : bool ,
746- ) -> Result < ( ) > {
736+ fn finalize_component ( component_dir : & Path , mpn : & str , manufacturer : Option < & str > ) -> Result < ( ) > {
747737 let sanitized_mpn = pcb_component_gen:: sanitize_mpn_for_path ( mpn) ;
748738 let symbol_path = component_dir. join ( format ! ( "{}.kicad_sym" , & sanitized_mpn) ) ;
749739 let footprint_path = component_dir. join ( format ! ( "{}.kicad_mod" , & sanitized_mpn) ) ;
750740 let step_path = component_dir. join ( format ! ( "{}.step" , & sanitized_mpn) ) ;
741+ let datasheet_path = component_dir. join ( format ! ( "{}.pdf" , & sanitized_mpn) ) ;
751742
752- // Embed STEP into footprint if both exist
753- if footprint_path. exists ( ) && step_path. exists ( ) {
754- embed_step_into_footprint_file ( & footprint_path, & step_path, true ) ?;
743+ if footprint_path. exists ( ) {
744+ if step_path. exists ( ) {
745+ embed_step_into_footprint_file ( & footprint_path, & step_path, true ) ?;
746+ } else {
747+ format_kicad_sexpr_file ( & footprint_path) ?;
748+ }
755749 }
756750
757- // Generate .zen file if symbol exists
758- if symbol_path. exists ( ) {
759- let symbol_lib = pcb_eda:: SymbolLibrary :: from_file ( & symbol_path) ?;
760- let symbol = symbol_lib
761- . first_symbol ( )
762- . ok_or_else ( || anyhow:: anyhow!( "No symbols in library" ) ) ?;
751+ if !symbol_path. exists ( ) {
752+ return Ok ( ( ) ) ;
753+ }
763754
764- let content = generate_zen_file (
765- mpn,
766- & sanitized_mpn,
767- symbol,
768- & format ! ( "{}.kicad_sym" , & sanitized_mpn) ,
769- has_footprint
770- . then ( || format ! ( "{}.kicad_mod" , & sanitized_mpn) )
771- . as_deref ( ) ,
772- has_datasheet
773- . then ( || format ! ( "{}.pdf" , & sanitized_mpn) )
774- . as_deref ( ) ,
775- manufacturer,
776- ) ?;
755+ let mut symbol_source = fs:: read_to_string ( & symbol_path)
756+ . with_context ( || format ! ( "Failed to read KiCad symbol {}" , symbol_path. display( ) ) ) ?;
777757
778- let zen_file = component_dir. join ( format ! ( "{}.zen" , & sanitized_mpn) ) ;
779- write_component_files ( & zen_file, component_dir, & content) ?;
758+ if footprint_path. exists ( ) {
759+ let footprint_path_str = footprint_path. to_string_lossy ( ) ;
760+ let ( footprint_ref, _) = pcb_sch:: kicad_netlist:: format_footprint ( & footprint_path_str) ;
761+ symbol_source = rewrite_symbol_footprint_property_text ( & symbol_source, & footprint_ref) ?;
780762 }
781763
764+ let symbol_formatted = format_kicad_sexpr_source ( & symbol_source, & symbol_path) ?;
765+ fs:: write ( & symbol_path, & symbol_formatted)
766+ . with_context ( || format ! ( "Failed to write KiCad symbol {}" , symbol_path. display( ) ) ) ?;
767+
768+ // Generate .zen file from the exact symbol content we just wrote.
769+ let symbol_lib = pcb_eda:: SymbolLibrary :: from_string ( & symbol_formatted, "kicad_sym" ) ?;
770+ let symbol = symbol_lib
771+ . first_symbol ( )
772+ . ok_or_else ( || anyhow:: anyhow!( "No symbols in library" ) ) ?;
773+
774+ let content = generate_zen_file (
775+ mpn,
776+ & sanitized_mpn,
777+ symbol,
778+ & format ! ( "{}.kicad_sym" , & sanitized_mpn) ,
779+ footprint_path
780+ . exists ( )
781+ . then ( || format ! ( "{}.kicad_mod" , & sanitized_mpn) )
782+ . as_deref ( ) ,
783+ datasheet_path
784+ . exists ( )
785+ . then ( || format ! ( "{}.pdf" , & sanitized_mpn) )
786+ . as_deref ( ) ,
787+ manufacturer,
788+ ) ?;
789+
790+ let zen_file = component_dir. join ( format ! ( "{}.zen" , & sanitized_mpn) ) ;
791+ write_component_files ( & zen_file, component_dir, & content) ?;
792+
793+ Ok ( ( ) )
794+ }
795+
796+ fn rewrite_symbol_footprint_property_text ( source : & str , footprint_ref : & str ) -> Result < String > {
797+ let parsed = pcb_sexpr:: parse ( source) . map_err ( |e| anyhow:: anyhow!( e) ) ?;
798+ let mut patches = PatchSet :: new ( ) ;
799+
800+ parsed. walk ( |node, _ctx| {
801+ let Some ( items) = node. as_list ( ) else {
802+ return ;
803+ } ;
804+
805+ let is_footprint_property = items. first ( ) . and_then ( |n| n. as_sym ( ) ) == Some ( "property" )
806+ && items. get ( 1 ) . and_then ( |n| n. as_str ( ) . or_else ( || n. as_sym ( ) ) ) == Some ( "Footprint" ) ;
807+ if !is_footprint_property {
808+ return ;
809+ }
810+
811+ let Some ( value_node) = items. get ( 2 ) else {
812+ return ;
813+ } ;
814+ let current = value_node. as_str ( ) . or_else ( || value_node. as_sym ( ) ) ;
815+ if current != Some ( footprint_ref) {
816+ patches. replace_string ( value_node. span , footprint_ref) ;
817+ }
818+ } ) ;
819+
820+ let mut out = Vec :: new ( ) ;
821+ patches
822+ . write_to ( source, & mut out)
823+ . context ( "Failed to apply Footprint property patch" ) ?;
824+ let updated = String :: from_utf8 ( out) . context ( "Patched symbol is not valid UTF-8" ) ?;
825+ Ok ( updated)
826+ }
827+
828+ fn format_kicad_sexpr_source ( source : & str , path_for_error : & Path ) -> Result < String > {
829+ pcb_sexpr:: parse ( source)
830+ . map_err ( |e| anyhow:: anyhow!( e) )
831+ . with_context ( || {
832+ format ! (
833+ "Failed to parse KiCad S-expression file {}" ,
834+ path_for_error. display( )
835+ )
836+ } ) ?;
837+
838+ Ok ( prettify ( source, FormatMode :: Normal ) )
839+ }
840+
841+ fn format_kicad_sexpr_file ( path : & Path ) -> Result < ( ) > {
842+ let source = fs:: read_to_string ( path)
843+ . with_context ( || format ! ( "Failed to read KiCad file {}" , path. display( ) ) ) ?;
844+ let formatted = format_kicad_sexpr_source ( & source, path) ?;
845+ fs:: write ( path, formatted)
846+ . with_context ( || format ! ( "Failed to write KiCad file {}" , path. display( ) ) ) ?;
847+
782848 Ok ( ( ) )
783849}
784850
@@ -1170,13 +1236,7 @@ fn execute_from_dir(dir: &Path, workspace_root: &Path) -> Result<()> {
11701236
11711237 // Finalize: embed STEP, generate .zen file
11721238 println ! ( "{} Generating .zen file..." , "→" . blue( ) . bold( ) ) ;
1173- finalize_component (
1174- & component_dir,
1175- & mpn,
1176- manufacturer. as_deref ( ) ,
1177- has_footprint,
1178- has_datasheet,
1179- ) ?;
1239+ finalize_component ( & component_dir, & mpn, manufacturer. as_deref ( ) ) ?;
11801240
11811241 // Show result
11821242 let display_path = zen_file. strip_prefix ( workspace_root) . unwrap_or ( & zen_file) ;
@@ -1921,4 +1981,18 @@ mod tests {
19211981 assert ! ( zen_content. contains( "\" VIN\" : Pins.VIN" ) ) ;
19221982 assert ! ( zen_content. contains( "\" VOUT\" : Pins.VOUT" ) ) ;
19231983 }
1984+
1985+ #[ test]
1986+ fn test_rewrite_symbol_footprint_property_text ( ) {
1987+ let symbol = r#"(kicad_symbol_lib
1988+ (symbol "TEST"
1989+ (property "Reference" "U" (at 0 0 0))
1990+ (property "Footprint" "OldLib:OldFootprint" (at 0 0 0))
1991+ )
1992+ )"# ;
1993+ let updated =
1994+ rewrite_symbol_footprint_property_text ( symbol, "NewLib:NewFootprint" ) . unwrap ( ) ;
1995+ assert ! ( updated. contains( "(property \" Footprint\" \" NewLib:NewFootprint\" " ) ) ;
1996+ assert ! ( !updated. contains( "OldLib:OldFootprint" ) ) ;
1997+ }
19241998}
0 commit comments