Фикс скрипта

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

View file

@ -8,12 +8,24 @@ import json
import os import os
import re import re
import shutil import shutil
import subprocess
import sys import sys
import tempfile
from datetime import datetime from datetime import datetime
from pathlib import Path from pathlib import Path
ENV_VARS = ("KICAD_LIB_DIR_USER", "KICAD_PDF_DIR_USER", "KICAD_3D_PARTY_USER") 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: 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.") 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: def backup(path: Path) -> Path:
stamp = datetime.now().strftime("%Y%m%d-%H%M%S") stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
target = path.with_name(f"{path.name}.bak-{stamp}") 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: def main() -> int:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Configure KiCad paths and library tables for KiCad User Library." 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("--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("--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("--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() 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) config_path = detect_kicad_config(args.kicad_version)
existing = read_existing_vars(config_path) 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(): 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) 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)] backups = [backup(config_path)]
config_dir = config_path.parent config_dir = config_path.parent
for table_name in ("sym-lib-table", "fp-lib-table"): for table_name in ("sym-lib-table", "fp-lib-table"):
@ -193,6 +566,8 @@ def main() -> int:
print("Library table entries updated:") print("Library table entries updated:")
print(" sym-lib-table: eda-core, sym-lib-table") print(" sym-lib-table: eda-core, sym-lib-table")
print(" fp-lib-table: fp-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:") print("Backups:")
for item in backups: for item in backups:
print(f" {item}") print(f" {item}")