kicad-validity A1: kicad_tools --resave — full-parse rewrite in current format

New --resave <file> <outdir> subcommand (root docs/features/kicad-validity/
0001): .kicad_sch one file per sheet mirroring the hierarchy layout,
.kicad_pcb via pcbToolsResaveBoard callback (mirror of the lint callback),
.kicad_sym/.lib via ConvertLibrary. Exit 0 ok / 2 usage / 4 input invalid /
5 write failed — only 4 marks the input invalid for the upload gate.
The s-expr schematic save stub is now #ifndef KICAD_TOOLS_COMBINED: the
merged image links the real writer (+65 KB), standalone sym_convert keeps
its diet. Board save needed no un-stubbing (pcb_io/ survives the CMake
prune).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HLua64PCVwkQ1hpWdaf1Gm
This commit is contained in:
Gergő Törcsvári 2026-07-14 09:31:17 +02:00
commit c51ccf82ba
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
4 changed files with 237 additions and 3 deletions

View file

@ -17,6 +17,12 @@
* (.kicad_pcb files get a FULL parse here the pcbnew
* parser is linked; the lint driver calls back into
* pcbToolsLintBoard on the pcb side)
* kicad_tools --resave <file> <outdir>
* (full parse + rewrite in the current file-format
* version kicad-validity 0001; .kicad_pcb via the
* pcbToolsResaveBoard callback, .kicad_sch one file per
* sheet, .kicad_sym/.lib via ConvertLibrary. Exit 4 =
* input invalid, 5 = write failed)
* kicad_tools --erc [--json] [--strict] <file.kicad_sch> [<out>]
* kicad_tools --netlist [--xml] <file.kicad_sch> [<out>]
* kicad_tools --bom <file.kicad_sch> [<out>]

View file

@ -679,6 +679,68 @@ int pcbToolsLintBoard( const char* aInPath, std::string& aError )
return -1;
}
}
// Full-parse board resave for the merged image's --resave driver (also on the
// eeschema side): a bare load like pcbToolsLintBoard, then the s-expr writer
// rewrites the board in the current format version. Zones are written
// as-saved (no refill) — a format upgrade must not change geometry. Returns
// 0 resaved / 4 load or parse failed / 5 write failed, with aError filled.
int pcbToolsResaveBoard( const char* aInPath, const char* aOutPath, std::string& aError )
{
wxFileName fn( wxString::FromUTF8( aInPath ) );
fn.MakeAbsolute();
kiRuntime();
std::unique_ptr<BOARD> brd;
try
{
brd.reset( PCB_IO_MGR::Load( PCB_IO_MGR::KICAD_SEXP, fn.GetFullPath() ) );
if( !brd )
{
aError = std::string( aInPath ) + ": error: failed to load board";
return 4;
}
}
catch( PARSE_ERROR& pe )
{
char buf[1024];
std::snprintf( buf, sizeof( buf ), "%s:%d:%d: error: %s", aInPath, pe.lineNumber,
pe.byteIndex, (const char*) pe.ParseProblem().ToUTF8() );
aError = buf;
return 4;
}
catch( const IO_ERROR& ioe )
{
aError = std::string( aInPath ) + ": error: " + std::string( ioe.Problem().ToUTF8() );
return 4;
}
catch( const std::exception& e )
{
aError = std::string( aInPath ) + ": error: " + e.what();
return 4;
}
try
{
PCB_IO_MGR::Save( PCB_IO_MGR::KICAD_SEXP, wxString::FromUTF8( aOutPath ), brd.get() );
}
catch( const IO_ERROR& ioe )
{
aError = std::string( aOutPath ) + ": error: " + std::string( ioe.Problem().ToUTF8() );
return 5;
}
catch( const std::exception& e )
{
aError = std::string( aOutPath ) + ": error: " + e.what();
return 5;
}
return 0;
}
#endif // KICAD_TOOLS_COMBINED

View file

@ -99,6 +99,9 @@
// Merged image only: full-parse board lint via the pcbnew side
// (pcb_convert_main.cpp) — the standalone eeschema tree has no pcbnew parser.
int pcbToolsLintBoard( const char* aInPath, std::string& aError );
// Merged image only: board load + s-expr rewrite for --resave (kicad-validity
// 0001). 0 resaved / 4 load failed / 5 write failed.
int pcbToolsResaveBoard( const char* aInPath, const char* aOutPath, std::string& aError );
#endif
#include <connection_graph.h>
@ -700,6 +703,141 @@ wxString defaultOutPath( const wxFileName& aIn, const wxString& aSuffix, const w
return out.GetFullPath();
}
// ── headless resave — format upgrade (kicad-validity 0001) ───────────────────
// Full parse, then write back in the current file-format version. The write
// goes to <outdir> as files (never stdout): the tools-job runner's contract is
// "walk the output dir", and a hierarchical schematic produces one file per
// sheet, mirroring the sheet files' layout relative to the root file's
// directory (sheets outside it flatten to their basename). Zones/geometry are
// written as-loaded — a format upgrade must not change content.
// Exit: 0 resaved / 2 usage / 4 load or parse failed / 5 write failed. Only 4
// means "the input is not valid KiCad" — the upload gate keys off it.
int resaveSchematic( const char* aInPath, const wxFileName& aInFn, const wxString& aOutDir )
{
std::unique_ptr<SCHEMATIC> schematic = loadSchematicHeadless( aInPath );
if( !schematic )
return 4;
IO_RELEASER<SCH_IO> pi( SCH_IO_MGR::FindPlugin( SCH_IO_MGR::SCH_KICAD ) );
const wxString rootDir = aInFn.GetPath( wxPATH_GET_SEPARATOR );
SCH_SHEET_LIST sheetList = schematic->BuildSheetListSortedByPageNumbers();
std::unordered_set<SCH_SCREEN*> saved;
int files = 0;
for( SCH_SHEET_PATH& path : sheetList )
{
SCH_SHEET* sheet = path.Last();
SCH_SCREEN* screen = sheet ? sheet->GetScreen() : nullptr;
if( !screen || !saved.insert( screen ).second )
continue;
wxFileName srcFn( screen->GetFileName() );
srcFn.MakeAbsolute();
wxString rel;
if( srcFn.GetFullPath().StartsWith( rootDir, &rel ) )
; // rel = path under the root file's directory
else
rel = srcFn.GetFullName();
wxFileName outFn( aOutDir + rel );
if( !outFn.DirExists() && !outFn.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) )
{
std::fprintf( stderr, "%s: error: cannot create output directory %s\n", aInPath,
(const char*) outFn.GetPath().ToUTF8() );
return 5;
}
try
{
pi->SaveSchematicFile( outFn.GetFullPath(), sheet, schematic.get() );
}
catch( const IO_ERROR& ioe )
{
std::fprintf( stderr, "%s: error: %s\n",
(const char*) outFn.GetFullPath().ToUTF8(),
(const char*) ioe.Problem().ToUTF8() );
return 5;
}
files++;
}
std::fprintf( stderr, "%s: OK (resave, %d sheet files) -> %s\n", aInPath, files,
(const char*) aOutDir.ToUTF8() );
return 0;
}
int runResave( const char* aInPath, const char* aOutDir )
{
wxFileName inFn( wxString::FromUTF8( aInPath ) );
inFn.MakeAbsolute();
const wxString ext = inFn.GetExt().Lower();
wxFileName outDirFn = wxFileName::DirName( wxString::FromUTF8( aOutDir ) );
outDirFn.MakeAbsolute();
const wxString outDir = outDirFn.GetPath( wxPATH_GET_SEPARATOR );
if( !outDirFn.DirExists() && !outDirFn.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) )
{
std::fprintf( stderr, "%s: error: cannot create output directory %s\n", aInPath,
(const char*) outDir.ToUTF8() );
return 5;
}
if( ext == wxS( "kicad_sch" ) )
return resaveSchematic( aInPath, inFn, outDir );
if( ext == wxS( "kicad_sym" ) || ext == wxS( "lib" ) )
{
// ConvertLibrary reads any recognized library format and writes the
// current s-expr format — resave and legacy upgrade in one call.
const wxString outPath = outDir + inFn.GetName() + wxS( ".kicad_sym" );
if( !SCH_IO_MGR::ConvertLibrary( nullptr, inFn.GetFullPath(), outPath ) )
{
std::fprintf( stderr, "%s: error: failed to convert symbol library\n", aInPath );
return 4;
}
std::fprintf( stderr, "%s: OK (resave) -> %s\n", aInPath,
(const char*) outPath.ToUTF8() );
return 0;
}
if( ext == wxS( "kicad_pcb" ) )
{
#ifdef KICAD_TOOLS_COMBINED
const wxString outPath = outDir + inFn.GetFullName();
std::string error;
const int rc = pcbToolsResaveBoard( aInPath, (const char*) outPath.ToUTF8(), error );
if( rc != 0 )
std::fprintf( stderr, "%s\n", error.c_str() );
else
std::fprintf( stderr, "%s: OK (resave) -> %s\n", aInPath,
(const char*) outPath.ToUTF8() );
return rc;
#else
std::fprintf( stderr, "%s: error: board resave requires the merged kicad_tools build\n",
aInPath );
return 2;
#endif
}
std::fprintf( stderr, "%s: error: unsupported extension for --resave\n", aInPath );
return 2;
}
// ── headless ERC ──────────────────────────────────────────────────────────────
int runErc( const char* aInPath, bool aJson, bool aStrict, const char* aOutPath )
@ -1082,6 +1220,28 @@ int symConvertMain( int argc, char** argv )
return allOk ? 0 : 1;
}
if( argc >= 2 && std::strcmp( argv[1], "--resave" ) == 0 )
{
// Same rationale as --lint: headless runtime, parseable output.
wxDisableAsserts();
if( argc < 4 )
{
std::fprintf( stderr, "usage: kicad_tools --resave <file> <outdir>\n" );
return 2;
}
try
{
return runResave( argv[2], argv[3] );
}
catch( const std::exception& e )
{
std::fprintf( stderr, "%s: error: %s\n", argv[2], e.what() );
return 4;
}
}
if( argc >= 2 && std::strcmp( argv[1], "--convert-lib" ) == 0 && argc >= 4 )
{
// The legacy plugin asserts on relative paths and (worse) writes an
@ -1109,6 +1269,7 @@ int symConvertMain( int argc, char** argv )
std::fprintf( stderr, "usage: kicad_tools --convert-lib <input.lib> <output.kicad_sym>\n"
" kicad_tools --lint [--strict] <file> [<file>...]\n"
" kicad_tools --resave <file> <outdir>\n"
" kicad_tools --erc [--json] [--strict] <file.kicad_sch> [<out>]\n"
" kicad_tools --netlist [--xml] <file.kicad_sch> [<out>]\n"
" kicad_tools --bom <file.kicad_sch> [<out>]\n"

View file

@ -34,11 +34,15 @@
// ── (b) schematic-file entry points ──────────────────────────────────────────
// Originally BOTH plugins' load+save were stubbed, severing the whole
// schematic half. The --lint mode (ysync 0009 §7) needs the REAL s-expr
// LoadSchematicFile back, so only three of the four stay stubbed: the s-expr
// SAVE (lint/convert never write schematics) and the LEGACY load+save (.sch
// lint unsupported). The re-rooted schematic object model costs binary size —
// LoadSchematicFile back, and the merged kicad_tools image additionally
// restores the s-expr SAVE for --resave (kicad-validity 0001 format upgrade)
// — its serializer largely rides TUs the lint tier already links. The
// standalone sym_convert keeps the save stubbed (lint/convert never write
// schematics), and the LEGACY load+save stay stubbed everywhere (.sch
// unsupported). The re-rooted schematic object model costs binary size —
// the price of the lint tier riding this binary instead of a second wasm.
#ifndef KICAD_TOOLS_COMBINED
void SCH_IO_KICAD_SEXPR::SaveSchematicFile( const wxString& aFileName, SCH_SHEET*, SCHEMATIC*,
const std::map<std::string, UTF8>* )
{
@ -46,6 +50,7 @@ void SCH_IO_KICAD_SEXPR::SaveSchematicFile( const wxString& aFileName, SCH_SHEET
wxS( "sym_convert: schematic saving is compiled out (stub); cannot save '%s'" ),
aFileName ) );
}
#endif // !KICAD_TOOLS_COMBINED
SCH_SHEET* SCH_IO_KICAD_LEGACY::LoadSchematicFile( const wxString& aFileName, SCHEMATIC*,