Coverage for src / ezcompiler / adapters / _pyinstaller_compiler.py: 14.15%

66 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-05-01 00:22 +0000

1# /////////////////////////////////////////////////////////////// 

2# PYINSTALLER_COMPILER - PyInstaller compiler implementation 

3# Project: ezcompiler 

4# /////////////////////////////////////////////////////////////// 

5 

6""" 

7PyInstaller compiler - PyInstaller compiler implementation for EzCompiler. 

8 

9This module provides a compiler implementation using PyInstaller, which 

10creates a single executable file with all dependencies bundled. 

11 

12Compilation is executed in a subprocess to isolate PyInstaller stdout/stderr 

13from the main process (preserving DLP rendering). 

14 

15Protocols layer can use WARNING and ERROR log levels. 

16""" 

17 

18from __future__ import annotations 

19 

20# /////////////////////////////////////////////////////////////// 

21# IMPORTS 

22# /////////////////////////////////////////////////////////////// 

23# Standard library imports 

24import shutil 

25import subprocess 

26import sys 

27from pathlib import Path 

28 

29# Local imports 

30from ..shared import CompilerConfig 

31from ..shared.exceptions import CompilationError 

32from .base_compiler import BaseCompiler 

33 

34# /////////////////////////////////////////////////////////////// 

35# CLASSES 

36# /////////////////////////////////////////////////////////////// 

37 

38 

39class PyInstallerCompiler(BaseCompiler): 

40 """ 

41 PyInstaller compiler implementation. 

42 

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. 

46 

47 Compilation runs in a separate subprocess to prevent PyInstaller 

48 output from interfering with the main process display (DLP). 

49 

50 Attributes: 

51 _config: CompilerConfig with project settings 

52 

53 Example: 

54 >>> config = CompilerConfig(...) 

55 >>> compiler = PyInstallerCompiler(config) 

56 >>> compiler.compile(console=True) 

57 """ 

58 

59 # //////////////////////////////////////////////// 

60 # INITIALIZATION 

61 # //////////////////////////////////////////////// 

62 

63 def __init__(self, config: CompilerConfig) -> None: 

64 """ 

65 Initialize PyInstaller compiler. 

66 

67 Args: 

68 config: CompilerConfig instance with project settings 

69 

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 

75 

76 # //////////////////////////////////////////////// 

77 # COMPILER INTERFACE METHODS 

78 # //////////////////////////////////////////////// 

79 

80 def get_compiler_name(self) -> str: 

81 """ 

82 Get the name of this compiler. 

83 

84 Returns: 

85 str: Display name "PyInstaller (Empaquetée)" 

86 

87 Example: 

88 >>> compiler = PyInstallerCompiler(config) 

89 >>> print(compiler.get_compiler_name()) 

90 'PyInstaller (Empaquetée)' 

91 """ 

92 return "PyInstaller (Empaquetée)" 

93 

94 def compile(self, console: bool = True) -> None: 

95 """ 

96 Compile the project using PyInstaller in a subprocess. 

97 

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. 

101 

102 Args: 

103 console: Whether to show console window (default: True) 

104 

105 Raises: 

106 CompilationError: If compilation fails 

107 

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. 

112 

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() 

122 

123 # Determine output type and ZIP behavior 

124 from ..utils._compiler_utils import CompilerUtils 

125 

126 onefile = CompilerUtils.check_onefile_mode() 

127 self._zip_needed = not onefile 

128 

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 ] 

142 

143 # Add version file if it exists 

144 if ( 

145 self._config.version_filename 

146 and Path(self._config.version_filename).exists() 

147 ): 

148 cmd.append(f"--version-file={self._config.version_filename}") 

149 

150 # Add icon if specified 

151 if self._config.icon: 

152 cmd.append(f"--icon={self._config.icon}") 

153 

154 # Add include files 

155 for file in self._config.include_files.get("files", []): 

156 cmd.append(f"--add-data={file};.") 

157 

158 # Add include folders 

159 for folder in self._config.include_files.get("folders", []): 

160 cmd.append(f"--add-data={folder};{folder}") 

161 

162 # Add hidden imports (packages and includes) 

163 for pkg in self._config.packages + self._config.includes: 

164 cmd.append(f"--hidden-import={pkg}") 

165 

166 # Add excluded modules 

167 for mod in self._config.excludes: 

168 cmd.append(f"--exclude-module={mod}") 

169 

170 # Advanced options 

171 if self._config.optimize: 

172 cmd.append("--optimize=1") 

173 

174 if self._config.strip: 

175 cmd.append("--strip") 

176 

177 if self._config.debug: 

178 cmd.append("--debug=all") 

179 

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: 

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 else: 

189 cmd.append(f"--{key}={value}") 

190 

191 # Run PyInstaller in subprocess with captured output 

192 result = subprocess.run( # noqa: S603 

193 cmd, 

194 check=False, 

195 capture_output=True, 

196 text=True, 

197 ) 

198 

199 if result.returncode != 0: 

200 raw_output = result.stderr or result.stdout 

201 error_detail = self._extract_error_summary(raw_output) 

202 raise CompilationError( 

203 f"PyInstaller compilation failed: {error_detail}" 

204 ) 

205 

206 # Flatten output: PyInstaller --onedir creates a subfolder 

207 # named after the project inside distpath. Move its contents 

208 # up to output_folder so the layout matches Cx_Freeze. 

209 if not onefile: 

210 nested = self._config.output_folder / self._config.project_name 

211 if nested.is_dir(): 

212 for item in nested.iterdir(): 

213 dest = self._config.output_folder / item.name 

214 if dest.exists(): 

215 if dest.is_dir(): 

216 shutil.rmtree(dest) 

217 else: 

218 dest.unlink() 

219 shutil.move(str(item), str(dest)) 

220 nested.rmdir() 

221 

222 except Exception as e: 

223 if isinstance(e, CompilationError): 

224 raise 

225 raise CompilationError(f"PyInstaller compilation failed: {str(e)}") from e