Skip to content

Commit e5a2210

Browse files
committed
refactor(sysdeps) : centralisation de la logique métier dans le Core, ajout du support macOS et amélioration de la robustesse
1 parent 196819c commit e5a2210

2 files changed

Lines changed: 103 additions & 45 deletions

File tree

pycompiler_ark/Core/SysDependencyManager.py

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,49 @@ def detect_linux_package_manager(self) -> Optional[str]:
6565
return pm
6666
return None
6767

68+
def detect_macos_package_manager(self) -> Optional[str]:
69+
if shutil.which("brew"):
70+
return "brew"
71+
return None
72+
6873
def which(self, cmd: str) -> Optional[str]:
6974
return shutil.which(cmd)
7075

76+
def get_install_command(self, packages: list[str]) -> Optional[str]:
77+
"""
78+
Generate the appropriate installation command string for the current platform.
79+
Returns None if no supported package manager is found.
80+
"""
81+
if not packages:
82+
return None
83+
84+
system = platform.system()
85+
if system == "Linux":
86+
pm = self.detect_linux_package_manager()
87+
if not pm:
88+
return None
89+
pkgs = " ".join(packages)
90+
# Standard install command for most Linux package managers
91+
return f"{pm} install -y {pkgs}"
92+
93+
elif system == "Windows":
94+
if not shutil.which("winget"):
95+
return None
96+
# Building a combined winget command
97+
parts = [
98+
f"winget install --id {p} --silent --accept-source-agreements --accept-package-agreements"
99+
for p in packages
100+
]
101+
return " && ".join(parts)
102+
103+
elif system == "Darwin": # macOS
104+
pm = self.detect_macos_package_manager()
105+
if pm == "brew":
106+
pkgs = " ".join(packages)
107+
return f"brew install {pkgs}"
108+
109+
return None
110+
71111
def shell_run(
72112
self,
73113
cmd: Union[str, list[str]],
@@ -77,9 +117,6 @@ def shell_run(
77117
) -> Optional[QProcess]:
78118
"""Runs a command without elevation."""
79119
try:
80-
# IMPORTANT: We create QProcess WITHOUT a parent here.
81-
# This allows it to be created and waited for (waitForFinished)
82-
# in the background thread without violating Qt thread ownership rules.
83120
proc = QProcess()
84121
if cwd:
85122
proc.setWorkingDirectory(cwd)
@@ -88,20 +125,37 @@ def shell_run(
88125
proc.setProgram(cmd[0])
89126
proc.setArguments(cmd[1:])
90127
else:
91-
proc.setProgram("/bin/bash" if platform.system() != "Windows" else "cmd.exe")
92-
args = ["-lc", cmd] if platform.system() != "Windows" else ["/c", cmd]
128+
proc.setProgram(
129+
"/bin/bash" if platform.system() != "Windows" else "cmd.exe"
130+
)
131+
args = (
132+
["-lc", cmd]
133+
if platform.system() != "Windows"
134+
else ["/c", cmd]
135+
)
93136
proc.setArguments(args)
94137

95-
if on_output:
96-
proc.readyReadStandardOutput.connect(lambda: on_output(proc.readAllStandardOutput().data().decode()))
97-
proc.readyReadStandardError.connect(lambda: on_output(proc.readAllStandardError().data().decode()))
138+
def _read_output():
139+
# Use errors='replace' to avoid crashes on Windows with non-UTF8 encodings
140+
raw = proc.readAllStandardOutput().data().decode(errors="replace")
141+
if on_output:
142+
on_output(raw)
143+
144+
def _read_error():
145+
raw = proc.readAllStandardError().data().decode(errors="replace")
146+
if on_output:
147+
on_output(raw)
148+
149+
proc.readyReadStandardOutput.connect(_read_output)
150+
proc.readyReadStandardError.connect(_read_error)
98151

99152
def _finished_wrapper(ec, es):
100-
if on_finished: on_finished(ec, es)
153+
if on_finished:
154+
on_finished(ec, es)
101155
self._unregister_task(proc)
102156

103157
proc.finished.connect(_finished_wrapper)
104-
158+
105159
# Note: The process is registered for global cleanup tracking
106160
self._register_task(proc, None, "commande système", "system command")
107161
proc.start()

pycompiler_ark/Ui/Gui/Dialogs/SysDependencyUI.py

Lines changed: 39 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -109,57 +109,61 @@ def _close():
109109
if self._progress_dlg:
110110
self._progress_dlg.close()
111111
self._progress_dlg = None
112+
112113
self._invoke_gui(_close)
113114

114-
def install_packages_linux(self, packages: list[str]) -> Optional[QProcess]:
115-
pm = self.detect_linux_package_manager()
116-
if not pm:
117-
self.msg_error("Gestionnaire de paquets non détecté.", "Package manager not detected.")
115+
def install_packages(self, packages: list[str]) -> Optional[QProcess]:
116+
"""
117+
Unified entry point for package installation with GUI feedback.
118+
"""
119+
cmd = self.get_install_command(packages)
120+
if not cmd:
121+
self.msg_error(
122+
"Impossible de générer la commande d'installation (Gestionnaire de paquets manquant).",
123+
"Cannot generate installation command (Missing package manager).",
124+
)
118125
return None
119126

120-
pkgs = " ".join(packages)
121-
cmd = f"{pm} install -y {pkgs}"
122-
123-
self._show_progress("Installation système", "System installation", "Installation des dépendances...", "Installing dependencies...")
127+
self._show_progress(
128+
"Installation système",
129+
"System installation",
130+
"Installation des dépendances...",
131+
"Installing dependencies...",
132+
)
124133

125134
def _on_output(text: str):
126135
lines = [l for l in text.strip().splitlines() if l.strip()]
127-
if lines: self._update_progress(lines[-1][:100])
136+
if lines:
137+
self._update_progress(lines[-1][:100])
128138

129139
def _on_finished(ec, es):
130140
self._close_progress()
131-
if ec != 0: self.msg_error("L'installation a échoué.", "Installation failed.")
141+
if ec != 0:
142+
self.msg_error(
143+
"L'installation a échoué.", "Installation failed."
144+
)
132145

133-
return self.run_elevated_shell(cmd, on_output=_on_output, on_finished=_on_finished)
146+
# Elevation is required for system packages
147+
proc = self.run_elevated_shell(
148+
cmd, on_output=_on_output, on_finished=_on_finished
149+
)
134150

135-
def install_packages_windows(self, packages: list[dict]) -> Optional[QProcess]:
136-
if not shutil.which("winget"):
137-
self.msg_error("winget est requis sur Windows.", "winget is required on Windows.")
138-
return None
151+
if proc and self._progress_dlg:
152+
# Handle cancellation: if user cancels the dialog, kill the process
153+
self._progress_dlg.canceled.connect(proc.terminate)
139154

140-
ids = [pkg.get("id") for pkg in packages if pkg.get("id")]
141-
if not ids: return None
142-
143-
cmd_parts = [f"winget install --id {pid} --silent --accept-source-agreements --accept-package-agreements" for pid in ids]
144-
full_cmd = " && ".join(cmd_parts)
145-
146-
self._show_progress("Installation Windows", "Windows installation", "Préparation de winget...", "Preparing winget...")
155+
return proc
147156

148-
def _on_output(text: str):
149-
lines = [l for l in text.strip().splitlines() if l.strip()]
150-
if lines: self._update_progress(lines[-1][:100])
157+
def install_packages_linux(self, packages: list[str]) -> Optional[QProcess]:
158+
# Deprecated: use install_packages
159+
return self.install_packages(packages)
151160

152-
def _on_finished(ec, es):
153-
self._close_progress()
154-
if ec != 0: self.msg_error("L'installation winget a échoué.", "winget installation failed.")
161+
def install_packages_windows(self, packages: list[dict]) -> Optional[QProcess]:
162+
# Deprecated: use install_packages
163+
ids = [pkg.get("id") for pkg in packages if pkg.get("id")]
164+
return self.install_packages(ids)
155165

156-
return self.shell_run(full_cmd, on_output=_on_output, on_finished=_on_finished)
157166

158167
def install_system_packages(packages: list[str], gui) -> bool:
159168
mgr = SysDependencyUI(gui)
160-
if platform.system() == "Linux":
161-
return mgr.install_packages_linux(packages) is not None
162-
elif platform.system() == "Windows":
163-
win_pkgs = [{"id": p} for p in packages]
164-
return mgr.install_packages_windows(win_pkgs) is not None
165-
return False
169+
return mgr.install_packages(packages) is not None

0 commit comments

Comments
 (0)