|
5 | 5 | import json |
6 | 6 | import logging |
7 | 7 | from pathlib import Path |
| 8 | +from types import SimpleNamespace |
8 | 9 |
|
9 | 10 | from src.app.bootstrap import clean_work_dir, initialize_cache_manager |
10 | 11 | from src.app.diff_report import collect_artifact_state, generate_diff_report, save_diff_report |
|
20 | 21 | from src.utils.otatools_manager import OtaToolsManager |
21 | 22 |
|
22 | 23 | DEFAULT_PHASES = ["system", "apk", "framework", "firmware"] |
| 24 | +REPACK_CHECKPOINT_NAME = "repack-context.json" |
23 | 25 |
|
24 | 26 |
|
25 | 27 | def resolve_work_paths(work_dir: str | Path) -> tuple[Path, Path, Path, Path]: |
@@ -76,6 +78,17 @@ def determine_pack_settings(args, ctx: PortingContext, logger: logging.Logger) - |
76 | 78 | f"(from {'CLI' if args.ksu else 'config'})" |
77 | 79 | ) |
78 | 80 |
|
| 81 | + pack_cfg = ctx.device_config.get("pack", {}) |
| 82 | + config_custom_avb_chain = False |
| 83 | + if isinstance(pack_cfg, dict): |
| 84 | + config_custom_avb_chain = bool(pack_cfg.get("custom_avb_chain", False)) |
| 85 | + ctx.enable_custom_avb_chain = bool(args.custom_avb_chain or config_custom_avb_chain) |
| 86 | + logger.info( |
| 87 | + "Custom AVB chain: %s (from %s)", |
| 88 | + "enabled" if ctx.enable_custom_avb_chain else "disabled", |
| 89 | + "CLI" if args.custom_avb_chain else "config", |
| 90 | + ) |
| 91 | + |
79 | 92 | pack_type = args.pack_type or ctx.device_config.get("pack", {}).get("type", "payload") |
80 | 93 | fs_type = args.fs_type or ctx.device_config.get("pack", {}).get("fs_type", "erofs") |
81 | 94 | logger.info(f"Pack Type: {pack_type} (from {'CLI' if args.pack_type else 'config'})") |
@@ -133,6 +146,77 @@ def run_repacking( |
133 | 146 | packer.pack_ota_payload() |
134 | 147 |
|
135 | 148 |
|
| 149 | +def _checkpoint_path(work_dir: Path) -> Path: |
| 150 | + return work_dir / REPACK_CHECKPOINT_NAME |
| 151 | + |
| 152 | + |
| 153 | +def save_repack_checkpoint(ctx: PortingContext, work_dir: Path) -> Path: |
| 154 | + def as_str(value, default: str = "") -> str: |
| 155 | + return value if isinstance(value, str) else default |
| 156 | + |
| 157 | + def as_bool(value, default: bool = False) -> bool: |
| 158 | + return bool(value) if isinstance(value, (bool, int, str)) else default |
| 159 | + |
| 160 | + raw_device_config = getattr(ctx, "device_config", {}) |
| 161 | + device_config = raw_device_config if isinstance(raw_device_config, dict) else {} |
| 162 | + payload = { |
| 163 | + "stock_rom_code": as_str(getattr(ctx, "stock_rom_code", ""), "unknown"), |
| 164 | + "target_rom_version": as_str(getattr(ctx, "target_rom_version", ""), ""), |
| 165 | + "security_patch": as_str(getattr(ctx, "security_patch", ""), "Unknown"), |
| 166 | + "is_ab_device": as_bool(getattr(ctx, "is_ab_device", False), False), |
| 167 | + "base_android_version": as_str(getattr(ctx, "base_android_version", ""), "0"), |
| 168 | + "port_android_version": as_str(getattr(ctx, "port_android_version", ""), "0"), |
| 169 | + "is_port_eu_rom": as_bool(getattr(ctx, "is_port_eu_rom", False), False), |
| 170 | + "is_port_global_rom": as_bool(getattr(ctx, "is_port_global_rom", False), False), |
| 171 | + "port_global_region": as_str(getattr(ctx, "port_global_region", ""), ""), |
| 172 | + "device_config": device_config, |
| 173 | + } |
| 174 | + path = _checkpoint_path(work_dir) |
| 175 | + path.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8") |
| 176 | + return path |
| 177 | + |
| 178 | + |
| 179 | +def load_repack_checkpoint(work_dir: Path, target_work_dir: Path, logger: logging.Logger): |
| 180 | + path = _checkpoint_path(work_dir) |
| 181 | + if not path.exists(): |
| 182 | + raise FileNotFoundError(f"Repack checkpoint not found: {path}") |
| 183 | + |
| 184 | + data = json.loads(path.read_text(encoding="utf-8")) |
| 185 | + |
| 186 | + def get_target_prop_file(part_name: str): |
| 187 | + part_dir: Path = target_work_dir / part_name |
| 188 | + candidates = [ |
| 189 | + part_dir / "build.prop", |
| 190 | + part_dir / "system" / "build.prop", |
| 191 | + part_dir / "etc" / "build.prop", |
| 192 | + ] |
| 193 | + for candidate in candidates: |
| 194 | + if candidate.exists(): |
| 195 | + return candidate |
| 196 | + return None |
| 197 | + |
| 198 | + ctx = SimpleNamespace( |
| 199 | + stock_rom_code=data.get("stock_rom_code", "unknown"), |
| 200 | + target_rom_version=data.get("target_rom_version", ""), |
| 201 | + security_patch=data.get("security_patch", "Unknown"), |
| 202 | + is_ab_device=bool(data.get("is_ab_device", False)), |
| 203 | + base_android_version=data.get("base_android_version", "0"), |
| 204 | + port_android_version=data.get("port_android_version", "0"), |
| 205 | + is_port_eu_rom=bool(data.get("is_port_eu_rom", False)), |
| 206 | + is_port_global_rom=bool(data.get("is_port_global_rom", False)), |
| 207 | + port_global_region=data.get("port_global_region", ""), |
| 208 | + target_dir=target_work_dir, |
| 209 | + target_config_dir=target_work_dir / "config", |
| 210 | + repack_images_dir=target_work_dir / "repack_images", |
| 211 | + device_config=data.get("device_config", {}), |
| 212 | + enable_ksu=False, |
| 213 | + enable_custom_avb_chain=False, |
| 214 | + get_target_prop_file=get_target_prop_file, |
| 215 | + ) |
| 216 | + logger.info("Loaded repack checkpoint from %s", path) |
| 217 | + return ctx |
| 218 | + |
| 219 | + |
136 | 220 | def log_diff_report_summary(diff_report: dict[str, object], logger: logging.Logger) -> None: |
137 | 221 | """Log a compact summary for generated artifact diff reports.""" |
138 | 222 | summary = diff_report.get("summary", {}) |
@@ -279,6 +363,19 @@ def execute_porting(args, logger: logging.Logger) -> int: |
279 | 363 | resolve_remote_inputs(args, is_official_modify, logger) |
280 | 364 |
|
281 | 365 | work_dir, stock_work_dir, port_work_dir, target_work_dir = resolve_work_paths(args.work_dir) |
| 366 | + |
| 367 | + if getattr(args, "resume_from_packer", False): |
| 368 | + logger.info("Resume mode: packer-only repacking from existing target workspace.") |
| 369 | + try: |
| 370 | + ctx = load_repack_checkpoint(work_dir, target_work_dir, logger) |
| 371 | + except (FileNotFoundError, json.JSONDecodeError) as exc: |
| 372 | + logger.error(str(exc)) |
| 373 | + return 2 |
| 374 | + pack_type, fs_type = determine_pack_settings(args, ctx, logger) # type: ignore[arg-type] |
| 375 | + run_repacking(ctx, ["repack"], pack_type, fs_type, target_work_dir, logger) |
| 376 | + logger.info("Repack-only resume completed successfully.") |
| 377 | + return 0 |
| 378 | + |
282 | 379 | snapshot_manager = ( |
283 | 380 | StageSnapshotManager(args.snapshot_dir or (work_dir / "snapshots"), logger) |
284 | 381 | if args.enable_snapshots or args.rollback_to_snapshot |
@@ -379,6 +476,8 @@ def execute_porting(args, logger: logging.Logger) -> int: |
379 | 476 | cache_manager.cache_partitions = True |
380 | 477 |
|
381 | 478 | pack_type, fs_type = determine_pack_settings(args, ctx, logger) |
| 479 | + checkpoint_path = save_repack_checkpoint(ctx, work_dir) |
| 480 | + logger.info("Saved repack checkpoint to: %s", checkpoint_path) |
382 | 481 |
|
383 | 482 | work_dir.mkdir(parents=True, exist_ok=True) |
384 | 483 | stock.export_props(work_dir / "stock_debug.prop") |
|
0 commit comments