Coverage for src/ezcompiler/services/updater_service.py: 82.72%
69 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-04 11:22 +0000
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-04 11:22 +0000
1# ///////////////////////////////////////////////////////////////
2# UPDATER_SERVICE - Update client generation service
3# Project: ezcompiler
4# ///////////////////////////////////////////////////////////////
6"""
7Updater service - Generates auto-update client files for compiled apps.
9Produces update.py, settings.py, and copies root.json from the local
10TUF repository. Files are meant to be embedded in the compiled bundle
11via include_files.
12"""
14from __future__ import annotations
16import shutil
17from pathlib import Path
19from ..shared import CompilerConfig
20from ..shared._constants import UPDATE_SUBDIR
21from ..shared.exceptions import UpdaterConfigError, UpdaterGenerationError
23_TEMPLATES_DIR = Path(__file__).parent.parent / "assets" / "templates" / "updater"
24_ROOT_JSON = "root.json"
27class UpdaterService:
28 """Service that generates the tufup client update files."""
30 @staticmethod
31 def generate(config: CompilerConfig, output_dir: Path) -> list[Path]:
32 """Generate update.py, settings.py, and copy root.json.
34 Args:
35 config: Compiler configuration (must have tuf_enabled=True).
36 output_dir: Directory where files are written (created if absent).
38 Returns:
39 List of generated file paths [settings.py, update.py, root.json].
41 Raises:
42 UpdaterConfigError: If config is invalid (tuf_enabled=False,
43 root.json absent, repo_public_url missing for non-disk).
44 UpdaterGenerationError: If writing files fails.
45 """
46 UpdaterService._validate(config)
47 update_url = UpdaterService._resolve_update_url(config)
49 try:
50 output_dir.mkdir(parents=True, exist_ok=True)
51 generated: list[Path] = []
52 generated.append(
53 UpdaterService._render_template(
54 "settings", config, update_url, output_dir
55 )
56 )
57 generated.append(
58 UpdaterService._render_template(
59 "update", config, update_url, output_dir
60 )
61 )
62 generated.append(UpdaterService._copy_root_json(config, output_dir))
63 return generated
64 except (UpdaterConfigError, UpdaterGenerationError):
65 raise
66 except Exception as exc:
67 raise UpdaterGenerationError(
68 f"Failed to generate updater files in {output_dir}: {exc}"
69 ) from exc
71 @staticmethod
72 def _validate(config: CompilerConfig) -> None:
73 if not config.tuf_enabled:
74 raise UpdaterConfigError(
75 "tuf_enabled must be True to generate updater files. "
76 "Set tuf_enabled=True in your config."
77 )
78 if config.tuf_repo_dir is None: 78 ↛ 79line 78 didn't jump to line 79 because the condition on line 78 was never true
79 raise UpdaterConfigError("tuf_repo_dir is required to locate root.json.")
80 root_json = config.tuf_repo_dir / "metadata" / _ROOT_JSON
81 if not root_json.exists():
82 raise UpdaterConfigError(
83 f"root.json not found at {root_json}. "
84 "Run 'ezcompiler release init' first to initialise the TUF repository."
85 )
87 @staticmethod
88 def _resolve_update_url(config: CompilerConfig) -> str:
89 # The client URL must mirror where UploaderService writes the TUF
90 # tree: disk/server place it under the UPDATE_SUBDIR subdir, while r2
91 # uploads straight to the bucket prefix. Sharing the constant keeps
92 # both sides in sync.
93 if config.repo_destination == "disk":
94 endpoint = config.repo_endpoint.rstrip("/").replace("\\", "/")
95 if not endpoint.startswith("/"): 95 ↛ 96line 95 didn't jump to line 96 because the condition on line 95 was never true
96 endpoint = f"/{endpoint}"
97 return f"file://{endpoint}/{UPDATE_SUBDIR}"
98 if config.repo_destination == "r2":
99 return config.repo_public_url.rstrip("/")
100 # server
101 return config.repo_public_url.rstrip("/") + f"/{UPDATE_SUBDIR}"
103 @staticmethod
104 def _render_template(
105 name: str,
106 config: CompilerConfig,
107 update_url: str,
108 output_dir: Path,
109 ) -> Path:
110 tmpl_path = _TEMPLATES_DIR / f"{name}.py.template"
111 try:
112 content = tmpl_path.read_text(encoding="utf-8")
113 except OSError as exc:
114 raise UpdaterGenerationError(
115 f"Cannot read template {tmpl_path}: {exc}"
116 ) from exc
118 content = content.replace("#APP_NAME#", config.project_name)
119 content = content.replace("#VERSION#", config.version)
120 content = content.replace("#UPDATE_URL#", update_url)
122 dest = output_dir / f"{name}.py"
123 try:
124 dest.write_text(content, encoding="utf-8")
125 except OSError as exc:
126 raise UpdaterGenerationError(f"Cannot write {dest}: {exc}") from exc
127 return dest
129 @staticmethod
130 def _copy_root_json(config: CompilerConfig, output_dir: Path) -> Path:
131 assert config.tuf_repo_dir is not None # guaranteed by _validate
132 src = config.tuf_repo_dir / "metadata" / _ROOT_JSON
133 dst = output_dir / _ROOT_JSON
134 try:
135 shutil.copy2(src, dst)
136 except OSError as exc:
137 raise UpdaterGenerationError(
138 f"Cannot copy root.json from {src} to {dst}: {exc}"
139 ) from exc
140 return dst