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

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

2# INSTALLER_SERVICE - Installer build orchestration service 

3# Project: ezcompiler 

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

5 

6""" 

7Installer service - Orchestrates first-deployment installer packaging. 

8 

9Builds a setup.exe from a compiled bundle via an installer adapter 

10(InstallerFactory). Mirrors ReleaseService's structure. 

11""" 

12 

13from __future__ import annotations 

14 

15# /////////////////////////////////////////////////////////////// 

16# IMPORTS 

17# /////////////////////////////////////////////////////////////// 

18import logging 

19from pathlib import Path 

20from typing import TYPE_CHECKING 

21 

22from ..adapters import InstallerFactory 

23from ..adapters._iss_renderer import render_iss 

24from ..shared import InstallerConfig 

25from ..shared.exceptions import InstallerConfigError 

26 

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 

29 

30# /////////////////////////////////////////////////////////////// 

31# CONSTANTS 

32# /////////////////////////////////////////////////////////////// 

33 

34_logger = logging.getLogger(__name__) 

35 

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

37# CLASSES 

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

39 

40 

41class InstallerService: 

42 """Service orchestrating first-deployment installer packaging.""" 

43 

44 # ------------------------------------------------ 

45 # BUILD METHODS 

46 # ------------------------------------------------ 

47 

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. 

62 

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``. 

77 

78 Returns: 

79 Path: The produced setup.exe path. 

80 

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 ) 

94 

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``. 

100 

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. 

106 

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. 

112 

113 Returns: 

114 Path: ``output_path``, once written. 

115 

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 ) 

124 

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 ) 

132 

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 ) 

141 

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 ) 

149 

150 output_path.parent.mkdir(parents=True, exist_ok=True) 

151 output_path.write_text(content, encoding="utf-8-sig") 

152 return output_path