Coverage for src / ezcompiler / adapters / cx_freeze_compiler.py: 32.20%

53 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-27 06:49 +0000

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

2# CX_FREEZE_COMPILER - Cx_Freeze compiler implementation 

3# Project: ezcompiler 

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

5 

6""" 

7Cx_Freeze compiler - Cx_Freeze compiler implementation for EzCompiler. 

8 

9This module provides a compiler implementation using Cx_Freeze, which 

10creates a directory containing the executable and all dependencies. 

11 

12Compilation is executed in a subprocess to isolate cx_Freeze 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 json 

25import subprocess 

26import sys 

27import tempfile 

28from pathlib import Path 

29from typing import Any 

30 

31# Local imports 

32from ..shared.compiler_config import CompilerConfig 

33from ..shared.exceptions import CompilationError 

34from .base_compiler import BaseCompiler 

35 

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

37# CLASSES 

38# /////////////////////////////////////////////////////////////// 

39 

40 

41class CxFreezeCompiler(BaseCompiler): 

42 """ 

43 Cx_Freeze compiler implementation. 

44 

45 Handles project compilation using Cx_Freeze, which creates a 

46 directory structure containing the executable and all dependencies. 

47 The output is typically zipped for distribution. 

48 

49 Compilation runs in a separate subprocess to prevent cx_Freeze 

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

51 

52 Attributes: 

53 _config: CompilerConfig with project settings 

54 

55 Example: 

56 >>> config = CompilerConfig(...) 

57 >>> compiler = CxFreezeCompiler(config) 

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

59 """ 

60 

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

62 # INITIALIZATION 

63 # //////////////////////////////////////////////// 

64 

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

66 """ 

67 Initialize Cx_Freeze compiler. 

68 

69 Args: 

70 config: CompilerConfig instance with project settings 

71 

72 Note: 

73 Cx_Freeze output requires zipping, so _zip_needed is set to True. 

74 """ 

75 super().__init__(config) 

76 self._zip_needed = True # Cx_Freeze always needs zipping 

77 

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

79 # COMPILER INTERFACE METHODS 

80 # //////////////////////////////////////////////// 

81 

82 def get_compiler_name(self) -> str: 

83 """ 

84 Get the name of this compiler. 

85 

86 Returns: 

87 str: Display name "Cx_Freeze" 

88 

89 Example: 

90 >>> compiler = CxFreezeCompiler(config) 

91 >>> print(compiler.get_compiler_name()) 

92 'Cx_Freeze' 

93 """ 

94 return "Cx_Freeze" 

95 

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

97 """ 

98 Compile the project using Cx_Freeze in a subprocess. 

99 

100 Generates a temporary setup script and executes it in a separate 

101 process to isolate cx_Freeze stdout/stderr from the main process. 

102 

103 Args: 

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

105 

106 Raises: 

107 CompilationError: If compilation fails 

108 

109 Note: 

110 On Windows with console=False, uses Win32GUI base. 

111 Runs in subprocess to preserve DLP rendering in main process. 

112 

113 Example: 

114 >>> config = CompilerConfig(...) 

115 >>> compiler = CxFreezeCompiler(config) 

116 >>> compiler.compile(console=False) 

117 """ 

118 try: 

119 # Validate and prepare 

120 self._validate_config() 

121 self._prepare_output_directory() 

122 

123 # Prepare include files data 

124 data = self._get_include_files_data() 

125 

126 # Determine base for executable (Win32GUI for no-console on Windows) 

127 from ..utils.compiler_utils import CompilerUtils 

128 

129 base = CompilerUtils.get_windows_base_for_console(console) 

130 

131 # Normalize version to PEP 440 format to avoid setuptools warning 

132 from packaging.version import Version 

133 

134 normalized_version = str(Version(self._config.version)) 

135 

136 # Build default build_exe options 

137 build_exe_options = { 

138 "include_files": data, 

139 "packages": self._config.packages, 

140 "includes": self._config.includes, 

141 "excludes": self._config.excludes, 

142 "build_exe": str(self._config.output_folder), 

143 "optimize": 1 if self._config.optimize else 0, 

144 "silent_level": 0 if self._config.debug else 1, 

145 } 

146 

147 # Merge with compiler-specific options (overrides defaults) 

148 if self._config.compiler_options: 

149 build_exe_options.update(self._config.compiler_options) 

150 

151 # Build setup script configuration 

152 setup_config = { 

153 "name": self._config.project_name, 

154 "version": normalized_version, 

155 "description": self._config.project_description, 

156 "author": self._config.author, 

157 "main_file": self._config.main_file, 

158 "target_name": f"{self._config.project_name}.exe", 

159 "base": base, 

160 "icon": self._config.icon if self._config.icon else None, 

161 "debug": self._config.debug, 

162 "build_exe_options": build_exe_options, 

163 } 

164 

165 # Generate and execute temporary setup script 

166 self._run_setup_subprocess(setup_config) 

167 

168 except Exception as e: 

169 if isinstance(e, CompilationError): 

170 raise 

171 raise CompilationError(f"Cx_Freeze compilation failed: {str(e)}") from e 

172 

173 # //////////////////////////////////////////////// 

174 # PRIVATE METHODS 

175 # //////////////////////////////////////////////// 

176 

177 _SETUP_SCRIPT = """\ 

178import sys 

179import json 

180import warnings 

181from pathlib import Path 

182 

183sys.setrecursionlimit(5000) 

184 

185from cx_Freeze import Executable, setup 

186 

187config = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) 

188 

189# Get build_exe_options directly from config (already merged with compiler_options) 

190build_exe_options = config["build_exe_options"] 

191 

192executables = [ 

193 Executable( 

194 config["main_file"], 

195 base=config["base"], 

196 target_name=config["target_name"], 

197 icon=config["icon"], 

198 init_script=config.get("init_script"), 

199 ) 

200] 

201 

202sys.argv = [sys.argv[0], "build_exe"] 

203 

204with warnings.catch_warnings(): 

205 warnings.filterwarnings("ignore", category=FutureWarning) 

206 setup( 

207 name=config["name"], 

208 version=config["version"], 

209 description=config["description"], 

210 author=config["author"], 

211 options={"build_exe": build_exe_options}, 

212 executables=executables, 

213 ) 

214""" 

215 

216 def _run_setup_subprocess(self, setup_config: dict[str, Any]) -> None: 

217 """ 

218 Execute cx_Freeze setup in a subprocess. 

219 

220 Writes the configuration as a separate JSON file and runs 

221 a setup script that reads it, avoiding escape issues with 

222 inline JSON in Python strings. 

223 

224 Args: 

225 setup_config: Configuration dictionary for the setup script 

226 

227 Raises: 

228 CompilationError: If the subprocess fails 

229 """ 

230 project_dir = Path(setup_config["main_file"]).resolve().parent 

231 

232 # Write temporary config JSON file 

233 config_fd, config_file_str = tempfile.mkstemp( 

234 suffix="_cx_config.json", dir=str(project_dir) 

235 ) 

236 config_file = Path(config_file_str) 

237 

238 # Write temporary setup script 

239 script_fd, script_file_str = tempfile.mkstemp( 

240 suffix="_cx_setup.py", dir=str(project_dir) 

241 ) 

242 script_file = Path(script_file_str) 

243 

244 try: 

245 with open(config_fd, "w", encoding="utf-8") as f: 

246 json.dump(setup_config, f) 

247 

248 with open(script_fd, "w", encoding="utf-8") as f: 

249 f.write(self._SETUP_SCRIPT) 

250 

251 # Run cx_Freeze in subprocess with captured output 

252 result = subprocess.run( # noqa: S603 

253 [sys.executable, str(script_file), str(config_file)], 

254 check=False, 

255 capture_output=True, 

256 text=True, 

257 cwd=str(project_dir), 

258 ) 

259 

260 if result.returncode != 0: 

261 raw_output = result.stderr or result.stdout 

262 error_detail = self._extract_error_summary(raw_output) 

263 raise CompilationError(f"Cx_Freeze compilation failed: {error_detail}") 

264 

265 finally: 

266 script_file.unlink(missing_ok=True) 

267 config_file.unlink(missing_ok=True)