Build optimization: pre-built PM3 binaries, ARM64 CI, base image caching

Replace PM3 compile-from-source in pi-gen with pre-built tarball extraction
(saves 43-58 min). Merge stagePM3 into stageDangerousPi as 02-pm3-install
substage, renumber all subsequent substages. Switch CI PM3 build to native
ARM64 runner (ubuntu-24.04-arm64) eliminating QEMU overhead. Add weekly
base-image workflow for pre-baking stages 0-2. Support PM3_TARBALL,
BASE_IMAGE, and APT_PROXY env vars in build-image.sh.

Also includes prior Phase 5 work: theme system, design system integration,
component update system, OS updates, CI build pipeline, and test results.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
michael
2026-03-04 12:01:01 -08:00
parent 2ec89041ef
commit a9acdb85ce
163 changed files with 8124 additions and 921 deletions

View File

@@ -3,9 +3,12 @@
This service provides software update operations that can be consumed by
multiple interfaces (REST API, BLE GATT, etc.).
"""
from typing import Optional, Dict, Any
from dataclasses import asdict
from typing import Optional, Dict, Any, List
from ..managers.update_manager import UpdateManager, UpdateProgress, UpdateStatus
from ..managers.update_manager import (
UpdateManager, UpdateProgress, UpdateStatus, ComponentId,
)
from .pm3_service import PM3ServiceError, PM3ServiceResult
@@ -156,6 +159,15 @@ class UpdateService:
try:
progress = await self.update_manager.get_progress()
comp_list = []
for comp_id, cp in progress.component_progress.items():
comp_list.append({
"component_id": cp.component_id,
"status": cp.status.value,
"download_progress": cp.download_progress,
"error_message": cp.error_message,
})
return PM3ServiceResult(
success=True,
data={
@@ -164,7 +176,9 @@ class UpdateService:
"available_version": progress.available_version,
"download_progress": progress.download_progress,
"error_message": progress.error_message,
"last_check": progress.last_check
"last_check": progress.last_check,
"active_component": progress.active_component,
"components": comp_list,
}
)
@@ -310,3 +324,151 @@ class UpdateService:
details=str(e)
)
)
# ------------------------------------------------------------------
# Component-level operations
# ------------------------------------------------------------------
async def get_installed_components(self) -> PM3ServiceResult:
"""Get the installed component manifest.
Returns:
PM3ServiceResult with component manifest data
"""
try:
manifest = self.update_manager.get_manifest()
components = {}
for comp_id, comp in manifest.components.items():
components[comp_id] = asdict(comp)
return PM3ServiceResult(
success=True,
data={
"schema_version": manifest.schema_version,
"system_version": manifest.system_version,
"components": components,
"plugins": manifest.plugins,
"last_updated": manifest.last_updated,
}
)
except Exception as e:
return PM3ServiceResult(
success=False,
error=PM3ServiceError(
code="manifest_error",
message="Failed to get component manifest",
details=str(e)
)
)
async def download_component(self, component_id: str) -> PM3ServiceResult:
"""Download a specific component update.
Args:
component_id: Component to download (pm3, frontend, backend, theme)
Returns:
PM3ServiceResult indicating download success/failure
"""
try:
success = await self.update_manager.download_component(component_id)
return PM3ServiceResult(
success=True,
data={
"message": f"Component {component_id} downloaded successfully",
"component_id": component_id,
"ready_to_install": True,
}
)
except ValueError as e:
return PM3ServiceResult(
success=False,
error=PM3ServiceError(
code="component_not_available",
message=str(e)
)
)
except Exception as e:
return PM3ServiceResult(
success=False,
error=PM3ServiceError(
code="component_download_error",
message=f"Error downloading {component_id}",
details=str(e)
)
)
async def install_component(self, component_id: str) -> PM3ServiceResult:
"""Install a downloaded component update.
Args:
component_id: Component to install
Returns:
PM3ServiceResult indicating install success/failure
"""
try:
success = await self.update_manager.install_component(component_id)
return PM3ServiceResult(
success=True,
data={
"message": f"Component {component_id} installed successfully",
"component_id": component_id,
"requires_restart": component_id in (
ComponentId.BACKEND, ComponentId.PM3
),
}
)
except ValueError as e:
return PM3ServiceResult(
success=False,
error=PM3ServiceError(
code="component_install_error",
message=str(e)
)
)
except Exception as e:
return PM3ServiceResult(
success=False,
error=PM3ServiceError(
code="component_install_error",
message=f"Error installing {component_id}",
details=str(e)
)
)
async def rollback_component(self, component_id: str) -> PM3ServiceResult:
"""Rollback a component to its previous version.
Args:
component_id: Component to rollback
Returns:
PM3ServiceResult indicating rollback success/failure
"""
try:
success = await self.update_manager.rollback_component(component_id)
return PM3ServiceResult(
success=True,
data={
"message": f"Component {component_id} rolled back successfully",
"component_id": component_id,
}
)
except ValueError as e:
return PM3ServiceResult(
success=False,
error=PM3ServiceError(
code="rollback_error",
message=str(e)
)
)
except Exception as e:
return PM3ServiceResult(
success=False,
error=PM3ServiceError(
code="rollback_error",
message=f"Error rolling back {component_id}",
details=str(e)
)
)