chore: normalize all tracked files to LF line endings
Mechanical application of the `.gitattributes` rules from the prior commit. All 50 files differ only in line endings — verified by `git diff --cached --ignore-all-space` being empty. Before: main had 42 CRLF + 27 LF Python files plus mixed-ending in YAML, templates, and shell scripts. After: every text file is LF (except the Windows-native *.ps1, *.bat scripts which remain CRLF per gitattributes). This eliminates the noisy-diff failure mode seen in PR #102, where a small logic change produced a 918-line diff due to whole-file CRLF→LF conversion.
This commit is contained in:
@@ -1 +1 @@
|
||||
"""Utility modules for KiCAD MCP Server"""
|
||||
"""Utility modules for KiCAD MCP Server"""
|
||||
|
||||
@@ -1,349 +1,349 @@
|
||||
"""
|
||||
KiCAD Process Management Utilities
|
||||
|
||||
Detects if KiCAD is running and provides auto-launch functionality.
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import time
|
||||
from ctypes import wintypes
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class KiCADProcessManager:
|
||||
"""Manages KiCAD process detection and launching"""
|
||||
|
||||
@staticmethod
|
||||
def _windows_list_processes() -> List[dict]:
|
||||
"""List running processes on Windows using Toolhelp API."""
|
||||
processes: List[dict] = []
|
||||
try:
|
||||
TH32CS_SNAPPROCESS = 0x00000002
|
||||
try:
|
||||
ulong_ptr = wintypes.ULONG_PTR # type: ignore[attr-defined]
|
||||
except AttributeError:
|
||||
ulong_ptr = (
|
||||
ctypes.c_ulonglong if ctypes.sizeof(ctypes.c_void_p) == 8 else ctypes.c_ulong
|
||||
)
|
||||
|
||||
class PROCESSENTRY32W(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("dwSize", wintypes.DWORD),
|
||||
("cntUsage", wintypes.DWORD),
|
||||
("th32ProcessID", wintypes.DWORD),
|
||||
("th32DefaultHeapID", ulong_ptr),
|
||||
("th32ModuleID", wintypes.DWORD),
|
||||
("cntThreads", wintypes.DWORD),
|
||||
("th32ParentProcessID", wintypes.DWORD),
|
||||
("pcPriClassBase", wintypes.LONG),
|
||||
("dwFlags", wintypes.DWORD),
|
||||
("szExeFile", wintypes.WCHAR * wintypes.MAX_PATH),
|
||||
]
|
||||
|
||||
CreateToolhelp32Snapshot = ctypes.windll.kernel32.CreateToolhelp32Snapshot
|
||||
Process32FirstW = ctypes.windll.kernel32.Process32FirstW
|
||||
Process32NextW = ctypes.windll.kernel32.Process32NextW
|
||||
CloseHandle = ctypes.windll.kernel32.CloseHandle
|
||||
|
||||
snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
|
||||
if snapshot == wintypes.HANDLE(-1).value:
|
||||
return processes
|
||||
|
||||
entry = PROCESSENTRY32W()
|
||||
entry.dwSize = ctypes.sizeof(PROCESSENTRY32W)
|
||||
|
||||
if Process32FirstW(snapshot, ctypes.byref(entry)):
|
||||
while True:
|
||||
processes.append(
|
||||
{
|
||||
"pid": str(entry.th32ProcessID),
|
||||
"name": entry.szExeFile,
|
||||
"command": entry.szExeFile,
|
||||
}
|
||||
)
|
||||
if not Process32NextW(snapshot, ctypes.byref(entry)):
|
||||
break
|
||||
|
||||
CloseHandle(snapshot)
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing Windows processes: {e}")
|
||||
|
||||
return processes
|
||||
|
||||
@staticmethod
|
||||
def is_running() -> bool:
|
||||
"""
|
||||
Check if KiCAD is currently running
|
||||
|
||||
Returns:
|
||||
True if KiCAD process found, False otherwise
|
||||
"""
|
||||
system = platform.system()
|
||||
|
||||
try:
|
||||
if system == "Linux":
|
||||
# Check for actual pcbnew/kicad binaries (not python scripts)
|
||||
# Use exact process name matching to avoid matching our own kicad_interface.py
|
||||
result = subprocess.run(
|
||||
["pgrep", "-x", "pcbnew|kicad"], capture_output=True, text=True
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
# Also check with -f for full path matching, but exclude our script
|
||||
result = subprocess.run(
|
||||
["pgrep", "-f", "/pcbnew|/kicad"], capture_output=True, text=True
|
||||
)
|
||||
# Double-check it's not our own process
|
||||
if result.returncode == 0:
|
||||
pids = result.stdout.strip().split("\n")
|
||||
for pid in pids:
|
||||
try:
|
||||
cmdline = subprocess.run(
|
||||
["ps", "-p", pid, "-o", "command="], capture_output=True, text=True
|
||||
)
|
||||
if "kicad_interface.py" not in cmdline.stdout:
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
return False
|
||||
|
||||
elif system == "Darwin": # macOS
|
||||
result = subprocess.run(
|
||||
["pgrep", "-f", "KiCad|pcbnew"], capture_output=True, text=True
|
||||
)
|
||||
return result.returncode == 0
|
||||
|
||||
elif system == "Windows":
|
||||
processes = KiCADProcessManager._windows_list_processes()
|
||||
for proc in processes:
|
||||
name = (proc.get("name") or "").lower()
|
||||
if name in ("pcbnew.exe", "kicad.exe"):
|
||||
return True
|
||||
return False
|
||||
|
||||
else:
|
||||
logger.warning(f"Process detection not implemented for {system}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking if KiCAD is running: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_executable_path() -> Optional[Path]:
|
||||
"""
|
||||
Get path to KiCAD executable
|
||||
|
||||
Returns:
|
||||
Path to pcbnew/kicad executable, or None if not found
|
||||
"""
|
||||
system = platform.system()
|
||||
|
||||
# Try to find executable in PATH first
|
||||
for cmd in ["pcbnew", "kicad"]:
|
||||
result = subprocess.run(
|
||||
["which", cmd] if system != "Windows" else ["where", cmd],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="mbcs" if system == "Windows" else None,
|
||||
errors="ignore" if system == "Windows" else None,
|
||||
timeout=5 if system == "Windows" else None,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
exe_path = result.stdout.strip().split("\n")[0]
|
||||
logger.info(f"Found KiCAD executable: {exe_path}")
|
||||
return Path(exe_path)
|
||||
|
||||
# Platform-specific default paths
|
||||
if system == "Linux":
|
||||
candidates = [
|
||||
Path("/usr/bin/pcbnew"),
|
||||
Path("/usr/local/bin/pcbnew"),
|
||||
Path("/usr/bin/kicad"),
|
||||
]
|
||||
elif system == "Darwin": # macOS
|
||||
candidates = [
|
||||
Path("/Applications/KiCad/KiCad.app/Contents/MacOS/kicad"),
|
||||
Path("/Applications/KiCad/pcbnew.app/Contents/MacOS/pcbnew"),
|
||||
]
|
||||
elif system == "Windows":
|
||||
candidates = [
|
||||
Path("C:/Program Files/KiCad/9.0/bin/pcbnew.exe"),
|
||||
Path("C:/Program Files/KiCad/8.0/bin/pcbnew.exe"),
|
||||
Path("C:/Program Files (x86)/KiCad/9.0/bin/pcbnew.exe"),
|
||||
]
|
||||
else:
|
||||
candidates = []
|
||||
|
||||
for path in candidates:
|
||||
if path.exists():
|
||||
logger.info(f"Found KiCAD executable: {path}")
|
||||
return path
|
||||
|
||||
logger.warning("Could not find KiCAD executable")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def launch(project_path: Optional[Path] = None, wait_for_start: bool = True) -> bool:
|
||||
"""
|
||||
Launch KiCAD PCB Editor
|
||||
|
||||
Args:
|
||||
project_path: Optional path to .kicad_pcb file to open
|
||||
wait_for_start: Wait for process to start before returning
|
||||
|
||||
Returns:
|
||||
True if launch successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Check if already running
|
||||
if KiCADProcessManager.is_running():
|
||||
logger.info("KiCAD is already running")
|
||||
return True
|
||||
|
||||
# Find executable
|
||||
exe_path = KiCADProcessManager.get_executable_path()
|
||||
if not exe_path:
|
||||
logger.error("Cannot launch KiCAD: executable not found")
|
||||
return False
|
||||
|
||||
# Build command
|
||||
cmd = [str(exe_path)]
|
||||
if project_path:
|
||||
cmd.append(str(project_path))
|
||||
|
||||
logger.info(f"Launching KiCAD: {' '.join(cmd)}")
|
||||
|
||||
# Launch process in background
|
||||
system = platform.system()
|
||||
if system == "Windows":
|
||||
# Windows: Use CREATE_NEW_PROCESS_GROUP to detach
|
||||
subprocess.Popen(
|
||||
cmd,
|
||||
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
else:
|
||||
# Unix: Use nohup or start in background
|
||||
subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
start_new_session=True,
|
||||
)
|
||||
|
||||
# Wait for process to start
|
||||
if wait_for_start:
|
||||
logger.info("Waiting for KiCAD to start...")
|
||||
for i in range(10): # Wait up to 5 seconds
|
||||
time.sleep(0.5)
|
||||
if KiCADProcessManager.is_running():
|
||||
logger.info("✓ KiCAD started successfully")
|
||||
return True
|
||||
|
||||
logger.warning("KiCAD process not detected after launch")
|
||||
# Return True anyway, it might be starting
|
||||
return True
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error launching KiCAD: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_process_info() -> List[dict]:
|
||||
"""
|
||||
Get information about running KiCAD processes
|
||||
|
||||
Returns:
|
||||
List of process info dicts with pid, name, and command
|
||||
"""
|
||||
system = platform.system()
|
||||
processes = []
|
||||
|
||||
try:
|
||||
if system in ["Linux", "Darwin"]:
|
||||
result = subprocess.run(["ps", "aux"], capture_output=True, text=True)
|
||||
for line in result.stdout.split("\n"):
|
||||
# Only match actual KiCAD binaries, not our MCP server processes
|
||||
if (
|
||||
("pcbnew" in line.lower() or "kicad" in line.lower())
|
||||
and "kicad_interface.py" not in line
|
||||
and "grep" not in line
|
||||
):
|
||||
# More specific check: must have /pcbnew or /kicad in the path
|
||||
if "/pcbnew" in line or "/kicad" in line or "KiCad.app" in line:
|
||||
parts = line.split()
|
||||
if len(parts) >= 11:
|
||||
processes.append(
|
||||
{
|
||||
"pid": parts[1],
|
||||
"name": parts[10],
|
||||
"command": " ".join(parts[10:]),
|
||||
}
|
||||
)
|
||||
|
||||
elif system == "Windows":
|
||||
for proc in KiCADProcessManager._windows_list_processes():
|
||||
name = (proc.get("name") or "").lower()
|
||||
if "pcbnew" in name or "kicad" in name:
|
||||
processes.append(proc)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting process info: {e}")
|
||||
|
||||
return processes
|
||||
|
||||
|
||||
def check_and_launch_kicad(project_path: Optional[Path] = None, auto_launch: bool = True) -> dict:
|
||||
"""
|
||||
Check if KiCAD is running and optionally launch it
|
||||
|
||||
Args:
|
||||
project_path: Optional path to .kicad_pcb file to open
|
||||
auto_launch: If True, launch KiCAD if not running
|
||||
|
||||
Returns:
|
||||
Dict with status information
|
||||
"""
|
||||
manager = KiCADProcessManager()
|
||||
|
||||
is_running = manager.is_running()
|
||||
|
||||
if is_running:
|
||||
processes = manager.get_process_info()
|
||||
return {
|
||||
"running": True,
|
||||
"launched": False,
|
||||
"processes": processes,
|
||||
"message": "KiCAD is already running",
|
||||
}
|
||||
|
||||
if not auto_launch:
|
||||
return {
|
||||
"running": False,
|
||||
"launched": False,
|
||||
"processes": [],
|
||||
"message": "KiCAD is not running (auto-launch disabled)",
|
||||
}
|
||||
|
||||
# Try to launch
|
||||
logger.info("KiCAD not detected, attempting to launch...")
|
||||
success = manager.launch(project_path)
|
||||
|
||||
return {
|
||||
"running": success,
|
||||
"launched": success,
|
||||
"processes": manager.get_process_info() if success else [],
|
||||
"message": "KiCAD launched successfully" if success else "Failed to launch KiCAD",
|
||||
"project": str(project_path) if project_path else None,
|
||||
}
|
||||
"""
|
||||
KiCAD Process Management Utilities
|
||||
|
||||
Detects if KiCAD is running and provides auto-launch functionality.
|
||||
"""
|
||||
|
||||
import ctypes
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
import time
|
||||
from ctypes import wintypes
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class KiCADProcessManager:
|
||||
"""Manages KiCAD process detection and launching"""
|
||||
|
||||
@staticmethod
|
||||
def _windows_list_processes() -> List[dict]:
|
||||
"""List running processes on Windows using Toolhelp API."""
|
||||
processes: List[dict] = []
|
||||
try:
|
||||
TH32CS_SNAPPROCESS = 0x00000002
|
||||
try:
|
||||
ulong_ptr = wintypes.ULONG_PTR # type: ignore[attr-defined]
|
||||
except AttributeError:
|
||||
ulong_ptr = (
|
||||
ctypes.c_ulonglong if ctypes.sizeof(ctypes.c_void_p) == 8 else ctypes.c_ulong
|
||||
)
|
||||
|
||||
class PROCESSENTRY32W(ctypes.Structure):
|
||||
_fields_ = [
|
||||
("dwSize", wintypes.DWORD),
|
||||
("cntUsage", wintypes.DWORD),
|
||||
("th32ProcessID", wintypes.DWORD),
|
||||
("th32DefaultHeapID", ulong_ptr),
|
||||
("th32ModuleID", wintypes.DWORD),
|
||||
("cntThreads", wintypes.DWORD),
|
||||
("th32ParentProcessID", wintypes.DWORD),
|
||||
("pcPriClassBase", wintypes.LONG),
|
||||
("dwFlags", wintypes.DWORD),
|
||||
("szExeFile", wintypes.WCHAR * wintypes.MAX_PATH),
|
||||
]
|
||||
|
||||
CreateToolhelp32Snapshot = ctypes.windll.kernel32.CreateToolhelp32Snapshot
|
||||
Process32FirstW = ctypes.windll.kernel32.Process32FirstW
|
||||
Process32NextW = ctypes.windll.kernel32.Process32NextW
|
||||
CloseHandle = ctypes.windll.kernel32.CloseHandle
|
||||
|
||||
snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
|
||||
if snapshot == wintypes.HANDLE(-1).value:
|
||||
return processes
|
||||
|
||||
entry = PROCESSENTRY32W()
|
||||
entry.dwSize = ctypes.sizeof(PROCESSENTRY32W)
|
||||
|
||||
if Process32FirstW(snapshot, ctypes.byref(entry)):
|
||||
while True:
|
||||
processes.append(
|
||||
{
|
||||
"pid": str(entry.th32ProcessID),
|
||||
"name": entry.szExeFile,
|
||||
"command": entry.szExeFile,
|
||||
}
|
||||
)
|
||||
if not Process32NextW(snapshot, ctypes.byref(entry)):
|
||||
break
|
||||
|
||||
CloseHandle(snapshot)
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing Windows processes: {e}")
|
||||
|
||||
return processes
|
||||
|
||||
@staticmethod
|
||||
def is_running() -> bool:
|
||||
"""
|
||||
Check if KiCAD is currently running
|
||||
|
||||
Returns:
|
||||
True if KiCAD process found, False otherwise
|
||||
"""
|
||||
system = platform.system()
|
||||
|
||||
try:
|
||||
if system == "Linux":
|
||||
# Check for actual pcbnew/kicad binaries (not python scripts)
|
||||
# Use exact process name matching to avoid matching our own kicad_interface.py
|
||||
result = subprocess.run(
|
||||
["pgrep", "-x", "pcbnew|kicad"], capture_output=True, text=True
|
||||
)
|
||||
if result.returncode == 0:
|
||||
return True
|
||||
# Also check with -f for full path matching, but exclude our script
|
||||
result = subprocess.run(
|
||||
["pgrep", "-f", "/pcbnew|/kicad"], capture_output=True, text=True
|
||||
)
|
||||
# Double-check it's not our own process
|
||||
if result.returncode == 0:
|
||||
pids = result.stdout.strip().split("\n")
|
||||
for pid in pids:
|
||||
try:
|
||||
cmdline = subprocess.run(
|
||||
["ps", "-p", pid, "-o", "command="], capture_output=True, text=True
|
||||
)
|
||||
if "kicad_interface.py" not in cmdline.stdout:
|
||||
return True
|
||||
except:
|
||||
pass
|
||||
return False
|
||||
|
||||
elif system == "Darwin": # macOS
|
||||
result = subprocess.run(
|
||||
["pgrep", "-f", "KiCad|pcbnew"], capture_output=True, text=True
|
||||
)
|
||||
return result.returncode == 0
|
||||
|
||||
elif system == "Windows":
|
||||
processes = KiCADProcessManager._windows_list_processes()
|
||||
for proc in processes:
|
||||
name = (proc.get("name") or "").lower()
|
||||
if name in ("pcbnew.exe", "kicad.exe"):
|
||||
return True
|
||||
return False
|
||||
|
||||
else:
|
||||
logger.warning(f"Process detection not implemented for {system}")
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking if KiCAD is running: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_executable_path() -> Optional[Path]:
|
||||
"""
|
||||
Get path to KiCAD executable
|
||||
|
||||
Returns:
|
||||
Path to pcbnew/kicad executable, or None if not found
|
||||
"""
|
||||
system = platform.system()
|
||||
|
||||
# Try to find executable in PATH first
|
||||
for cmd in ["pcbnew", "kicad"]:
|
||||
result = subprocess.run(
|
||||
["which", cmd] if system != "Windows" else ["where", cmd],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="mbcs" if system == "Windows" else None,
|
||||
errors="ignore" if system == "Windows" else None,
|
||||
timeout=5 if system == "Windows" else None,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
exe_path = result.stdout.strip().split("\n")[0]
|
||||
logger.info(f"Found KiCAD executable: {exe_path}")
|
||||
return Path(exe_path)
|
||||
|
||||
# Platform-specific default paths
|
||||
if system == "Linux":
|
||||
candidates = [
|
||||
Path("/usr/bin/pcbnew"),
|
||||
Path("/usr/local/bin/pcbnew"),
|
||||
Path("/usr/bin/kicad"),
|
||||
]
|
||||
elif system == "Darwin": # macOS
|
||||
candidates = [
|
||||
Path("/Applications/KiCad/KiCad.app/Contents/MacOS/kicad"),
|
||||
Path("/Applications/KiCad/pcbnew.app/Contents/MacOS/pcbnew"),
|
||||
]
|
||||
elif system == "Windows":
|
||||
candidates = [
|
||||
Path("C:/Program Files/KiCad/9.0/bin/pcbnew.exe"),
|
||||
Path("C:/Program Files/KiCad/8.0/bin/pcbnew.exe"),
|
||||
Path("C:/Program Files (x86)/KiCad/9.0/bin/pcbnew.exe"),
|
||||
]
|
||||
else:
|
||||
candidates = []
|
||||
|
||||
for path in candidates:
|
||||
if path.exists():
|
||||
logger.info(f"Found KiCAD executable: {path}")
|
||||
return path
|
||||
|
||||
logger.warning("Could not find KiCAD executable")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def launch(project_path: Optional[Path] = None, wait_for_start: bool = True) -> bool:
|
||||
"""
|
||||
Launch KiCAD PCB Editor
|
||||
|
||||
Args:
|
||||
project_path: Optional path to .kicad_pcb file to open
|
||||
wait_for_start: Wait for process to start before returning
|
||||
|
||||
Returns:
|
||||
True if launch successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
# Check if already running
|
||||
if KiCADProcessManager.is_running():
|
||||
logger.info("KiCAD is already running")
|
||||
return True
|
||||
|
||||
# Find executable
|
||||
exe_path = KiCADProcessManager.get_executable_path()
|
||||
if not exe_path:
|
||||
logger.error("Cannot launch KiCAD: executable not found")
|
||||
return False
|
||||
|
||||
# Build command
|
||||
cmd = [str(exe_path)]
|
||||
if project_path:
|
||||
cmd.append(str(project_path))
|
||||
|
||||
logger.info(f"Launching KiCAD: {' '.join(cmd)}")
|
||||
|
||||
# Launch process in background
|
||||
system = platform.system()
|
||||
if system == "Windows":
|
||||
# Windows: Use CREATE_NEW_PROCESS_GROUP to detach
|
||||
subprocess.Popen(
|
||||
cmd,
|
||||
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
else:
|
||||
# Unix: Use nohup or start in background
|
||||
subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
start_new_session=True,
|
||||
)
|
||||
|
||||
# Wait for process to start
|
||||
if wait_for_start:
|
||||
logger.info("Waiting for KiCAD to start...")
|
||||
for i in range(10): # Wait up to 5 seconds
|
||||
time.sleep(0.5)
|
||||
if KiCADProcessManager.is_running():
|
||||
logger.info("✓ KiCAD started successfully")
|
||||
return True
|
||||
|
||||
logger.warning("KiCAD process not detected after launch")
|
||||
# Return True anyway, it might be starting
|
||||
return True
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error launching KiCAD: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def get_process_info() -> List[dict]:
|
||||
"""
|
||||
Get information about running KiCAD processes
|
||||
|
||||
Returns:
|
||||
List of process info dicts with pid, name, and command
|
||||
"""
|
||||
system = platform.system()
|
||||
processes = []
|
||||
|
||||
try:
|
||||
if system in ["Linux", "Darwin"]:
|
||||
result = subprocess.run(["ps", "aux"], capture_output=True, text=True)
|
||||
for line in result.stdout.split("\n"):
|
||||
# Only match actual KiCAD binaries, not our MCP server processes
|
||||
if (
|
||||
("pcbnew" in line.lower() or "kicad" in line.lower())
|
||||
and "kicad_interface.py" not in line
|
||||
and "grep" not in line
|
||||
):
|
||||
# More specific check: must have /pcbnew or /kicad in the path
|
||||
if "/pcbnew" in line or "/kicad" in line or "KiCad.app" in line:
|
||||
parts = line.split()
|
||||
if len(parts) >= 11:
|
||||
processes.append(
|
||||
{
|
||||
"pid": parts[1],
|
||||
"name": parts[10],
|
||||
"command": " ".join(parts[10:]),
|
||||
}
|
||||
)
|
||||
|
||||
elif system == "Windows":
|
||||
for proc in KiCADProcessManager._windows_list_processes():
|
||||
name = (proc.get("name") or "").lower()
|
||||
if "pcbnew" in name or "kicad" in name:
|
||||
processes.append(proc)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting process info: {e}")
|
||||
|
||||
return processes
|
||||
|
||||
|
||||
def check_and_launch_kicad(project_path: Optional[Path] = None, auto_launch: bool = True) -> dict:
|
||||
"""
|
||||
Check if KiCAD is running and optionally launch it
|
||||
|
||||
Args:
|
||||
project_path: Optional path to .kicad_pcb file to open
|
||||
auto_launch: If True, launch KiCAD if not running
|
||||
|
||||
Returns:
|
||||
Dict with status information
|
||||
"""
|
||||
manager = KiCADProcessManager()
|
||||
|
||||
is_running = manager.is_running()
|
||||
|
||||
if is_running:
|
||||
processes = manager.get_process_info()
|
||||
return {
|
||||
"running": True,
|
||||
"launched": False,
|
||||
"processes": processes,
|
||||
"message": "KiCAD is already running",
|
||||
}
|
||||
|
||||
if not auto_launch:
|
||||
return {
|
||||
"running": False,
|
||||
"launched": False,
|
||||
"processes": [],
|
||||
"message": "KiCAD is not running (auto-launch disabled)",
|
||||
}
|
||||
|
||||
# Try to launch
|
||||
logger.info("KiCAD not detected, attempting to launch...")
|
||||
success = manager.launch(project_path)
|
||||
|
||||
return {
|
||||
"running": success,
|
||||
"launched": success,
|
||||
"processes": manager.get_process_info() if success else [],
|
||||
"message": "KiCAD launched successfully" if success else "Failed to launch KiCAD",
|
||||
"project": str(project_path) if project_path else None,
|
||||
}
|
||||
|
||||
@@ -1,325 +1,325 @@
|
||||
"""
|
||||
Platform detection and path utilities for cross-platform compatibility
|
||||
|
||||
This module provides helpers for detecting the current platform and
|
||||
getting appropriate paths for KiCAD, configuration, logs, etc.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PlatformHelper:
|
||||
"""Platform detection and path resolution utilities"""
|
||||
|
||||
@staticmethod
|
||||
def is_windows() -> bool:
|
||||
"""Check if running on Windows"""
|
||||
return platform.system() == "Windows"
|
||||
|
||||
@staticmethod
|
||||
def is_linux() -> bool:
|
||||
"""Check if running on Linux"""
|
||||
return platform.system() == "Linux"
|
||||
|
||||
@staticmethod
|
||||
def is_macos() -> bool:
|
||||
"""Check if running on macOS"""
|
||||
return platform.system() == "Darwin"
|
||||
|
||||
@staticmethod
|
||||
def get_platform_name() -> str:
|
||||
"""Get human-readable platform name"""
|
||||
system = platform.system()
|
||||
if system == "Darwin":
|
||||
return "macOS"
|
||||
return system
|
||||
|
||||
@staticmethod
|
||||
def get_kicad_python_paths() -> List[Path]:
|
||||
"""
|
||||
Get potential KiCAD Python dist-packages paths for current platform
|
||||
|
||||
Returns:
|
||||
List of potential paths to check (in priority order)
|
||||
"""
|
||||
paths = []
|
||||
|
||||
if PlatformHelper.is_windows():
|
||||
# Windows: Check Program Files
|
||||
program_files = [
|
||||
Path("C:/Program Files/KiCad"),
|
||||
Path("C:/Program Files (x86)/KiCad"),
|
||||
]
|
||||
for pf in program_files:
|
||||
# Check multiple KiCAD versions
|
||||
for version in ["9.0", "9.1", "10.0", "8.0"]:
|
||||
path = pf / version / "lib" / "python3" / "dist-packages"
|
||||
if path.exists():
|
||||
paths.append(path)
|
||||
|
||||
elif PlatformHelper.is_linux():
|
||||
# Linux: Check common installation paths
|
||||
candidates = [
|
||||
Path("/usr/lib/kicad/lib/python3/dist-packages"),
|
||||
Path("/usr/share/kicad/scripting/plugins"),
|
||||
Path("/usr/local/lib/kicad/lib/python3/dist-packages"),
|
||||
Path.home() / ".local/lib/kicad/lib/python3/dist-packages",
|
||||
]
|
||||
|
||||
# Also check based on Python version
|
||||
py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
||||
candidates.extend(
|
||||
[
|
||||
Path(f"/usr/lib/python{py_version}/dist-packages/kicad"),
|
||||
Path(f"/usr/local/lib/python{py_version}/dist-packages/kicad"),
|
||||
]
|
||||
)
|
||||
|
||||
# Check system Python dist-packages (modern KiCAD 9+ on Ubuntu/Debian)
|
||||
# This is where pcbnew.py typically lives on modern systems
|
||||
candidates.extend(
|
||||
[
|
||||
Path(f"/usr/lib/python3/dist-packages"),
|
||||
Path(f"/usr/lib/python{py_version}/dist-packages"),
|
||||
Path(f"/usr/local/lib/python3/dist-packages"),
|
||||
Path(f"/usr/local/lib/python{py_version}/dist-packages"),
|
||||
]
|
||||
)
|
||||
|
||||
paths = [p for p in candidates if p.exists()]
|
||||
|
||||
elif PlatformHelper.is_macos():
|
||||
# macOS: Check multiple KiCAD application bundle locations
|
||||
kicad_app_paths = [
|
||||
Path("/Applications/KiCad/KiCad.app"),
|
||||
Path("/Applications/KiCAD/KiCad.app"), # Alternative capitalization
|
||||
Path.home() / "Applications" / "KiCad" / "KiCad.app", # User Applications
|
||||
]
|
||||
|
||||
# Check Python framework paths in each KiCAD installation
|
||||
for kicad_app in kicad_app_paths:
|
||||
if kicad_app.exists():
|
||||
for version in ["3.9", "3.10", "3.11", "3.12", "3.13"]:
|
||||
path = (
|
||||
kicad_app
|
||||
/ "Contents"
|
||||
/ "Frameworks"
|
||||
/ "Python.framework"
|
||||
/ "Versions"
|
||||
/ version
|
||||
/ "lib"
|
||||
/ f"python{version}"
|
||||
/ "site-packages"
|
||||
)
|
||||
if path.exists():
|
||||
paths.append(path)
|
||||
|
||||
# Also check Homebrew Python site-packages (if pcbnew installed via pip)
|
||||
homebrew_paths = [
|
||||
Path("/opt/homebrew/lib/python3.12/site-packages"), # Apple Silicon
|
||||
Path("/opt/homebrew/lib/python3.11/site-packages"),
|
||||
Path("/usr/local/lib/python3.12/site-packages"), # Intel Mac
|
||||
Path("/usr/local/lib/python3.11/site-packages"),
|
||||
]
|
||||
for hp in homebrew_paths:
|
||||
pcbnew_path = hp / "pcbnew.py"
|
||||
if pcbnew_path.exists():
|
||||
paths.append(hp)
|
||||
|
||||
if not paths:
|
||||
logger.warning(f"No KiCAD Python paths found for {PlatformHelper.get_platform_name()}")
|
||||
else:
|
||||
logger.info(f"Found {len(paths)} potential KiCAD Python paths")
|
||||
|
||||
return paths
|
||||
|
||||
@staticmethod
|
||||
def get_kicad_python_path() -> Optional[Path]:
|
||||
"""
|
||||
Get the first valid KiCAD Python path
|
||||
|
||||
Returns:
|
||||
Path to KiCAD Python dist-packages, or None if not found
|
||||
"""
|
||||
paths = PlatformHelper.get_kicad_python_paths()
|
||||
return paths[0] if paths else None
|
||||
|
||||
@staticmethod
|
||||
def get_kicad_library_search_paths() -> List[str]:
|
||||
"""
|
||||
Get platform-appropriate KiCAD symbol library search paths
|
||||
|
||||
Returns:
|
||||
List of glob patterns for finding .kicad_sym files
|
||||
"""
|
||||
patterns = []
|
||||
|
||||
if PlatformHelper.is_windows():
|
||||
patterns = [
|
||||
"C:/Program Files/KiCad/*/share/kicad/symbols/*.kicad_sym",
|
||||
"C:/Program Files (x86)/KiCad/*/share/kicad/symbols/*.kicad_sym",
|
||||
]
|
||||
elif PlatformHelper.is_linux():
|
||||
patterns = [
|
||||
"/usr/share/kicad/symbols/*.kicad_sym",
|
||||
"/usr/local/share/kicad/symbols/*.kicad_sym",
|
||||
str(Path.home() / ".local/share/kicad/symbols/*.kicad_sym"),
|
||||
]
|
||||
elif PlatformHelper.is_macos():
|
||||
patterns = [
|
||||
"/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols/*.kicad_sym",
|
||||
"/Applications/KiCAD/KiCad.app/Contents/SharedSupport/symbols/*.kicad_sym",
|
||||
str(
|
||||
Path.home()
|
||||
/ "Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols/*.kicad_sym"
|
||||
),
|
||||
]
|
||||
|
||||
# Add user library paths for all platforms
|
||||
patterns.append(str(Path.home() / "Documents" / "KiCad" / "*" / "symbols" / "*.kicad_sym"))
|
||||
|
||||
return patterns
|
||||
|
||||
@staticmethod
|
||||
def get_config_dir() -> Path:
|
||||
r"""
|
||||
Get appropriate configuration directory for current platform
|
||||
|
||||
Follows platform conventions:
|
||||
- Windows: %USERPROFILE%\.kicad-mcp
|
||||
- Linux: $XDG_CONFIG_HOME/kicad-mcp or ~/.config/kicad-mcp
|
||||
- macOS: ~/Library/Application Support/kicad-mcp
|
||||
|
||||
Returns:
|
||||
Path to configuration directory
|
||||
"""
|
||||
if PlatformHelper.is_windows():
|
||||
return Path.home() / ".kicad-mcp"
|
||||
elif PlatformHelper.is_linux():
|
||||
# Use XDG Base Directory specification
|
||||
xdg_config = os.environ.get("XDG_CONFIG_HOME")
|
||||
if xdg_config:
|
||||
xdg_config_path = Path(xdg_config).expanduser()
|
||||
if xdg_config_path.is_absolute():
|
||||
return xdg_config_path / "kicad-mcp"
|
||||
logger.warning("Ignoring relative XDG_CONFIG_HOME: %s", xdg_config)
|
||||
return Path.home() / ".config" / "kicad-mcp"
|
||||
elif PlatformHelper.is_macos():
|
||||
return Path.home() / "Library" / "Application Support" / "kicad-mcp"
|
||||
else:
|
||||
# Fallback for unknown platforms
|
||||
return Path.home() / ".kicad-mcp"
|
||||
|
||||
@staticmethod
|
||||
def get_log_dir() -> Path:
|
||||
"""
|
||||
Get appropriate log directory for current platform
|
||||
|
||||
Returns:
|
||||
Path to log directory
|
||||
"""
|
||||
config_dir = PlatformHelper.get_config_dir()
|
||||
return config_dir / "logs"
|
||||
|
||||
@staticmethod
|
||||
def get_cache_dir() -> Path:
|
||||
r"""
|
||||
Get appropriate cache directory for current platform
|
||||
|
||||
Follows platform conventions:
|
||||
- Windows: %USERPROFILE%\.kicad-mcp\cache
|
||||
- Linux: $XDG_CACHE_HOME/kicad-mcp or ~/.cache/kicad-mcp
|
||||
- macOS: ~/Library/Caches/kicad-mcp
|
||||
|
||||
Returns:
|
||||
Path to cache directory
|
||||
"""
|
||||
if PlatformHelper.is_windows():
|
||||
return PlatformHelper.get_config_dir() / "cache"
|
||||
elif PlatformHelper.is_linux():
|
||||
xdg_cache = os.environ.get("XDG_CACHE_HOME")
|
||||
if xdg_cache:
|
||||
xdg_cache_path = Path(xdg_cache).expanduser()
|
||||
if xdg_cache_path.is_absolute():
|
||||
return xdg_cache_path / "kicad-mcp"
|
||||
logger.warning("Ignoring relative XDG_CACHE_HOME: %s", xdg_cache)
|
||||
return Path.home() / ".cache" / "kicad-mcp"
|
||||
elif PlatformHelper.is_macos():
|
||||
return Path.home() / "Library" / "Caches" / "kicad-mcp"
|
||||
else:
|
||||
return PlatformHelper.get_config_dir() / "cache"
|
||||
|
||||
@staticmethod
|
||||
def ensure_directories() -> None:
|
||||
"""Create all necessary directories if they don't exist"""
|
||||
dirs_to_create = [
|
||||
PlatformHelper.get_config_dir(),
|
||||
PlatformHelper.get_log_dir(),
|
||||
PlatformHelper.get_cache_dir(),
|
||||
]
|
||||
|
||||
for directory in dirs_to_create:
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
logger.debug(f"Ensured directory exists: {directory}")
|
||||
|
||||
@staticmethod
|
||||
def get_python_executable() -> Path:
|
||||
"""Get path to current Python executable"""
|
||||
return Path(sys.executable)
|
||||
|
||||
@staticmethod
|
||||
def add_kicad_to_python_path() -> bool:
|
||||
"""
|
||||
Add KiCAD Python paths to sys.path
|
||||
|
||||
Returns:
|
||||
True if at least one path was added, False otherwise
|
||||
"""
|
||||
paths_added = False
|
||||
|
||||
for path in PlatformHelper.get_kicad_python_paths():
|
||||
if str(path) not in sys.path:
|
||||
sys.path.insert(0, str(path))
|
||||
logger.info(f"Added to Python path: {path}")
|
||||
paths_added = True
|
||||
|
||||
return paths_added
|
||||
|
||||
|
||||
# Convenience function for quick platform detection
|
||||
def detect_platform() -> dict:
|
||||
"""
|
||||
Detect platform and return useful information
|
||||
|
||||
Returns:
|
||||
Dictionary with platform information
|
||||
"""
|
||||
return {
|
||||
"system": platform.system(),
|
||||
"platform": PlatformHelper.get_platform_name(),
|
||||
"is_windows": PlatformHelper.is_windows(),
|
||||
"is_linux": PlatformHelper.is_linux(),
|
||||
"is_macos": PlatformHelper.is_macos(),
|
||||
"python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
||||
"python_executable": str(PlatformHelper.get_python_executable()),
|
||||
"config_dir": str(PlatformHelper.get_config_dir()),
|
||||
"log_dir": str(PlatformHelper.get_log_dir()),
|
||||
"cache_dir": str(PlatformHelper.get_cache_dir()),
|
||||
"kicad_python_paths": [str(p) for p in PlatformHelper.get_kicad_python_paths()],
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Quick test/diagnostic
|
||||
import json
|
||||
|
||||
info = detect_platform()
|
||||
print("Platform Information:")
|
||||
print(json.dumps(info, indent=2))
|
||||
"""
|
||||
Platform detection and path utilities for cross-platform compatibility
|
||||
|
||||
This module provides helpers for detecting the current platform and
|
||||
getting appropriate paths for KiCAD, configuration, logs, etc.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PlatformHelper:
|
||||
"""Platform detection and path resolution utilities"""
|
||||
|
||||
@staticmethod
|
||||
def is_windows() -> bool:
|
||||
"""Check if running on Windows"""
|
||||
return platform.system() == "Windows"
|
||||
|
||||
@staticmethod
|
||||
def is_linux() -> bool:
|
||||
"""Check if running on Linux"""
|
||||
return platform.system() == "Linux"
|
||||
|
||||
@staticmethod
|
||||
def is_macos() -> bool:
|
||||
"""Check if running on macOS"""
|
||||
return platform.system() == "Darwin"
|
||||
|
||||
@staticmethod
|
||||
def get_platform_name() -> str:
|
||||
"""Get human-readable platform name"""
|
||||
system = platform.system()
|
||||
if system == "Darwin":
|
||||
return "macOS"
|
||||
return system
|
||||
|
||||
@staticmethod
|
||||
def get_kicad_python_paths() -> List[Path]:
|
||||
"""
|
||||
Get potential KiCAD Python dist-packages paths for current platform
|
||||
|
||||
Returns:
|
||||
List of potential paths to check (in priority order)
|
||||
"""
|
||||
paths = []
|
||||
|
||||
if PlatformHelper.is_windows():
|
||||
# Windows: Check Program Files
|
||||
program_files = [
|
||||
Path("C:/Program Files/KiCad"),
|
||||
Path("C:/Program Files (x86)/KiCad"),
|
||||
]
|
||||
for pf in program_files:
|
||||
# Check multiple KiCAD versions
|
||||
for version in ["9.0", "9.1", "10.0", "8.0"]:
|
||||
path = pf / version / "lib" / "python3" / "dist-packages"
|
||||
if path.exists():
|
||||
paths.append(path)
|
||||
|
||||
elif PlatformHelper.is_linux():
|
||||
# Linux: Check common installation paths
|
||||
candidates = [
|
||||
Path("/usr/lib/kicad/lib/python3/dist-packages"),
|
||||
Path("/usr/share/kicad/scripting/plugins"),
|
||||
Path("/usr/local/lib/kicad/lib/python3/dist-packages"),
|
||||
Path.home() / ".local/lib/kicad/lib/python3/dist-packages",
|
||||
]
|
||||
|
||||
# Also check based on Python version
|
||||
py_version = f"{sys.version_info.major}.{sys.version_info.minor}"
|
||||
candidates.extend(
|
||||
[
|
||||
Path(f"/usr/lib/python{py_version}/dist-packages/kicad"),
|
||||
Path(f"/usr/local/lib/python{py_version}/dist-packages/kicad"),
|
||||
]
|
||||
)
|
||||
|
||||
# Check system Python dist-packages (modern KiCAD 9+ on Ubuntu/Debian)
|
||||
# This is where pcbnew.py typically lives on modern systems
|
||||
candidates.extend(
|
||||
[
|
||||
Path(f"/usr/lib/python3/dist-packages"),
|
||||
Path(f"/usr/lib/python{py_version}/dist-packages"),
|
||||
Path(f"/usr/local/lib/python3/dist-packages"),
|
||||
Path(f"/usr/local/lib/python{py_version}/dist-packages"),
|
||||
]
|
||||
)
|
||||
|
||||
paths = [p for p in candidates if p.exists()]
|
||||
|
||||
elif PlatformHelper.is_macos():
|
||||
# macOS: Check multiple KiCAD application bundle locations
|
||||
kicad_app_paths = [
|
||||
Path("/Applications/KiCad/KiCad.app"),
|
||||
Path("/Applications/KiCAD/KiCad.app"), # Alternative capitalization
|
||||
Path.home() / "Applications" / "KiCad" / "KiCad.app", # User Applications
|
||||
]
|
||||
|
||||
# Check Python framework paths in each KiCAD installation
|
||||
for kicad_app in kicad_app_paths:
|
||||
if kicad_app.exists():
|
||||
for version in ["3.9", "3.10", "3.11", "3.12", "3.13"]:
|
||||
path = (
|
||||
kicad_app
|
||||
/ "Contents"
|
||||
/ "Frameworks"
|
||||
/ "Python.framework"
|
||||
/ "Versions"
|
||||
/ version
|
||||
/ "lib"
|
||||
/ f"python{version}"
|
||||
/ "site-packages"
|
||||
)
|
||||
if path.exists():
|
||||
paths.append(path)
|
||||
|
||||
# Also check Homebrew Python site-packages (if pcbnew installed via pip)
|
||||
homebrew_paths = [
|
||||
Path("/opt/homebrew/lib/python3.12/site-packages"), # Apple Silicon
|
||||
Path("/opt/homebrew/lib/python3.11/site-packages"),
|
||||
Path("/usr/local/lib/python3.12/site-packages"), # Intel Mac
|
||||
Path("/usr/local/lib/python3.11/site-packages"),
|
||||
]
|
||||
for hp in homebrew_paths:
|
||||
pcbnew_path = hp / "pcbnew.py"
|
||||
if pcbnew_path.exists():
|
||||
paths.append(hp)
|
||||
|
||||
if not paths:
|
||||
logger.warning(f"No KiCAD Python paths found for {PlatformHelper.get_platform_name()}")
|
||||
else:
|
||||
logger.info(f"Found {len(paths)} potential KiCAD Python paths")
|
||||
|
||||
return paths
|
||||
|
||||
@staticmethod
|
||||
def get_kicad_python_path() -> Optional[Path]:
|
||||
"""
|
||||
Get the first valid KiCAD Python path
|
||||
|
||||
Returns:
|
||||
Path to KiCAD Python dist-packages, or None if not found
|
||||
"""
|
||||
paths = PlatformHelper.get_kicad_python_paths()
|
||||
return paths[0] if paths else None
|
||||
|
||||
@staticmethod
|
||||
def get_kicad_library_search_paths() -> List[str]:
|
||||
"""
|
||||
Get platform-appropriate KiCAD symbol library search paths
|
||||
|
||||
Returns:
|
||||
List of glob patterns for finding .kicad_sym files
|
||||
"""
|
||||
patterns = []
|
||||
|
||||
if PlatformHelper.is_windows():
|
||||
patterns = [
|
||||
"C:/Program Files/KiCad/*/share/kicad/symbols/*.kicad_sym",
|
||||
"C:/Program Files (x86)/KiCad/*/share/kicad/symbols/*.kicad_sym",
|
||||
]
|
||||
elif PlatformHelper.is_linux():
|
||||
patterns = [
|
||||
"/usr/share/kicad/symbols/*.kicad_sym",
|
||||
"/usr/local/share/kicad/symbols/*.kicad_sym",
|
||||
str(Path.home() / ".local/share/kicad/symbols/*.kicad_sym"),
|
||||
]
|
||||
elif PlatformHelper.is_macos():
|
||||
patterns = [
|
||||
"/Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols/*.kicad_sym",
|
||||
"/Applications/KiCAD/KiCad.app/Contents/SharedSupport/symbols/*.kicad_sym",
|
||||
str(
|
||||
Path.home()
|
||||
/ "Applications/KiCad/KiCad.app/Contents/SharedSupport/symbols/*.kicad_sym"
|
||||
),
|
||||
]
|
||||
|
||||
# Add user library paths for all platforms
|
||||
patterns.append(str(Path.home() / "Documents" / "KiCad" / "*" / "symbols" / "*.kicad_sym"))
|
||||
|
||||
return patterns
|
||||
|
||||
@staticmethod
|
||||
def get_config_dir() -> Path:
|
||||
r"""
|
||||
Get appropriate configuration directory for current platform
|
||||
|
||||
Follows platform conventions:
|
||||
- Windows: %USERPROFILE%\.kicad-mcp
|
||||
- Linux: $XDG_CONFIG_HOME/kicad-mcp or ~/.config/kicad-mcp
|
||||
- macOS: ~/Library/Application Support/kicad-mcp
|
||||
|
||||
Returns:
|
||||
Path to configuration directory
|
||||
"""
|
||||
if PlatformHelper.is_windows():
|
||||
return Path.home() / ".kicad-mcp"
|
||||
elif PlatformHelper.is_linux():
|
||||
# Use XDG Base Directory specification
|
||||
xdg_config = os.environ.get("XDG_CONFIG_HOME")
|
||||
if xdg_config:
|
||||
xdg_config_path = Path(xdg_config).expanduser()
|
||||
if xdg_config_path.is_absolute():
|
||||
return xdg_config_path / "kicad-mcp"
|
||||
logger.warning("Ignoring relative XDG_CONFIG_HOME: %s", xdg_config)
|
||||
return Path.home() / ".config" / "kicad-mcp"
|
||||
elif PlatformHelper.is_macos():
|
||||
return Path.home() / "Library" / "Application Support" / "kicad-mcp"
|
||||
else:
|
||||
# Fallback for unknown platforms
|
||||
return Path.home() / ".kicad-mcp"
|
||||
|
||||
@staticmethod
|
||||
def get_log_dir() -> Path:
|
||||
"""
|
||||
Get appropriate log directory for current platform
|
||||
|
||||
Returns:
|
||||
Path to log directory
|
||||
"""
|
||||
config_dir = PlatformHelper.get_config_dir()
|
||||
return config_dir / "logs"
|
||||
|
||||
@staticmethod
|
||||
def get_cache_dir() -> Path:
|
||||
r"""
|
||||
Get appropriate cache directory for current platform
|
||||
|
||||
Follows platform conventions:
|
||||
- Windows: %USERPROFILE%\.kicad-mcp\cache
|
||||
- Linux: $XDG_CACHE_HOME/kicad-mcp or ~/.cache/kicad-mcp
|
||||
- macOS: ~/Library/Caches/kicad-mcp
|
||||
|
||||
Returns:
|
||||
Path to cache directory
|
||||
"""
|
||||
if PlatformHelper.is_windows():
|
||||
return PlatformHelper.get_config_dir() / "cache"
|
||||
elif PlatformHelper.is_linux():
|
||||
xdg_cache = os.environ.get("XDG_CACHE_HOME")
|
||||
if xdg_cache:
|
||||
xdg_cache_path = Path(xdg_cache).expanduser()
|
||||
if xdg_cache_path.is_absolute():
|
||||
return xdg_cache_path / "kicad-mcp"
|
||||
logger.warning("Ignoring relative XDG_CACHE_HOME: %s", xdg_cache)
|
||||
return Path.home() / ".cache" / "kicad-mcp"
|
||||
elif PlatformHelper.is_macos():
|
||||
return Path.home() / "Library" / "Caches" / "kicad-mcp"
|
||||
else:
|
||||
return PlatformHelper.get_config_dir() / "cache"
|
||||
|
||||
@staticmethod
|
||||
def ensure_directories() -> None:
|
||||
"""Create all necessary directories if they don't exist"""
|
||||
dirs_to_create = [
|
||||
PlatformHelper.get_config_dir(),
|
||||
PlatformHelper.get_log_dir(),
|
||||
PlatformHelper.get_cache_dir(),
|
||||
]
|
||||
|
||||
for directory in dirs_to_create:
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
logger.debug(f"Ensured directory exists: {directory}")
|
||||
|
||||
@staticmethod
|
||||
def get_python_executable() -> Path:
|
||||
"""Get path to current Python executable"""
|
||||
return Path(sys.executable)
|
||||
|
||||
@staticmethod
|
||||
def add_kicad_to_python_path() -> bool:
|
||||
"""
|
||||
Add KiCAD Python paths to sys.path
|
||||
|
||||
Returns:
|
||||
True if at least one path was added, False otherwise
|
||||
"""
|
||||
paths_added = False
|
||||
|
||||
for path in PlatformHelper.get_kicad_python_paths():
|
||||
if str(path) not in sys.path:
|
||||
sys.path.insert(0, str(path))
|
||||
logger.info(f"Added to Python path: {path}")
|
||||
paths_added = True
|
||||
|
||||
return paths_added
|
||||
|
||||
|
||||
# Convenience function for quick platform detection
|
||||
def detect_platform() -> dict:
|
||||
"""
|
||||
Detect platform and return useful information
|
||||
|
||||
Returns:
|
||||
Dictionary with platform information
|
||||
"""
|
||||
return {
|
||||
"system": platform.system(),
|
||||
"platform": PlatformHelper.get_platform_name(),
|
||||
"is_windows": PlatformHelper.is_windows(),
|
||||
"is_linux": PlatformHelper.is_linux(),
|
||||
"is_macos": PlatformHelper.is_macos(),
|
||||
"python_version": f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}",
|
||||
"python_executable": str(PlatformHelper.get_python_executable()),
|
||||
"config_dir": str(PlatformHelper.get_config_dir()),
|
||||
"log_dir": str(PlatformHelper.get_log_dir()),
|
||||
"cache_dir": str(PlatformHelper.get_cache_dir()),
|
||||
"kicad_python_paths": [str(p) for p in PlatformHelper.get_kicad_python_paths()],
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Quick test/diagnostic
|
||||
import json
|
||||
|
||||
info = detect_platform()
|
||||
print("Platform Information:")
|
||||
print(json.dumps(info, indent=2))
|
||||
|
||||
Reference in New Issue
Block a user