From 90bc4e0222d5b203fc6746f72a90bd296836ca74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gerg=C5=91=20T=C3=B6rcsv=C3=A1ri?= Date: Mon, 24 Aug 2026 14:46:43 +0200 Subject: [PATCH] batched upload resave: --resave-batch CLI + one staging pass per bulk upload (tasks-runner 0004) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kicad_tools --resave-batch ...: N resaves in one process (one WASM init — marginal resave is ~50ms vs ~0.3s/process). Outputs in //, per-file "RESAVE-BATCH " stderr verdicts with the single-file 0/4/5 contract; exit 0 = loop completed, so an invalid file mid-batch can't mask its neighbors. loadSchematicHeadless now binds each schematic to ITS project via SETTINGS_MANAGER::GetProject, never Prj(): with aSetActive=false projects accumulate across batch entries and Prj() keeps returning the first one — the writer then stamped a wrong/empty project name into saved symbol-instance data. (UnloadProject is no fix: unloading the active project immediately reloads a null "" project.) Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01AH2iPekUGsEAYnMUD5BmAi --- tests/tools/cli-contract.ts | 39 ++++++++++++++++++++- wasm/cli/kicad_tools_main.cpp | 6 ++++ wasm/cli/sym_convert_main.cpp | 66 +++++++++++++++++++++++++++++++++-- 3 files changed, 108 insertions(+), 3 deletions(-) diff --git a/tests/tools/cli-contract.ts b/tests/tools/cli-contract.ts index ccb14ca..ae88a28 100644 --- a/tests/tools/cli-contract.ts +++ b/tests/tools/cli-contract.ts @@ -10,7 +10,7 @@ * * Run: cd tests && npm run tools:contract */ -import { execFileSync } from "node:child_process"; +import { execFileSync, spawnSync } from "node:child_process"; import { existsSync, mkdtempSync, @@ -127,6 +127,43 @@ try { console.log("skip footprint fixtures (kicad submodule not initialized)"); } + // --- resave-batch: N files, one process ---------------------------------- + // The bulk-upload normalization mode: outputs under //, + // per-file verdicts as "RESAVE-BATCH " stderr lines with + // the single-file code contract, process exit 0 when the loop completed — + // an invalid file mid-batch must not mask its neighbors. + { + const badMid = out("mid-garbage.kicad_sch"); + writeFileSync(badMid, "not a schematic ("); + const r = spawnSync("node", [cli, "--resave-batch", out("batch"), demoPcb, badMid, demoSch], { + stdio: ["ignore", "ignore", "pipe"], + }); + const stderr = r.stderr?.toString() ?? ""; + const codes = new Map(); + for (const m of stderr.matchAll(/^RESAVE-BATCH (\d+) (-?\d+)$/gm)) { + codes.set(Number(m[1]), Number(m[2])); + } + check("batch exits 0 despite an invalid entry", r.status === 0, `exit ${r.status}`); + check("batch reports one verdict line per entry", codes.size === 3, `${codes.size}`); + check("batch board entry verdicts 0", codes.get(0) === 0, `${codes.get(0)}`); + check("batch invalid entry verdicts 4", codes.get(1) === 4, `${codes.get(1)}`); + check("batch schematic entry verdicts 0", codes.get(2) === 0, `${codes.get(2)}`); + const producedPcb = path.join(out("batch"), "0", "demo.kicad_pcb"); + check( + "batch board output lands in its index dir and bumps the version", + existsSync(producedPcb) && version(producedPcb) > version(demoPcb), + ); + const producedSch = path.join(out("batch"), "2", "demo.kicad_sch"); + check("batch schematic output lands in its index dir", existsSync(producedSch)); + check( + "batch invalid entry produced no output dir", + !existsSync(path.join(out("batch"), "1", path.basename(badMid))), + ); + check("batch outputs lint clean", run(["--lint", producedPcb, producedSch]).code === 0); + + check("batch usage (no files) exits 2", run(["--resave-batch", out("bu")]).code === 2); + } + // --- exit-code contract --------------------------------------------------- { check("usage (no args) exits 2", run(["--resave"]).code === 2); diff --git a/wasm/cli/kicad_tools_main.cpp b/wasm/cli/kicad_tools_main.cpp index 51bce80..a138d84 100644 --- a/wasm/cli/kicad_tools_main.cpp +++ b/wasm/cli/kicad_tools_main.cpp @@ -25,6 +25,12 @@ * pcbToolsResaveBoard callback, .kicad_sch one file per * sheet, .kicad_sym/.lib via ConvertLibrary. Exit 4 = * input invalid, 5 = write failed) + * kicad_tools --resave-batch [...] + * (N resaves, one runtime init — bulk-upload + * normalization. Outputs in //; per-file + * verdicts as "RESAVE-BATCH " stderr + * lines with the single-file code contract; exit 0 when + * the loop completed) * kicad_tools --erc [--json] [--strict] [] * kicad_tools --netlist [--xml] [] * kicad_tools --bom [] diff --git a/wasm/cli/sym_convert_main.cpp b/wasm/cli/sym_convert_main.cpp index 6499824..316d339 100644 --- a/wasm/cli/sym_convert_main.cpp +++ b/wasm/cli/sym_convert_main.cpp @@ -616,8 +616,15 @@ std::unique_ptr loadSchematicHeadless( const char* aInPath ) wxFileName pro( fn ); pro.SetExt( wxS( "kicad_pro" ) ); trace( "loadSchematicHeadless: LoadProject" ); - manager.LoadProject( pro.FileExists() ? pro.GetFullPath() : wxString( wxEmptyString ), false ); - PROJECT& project = manager.Prj(); + const wxString proPath = pro.FileExists() ? pro.GetFullPath() : wxString( wxEmptyString ); + manager.LoadProject( proPath, false ); + // Bind THIS file's project, not Prj(): with aSetActive=false projects + // accumulate across --resave-batch entries and Prj() keeps returning the + // first one — the s-expr writer would then stamp the wrong (or an empty) + // project name into the saved symbol-instance data. GetProject resolves + // the exact project the LoadProject call above created (or reused). + PROJECT* loaded = manager.GetProject( proPath ); + PROJECT& project = loaded ? *loaded : manager.Prj(); project.SetElem( PROJECT::ELEM::LEGACY_SYMBOL_LIBS, nullptr ); auto schematic = std::make_unique( &project ); @@ -1258,6 +1265,60 @@ int symConvertMain( int argc, char** argv ) } } + if( argc >= 2 && std::strcmp( argv[1], "--resave-batch" ) == 0 ) + { + // Batch flavor of --resave (bulk-upload normalization): one process, + // one runtime init, N inputs. Each input's outputs land in + // // (index = position in the input list) so + // hierarchical-schematic sheet outputs from different roots never + // collide. Per-file verdicts use the single-file exit contract + // (0/2/4/5) and are reported on stderr as + // RESAVE-BATCH + // The process exits 0 when the batch LOOP completed — per-file + // failures live in the verdict lines, so one bad file can't mask the + // rest of the batch. Exit 2 = usage. + wxDisableAsserts(); + + if( argc < 4 ) + { + std::fprintf( stderr, + "usage: kicad_tools --resave-batch [...]\n" ); + return 2; + } + + for( int n = 3; n < argc; n++ ) + { + const int index = n - 3; + char sub[32]; + std::snprintf( sub, sizeof( sub ), "/%d", index ); + const std::string outDir = std::string( argv[2] ) + sub; + int rc; + + try + { + rc = runResave( argv[n], outDir.c_str() ); + } + catch( const std::exception& e ) + { + std::fprintf( stderr, "%s: error: %s\n", argv[n], e.what() ); + rc = 4; + } + + // Entries can come from DIFFERENT projects (a bulk upload of a + // multi-project tree); with aSetActive=false the loaded projects + // simply accumulate. That is safe because loadSchematicHeadless + // binds each schematic to ITS OWN project via GetProject — never + // to Prj(), which keeps returning the first loaded project. + // (UnloadProject is no help here: unloading the active project + // immediately reloads a null "" project, which would then shadow + // every later entry's project name in the saved instance data.) + + std::fprintf( stderr, "RESAVE-BATCH %d %d\n", index, rc ); + } + + return 0; + } + if( argc >= 2 && std::strcmp( argv[1], "--convert-lib" ) == 0 && argc >= 4 ) { // The legacy plugin asserts on relative paths and (worse) writes an @@ -1286,6 +1347,7 @@ int symConvertMain( int argc, char** argv ) std::fprintf( stderr, "usage: kicad_tools --convert-lib \n" " kicad_tools --lint [--strict] [...]\n" " kicad_tools --resave \n" + " kicad_tools --resave-batch [...]\n" " kicad_tools --erc [--json] [--strict] []\n" " kicad_tools --netlist [--xml] []\n" " kicad_tools --bom []\n"