Skip to content

v6.54.0

Choose a tag to compare

@github-actions github-actions released this 10 May 01:07
· 95 commits to main since this release

v6.54.0 (2026-05-10)

Bug Fixes

  • fix(plugin): retain plugin instance at module level — prevents GC-caused re-invoke failure

Root cause identified from lifecycle trace data:

jBOM was using JBOMFabricationPlugin().register() — the temporary Python
object has no references after register() returns and may be GC'd by
CPython's reference counting.

KiCad's ActionPlugin C++ registry stores a pointer to the Python-side
ActionPlugin wrapper. If KiCad did not increment the Python refcount when
registering (common in SWIG bindings), CPython can collect the Python wrapper
between invocations. KiCad's C++ call to plugin->Run() on the second click
then finds a dead/recycled Python object and silently suppresses the invocation
— Run() is never called, _run_count never reaches #2.

Fix: store the instance in a module-level variable _plugin_instance before
calling register(). The module persists for the lifetime of the KiCad
session, keeping the Python wrapper alive indefinitely.

This exactly mirrors Fabrication-Toolkit's confirmed-working pattern:
plugin = Plugin()
plugin.register()

The logging added in the previous commit will confirm the fix: on the second
click we should see 'Run() invoked #2' in the scripting console.

Co-Authored-By: Oz oz-agent@warp.dev (e39f87c)

  • fix(plugin): add pcbnew.Refresh() before Destroy() — toolbar re-invoke experiment

Path 1 experiment: add pcbnew.Refresh() at every final teardown point via
_refresh_and_destroy() helper, mirroring FT's updateDisplay() pattern.

Hypothesis: pcbnew.Refresh() posts an update event to KiCad's wx main loop,
causing KiCad's toolbar UpdateUI handler to re-evaluate ActionPlugin button
state. Without it, the button remains inert after the first dialog close.

Applied at all teardown sites:

  • _on_complete else-branch (auto-close success path)
  • _close_and_open closure (debug/error Close button)
  • _on_close (X button)
  • _on_error Close button
  • Input panel Cancel button

If this fixes re-invoke, root cause analysis will follow to confirm the
mechanism (UpdateUI cycle vs. wxDialog destructor signal vs. other).
If not, next step is diagnostic print() logging to determine whether
Run() is called on the 2nd click or suppressed before that.

Co-Authored-By: Oz oz-agent@warp.dev (bf354cd)

  • fix(plugin): wx.Dialog+Show() for toolbar re-invoke; Finder opens in debug mode

Bug 1 — Finder not opening when debug=True then Close:
_on_complete stay_open branch bound Close to lambda _e: self.Destroy()
without calling _open_folder first. Replace with _close_and_open() closure
that honours open_folder before destroying.

Bug 2 — toolbar button only works once:
wx.Frame.init creates a wxFrame C++ object; KiCad's plugin framework
does not re-enable the toolbar button on wxFrame destruction. wxDialog
destruction does trigger the re-enable.

Changes:

  • JBOMFabricationDialog now inherits wx.Dialog (not wx.Frame), with
    wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER and parent=None.
  • parent=None is required: KiCad's C++ dialog tracking triggers the
    toolbar re-enable on wxDialog destruction regardless of Python parent;
    a non-None parent also risks window-hierarchy interference.
  • EVT_CLOSE overrides wx.Dialog's default hide-on-close to call Destroy()
    unconditionally (renamed _on_frame_close -> _on_close).
  • plugin.py: remove wx.FindWindowByName('PcbFrame') lookup and unused
    wx import; dialog constructor no longer takes a parent argument.

Co-Authored-By: Oz oz-agent@warp.dev (627baa1)

  • fix(plugin): correct archive variable expansion; stop setting board modified flag

dialog.py: _on_archive_template_changed now uses the same two-step expansion
as plugin.py._expand_archive_template():

  1. pcbnew.ExpandTextVars() for custom project variables
  2. jBOM TextVariableExpander for standard title block vars
    (${TITLE}, ${REVISION}, ${DATE}, etc.)
    The previous version only ran step 1; since KiCad 10's ExpandTextVars
    does not expand ${TITLE}/${REVISION} from the title block, the unexpanded
    string '_${REVISION}' was being normalised to 'TITLE___REVISION'.

_fill_zones(): removed pcbnew.Refresh() call. Refresh() sets the board's
internal modified flag, which KiCad surfaces as an unsaved-changes prompt
even if the user only opened and cancelled the plugin. The zone fill is
still applied (affects Gerber export); only the live editor redraw is skipped.

Co-Authored-By: Oz oz-agent@warp.dev (58bb579)

  • fix(plugin): archive template re-expands live; add Run() traceback guard

dialog.py: _on_archive_template_changed now calls pcbnew.ExpandTextVars()
on every keystroke to re-expand the edited template, then updates both
the preview label AND self._archive_name so Generate uses the correct stem.
Fallback: use literal template text when pcbnew expansion is unavailable.

plugin.py: wrap Run() body in try/except with traceback.print_exc(sys.stderr)
so that second-click failures are surfaced in KiCad's Scripting Console
rather than being silently swallowed by KiCad's plugin framework.

Co-Authored-By: Oz oz-agent@warp.dev (6f70cb4)

  • fix(discovery): prefer directory-name .kicad_pro when multiple exist

When a project directory contains multiple .kicad_pro files (e.g. a main
project alongside a sub-project or variant), prefer the file whose stem
matches the directory name — the canonical KiCad convention.

Falls back to alphabetically first when no directory-name match exists.

This fixes POS (CPL) generation in projects with multi-.kicad_pro dirs,
unblocking cpl.csv output in the KiCad plugin smoke test.

Updated tests to reflect the new directory-name-preference behavior.

Co-Authored-By: Oz oz-agent@warp.dev (b1354c6)

  • fix(py39): add 'from future import annotations' to files missing it

KiCad 10 bundles Python 3.9 which does not support the X | Y union type
syntax natively at runtime. PEP 563 (from future import annotations)
makes all annotations lazy strings, enabling X | None syntax in Python 3.9.

Affected files (all were missing the future import):

  • services/project_discovery.py
  • services/project_file_resolver.py
  • services/inventory_reader.py
  • cli/bom.py
  • cli/parts.py
  • cli/pos.py

This fixes the TypeError that prevented POSWorkflow from loading inside
KiCad 10's embedded Python, causing cpl.csv to not be generated.

Also installed PyYAML into KiCad 10's bundled Python 3.9 (dev loop only;
PCM archive will vendor it automatically).

1240 tests passing.

Co-Authored-By: Oz oz-agent@warp.dev (9f05b6e)

  • fix(plugin): Close button actually closes dialog; success auto-closes

Two bugs fixed:

  1. _on_complete and _on_error changed the Cancel button label to 'Close'
    but left it bound to _on_progress_cancel which only disabled the button
    without calling EndModal. Rebind via Unbind+Bind to EndModal instead.

  2. Dialog stayed open whenever diagnostics were non-empty, which is always
    (resolution notes are collected unconditionally per ADR 0006). Changed
    condition: stay open only on actual failure (production_dir is None) or
    debug mode. Successful generation with diagnostics now auto-closes and
    opens Finder as intended.

Also: PyYAML confirmed missing from KiCad 10 bundled Python and installed
manually for the dev loop. PCM archive will vendor it automatically.
This fixes the 'only Generic fabricator' issue.

Co-Authored-By: Oz oz-agent@warp.dev (c4af1ff)

  • fix(plugin): fallback archive name to PCB filename stem when title block empty

When the board has no title block title, use the PCB filename stem
(e.g. 'cpNode-Xiao-68x90' from 'cpNode-Xiao-68x90.kicad_pcb') as the
archive name, with revision appended if present. Avoids the double-failure
where empty title block + missing .kicad_pro at guessed path both fail
and produce '(unknown)'.

Co-Authored-By: Oz oz-agent@warp.dev (c55ca31)

  • fix(plugin): skip gerbers, use pcbnew title block directly, surface config error

Three smoke-test fixes:

  • skip_gerbers=True: running kicad-cli subprocess from inside a KiCad plugin
    causes a hang on macOS (two KiCad instances conflict). Gerbers disabled until
    native pcbnew PLOT_CONTROLLER path is implemented.
  • Archive name: use pcbnew.GetBoard().GetTitleBlock() directly instead of
    reading from disk — faster and avoids kicad_pro filename guessing.
    Falls back to the disk-based create_metadata() path if board lookup fails.
  • Fabricator config load error: surface the exception in the Archive label
    instead of silently falling back to [Generic] only, making the root cause
    visible in the KiCad dialog.

1219 tests passing.

Co-Authored-By: Oz oz-agent@warp.dev (7491c90)

Features

  • feat(plugin): implement PcbnewGerberGenerator; fix three #227 blockers

Blocker 1 — Gerbers via pcbnew (not kicad-cli):

  • Add src/jbom/plugin/gerber_generator.py with PcbnewGerberGenerator
    using pcbnew.PLOT_CONTROLLER + EXCELLON_WRITER (ported from FT process.py).
  • Reads fabricator config's gerbers.layers stanza; uses board.GetLayerID()
    for layer resolution; skips UNDEFINED_LAYER with a diagnostic.
  • Lazy-imports pcbnew inside generate() so the module is importable without KiCad.
  • Remove skip_gerbers=True from dialog._worker(); replace FabricationWorkflow
    with direct A2 orchestration: BOMWorkflow → BOMWriter, POSWorkflow → POSWriter,
    PcbnewGerberGenerator → GerberPackager, BackupService (all three artifacts
    in one atomic backup).

Blocker 2 — Toolbar button works on every click:

  • dialog.py: JBOMFabricationDialog(wx.Dialog) → JBOMFabricationDialog(wx.Frame)
    with FRAME_FLOAT_ON_PARENT style; EVT_CLOSE handler added.
  • Cancel button explicitly bound to self.Destroy() (wx.Frame does not
    auto-handle wx.ID_CANCEL).
  • All EndModal() calls replaced with self.Destroy().
  • plugin.py: ShowModal() → Show(); dlg.Destroy() removed (frame owns lifecycle).

Blocker 3 — Gerber zip in production/ AND in backup:

  • Direct consequence of A2 orchestration above: BackupService is called
    once, after all three artifacts exist, producing a complete backup.

Tests: 13 new tests in tests/plugin/test_pcbnew_gerber_generator.py covering
importability, _load_gerber_policy (JLC config, fallback), UNDEFINED_LAYER
skip, disabled-layer skip, plot_error path, artifact collection, and
no_artifacts path. All 1253 tests pass.

Closes blockers 1, 2, 3 from issue #227.

Co-Authored-By: Oz oz-agent@warp.dev (fbd6b30)

  • feat(plugin): archive name template with text variable expansion
  • TitleBlockMetadata gains date and company fields (both readers updated)
  • TextVariableExpander service: expand_text_variables(template, TitleBlockMetadata)
    handles ${TITLE}, ${REVISION}, ${DATE}/${ISSUE_DATE}, ${CURRENT_DATE},
    ${COMPANY}; unknown vars left unchanged for custom project variable pass-through
  • FabricationRequest gains archive_stem: str = '' pre-expanded override;
    workflow uses it directly when set, falls back to ProjectMetadata otherwise
  • _resolve_archive_stem() helper factors out duplicate stem logic from
    _run_gerbers_with_packaging and _create_backup
  • PluginOptions gains archive_name_template (default '${TITLE}_${REVISION}')
  • Plugin dialog: Archive row is now editable TextCtrl with italic preview label
  • plugin.py: pcbnew.ExpandTextVars() then jBOM expander fallback then PCB stem
  • 21 new TextVariableExpander unit tests; plugin options test updated

1240 tests passing.

Co-Authored-By: Oz oz-agent@warp.dev (e607d6e)

  • feat(plugin): Session B — full storyboard dialog, step_callback, fabricator names (#227)

Full fabrication dialog replacing the Session A stub:

  • src/jbom/plugin/dialog.py: JBOMFabricationDialog — two-panel layout
    (input → progress) per plugin_ux_storyboard.md. Fabricator dropdown
    populated from get_fabricators_with_names(), inventory file picker,
    all storyboard checkboxes (SMD only, Exclude DNP, Fill zones, Create
    backup [UI only — skip_backup not yet on FabricationRequest], Open folder,
    Apply corrections [grayed/#249], Debug). Background thread via
    threading.Thread; step_callback fires wx.CallAfter for per-step gauge
    updates (BOM / CPL / Gerbers / Backup). Auto-close on success; stays open
    on error or debug. Opens Finder/Explorer on production folder.
    load_options() on open, save_options() on Generate.

  • src/jbom/plugin/plugin.py: Run() resolves archive name from
    project_metadata (title block + revision) and passes it to the dialog.
    Stub dialog reference updated to JBOMFabricationDialog.

  • src/jbom/application/fabrication_orchestration.py: Add optional
    step_callback: Callable[[str, str], None] | None = None to
    FabricationWorkflow.run(). Naked Callable — workflow stays wx-unaware.
    Docstring records CLI forward path:
    lambda step, status: print(f'{step}: {status}')
    Backwards-compatible: all existing callers pass nothing (None).

  • src/jbom/config/fabricators.py: Add get_fabricators_with_names() →
    list[tuple[str, str]]. Reads FabricatorConfig.name — no YAML changes.

  • docs/dev/guides/plugin-dev-setup.md: symlink command, activation steps,
    sys.path bootstrap explanation, PCM build command, troubleshooting.

New tests (1219 total, +16 from Session B):
tests/plugin/test_fabricators_with_names.py — 8 tests
tests/services/test_fabrication_step_callback.py — 8 tests

Co-Authored-By: Oz oz-agent@warp.dev (ca7606c)

  • feat(plugin): Session A POC — loadable stub ActionPlugin, options, PCM builder (#227)

Implements issue #227 Session A scope:

  • src/jbom/plugin/init.py: FT-style guard registers ActionPlugin only
    inside KiCad (pcbnew in sys.modules). Safe no-op in CLI / test environments.
    sys.path bootstrap supports both PCM install (vendored jbom/) and dev-loop
    (symlink src/jbom/plugin → com_spcoast_jbom in KiCad scripting folder).

  • src/jbom/plugin/plugin.py: JBOMFabricationPlugin(pcbnew.ActionPlugin)
    with defaults() (toolbar button, name, description) and Run() that opens
    the stub dialog. Only imported inside KiCad.

  • src/jbom/plugin/dialog.py: JBOMStubDialog — wx.Dialog showing jBOM version
    and PCB path, with Cancel button. Validates plugin is loadable and clickable.
    Full storyboard dialog (fabricator dropdown, inventory picker, progress view)
    deferred to Session B.

  • src/jbom/plugin/options.py: PluginOptions dataclass + load_options() /
    save_options() using git-root resolution → .jbom/jbom-options.json, with
    ~/.jbom/ fallback. Persists fabricator and inventory_path.

  • metadata.json: PCM addon manifest for com.spcoast.jbom. Placeholder
    download_sha256 / sizes; updated by build script with --update-metadata.

  • scripts/build_pcm_package.py: PCM archive builder (stdlib only). Copies
    plugin adapter files + vendored jbom core + sexpdata + yaml (pure-Python only,
    compiled .so/.pyd excluded) into the PCM plugins/ layout, zips to
    dist/jbom-pcm-{version}.zip, prints sha256 and sizes.

  • tests/plugin/: 32 new tests covering guard importability (pcbnew absent),
    _is_standalone sentinel, inner module isolation, CLI independence, and full
    PluginOptions save/load round-trip with malformed-JSON resilience.

Dev-loop setup (macOS KiCad 9):
ln -s $PWD/src/jbom/plugin
~/Library/Application\ Support/kicad/9.0/scripting/plugins/com_spcoast_jbom

Note: use com_spcoast_jbom (not jbom) as the symlink name to avoid
a sys.path naming conflict with the importable jbom package.

Relates to ADR 0007 Phase 2, ADR 0005 Phase 4.

Co-Authored-By: Oz oz-agent@warp.dev (ec4f570)

Unknown

  • diag(plugin): add lifecycle logging for toolbar re-invoke investigation

Adds [jBOM plugin] / [jBOM dialog] print() traces to stderr at every
key lifecycle point, visible in KiCad's scripting console
(Tools -> Scripting Console).

Second click: presence/absence of Run() #2 is the key data point.

Co-Authored-By: Oz oz-agent@warp.dev (5be647a)