diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index 2412235c5..0338cfffd 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -227,6 +227,9 @@ jobs: cp -r "$GITHUB_WORKSPACE/titleblocks" "$FILES/titleblocks" || true cp -r "$GITHUB_WORKSPACE/examples" "$FILES/examples" || true cp -r "$GITHUB_WORKSPACE/fonts" "$FILES/fonts" || true + # The MCP server finds bin/QElectroTech.exe and elements/ from mcp/. + mkdir -p "$FILES/mcp" + cp "$GITHUB_WORKSPACE/misc/qet-mcp/qet_mcp.py" "$GITHUB_WORKSPACE/misc/qet-mcp/README.md" "$FILES/mcp/" # --- Translations --- # Since PR #751 the .qm files are no longer tracked in git: lrelease @@ -305,7 +308,7 @@ jobs: && echo " OK : $f" \ || echo " MISSING: $f" done - for d in ico elements lang titleblocks fonts examples bin; do + for d in ico elements lang titleblocks fonts examples bin mcp; do [ -d "$FILES/$d" ] \ && echo " OK : $d/" \ || echo " MISSING: $d/" diff --git a/CMakeLists.txt b/CMakeLists.txt index a14f309f1..120f53a40 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -358,6 +358,10 @@ if (NOT MINGW) install(DIRECTORY elements DESTINATION share/qelectrotech) install(DIRECTORY examples DESTINATION share/qelectrotech) install(DIRECTORY titleblocks DESTINATION share/qelectrotech) + # The MCP server (misc/qet-mcp) finds this QElectroTech and its elements + # from here: /share/qelectrotech/mcp -> /bin. + install(PROGRAMS misc/qet-mcp/qet_mcp.py DESTINATION share/qelectrotech/mcp) + install(FILES misc/qet-mcp/README.md DESTINATION share/qelectrotech/mcp) install(FILES LICENSE ELEMENTS.LICENSE CREDIT README ChangeLog DESTINATION share/doc/qelectrotech) if(UNIX AND NOT APPLE) install(FILES misc/org.qelectrotech.qelectrotech.desktop DESTINATION share/applications) diff --git a/build-aux/windows/QET64.nsi b/build-aux/windows/QET64.nsi index cc1d81bd0..b1967b08f 100644 --- a/build-aux/windows/QET64.nsi +++ b/build-aux/windows/QET64.nsi @@ -274,6 +274,13 @@ Section "$(Fonts)" SEC05 File /nonfatal /r "./files/fonts" SectionEnd +; The MCP server lets an AI assistant open, check and edit drawings. It does +; nothing until a user sets up an assistant to start it. +Section "$(Mcp)" SEC06 + SetOutPath "$INSTDIR\mcp" + File /nonfatal "./files/mcp/*" +SectionEnd + ;-------------------------------- ; Component descriptions !insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN @@ -282,6 +289,7 @@ SectionEnd !insertmacro MUI_DESCRIPTION_TEXT ${SEC03} $(var3) !insertmacro MUI_DESCRIPTION_TEXT ${SEC04} $(var4) !insertmacro MUI_DESCRIPTION_TEXT ${SEC05} $(var5) + !insertmacro MUI_DESCRIPTION_TEXT ${SEC06} $(var6) !insertmacro MUI_FUNCTION_DESCRIPTION_END ;-------------------------------- diff --git a/build-aux/windows/lang_extra.nsh b/build-aux/windows/lang_extra.nsh index c76f128fd..2f51fafa6 100644 --- a/build-aux/windows/lang_extra.nsh +++ b/build-aux/windows/lang_extra.nsh @@ -19,6 +19,8 @@ LangString var3 ${LANG_ENGLISH} "Examples of cartridges" LangString var4 ${LANG_ENGLISH} "Examples of diagrams" LangString var5 ${LANG_ENGLISH} "Fonts" + LangString Mcp ${LANG_ENGLISH} "AI assistant (MCP)" + LangString var6 ${LANG_ENGLISH} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_ENGLISH} "Uninstallation of the previous version failed.$\nPlease uninstall ${SOFT_NAME} manually before continuing." @@ -43,6 +45,8 @@ LangString var3 ${LANG_KOREAN} "표제란 예제" LangString var4 ${LANG_KOREAN} "도면 예제" LangString var5 ${LANG_KOREAN} "글꼴" + LangString Mcp ${LANG_KOREAN} "AI assistant (MCP)" + LangString var6 ${LANG_KOREAN} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_KOREAN} "이전 버전을 제거하지 못했습니다.$\n계속하기 전에 ${SOFT_NAME}을(를) 수동으로 제거해 주세요." @@ -67,6 +71,8 @@ LangString var3 ${LANG_POLISH} "Przykłady tabliczek rysunkowych" LangString var4 ${LANG_POLISH} "Przykłady schematów" LangString var5 ${LANG_POLISH} "Czcionki" + LangString Mcp ${LANG_POLISH} "AI assistant (MCP)" + LangString var6 ${LANG_POLISH} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_POLISH} "Odinstalowanie poprzedniej wersji nie powiodło się.$\nPrzed kontynuowaniem odinstaluj ręcznie program ${SOFT_NAME}." @@ -91,6 +97,8 @@ LangString var3 ${LANG_GREEK} "Παραδείγματα πινακίδων" LangString var4 ${LANG_GREEK} "Παραδείγματα διαγραμμάτων" LangString var5 ${LANG_GREEK} "Γραμματοσειρές" + LangString Mcp ${LANG_GREEK} "AI assistant (MCP)" + LangString var6 ${LANG_GREEK} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_GREEK} "Η απεγκατάσταση της προηγούμενης έκδοσης απέτυχε.$\nΠαρακαλώ απεγκαταστήστε χειροκίνητα το ${SOFT_NAME} πριν συνεχίσετε." @@ -115,6 +123,8 @@ LangString var3 ${LANG_CZECH} "Příklady popisových polí" LangString var4 ${LANG_CZECH} "Příklady schémat" LangString var5 ${LANG_CZECH} "Písma" + LangString Mcp ${LANG_CZECH} "AI assistant (MCP)" + LangString var6 ${LANG_CZECH} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_CZECH} "Odinstalování předchozí verze se nezdařilo.$\nPřed pokračováním prosím odinstalujte ${SOFT_NAME} ručně." @@ -139,6 +149,8 @@ LangString var3 ${LANG_SPANISH} "Ejemplos de cartelas" LangString var4 ${LANG_SPANISH} "Ejemplos de esquemas" LangString var5 ${LANG_SPANISH} "Fuentes" + LangString Mcp ${LANG_SPANISH} "AI assistant (MCP)" + LangString var6 ${LANG_SPANISH} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_SPANISH} "La desinstalación de la versión anterior ha fallado.$\nPor favor, desinstale ${SOFT_NAME} manualmente antes de continuar." @@ -163,6 +175,8 @@ LangString var3 ${LANG_GERMAN} "Schriftfeld-Beispiele" LangString var4 ${LANG_GERMAN} "Schaltplan-Beispiele" LangString var5 ${LANG_GERMAN} "Schriften" + LangString Mcp ${LANG_GERMAN} "AI assistant (MCP)" + LangString var6 ${LANG_GERMAN} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_GERMAN} "Die Deinstallation der vorherigen Version ist fehlgeschlagen.$\nBitte deinstallieren Sie ${SOFT_NAME} manuell, bevor Sie fortfahren." @@ -187,6 +201,8 @@ LangString var3 ${LANG_RUSSIAN} "Примеры штампов" LangString var4 ${LANG_RUSSIAN} "Примеры схем" LangString var5 ${LANG_RUSSIAN} "Шрифты" + LangString Mcp ${LANG_RUSSIAN} "AI assistant (MCP)" + LangString var6 ${LANG_RUSSIAN} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_RUSSIAN} "Удаление предыдущей версии завершилось с ошибкой.$\nПожалуйста, удалите ${SOFT_NAME} вручную перед продолжением." @@ -211,6 +227,8 @@ LangString var3 ${LANG_ARABIC} "أمثلة على كتل العنوان" LangString var4 ${LANG_ARABIC} "أمثلة على المخططات" LangString var5 ${LANG_ARABIC} "الخطوط" + LangString Mcp ${LANG_ARABIC} "AI assistant (MCP)" + LangString var6 ${LANG_ARABIC} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_ARABIC} "فشل إلغاء تثبيت الإصدار السابق. يرجى إلغاء تثبيت ${SOFT_NAME} يدويًا قبل المتابعة." @@ -235,6 +253,8 @@ LangString var3 ${LANG_CATALAN} "Exemples de cartutxos" LangString var4 ${LANG_CATALAN} "Exemples d'esquemes" LangString var5 ${LANG_CATALAN} "Tipus de lletra" + LangString Mcp ${LANG_CATALAN} "AI assistant (MCP)" + LangString var6 ${LANG_CATALAN} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_CATALAN} "La desinstal·lació de la versió anterior ha fallat.$\nSi us plau, desinstal·leu ${SOFT_NAME} manualment abans de continuar." @@ -259,6 +279,8 @@ LangString var3 ${LANG_ITALIAN} "Cartigli di esempio" LangString var4 ${LANG_ITALIAN} "Schemi di esempio" LangString var5 ${LANG_ITALIAN} "Caratteri" + LangString Mcp ${LANG_ITALIAN} "AI assistant (MCP)" + LangString var6 ${LANG_ITALIAN} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_ITALIAN} "La disinstallazione della versione precedente non è riuscita.$\nSi prega di disinstallare ${SOFT_NAME} manualmente prima di continuare." @@ -283,6 +305,8 @@ LangString var3 ${LANG_PORTUGUESE} "Exemplos de legendas" LangString var4 ${LANG_PORTUGUESE} "Exemplos de esquemas" LangString var5 ${LANG_PORTUGUESE} "Fontes" + LangString Mcp ${LANG_PORTUGUESE} "AI assistant (MCP)" + LangString var6 ${LANG_PORTUGUESE} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_PORTUGUESE} "A desinstalação da versão anterior falhou.$\nPor favor, desinstale ${SOFT_NAME} manualmente antes de continuar." @@ -307,6 +331,8 @@ LangString var3 ${LANG_ROMANIAN} "Exemple de cartușe" LangString var4 ${LANG_ROMANIAN} "Exemple de scheme" LangString var5 ${LANG_ROMANIAN} "Fonturi" + LangString Mcp ${LANG_ROMANIAN} "AI assistant (MCP)" + LangString var6 ${LANG_ROMANIAN} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_ROMANIAN} "Dezinstalarea versiunii anterioare a eșuat.$\nVă rugăm să dezinstalați ${SOFT_NAME} manual înainte de a continua." @@ -331,6 +357,8 @@ LangString var3 ${LANG_CROATIAN} "Primjeri zaglavlja" LangString var4 ${LANG_CROATIAN} "Primjeri shema" LangString var5 ${LANG_CROATIAN} "Fontovi" + LangString Mcp ${LANG_CROATIAN} "AI assistant (MCP)" + LangString var6 ${LANG_CROATIAN} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_CROATIAN} "Deinstalacija prethodne verzije nije uspjela.$\nMolimo deinstalirajte ${SOFT_NAME} ručno prije nastavka." @@ -355,6 +383,8 @@ LangString var3 ${LANG_DUTCH} "Voorbeelden van titelblokken" LangString var4 ${LANG_DUTCH} "Voorbeelden van schema's" LangString var5 ${LANG_DUTCH} "Lettertypen" + LangString Mcp ${LANG_DUTCH} "AI assistant (MCP)" + LangString var6 ${LANG_DUTCH} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_DUTCH} "Het verwijderen van de vorige versie is mislukt.$\nVerwijder ${SOFT_NAME} handmatig voordat u verdergaat." @@ -379,6 +409,8 @@ LangString var3 ${LANG_DANISH} "Titel blokke eksempler" LangString var4 ${LANG_DANISH} "Diagram eksempler" LangString var5 ${LANG_DANISH} "Skrifttyper" + LangString Mcp ${LANG_DANISH} "AI assistant (MCP)" + LangString var6 ${LANG_DANISH} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_DANISH} "Afinstallation af den tidligere version mislykkedes.$\nAfinstaller venligst ${SOFT_NAME} manuelt, inden du fortsætter." diff --git a/build-aux/windows/lang_extra_fr.nsh b/build-aux/windows/lang_extra_fr.nsh index 322322b7d..dfb527009 100644 --- a/build-aux/windows/lang_extra_fr.nsh +++ b/build-aux/windows/lang_extra_fr.nsh @@ -19,4 +19,6 @@ LangString var3 ${LANG_FRENCH} "Exemples de cartouches" LangString var4 ${LANG_FRENCH} "Exemples de schémas" LangString var5 ${LANG_FRENCH} "Polices" + LangString Mcp ${LANG_FRENCH} "Assistant IA (MCP)" + LangString var6 ${LANG_FRENCH} "Permet à un assistant IA d'ouvrir, vérifier et modifier vos schémas (nécessite Python)" LangString uninstFailed ${LANG_FRENCH} "La désinstallation de la version précédente a échoué.$\nVeuillez désinstaller ${SOFT_NAME} manuellement avant de continuer." diff --git a/build-aux/windows/lang_extra_missing.nsh b/build-aux/windows/lang_extra_missing.nsh index 9313f6c90..a4a69bdd3 100644 --- a/build-aux/windows/lang_extra_missing.nsh +++ b/build-aux/windows/lang_extra_missing.nsh @@ -25,6 +25,8 @@ LangString var3 ${LANG_HUNGARIAN} "Rajzfejléc-példák" LangString var4 ${LANG_HUNGARIAN} "Kapcsolási rajz példák" LangString var5 ${LANG_HUNGARIAN} "Betűtípusok" + LangString Mcp ${LANG_HUNGARIAN} "AI assistant (MCP)" + LangString var6 ${LANG_HUNGARIAN} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_HUNGARIAN} "Az előző verzió eltávolítása nem sikerült.$\nKérjük, távolítsa el manuálisan a ${SOFT_NAME} programot, mielőtt folytatná." @@ -52,6 +54,8 @@ LangString var3 ${LANG_JAPANESE} "表題欄の例" LangString var4 ${LANG_JAPANESE} "回路図の例" LangString var5 ${LANG_JAPANESE} "フォント" + LangString Mcp ${LANG_JAPANESE} "AI assistant (MCP)" + LangString var6 ${LANG_JAPANESE} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_JAPANESE} "前のバージョンのアンインストールに失敗しました。$\n続行する前に、${SOFT_NAME} を手動でアンインストールしてください。" @@ -79,6 +83,8 @@ LangString var3 ${LANG_MONGOLIAN} "Гарчгийн блокын жишээнүүд" LangString var4 ${LANG_MONGOLIAN} "Схемийн жишээнүүд" LangString var5 ${LANG_MONGOLIAN} "Фонт" + LangString Mcp ${LANG_MONGOLIAN} "AI assistant (MCP)" + LangString var6 ${LANG_MONGOLIAN} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_MONGOLIAN} "Өмнөх хувилбарыг устгаж чадсангүй.$\nҮргэлжлүүлэхийн өмнө ${SOFT_NAME}-г гараар устгана уу." @@ -106,6 +112,8 @@ LangString var3 ${LANG_NORWEGIAN} "Eksempler på titelblokker" LangString var4 ${LANG_NORWEGIAN} "Eksempler på skjemaer" LangString var5 ${LANG_NORWEGIAN} "Skrifttyper" + LangString Mcp ${LANG_NORWEGIAN} "AI assistant (MCP)" + LangString var6 ${LANG_NORWEGIAN} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_NORWEGIAN} "Avinstallasjon av forrige versjon mislyktes.$\nVennligst avinstaller ${SOFT_NAME} manuelt før du fortsetter." @@ -143,6 +151,8 @@ LangString var3 ${LANG_PORTUGUESEBR} "Exemplos de legendas" LangString var4 ${LANG_PORTUGUESEBR} "Exemplos de esquemas" LangString var5 ${LANG_PORTUGUESEBR} "Fontes" + LangString Mcp ${LANG_PORTUGUESEBR} "AI assistant (MCP)" + LangString var6 ${LANG_PORTUGUESEBR} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_PORTUGUESEBR} "A desinstalação da versão anterior falhou.$\nPor favor, desinstale ${SOFT_NAME} manualmente antes de continuar." @@ -170,6 +180,8 @@ LangString var3 ${LANG_SERBIAN} "Примери заглавља" LangString var4 ${LANG_SERBIAN} "Примери шема" LangString var5 ${LANG_SERBIAN} "Фонтови" + LangString Mcp ${LANG_SERBIAN} "AI assistant (MCP)" + LangString var6 ${LANG_SERBIAN} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_SERBIAN} "Деинсталација претходне верзије није успела.$\nМолимо деинсталирајте ${SOFT_NAME} ручно пре наставка." @@ -197,6 +209,8 @@ LangString var3 ${LANG_SLOVAK} "Príklady popisových polí" LangString var4 ${LANG_SLOVAK} "Príklady schém" LangString var5 ${LANG_SLOVAK} "Písma" + LangString Mcp ${LANG_SLOVAK} "AI assistant (MCP)" + LangString var6 ${LANG_SLOVAK} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_SLOVAK} "Odinštalovanie predchádzajúcej verzie zlyhalo.$\nPred pokračovaním odinštalujte ${SOFT_NAME} ručne." @@ -224,6 +238,8 @@ LangString var3 ${LANG_SLOVENIAN} "Primeri glav risb" LangString var4 ${LANG_SLOVENIAN} "Primeri shem" LangString var5 ${LANG_SLOVENIAN} "Pisave" + LangString Mcp ${LANG_SLOVENIAN} "AI assistant (MCP)" + LangString var6 ${LANG_SLOVENIAN} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_SLOVENIAN} "Odstranitev prejšnje različice ni uspela.$\nPred nadaljevanjem ročno odstranite ${SOFT_NAME}." @@ -251,6 +267,8 @@ LangString var3 ${LANG_SWEDISH} "Exempel på ritningshuvuden" LangString var4 ${LANG_SWEDISH} "Exempel på scheman" LangString var5 ${LANG_SWEDISH} "Teckensnitt" + LangString Mcp ${LANG_SWEDISH} "AI assistant (MCP)" + LangString var6 ${LANG_SWEDISH} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_SWEDISH} "Avinstallationen av den föregående versionen misslyckades.$\nAvinstallera ${SOFT_NAME} manuellt innan du fortsätter." @@ -278,6 +296,8 @@ LangString var3 ${LANG_TURKISH} "Başlık bloğu örnekleri" LangString var4 ${LANG_TURKISH} "Şema örnekleri" LangString var5 ${LANG_TURKISH} "Yazı Tipleri" + LangString Mcp ${LANG_TURKISH} "AI assistant (MCP)" + LangString var6 ${LANG_TURKISH} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_TURKISH} "Önceki sürümün kaldırılması başarısız oldu.$\nDevam etmeden önce lütfen ${SOFT_NAME}'i manuel olarak kaldırın." @@ -305,6 +325,8 @@ LangString var3 ${LANG_UKRAINIAN} "Приклади штампів" LangString var4 ${LANG_UKRAINIAN} "Приклади схем" LangString var5 ${LANG_UKRAINIAN} "Шрифти" + LangString Mcp ${LANG_UKRAINIAN} "AI assistant (MCP)" + LangString var6 ${LANG_UKRAINIAN} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_UKRAINIAN} "Видалення попередньої версії завершилося помилкою.$\nБудь ласка, видаліть ${SOFT_NAME} вручну перед продовженням." @@ -332,4 +354,6 @@ LangString var3 ${LANG_SIMPCHINESE} "标题栏示例" LangString var4 ${LANG_SIMPCHINESE} "图纸示例" LangString var5 ${LANG_SIMPCHINESE} "字体" + LangString Mcp ${LANG_SIMPCHINESE} "AI assistant (MCP)" + LangString var6 ${LANG_SIMPCHINESE} "Lets an AI assistant open, check and edit your drawings (needs Python)" LangString uninstFailed ${LANG_SIMPCHINESE} "卸载旧版本失败。$\n请在继续之前手动卸载 ${SOFT_NAME}。" diff --git a/misc/qet-mcp/README.md b/misc/qet-mcp/README.md index f0640ea4f..3c082ace9 100644 --- a/misc/qet-mcp/README.md +++ b/misc/qet-mcp/README.md @@ -82,6 +82,28 @@ Register it with an MCP client, for example: QElectroTech (as `/share/qelectrotech/mcp/qet_mcp.py`, which also finds the installed element collection). +## Installed with QElectroTech + +QElectroTech's packages install the server next to the program, and from +there it finds that QElectroTech and its element collection by itself: + +| Package | Server | Finds | +|---|---|---| +| `make install`, Linux distributions | `/share/qelectrotech/mcp/qet_mcp.py` | `/bin/qelectrotech`, `/share/qelectrotech/elements` | +| Windows installer, MSI, portable folder | `\mcp\qet_mcp.py` (the "AI assistant (MCP)" component) | `\bin\QElectroTech.exe`, `\elements` | + +So a client configuration needs only the path to the server and the +workspace, for example on Windows: + +```json +"args": ["C:\\Program Files\\QElectroTech\\mcp\\qet_mcp.py"] +``` + +Snap and flatpak install it too. Their QElectroTech is built to run inside +the package's sandbox, and starting it from the server has not been tested: +the tools that read a file work, exports and edits may not. If they fail, +point `QET_BINARY` at a QElectroTech installed another way. + ## Using it from the Claude app A web chat in a browser cannot start a program on your computer, so it diff --git a/misc/qet-mcp/qet_mcp.py b/misc/qet-mcp/qet_mcp.py index 485a53bf8..ce11cd467 100755 --- a/misc/qet-mcp/qet_mcp.py +++ b/misc/qet-mcp/qet_mcp.py @@ -3189,12 +3189,24 @@ def _env_paths(name: str) -> list: return out -def _installed_prefix() -> Path | None: - """The install prefix when this script is /share/qelectrotech/mcp/.""" +def _installation() -> tuple | None: + """(program directory, element collection) of the QElectroTech this + script was installed with, or None when it runs from anywhere else. + + Two layouts, both put there by QElectroTech's own packaging: + /share/qelectrotech/mcp/ -> /bin, /share/qelectrotech/elements + (make install: Linux, snap, flatpak, macOS) + /mcp/ -> /bin, /elements + (the Windows installers and portable folder) + """ here = Path(__file__).resolve().parent - if here.name == "mcp" and here.parent.name == "qelectrotech" \ - and here.parent.parent.name == "share": - return here.parent.parent.parent + if here.name != "mcp": + return None + if here.parent.name == "qelectrotech" and here.parent.parent.name == "share": + prefix = here.parent.parent.parent + return prefix / "bin", here.parent / "elements" + if (here.parent / "bin").is_dir(): + return here.parent / "bin", here.parent / "elements" return None @@ -3208,10 +3220,12 @@ def resolve_binary() -> Path | None: env = os.environ.get("QET_BINARY", "").strip() if env: return Path(env).expanduser().resolve() - prefix = _installed_prefix() - if prefix is not None: - for name in ("qelectrotech", "qelectrotech.exe"): - cand = prefix / "bin" / name + install = _installation() + if install is not None: + # The Windows build names it QElectroTech.exe; only a case-sensitive + # file system tells the spellings apart. + for name in ("qelectrotech", "qelectrotech.exe", "QElectroTech.exe"): + cand = install[0] / name if cand.is_file(): return cand.resolve() found = shutil.which("qelectrotech") @@ -3220,11 +3234,9 @@ def resolve_binary() -> Path | None: def default_elements_dir() -> Path | None: """The element collection of the install this script ships in, if any.""" - prefix = _installed_prefix() - if prefix is not None: - coll = prefix / "share" / "qelectrotech" / "elements" - if coll.is_dir(): - return coll.resolve() + install = _installation() + if install is not None and install[1].is_dir(): + return install[1].resolve() return None diff --git a/misc/qet-mcp/test_qet_mcp.py b/misc/qet-mcp/test_qet_mcp.py index 6c98569d8..875da3324 100644 --- a/misc/qet-mcp/test_qet_mcp.py +++ b/misc/qet-mcp/test_qet_mcp.py @@ -2021,6 +2021,43 @@ class BinaryPolicy(unittest.TestCase): self.assertEqual(args["elements_dir"], str((prefix / "share" / "qelectrotech" / "elements").resolve())) + def installed_at(self, root: Path, mcp: Path): + """Import a copy of the server placed at mcp/qet_mcp.py.""" + import importlib.util + mcp.mkdir(parents=True) + shutil.copy2(HERE / "qet_mcp.py", mcp / "qet_mcp.py") + spec = importlib.util.spec_from_file_location(f"qet_mcp_{root.name}", mcp / "qet_mcp.py") + inst = importlib.util.module_from_spec(spec) + spec.loader.exec_module(inst) + return inst + + def test_the_windows_layout_is_found_too(self): + """The Windows installers and the portable folder put the server in + /mcp, the program in /bin as QElectroTech.exe, and the + collection in /elements.""" + root = Path(self.tmp.name) / "QElectroTech" + (root / "elements").mkdir(parents=True) + exe = fake_qet(root / "bin", "QElectroTech.exe") + inst = self.installed_at(root, root / "mcp") + del os.environ["QET_BINARY"] + args = {"project": str(self.root / "ok.qet")} + inst.enforce_path_policy("qet_query", args) + self.assertEqual(args["binary"], str(exe.resolve())) + self.assertEqual(args["elements_dir"], str((root / "elements").resolve())) + + def test_a_copy_saved_anywhere_is_not_an_install(self): + """A downloaded copy sitting beside some bin/ folder must not go + looking for QElectroTech there.""" + home = Path(self.tmp.name) / "home" + fake_qet(home / "bin") + inst = self.installed_at(home, home / "Downloads") + self.assertIsNone(inst._installation()) + + def test_a_folder_named_mcp_is_not_an_install(self): + """Without a bin/ beside it, an mcp/ folder is just a folder.""" + inst = self.installed_at(Path(self.tmp.name) / "x", Path(self.tmp.name) / "x" / "mcp") + self.assertIsNone(inst._installation()) + class ScriptingDisabledHint(unittest.TestCase): """QElectroTech may refuse to run scripts at all, and says so in French.