Merge pull request #1403 from ispyisail/fix/mcp-macos-no-copy
Linux build and tests / Build and test (Qt 6, Debug) (push) Failing after 2m1s

Fix assistant server tools failing on macOS: run the original program (#1178)
This commit is contained in:
Laurent Trinques
2026-10-09 01:54:06 +02:00
committed by GitHub
2 changed files with 35 additions and 16 deletions
+24 -11
View File
@@ -896,19 +896,32 @@ def tool_element_info(path: str) -> dict:
}
def _launch_executable(src: Path, sandbox: Path, windows: bool) -> Path:
"""The executable _run_qet() starts: a private copy, except on Windows.
def _copies_executable(platform: str = sys.platform) -> bool:
"""Whether _run_qet() runs a private copy of QElectroTech on @p platform:
not on Windows or macOS (see _launch_executable())."""
return platform not in ("win32", "darwin")
def _launch_executable(src: Path, sandbox: Path, copy: bool) -> Path:
"""The executable _run_qet() starts: a private copy when @p copy is set,
otherwise the original.
The copy gives each run its own SingleApplication key, which is derived
from the executable's path. On Windows a program loads its DLLs from its
own folder, so a copy on its own dies before main() (0xC0000135, DLL not
found) and nothing could ever be exported or edited there. Run the
original instead: every flag this server passes is a CLI export flag or
--run, and QElectroTech handles both and returns before it constructs
SingleApplication (main.cpp), so there is no instance to be handed to.
The copy stays elsewhere for builds from before that early return.
from the executable's path. Two systems cannot run a copy:
- Windows: a program loads its DLLs from its own folder, so a copy on
its own dies before main() (0xC0000135, DLL not found).
- macOS: a copy taken out of the signed .app bundle is killed at launch
(exit 137) and has lost the path to the bundle's Qt frameworks
(qelectrotech-source-mirror#1178).
There, run the original instead: every flag this server passes is a CLI
export flag or --run, and QElectroTech handles both and returns before
it constructs SingleApplication (main.cpp), so there is no instance to
be handed to. The copy stays elsewhere for builds from before that
early return.
"""
if windows:
if not copy:
return src
exe = sandbox / f"qet-mcp-{os.getpid()}"
shutil.copy2(src, exe)
@@ -996,7 +1009,7 @@ def _run_qet(binary: str, args: list[str], timeout: int = 180,
raise ValueError(f"not an executable: {src}")
with tempfile.TemporaryDirectory(prefix="qet-mcp-") as tmp:
sandbox = Path(tmp)
exe = _launch_executable(src, sandbox, os.name == "nt")
exe = _launch_executable(src, sandbox, _copies_executable())
home = sandbox / "home"
(home / ".config").mkdir(parents=True)
(home / ".local" / "share").mkdir(parents=True)
+11 -5
View File
@@ -3045,7 +3045,8 @@ class BinaryPolicy(unittest.TestCase):
class LaunchExecutable(unittest.TestCase):
"""Windows cannot run a lone copy of QElectroTech (F065): its DLLs sit
beside the original. Everywhere else the private copy stays."""
beside the original. Nor can macOS: a copy out of the signed .app is
killed at launch (#1178). Everywhere else the private copy stays."""
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
@@ -3059,9 +3060,14 @@ class LaunchExecutable(unittest.TestCase):
def tearDown(self):
self.tmp.cleanup()
def test_windows_runs_the_original(self):
self.assertEqual(m._launch_executable(self.src, self.sandbox, True), self.src)
self.assertEqual(list(self.sandbox.iterdir()), [], "nothing is copied on Windows")
def test_windows_and_macos_run_the_original(self):
self.assertEqual(m._launch_executable(self.src, self.sandbox, False), self.src)
self.assertEqual(list(self.sandbox.iterdir()), [], "nothing is copied")
def test_which_systems_copy(self):
self.assertFalse(m._copies_executable("win32"))
self.assertFalse(m._copies_executable("darwin"))
self.assertTrue(m._copies_executable("linux"))
def test_windows_keeps_its_own_qt_platform(self):
"""The Windows packages have no offscreen plugin; asking for it
@@ -3083,7 +3089,7 @@ class LaunchExecutable(unittest.TestCase):
self.assertEqual(env["QET_SETTINGS_DIR"], str(self.sandbox / ".config"))
def test_elsewhere_runs_a_private_copy(self):
exe = m._launch_executable(self.src, self.sandbox, False)
exe = m._launch_executable(self.src, self.sandbox, True)
self.assertEqual(exe.parent, self.sandbox)
self.assertNotEqual(exe, self.src)
self.assertEqual(exe.read_text(), self.src.read_text())