Coverage for src/ezcompiler/adapters/_pyinstaller_compiler.py: 61.06%
69 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-04 11:22 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-04 11:22 +0000
1# ///////////////////////////////////////////////////////////////
2# PYINSTALLER_COMPILER - PyInstaller compiler implementation
3# Project: ezcompiler
4# ///////////////////////////////////////////////////////////////
6"""
7PyInstaller compiler - PyInstaller compiler implementation for EzCompiler.
9This module provides a compiler implementation using PyInstaller, which
10creates a single executable file with all dependencies bundled.
12Compilation is executed in a subprocess to isolate PyInstaller stdout/stderr
13from the main process (preserving DLP rendering).
15Protocols layer can use WARNING and ERROR log levels.
16"""
18from __future__ import annotations
20# ///////////////////////////////////////////////////////////////
21# IMPORTS
22# ///////////////////////////////////////////////////////////////
23# Standard library imports
24import shutil
25import subprocess # nosec B404 - cœur d'un outil de compilation ; cmd interne, jamais shell=True
26import sys
27from pathlib import Path
29# Local imports
30from ..shared import CompilerConfig
31from ..shared.exceptions import CompilationError
32from .base_compiler import BaseCompiler
34# ///////////////////////////////////////////////////////////////
35# CLASSES
36# ///////////////////////////////////////////////////////////////
39class PyInstallerCompiler(BaseCompiler):
40 """
41 PyInstaller compiler implementation.
43 Handles project compilation using PyInstaller, which creates a
44 single executable file with all dependencies bundled. Can generate
45 either single-file or directory-based executables.
47 Compilation runs in a separate subprocess to prevent PyInstaller
48 output from interfering with the main process display (DLP).
50 Attributes:
51 _config: CompilerConfig with project settings
53 Example:
54 >>> config = CompilerConfig(...)
55 >>> compiler = PyInstallerCompiler(config)
56 >>> compiler.compile(console=True)
57 """
59 # ////////////////////////////////////////////////
60 # INITIALIZATION
61 # ////////////////////////////////////////////////
63 def __init__(self, config: CompilerConfig) -> None:
64 """
65 Initialize PyInstaller compiler.
67 Args:
68 config: CompilerConfig instance with project settings
70 Note:
71 PyInstaller creates single files, so _zip_needed is set to False.
72 """
73 super().__init__(config)
74 self._zip_needed = False # PyInstaller creates single file
76 # ////////////////////////////////////////////////
77 # COMPILER INTERFACE METHODS
78 # ////////////////////////////////////////////////
80 def get_compiler_name(self) -> str:
81 """
82 Get the name of this compiler.
84 Returns:
85 str: Display name "PyInstaller (Empaquetée)"
87 Example:
88 >>> compiler = PyInstallerCompiler(config)
89 >>> print(compiler.get_compiler_name())
90 'PyInstaller (Empaquetée)'
91 """
92 return "PyInstaller (Empaquetée)"
94 def compile(self, console: bool = True) -> None:
95 """
96 Compile the project using PyInstaller in a subprocess.
98 Validates configuration, prepares output directory, builds
99 PyInstaller command-line arguments, and runs compilation in
100 a separate process to isolate stdout/stderr from the DLP.
102 Args:
103 console: Whether to show console window (default: True)
105 Raises:
106 CompilationError: If compilation fails
108 Note:
109 Adds version file if it exists. Includes all configured
110 packages, includes, and applies excludes. Adds files and
111 folders with appropriate data paths.
113 Example:
114 >>> config = CompilerConfig(...)
115 >>> compiler = PyInstallerCompiler(config)
116 >>> compiler.compile(console=False)
117 """
118 try:
119 # Validate and prepare
120 self._validate_config()
121 self._prepare_output_directory()
123 # Determine output type and ZIP behavior
124 from ..utils._compiler_utils import CompilerUtils
126 onefile = CompilerUtils.check_onefile_mode()
127 self._zip_needed = not onefile
129 # Build PyInstaller command
130 cmd = [
131 sys.executable,
132 "-m",
133 "PyInstaller",
134 self._config.main_file,
135 "--console" if console else "--windowed",
136 "--onefile" if onefile else "--onedir",
137 "--clean",
138 "-y",
139 f"--distpath={self._config.output_folder}",
140 f"--name={self._config.project_name}",
141 ]
143 # Add version file if it exists
144 if ( 144 ↛ 148line 144 didn't jump to line 148 because the condition on line 144 was never true
145 self._config.version_filename
146 and Path(self._config.version_filename).exists()
147 ):
148 cmd.append(f"--version-file={self._config.version_filename}")
150 # Add icon if specified
151 if self._config.icon: 151 ↛ 152line 151 didn't jump to line 152 because the condition on line 151 was never true
152 cmd.append(f"--icon={self._config.icon}")
154 # Add include files
155 for file in self._config.include_files.get("files", []): 155 ↛ 156line 155 didn't jump to line 156 because the loop on line 155 never started
156 cmd.append(f"--add-data={file};.")
158 # Add include folders
159 for folder in self._config.include_files.get("folders", []): 159 ↛ 160line 159 didn't jump to line 160 because the loop on line 159 never started
160 cmd.append(f"--add-data={folder};{folder}")
162 # Add hidden imports (packages and includes)
163 for pkg in self._config.packages + self._config.includes: 163 ↛ 164line 163 didn't jump to line 164 because the loop on line 163 never started
164 cmd.append(f"--hidden-import={pkg}")
166 # Add excluded modules
167 for mod in self._config.excludes: 167 ↛ 168line 167 didn't jump to line 168 because the loop on line 167 never started
168 cmd.append(f"--exclude-module={mod}")
170 # Advanced options
171 if self._config.optimize: 171 ↛ 174line 171 didn't jump to line 174 because the condition on line 171 was always true
172 cmd.append("--optimize=1")
174 if self._config.strip: 174 ↛ 175line 174 didn't jump to line 175 because the condition on line 174 was never true
175 cmd.append("--strip")
177 if self._config.debug: 177 ↛ 178line 177 didn't jump to line 178 because the condition on line 177 was never true
178 cmd.append("--debug=all")
180 # Add compiler-specific options from config.compiler_options
181 # Format: {"option-name": "value"} -> --option-name=value
182 # {"option-flag": True} -> --option-flag
183 if self._config.compiler_options: 183 ↛ 195line 183 didn't jump to line 195 because the condition on line 183 was always true
184 for key, value in self._config.compiler_options.items():
185 if isinstance(value, bool):
186 if value: # Only add if True
187 cmd.append(f"--{key}")
188 elif isinstance(value, list):
189 for item in value:
190 cmd.append(f"--{key}={item}")
191 else:
192 cmd.append(f"--{key}={value}")
194 # Run PyInstaller in subprocess with captured output
195 result = subprocess.run( # noqa: S603 # nosec B603 - args en liste, entrées contrôlées (config dev), pas de shell
196 cmd,
197 check=False,
198 capture_output=True,
199 text=True,
200 )
202 if result.returncode != 0: 202 ↛ 203line 202 didn't jump to line 203 because the condition on line 202 was never true
203 raw_output = result.stderr or result.stdout
204 error_detail = self._extract_error_summary(raw_output)
205 raise CompilationError(
206 f"PyInstaller compilation failed: {error_detail}"
207 )
209 # Flatten output: PyInstaller --onedir creates a subfolder
210 # named after the project inside distpath. Move its contents
211 # up to output_folder so the layout matches Cx_Freeze.
212 if not onefile: 212 ↛ exitline 212 didn't return from function 'compile' because the condition on line 212 was always true
213 nested = self._config.output_folder / self._config.project_name
214 if nested.is_dir(): 214 ↛ 215line 214 didn't jump to line 215 because the condition on line 214 was never true
215 for item in nested.iterdir():
216 dest = self._config.output_folder / item.name
217 if dest.exists():
218 if dest.is_dir():
219 shutil.rmtree(dest)
220 else:
221 dest.unlink()
222 shutil.move(str(item), str(dest))
223 nested.rmdir()
225 except Exception as e:
226 if isinstance(e, CompilationError):
227 raise
228 raise CompilationError(f"PyInstaller compilation failed: {str(e)}") from e