app
Adom Desktop - KiCad Bridge
Public Made by Adomby adom
Reference implementation of the KiCad bridge — multi-instance Python server, forward path via kicad-cli, reverse path via in-process plugin. Most complex of the three bundled bridges.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
"""KiCad version check + upgrade via the OFFICIAL downloads.kicad.org installer.
Why not winget: winget's KiCad package lags the real KiCad release (often by a
minor version or more), and it can't pin a specific build. The official
`downloads.kicad.org` NSIS installer is the canonical, signed artifact — so the
bridge fetches THAT and installs it silently (`/S`).
- `check_for_updates` — read-only: current installed version vs latest STABLE
(resolved from KiCad's GitLab tags, baked fallback on a network blip), plus the
"your KiCad predates the IPC/kipy API — upgrade" nudge.
- `upgrade` — downloads the official installer and runs it silently. Needs
elevation for Program Files, so the user may see (and approve) a UAC prompt.
"""
import json
import re
import sys
import tempfile
import urllib.error
import urllib.request
from pathlib import Path
import kicad_detect
import proc
# Baked fallback if the GitLab tag query fails (network blip). Bump on new stable.
_FALLBACK_LATEST = "10.0.4"
_TAGS_URL = (
"https://gitlab.com/api/v4/projects/kicad%2Fcode%2Fkicad/repository/tags?per_page=30"
)
_DL_URL = "https://downloads.kicad.org/kicad/windows/explore/stable/download/kicad-{v}-x86_64.exe"
_UA = "adom-desktop-kicad-bridge"
def _is_stable(tag: str) -> bool:
"""True for a real stable tag like 10.0.4 / 9.0.9.1. Drops `-rcN` prereleases
and KiCad's `X.99.*` nightly/development tags (minor component == 99)."""
if not re.fullmatch(r"\d+(?:\.\d+)+", tag):
return False
parts = tag.split(".")
return not (len(parts) >= 2 and parts[1] == "99")
def latest_stable_version() -> tuple[str, str]:
"""Return (version, source) where source is 'gitlab' or 'fallback'."""
try:
req = urllib.request.Request(_TAGS_URL, headers={"User-Agent": _UA})
with urllib.request.urlopen(req, timeout=10) as r:
tags = [t["name"] for t in json.loads(r.read().decode())]
stables = sorted(
(t for t in tags if _is_stable(t)),
key=kicad_detect._version_tuple,
reverse=True,
)
if stables:
return stables[0], "gitlab"
except Exception:
pass
return _FALLBACK_LATEST, "fallback"
def download_url(version: str) -> str:
return _DL_URL.format(v=version)
def handle_check_for_updates(kicad_info: dict, args: dict) -> dict:
"""Read-only: is a newer KiCad available, and does the current one have the IPC API?"""
current = kicad_info.get("version") if kicad_info else None
latest, src = latest_stable_version()
cur_t = kicad_detect._version_tuple(current) if current else (0,)
lat_t = kicad_detect._version_tuple(latest)
upgrade_available = current is None or lat_t > cur_t
ipc = bool(kicad_info.get("ipcApiAvailable")) if kicad_info else False
if current is None:
rec = (
f"KiCad isn't installed. Run kicad_upgrade to install {latest} "
f"from the official downloads.kicad.org installer."
)
elif upgrade_available and not ipc:
rec = (
f"Your KiCad {current} predates the modern IPC / kipy automation API — "
f"upgrade to {latest} to unlock it (plus the latest fixes). Run kicad_upgrade."
)
elif upgrade_available:
rec = f"KiCad {latest} is available (you're on {current}). Run kicad_upgrade to update."
else:
rec = f"KiCad {current} is up to date (latest stable is {latest})."
return {
"success": True,
"currentVersion": current,
"latestVersion": latest,
"latestSource": src,
"upgradeAvailable": upgrade_available,
"ipcApiAvailable": ipc,
"downloadUrl": download_url(latest),
"recommendation": rec,
"_hint": (
"Run kicad_upgrade '{}' to install the latest (downloads the OFFICIAL "
"downloads.kicad.org installer + silent /S install — NOT winget, which lags). "
'Pass {"version":"X.Y.Z"} to pin a specific build.'
),
}
def handle_upgrade(kicad_info: dict, args: dict) -> dict:
"""Download the official KiCad installer and run it silently (/S)."""
if sys.platform != "win32":
return {
"success": False,
"error": "kicad_upgrade is Windows-only (it downloads the .exe installer).",
"errorCode": "unsupported_platform",
}
target = args.get("version") or latest_stable_version()[0]
current = kicad_info.get("version") if kicad_info else None
if (
current
and kicad_detect._version_tuple(current) >= kicad_detect._version_tuple(target)
and not args.get("force")
):
return {
"success": True,
"alreadyCurrent": True,
"currentVersion": current,
"targetVersion": target,
"_hint": f'KiCad {current} >= {target}; nothing to do. Pass {{"force":true}} to reinstall.',
}
url = download_url(target)
dest = Path(tempfile.gettempdir()) / f"kicad-{target}-x86_64.exe"
# Download — urllib follows the downloads.kicad.org -> s3.cern.ch redirect.
try:
req = urllib.request.Request(url, headers={"User-Agent": _UA})
total = 0
with urllib.request.urlopen(req, timeout=120) as r, open(dest, "wb") as f:
while True:
chunk = r.read(1 << 20)
if not chunk:
break
f.write(chunk)
total += len(chunk)
except (urllib.error.URLError, OSError) as e:
return {
"success": False,
"error": f"download failed: {e}",
"errorCode": "download_failed",
"url": url,
}
# Sanity: a real KiCad installer is a >50 MB Windows PE (MZ header).
if total < 50 * 1024 * 1024:
return {
"success": False,
"error": f"download too small ({total} bytes) — not a full installer",
"errorCode": "download_incomplete",
"url": url,
"path": str(dest),
}
with open(dest, "rb") as f:
if f.read(2) != b"MZ":
return {
"success": False,
"error": "downloaded file is not a Windows executable",
"errorCode": "download_corrupt",
"path": str(dest),
}
# Silent install (NSIS /S). Installing into Program Files needs elevation, so
# the user may get a UAC prompt to approve. Block until the installer exits.
try:
result = proc.run([str(dest), "/S"], timeout=900)
rc = result.returncode
except Exception as e:
return {
"success": False,
"error": f"installer launch failed: {e}",
"errorCode": "install_failed",
"path": str(dest),
"_hint": "If a UAC prompt appeared, approve it; or run the downloaded installer manually.",
}
# Confirm by re-scanning installed versions.
after = kicad_detect.detect_all_kicad_versions()
installed = [v["version"] for v in after]
now_has = any(
kicad_detect._version_tuple(v) >= kicad_detect._version_tuple(target)
for v in installed
)
return {
"success": bool(now_has),
"upgraded": bool(now_has),
"targetVersion": target,
"previousVersion": current,
"installerExit": rc,
"installedVersions": installed,
"downloadUrl": url,
"_hint": (
f"KiCad {target} installed."
if now_has
else (
f"Installer ran (exit {rc}) but {target} wasn't detected — it may have needed a "
f"UAC approval, or the bridge needs a restart to re-scan. Installed: "
f"{', '.join(installed) or 'none'}."
)
),
}