Initial commit: MIKILAB KiCad personal library

Self-contained KiCad library (symbols, footprints, 3D models, docs,
import/check tooling) with no dependency on kicad-personal-library.

- Fixed sym-lib-table/fp-lib-table: single (version 7) header, one entry
  per library, MIKILAB_<name> nicknames, ${KIPRJMOD}-relative URIs
- Resolved the SOT95P280X145-5N footprint collision (TPS7A2012PDBVR vs
  TPS7A2018PDBVR): confirmed byte-identical modulo KiCad's internal
  tedit timestamp, unified into one shared footprint
- Resolved a case-insensitive filename collision between the official
  Diode.kicad_sym and a custom diode.kicad_sym
- Wrapped standalone footprints (ESP32-S31-WROOM-3, IC_TPS63020DSJT,
  SOT95P280X145-5N) into their own .pretty libraries so they're
  actually registered in fp-lib-table
- Fixed broken symbol->footprint references (including one pointing at
  a nonexistent easyeda2kicad library)
- Added scripts/check_library.py, import_component.py, add_component.py,
  import_batch.py, and lib_common.py
This commit is contained in:
2026-08-16 13:08:59 +02:00
commit 93a521777d
618 changed files with 13362142 additions and 0 deletions
+240
View File
@@ -0,0 +1,240 @@
#!/usr/bin/env python3
"""
import_component.py
====================
Import a single component (symbol + optional footprint + optional 3D model)
into the MIKILAB KiCad library.
Usage:
python3 scripts/import_component.py \\
--name TPS7A2018PDBVR \\
--symbol /path/to/TPS7A2018PDBVR.kicad_sym \\
--footprint /path/to/SOT95P280X145-5N.kicad_mod \\
--model /path/to/TPS7A2018PDBVR.step \\
--category power
A component may be imported with just a symbol, symbol+footprint, or
symbol+footprint+3D model. --category is optional; if omitted it is
inferred from --name using the same classification rules used elsewhere
in this library.
Never overwrites an existing component. Never silently duplicates a
footprint that already exists byte-for-byte elsewhere in the library --
it is reused instead. A footprint with a colliding filename but different
content is given a distinct, semantically-derived name and the collision
is documented in the report and in MANIFEST.csv.
After a successful import, sym-lib-table and fp-lib-table are fully
regenerated from the contents of the library directory tree, which
guarantees there is never more than one "(version 7)" entry and that
every library on disk is registered exactly once.
"""
from __future__ import annotations
import argparse
import shutil
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import lib_common as lc
ROOT = lc.LIBRARY_ROOT
class ImportError_(Exception):
pass
def validate_args(args) -> None:
if not args.name.strip():
raise ImportError_("--name must not be empty")
for label, value in (("--symbol", args.symbol), ("--footprint", args.footprint), ("--model", args.model)):
if value is not None and not Path(value).expanduser().is_file():
raise ImportError_(f"{label} does not exist or is not a file: {value}")
if args.model and not args.footprint:
raise ImportError_("--model requires --footprint (a 3D model needs a footprint to attach to)")
if args.category and args.category not in lc.CATEGORIES:
raise ImportError_(
f"--category '{args.category}' is not one of the known categories: "
+ ", ".join(lc.CATEGORIES)
)
def import_symbol(name: str, category: str, src: Path, manifest_rows: list, report: list) -> Path:
existing = lc.find_symbol_by_name(ROOT, name)
if existing is not None:
raise ImportError_(
f"A component named '{name}' already exists: {existing.relative_to(ROOT)}. "
f"Refusing to overwrite -- choose a different --name or remove the existing "
f"component first."
)
dst = ROOT / "symbols" / category / f"{name}.kicad_sym"
dst.parent.mkdir(parents=True, exist_ok=True)
digest = lc.sha256_file(src)
shutil.copy2(src, dst)
manifest_rows.append(["symbol", str(src), str(dst.relative_to(ROOT)), "NEW", digest, f"category={category}"])
report.append(f" SYMBOL NEW {dst.relative_to(ROOT)}")
return dst
def import_footprint(name: str, category: str, src: Path, manifest_rows: list, report: list):
"""Returns (footprint_path_or_None_if_reused, fp_nickname, fp_name, status)."""
digest = lc.sha256_file(src)
existing = lc.find_footprint_by_hash(ROOT, digest)
if existing is not None:
pretty_dir = existing.parent
nickname = lc.fp_nickname(pretty_dir)
manifest_rows.append([
"footprint", str(src), str(existing.relative_to(ROOT)), "DUPLICATE", digest,
f"reused existing identical footprint instead of duplicating; category={category}",
])
report.append(f" FOOTPRINT DUPLICATE reused {existing.relative_to(ROOT)}")
return None, nickname, existing.stem, "DUPLICATE"
pretty_dir = ROOT / "footprints" / category / f"{name}.pretty"
basename = src.name
colliding = lc.find_footprints_by_basename(ROOT, basename)
if colliding:
new_basename = f"{name}_{src.stem}{src.suffix}"
note = (
f"filename collision with {', '.join(str(p.relative_to(ROOT)) for p in colliding)} "
f"(different content, verified by SHA256) -> renamed to '{new_basename}'"
)
target = pretty_dir / new_basename
status = "RENAMED_COLLISION"
else:
target = pretty_dir / basename
status = "NEW"
note = f"category={category}"
pretty_dir.mkdir(parents=True, exist_ok=True)
shutil.copy2(src, target)
manifest_rows.append(["footprint", str(src), str(target.relative_to(ROOT)), status, digest, note])
report.append(f" FOOTPRINT {status:11} {target.relative_to(ROOT)}")
nickname = lc.fp_nickname(pretty_dir)
return target, nickname, target.stem, status
def import_model(name: str, category: str, src: Path, footprint_path: Path | None, manifest_rows: list, report: list):
dst = ROOT / "3dmodels" / category / f"{name}{src.suffix.lower()}"
dst.parent.mkdir(parents=True, exist_ok=True)
digest = lc.sha256_file(src)
final, status = lc.unique_destination(dst, digest)
if status != "DUPLICATE":
shutil.copy2(src, final)
manifest_rows.append(["3d-model", str(src), str(final.relative_to(ROOT)), status, digest, f"category={category}"])
report.append(f" 3D MODEL {status:11} {final.relative_to(ROOT)}")
if footprint_path is None:
report.append(
" NOTE: footprint was reused from an existing shared library; the 3D model "
"was copied but NOT embedded in that footprint (it may already be used by "
"another component with a different 3D body -- assign it manually per-instance "
"in the PCB editor if needed)."
)
return
text = footprint_path.read_text(encoding="utf-8")
model_uri = "${KIPRJMOD}/" + str(final.relative_to(ROOT))
if "(model " in text:
report.append(
f" NOTE: {footprint_path.relative_to(ROOT)} already has a 3D model reference; "
f"leaving it untouched. New model is available at {final.relative_to(ROOT)}."
)
return
block = (
f" (model {model_uri}\n"
f" (offset (xyz 0 0 0))\n"
f" (scale (xyz 1 1 1))\n"
f" (rotate (xyz 0 0 0))\n"
f" )\n"
)
assert text.rstrip().endswith(")")
idx = text.rstrip().rfind(")")
text = text.rstrip()[:idx] + block + ")\n"
footprint_path.write_text(text, encoding="utf-8")
report.append(f" Linked 3D model into {footprint_path.relative_to(ROOT)}")
def main() -> int:
parser = argparse.ArgumentParser(description="Import a single component into the MIKILAB library.")
parser.add_argument("--name", required=True, help="Component name (used as the symbol/footprint base name)")
parser.add_argument("--category", choices=lc.CATEGORIES, help="MIKILAB category (auto-detected from --name if omitted)")
parser.add_argument("--symbol", required=True, help="Path to the source .kicad_sym file")
parser.add_argument("--footprint", help="Path to the source .kicad_mod file")
parser.add_argument("--model", help="Path to the source 3D model (.step/.stp/.wrl/.wrz)")
args = parser.parse_args()
try:
validate_args(args)
except ImportError_ as e:
print(f"ERROR: {e}")
return 1
name = args.name.strip()
category = args.category or lc.classify(name)
manifest_rows: list[list[str]] = []
report: list[str] = [f"Importing '{name}' into category '{category}'"]
try:
import_symbol(name, category, Path(args.symbol).expanduser().resolve(), manifest_rows, report)
except ImportError_ as e:
print(f"ERROR: {e}")
lc.append_manifest_rows(ROOT, [["symbol", args.symbol, "", "ERROR", "", str(e)]])
return 1
fp_path = None
if args.footprint:
fp_path, fp_nick, fp_name, _ = import_footprint(
name, category, Path(args.footprint).expanduser().resolve(), manifest_rows, report
)
if args.model:
model_target = fp_path if fp_path is not None else None
import_model(name, category, Path(args.model).expanduser().resolve(), model_target, manifest_rows, report)
sym_path = ROOT / "symbols" / category / f"{name}.kicad_sym"
text = sym_path.read_text(encoding="utf-8")
new_ref = f"{fp_nick}:{fp_name}"
new_text, changed = lc.set_symbol_footprint_property(text, new_ref)
if changed:
sym_path.write_text(new_text, encoding="utf-8")
report.append(f" Linked symbol Footprint property -> {new_ref}")
else:
report.append(
f" NOTE: could not find a 'Footprint' property in {sym_path.relative_to(ROOT)} "
f"to update automatically -- set it manually to '{new_ref}'"
)
lc.append_manifest_rows(ROOT, manifest_rows)
sym_entries = lc.write_sym_lib_table(ROOT)
fp_entries = lc.write_fp_lib_table(ROOT)
report.append(f"Regenerated sym-lib-table ({len(sym_entries)} libraries) and fp-lib-table ({len(fp_entries)} libraries)")
print("\n".join(report))
print("\nOK.")
return 0
if __name__ == "__main__":
sys.exit(main())