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
« 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# ///////////////////////////////////////////////////////////////
6"""
7Cx_Freeze compiler - Cx_Freeze compiler implementation for EzCompiler.
9This module provides a compiler implementation using Cx_Freeze, which
10creates a directory containing the executable and all dependencies.
12Compilation is executed in a subprocess to isolate cx_Freeze 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 json
25import subprocess
26import sys
27import tempfile
28from pathlib import Path
29from typing import Any
31# Local imports
32from ..shared.compiler_config import CompilerConfig
33from ..shared.exceptions import CompilationError
34from .base_compiler import BaseCompiler
36# ///////////////////////////////////////////////////////////////
37# CLASSES
38# ///////////////////////////////////////////////////////////////
41class CxFreezeCompiler(BaseCompiler):
42 """
43 Cx_Freeze compiler implementation.
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.
49 Compilation runs in a separate subprocess to prevent cx_Freeze
50 output from interfering with the main process display (DLP).
52 Attributes:
53 _config: CompilerConfig with project settings
55 Example:
56 >>> config = CompilerConfig(...)
57 >>> compiler = CxFreezeCompiler(config)
58 >>> compiler.compile(console=True)
59 """
61 # ////////////////////////////////////////////////
62 # INITIALIZATION
63 # ////////////////////////////////////////////////
65 def __init__(self, config: CompilerConfig) -> None:
66 """
67 Initialize Cx_Freeze compiler.
69 Args:
70 config: CompilerConfig instance with project settings
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
78 # ////////////////////////////////////////////////
79 # COMPILER INTERFACE METHODS
80 # ////////////////////////////////////////////////
82 def get_compiler_name(self) -> str:
83 """
84 Get the name of this compiler.
86 Returns:
87 str: Display name "Cx_Freeze"
89 Example:
90 >>> compiler = CxFreezeCompiler(config)
91 >>> print(compiler.get_compiler_name())
92 'Cx_Freeze'
93 """
94 return "Cx_Freeze"
96 def compile(self, console: bool = True) -> None:
97 """
98 Compile the project using Cx_Freeze in a subprocess.
100 Generates a temporary setup script and executes it in a separate
101 process to isolate cx_Freeze stdout/stderr from the main process.
103 Args:
104 console: Whether to show console window (default: True)
106 Raises:
107 CompilationError: If compilation fails
109 Note:
110 On Windows with console=False, uses Win32GUI base.
111 Runs in subprocess to preserve DLP rendering in main process.
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()
123 # Prepare include files data
124 data = self._get_include_files_data()
126 # Determine base for executable (Win32GUI for no-console on Windows)
127 from ..utils.compiler_utils import CompilerUtils
129 base = CompilerUtils.get_windows_base_for_console(console)
131 # Normalize version to PEP 440 format to avoid setuptools warning
132 from packaging.version import Version
134 normalized_version = str(Version(self._config.version))
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 }
147 # Merge with compiler-specific options (overrides defaults)
148 if self._config.compiler_options:
149 build_exe_options.update(self._config.compiler_options)
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 }
165 # Generate and execute temporary setup script
166 self._run_setup_subprocess(setup_config)
168 except Exception as e:
169 if isinstance(e, CompilationError):
170 raise
171 raise CompilationError(f"Cx_Freeze compilation failed: {str(e)}") from e
173 # ////////////////////////////////////////////////
174 # PRIVATE METHODS
175 # ////////////////////////////////////////////////
177 _SETUP_SCRIPT = """\
178import sys
179import json
180import warnings
181from pathlib import Path
183sys.setrecursionlimit(5000)
185from cx_Freeze import Executable, setup
187config = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
189# Get build_exe_options directly from config (already merged with compiler_options)
190build_exe_options = config["build_exe_options"]
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]
202sys.argv = [sys.argv[0], "build_exe"]
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"""
216 def _run_setup_subprocess(self, setup_config: dict[str, Any]) -> None:
217 """
218 Execute cx_Freeze setup in a subprocess.
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.
224 Args:
225 setup_config: Configuration dictionary for the setup script
227 Raises:
228 CompilationError: If the subprocess fails
229 """
230 project_dir = Path(setup_config["main_file"]).resolve().parent
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)
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)
244 try:
245 with open(config_fd, "w", encoding="utf-8") as f:
246 json.dump(setup_config, f)
248 with open(script_fd, "w", encoding="utf-8") as f:
249 f.write(self._SETUP_SCRIPT)
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 )
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}")
265 finally:
266 script_file.unlink(missing_ok=True)
267 config_file.unlink(missing_ok=True)