Coverage for src/ezcompiler/services/installer_service.py: 94.59%
29 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-16 16:37 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-16 16:37 +0000
1# ///////////////////////////////////////////////////////////////
2# INSTALLER_SERVICE - Installer build orchestration service
3# Project: ezcompiler
4# ///////////////////////////////////////////////////////////////
6"""
7Installer service - Orchestrates first-deployment installer packaging.
9Builds a setup.exe from a compiled bundle via an installer adapter
10(InstallerFactory). Mirrors ReleaseService's structure.
11"""
13from __future__ import annotations
15# ///////////////////////////////////////////////////////////////
16# IMPORTS
17# ///////////////////////////////////////////////////////////////
18import logging
19from pathlib import Path
20from typing import TYPE_CHECKING
22from ..adapters import InstallerFactory
23from ..adapters._iss_renderer import render_iss
24from ..shared import InstallerConfig
25from ..shared.exceptions import InstallerConfigError
27if TYPE_CHECKING: 27 ↛ 28line 27 didn't jump to line 28 because the condition on line 27 was never true
28 from ..shared._compiler_config import CompilerConfig
30# ///////////////////////////////////////////////////////////////
31# CONSTANTS
32# ///////////////////////////////////////////////////////////////
34_logger = logging.getLogger(__name__)
36# ///////////////////////////////////////////////////////////////
37# CLASSES
38# ///////////////////////////////////////////////////////////////
41class InstallerService:
42 """Service orchestrating first-deployment installer packaging."""
44 # ------------------------------------------------
45 # BUILD METHODS
46 # ------------------------------------------------
48 @staticmethod
49 def build_installer(
50 bundle_dir: Path,
51 app_name: str,
52 version: str,
53 output_dir: Path,
54 *,
55 installer_type: str = "innosetup",
56 installer_config: InstallerConfig | None = None,
57 company_name: str = "",
58 icon: str = "",
59 main_file: str = "",
60 ) -> Path:
61 """Build the installer executable for a compiled bundle.
63 Args:
64 bundle_dir: Directory containing the compiled application.
65 app_name: Application name.
66 version: Application version string.
67 output_dir: Directory where the setup.exe is produced.
68 installer_type: Installer backend to use (default: "innosetup").
69 installer_config: Config forwarded to the installer adapter.
70 company_name: Publisher name; feeds the AppId GUID derivation
71 (see ``_iss_renderer.resolve_app_id``) — must match whatever
72 ``generate iss`` uses for the same product, or the two
73 builds mint different AppIds for one product.
74 icon: Path to the setup wizard icon.
75 main_file: Entry-point script; disambiguates the main executable
76 when the bundle holds several and none matches ``app_name``.
78 Returns:
79 Path: The produced setup.exe path.
81 Raises:
82 InstallerError: When installer packaging fails.
83 """
84 installer = InstallerFactory.create_installer(installer_type, installer_config)
85 return installer.build(
86 bundle_dir=bundle_dir,
87 app_name=app_name,
88 version=version,
89 output_dir=output_dir,
90 company_name=company_name,
91 icon=icon,
92 main_file=main_file,
93 )
95 @staticmethod
96 def generate_iss_script(
97 config: CompilerConfig, output_path: Path, *, force: bool
98 ) -> Path:
99 """Render a standalone, committable ``.iss`` script for ``config``.
101 Unlike the ephemeral build path, the icon is emitted verbatim
102 (never absolutized): the standalone script is meant to be committed,
103 and an absolute machine-specific path would defeat that. A relative
104 icon triggers a warning, since ISCC resolves it against the ``.iss``
105 file's own directory rather than the process cwd.
107 Args:
108 config: Compiler configuration (provides installer, company_name,
109 icon and project_name).
110 output_path: Destination ``.iss`` file path.
111 force: When False, refuse to overwrite an existing file.
113 Returns:
114 Path: ``output_path``, once written.
116 Raises:
117 InstallerConfigError: If ``output_path`` already exists and
118 ``force`` is False.
119 """
120 if output_path.exists() and not force:
121 raise InstallerConfigError(
122 f"{output_path} already exists; pass --force to overwrite it"
123 )
125 if config.icon and not Path(config.icon).is_absolute():
126 _logger.warning(
127 "installer.icon %r is a relative path: ISCC resolves it "
128 "against the .iss file's own directory, not the current "
129 "working directory",
130 config.icon,
131 )
133 license_file = config.installer.license_file
134 if license_file is not None and not license_file.is_absolute():
135 _logger.warning(
136 "installer.license_file %s is a relative path: ISCC resolves "
137 "it against the .iss file's own directory, not the current "
138 "working directory",
139 license_file,
140 )
142 content = render_iss(
143 config.installer,
144 project_name=config.project_name,
145 company_name=config.company_name,
146 icon=config.icon,
147 standalone=True,
148 )
150 output_path.parent.mkdir(parents=True, exist_ok=True)
151 output_path.write_text(content, encoding="utf-8-sig")
152 return output_path