Фикс скрипта

This commit is contained in:
abcbbck-png 2026-07-02 11:00:44 +03:00
parent 092bc6bdd9
commit cd059e52d4
2 changed files with 387 additions and 0 deletions

View file

@ -27,8 +27,20 @@ PDF-документация и 3D-модели хранятся вне этог
- прописывает переменные путей в `Configure Paths`;
- добавляет подключение DB Library;
- подключает таблицы символов и футпринтов из этого репозитория;
- проверяет и при необходимости предлагает установить PostgreSQL ODBC-драйвер
для DB Library (`psqlodbc` на Arch/CachyOS, `odbc-postgresql` на Ubuntu/Debian);
- создаёт резервные копии изменяемых файлов настроек KiCad.
Для полностью неинтерактивного запуска с установкой/регистрацией ODBC-драйвера:
```bash
./scripts/setup_kicad_user_library.py \
--pdf-dir "<путь-к-папке-PDF>" \
--non-interactive \
--install-odbc-driver \
--strict-odbc-check
```
## Переменные KiCad
| Переменная | Назначение |

View file

@ -8,12 +8,24 @@ import json
import os
import re
import shutil
import subprocess
import sys
import tempfile
from datetime import datetime
from pathlib import Path
ENV_VARS = ("KICAD_LIB_DIR_USER", "KICAD_PDF_DIR_USER", "KICAD_3D_PARTY_USER")
DATABASE_LIBRARY_FILE = "eda-core.kicad_dbl"
DEFAULT_POSTGRES_ODBC_DRIVER = "PostgreSQL Unicode"
DEFAULT_POSTGRES_ODBC_DRIVER_LIBS = (
Path("/usr/lib/psqlodbcw.so"),
Path("/usr/lib/x86_64-linux-gnu/odbc/psqlodbcw.so"),
Path("/usr/lib/aarch64-linux-gnu/odbc/psqlodbcw.so"),
Path("/usr/lib/arm-linux-gnueabihf/odbc/psqlodbcw.so"),
Path("/usr/lib/odbc/psqlodbcw.so"),
)
AUR_HELPERS = ("paru", "yay", "pikaur")
def repo_root() -> Path:
@ -66,6 +78,19 @@ def ask_path(name: str, prompt: str, default: str | None, required: bool = True)
print("Path is required.")
def ask_yes_no(prompt: str, default: bool = True) -> bool:
suffix = " [Y/n]" if default else " [y/N]"
while True:
value = input(f"{prompt}{suffix}: ").strip().lower()
if not value:
return default
if value in {"y", "yes", "д", "да"}:
return True
if value in {"n", "no", "н", "нет"}:
return False
print("Please answer yes or no.")
def backup(path: Path) -> Path:
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
target = path.with_name(f"{path.name}.bak-{stamp}")
@ -137,6 +162,316 @@ def configure_library_tables(config_dir: Path) -> None:
)
def warn(message: str) -> None:
print(f"warning: {message}", file=sys.stderr)
def extract_odbc_driver(connection_string: str) -> str | None:
match = re.search(
r"(?:^|;)\s*Driver\s*=\s*(?:\{([^}]*)\}|([^;]*))",
connection_string,
flags=re.IGNORECASE,
)
if not match:
return None
return (match.group(1) or match.group(2) or "").strip() or None
def read_database_source(lib_dir: str) -> tuple[Path, dict[str, object]] | None:
dbl_path = Path(lib_dir).expanduser() / DATABASE_LIBRARY_FILE
if not dbl_path.exists():
warn(f"database library file does not exist: {dbl_path}")
return None
data = json.loads(dbl_path.read_text(encoding="utf-8"))
source = data.get("source")
if not isinstance(source, dict):
warn(f"database library source block is missing: {dbl_path}")
return None
if source.get("type") != "odbc":
return None
return dbl_path, source
def run_command(args: list[str]) -> subprocess.CompletedProcess[str] | None:
try:
return subprocess.run(
args,
check=False,
text=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
except FileNotFoundError:
return None
def run_interactive_command(args: list[str], env: dict[str, str] | None = None) -> bool:
print("+ " + " ".join(args))
try:
result = subprocess.run(args, check=False, env=env)
except FileNotFoundError:
warn(f"command not found: {args[0]}")
return False
return result.returncode == 0
def with_sudo(args: list[str]) -> list[str]:
if os.geteuid() == 0:
return args
if shutil.which("sudo") is None:
raise SystemExit("sudo is required for system package/ODBC registration changes")
return ["sudo", *args]
def read_os_release() -> dict[str, str]:
path = Path("/etc/os-release")
if not path.exists():
return {}
data = {}
for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
data[key] = value.strip().strip('"')
return data
def detect_linux_family() -> str | None:
data = read_os_release()
distro_id = data.get("ID", "").lower()
id_like = set(data.get("ID_LIKE", "").lower().split())
if distro_id in {"arch", "cachyos", "manjaro", "endeavouros", "garuda"} or "arch" in id_like:
return "arch"
if distro_id in {"ubuntu", "debian", "linuxmint", "pop"} or id_like & {"debian", "ubuntu"}:
return "debian"
return None
def find_aur_helper() -> str | None:
for helper in AUR_HELPERS:
path = shutil.which(helper)
if path:
return path
return None
def query_odbc_driver(driver: str) -> tuple[bool, str]:
result = run_command(["odbcinst", "-q", "-d", "-n", driver])
if result is None:
return False, ""
return result.returncode == 0, result.stdout + result.stderr
def parse_driver_library(odbcinst_output: str) -> Path | None:
for line in odbcinst_output.splitlines():
key, sep, value = line.partition("=")
if sep and key.strip().lower() == "driver" and value.strip():
return Path(value.strip())
return None
def expected_driver_library(driver: str) -> Path | None:
if driver.lower() == DEFAULT_POSTGRES_ODBC_DRIVER.lower():
return locate_postgres_odbc_library() or DEFAULT_POSTGRES_ODBC_DRIVER_LIBS[0]
return None
def locate_postgres_odbc_library() -> Path | None:
for path in DEFAULT_POSTGRES_ODBC_DRIVER_LIBS:
if path.exists():
return path
for base in (Path("/usr/lib"), Path("/usr/local/lib")):
if not base.exists():
continue
matches = sorted(base.glob("**/psqlodbcw.so"))
if matches:
return matches[0]
return None
def print_postgres_odbc_help(driver: str, driver_lib: Path | None) -> None:
if driver.lower() != DEFAULT_POSTGRES_ODBC_DRIVER.lower():
print("ODBC driver is not ready. Register the driver in /etc/odbcinst.ini.")
return
lib_path = driver_lib or locate_postgres_odbc_library() or DEFAULT_POSTGRES_ODBC_DRIVER_LIBS[0]
print("PostgreSQL ODBC setup:")
print(" Arch/CachyOS:")
print(" paru -S psqlodbc")
print(" # or: yay -S psqlodbc")
print(" Ubuntu/Debian:")
print(" sudo apt-get install unixodbc odbc-postgresql")
print(" Ensure /etc/odbcinst.ini contains:")
print(f" [{DEFAULT_POSTGRES_ODBC_DRIVER}]")
print(f" Description={DEFAULT_POSTGRES_ODBC_DRIVER}")
print(f" Driver={lib_path}")
print(" UsageCount=1")
print(" Check registration:")
print(f" odbcinst -q -d -n '{DEFAULT_POSTGRES_ODBC_DRIVER}'")
def install_postgres_odbc_package(non_interactive: bool) -> bool:
family = detect_linux_family()
if family == "arch":
if shutil.which("pacman") is not None:
pacman_cmd = ["pacman", "-S", "--needed"]
if non_interactive:
pacman_cmd.append("--noconfirm")
pacman_cmd.append("unixodbc")
if not run_interactive_command(with_sudo(pacman_cmd)):
return False
helper = find_aur_helper()
if helper is None:
warn("psqlodbc is an AUR package on Arch/CachyOS, but no AUR helper was found")
warn("install it manually, for example: paru -S psqlodbc")
return False
helper_cmd = [helper, "-S", "--needed"]
if non_interactive:
helper_cmd.append("--noconfirm")
helper_cmd.append("psqlodbc")
return run_interactive_command(helper_cmd)
if family == "debian":
if shutil.which("apt-get") is None:
warn("apt-get was not found")
return False
env = os.environ.copy()
if non_interactive:
env["DEBIAN_FRONTEND"] = "noninteractive"
return run_interactive_command(
with_sudo(["apt-get", "install", "-y", "unixodbc", "odbc-postgresql"]),
env=env,
)
warn("unsupported Linux distribution for automatic psqlODBC installation")
warn("supported families: Arch/CachyOS and Ubuntu/Debian")
return False
def register_odbc_driver(driver: str, driver_lib: Path) -> bool:
if shutil.which("odbcinst") is None:
warn("odbcinst was not found after package installation")
return False
if not driver_lib.exists():
warn(f"cannot register missing driver library: {driver_lib}")
return False
template = (
f"[{driver}]\n"
f"Description={driver}\n"
f"Driver={driver_lib}\n"
"UsageCount=1\n"
)
with tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False) as tmp:
tmp.write(template)
template_path = Path(tmp.name)
try:
return run_interactive_command(
with_sudo(["odbcinst", "-i", "-d", "-f", str(template_path)])
)
finally:
template_path.unlink(missing_ok=True)
def install_and_register_odbc_driver(driver: str, non_interactive: bool) -> bool:
if driver.lower() != DEFAULT_POSTGRES_ODBC_DRIVER.lower():
warn(f"automatic installation is only implemented for {DEFAULT_POSTGRES_ODBC_DRIVER!r}")
return False
driver_lib = locate_postgres_odbc_library()
if driver_lib is None or shutil.which("odbcinst") is None:
print("Installing PostgreSQL ODBC driver package...")
if not install_postgres_odbc_package(non_interactive):
return False
driver_lib = locate_postgres_odbc_library()
if driver_lib is None:
warn("PostgreSQL ODBC package installed, but psqlodbcw.so was not found")
return False
registered, odbcinst_output = query_odbc_driver(driver)
registered_lib = parse_driver_library(odbcinst_output) if registered else None
if registered and registered_lib == driver_lib and driver_lib.exists():
return True
print(f"Registering ODBC driver [{driver}] -> {driver_lib}")
if not register_odbc_driver(driver, driver_lib):
return False
registered, odbcinst_output = query_odbc_driver(driver)
registered_lib = parse_driver_library(odbcinst_output) if registered else None
return bool(registered and registered_lib and registered_lib.exists())
def check_database_odbc_driver(lib_dir: str) -> bool:
source_info = read_database_source(lib_dir)
if source_info is None:
return True
dbl_path, source = source_info
dsn = str(source.get("dsn") or "").strip()
connection_string = str(source.get("connection_string") or "")
driver = extract_odbc_driver(connection_string)
print("Database library ODBC check:")
print(f" file: {dbl_path}")
if not driver:
if dsn:
warn(f"database library uses DSN '{dsn}', driver lookup is left to unixODBC")
return True
warn("database library ODBC source has no Driver=... or DSN")
return False
print(f" driver: {driver}")
if shutil.which("odbcinst") is None:
warn("odbcinst was not found; KiCad cannot resolve ODBC drivers")
print_postgres_odbc_help(driver, expected_driver_library(driver))
return False
registered, odbcinst_output = query_odbc_driver(driver)
registered_lib = parse_driver_library(odbcinst_output) if registered else None
driver_lib = registered_lib or expected_driver_library(driver)
problems = []
if not registered:
problems.append(f"driver is not registered in /etc/odbcinst.ini as [{driver}]")
if driver_lib is not None and not driver_lib.exists():
problems.append(f"driver library does not exist: {driver_lib}")
if registered and registered_lib is None:
problems.append(f"registered driver [{driver}] has no Driver=... path")
if not problems:
if driver_lib is not None:
print(f" library: {driver_lib}")
print(" status: OK")
return True
for problem in problems:
warn(problem)
print_postgres_odbc_help(driver, driver_lib)
return False
def database_odbc_driver_name(lib_dir: str) -> str | None:
source_info = read_database_source(lib_dir)
if source_info is None:
return None
_, source = source_info
connection_string = str(source.get("connection_string") or "")
return extract_odbc_driver(connection_string)
def main() -> int:
parser = argparse.ArgumentParser(
description="Configure KiCad paths and library tables for KiCad User Library."
@ -146,7 +481,25 @@ def main() -> int:
parser.add_argument("--pdf-dir", help="Path to PDF datasheet directory")
parser.add_argument("--3d-dir", dest="models_dir", help="Path to user 3D models directory")
parser.add_argument("--non-interactive", action="store_true", help="Do not ask questions")
parser.add_argument("--skip-odbc-check", action="store_true", help="Do not check DB Library ODBC driver")
parser.add_argument(
"--install-odbc-driver",
action="store_true",
help="Install/register missing PostgreSQL ODBC driver for DB Library",
)
parser.add_argument(
"--no-install-odbc-driver",
action="store_true",
help="Do not offer automatic PostgreSQL ODBC driver installation",
)
parser.add_argument(
"--strict-odbc-check",
action="store_true",
help="Fail when DB Library ODBC driver is not configured",
)
args = parser.parse_args()
if args.install_odbc_driver and args.no_install_odbc_driver:
raise SystemExit("--install-odbc-driver and --no-install-odbc-driver are mutually exclusive")
config_path = detect_kicad_config(args.kicad_version)
existing = read_existing_vars(config_path)
@ -176,6 +529,26 @@ def main() -> int:
if key != "KICAD_LIB_DIR_USER" and not Path(value).expanduser().exists():
print(f"warning: {key} path does not exist yet: {value}", file=sys.stderr)
odbc_ok = True
if not args.skip_odbc_check:
odbc_ok = check_database_odbc_driver(lib_dir)
if not odbc_ok and not args.no_install_odbc_driver:
should_install = args.install_odbc_driver
if not should_install and not args.non_interactive:
should_install = ask_yes_no("Install/register PostgreSQL ODBC driver now?", True)
if should_install:
driver = database_odbc_driver_name(lib_dir)
if driver:
odbc_ok = install_and_register_odbc_driver(driver, args.non_interactive)
if odbc_ok:
odbc_ok = check_database_odbc_driver(lib_dir)
else:
warn("could not determine DB Library ODBC driver name")
if args.strict_odbc_check and not odbc_ok:
raise SystemExit("DB Library ODBC driver is not configured")
backups = [backup(config_path)]
config_dir = config_path.parent
for table_name in ("sym-lib-table", "fp-lib-table"):
@ -193,6 +566,8 @@ def main() -> int:
print("Library table entries updated:")
print(" sym-lib-table: eda-core, sym-lib-table")
print(" fp-lib-table: fp-lib-table")
if not args.skip_odbc_check and not odbc_ok:
print("Database ODBC check reported warnings above.")
print("Backups:")
for item in backups:
print(f" {item}")