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:
@@ -8,6 +8,7 @@ import asyncio
|
||||
import importlib.util
|
||||
import inspect
|
||||
import json
|
||||
import re
|
||||
import time
|
||||
from dataclasses import dataclass, asdict, field
|
||||
from datetime import datetime, timezone
|
||||
@@ -16,6 +17,8 @@ from pathlib import Path
|
||||
from typing import Optional, Dict, Any, List, Callable, Set
|
||||
import sys
|
||||
|
||||
import aiohttp
|
||||
|
||||
from .. import config
|
||||
|
||||
|
||||
@@ -400,6 +403,18 @@ class PluginBase:
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class PluginUpdateInfo:
|
||||
"""Information about an available plugin update."""
|
||||
plugin_id: str
|
||||
current_version: str
|
||||
available_version: str
|
||||
changelog: str = ""
|
||||
download_url: str = ""
|
||||
download_size: Optional[int] = None
|
||||
source_repo: str = ""
|
||||
|
||||
|
||||
class PluginManager:
|
||||
"""Manages plugin loading, enabling, lifecycle, and header widgets."""
|
||||
|
||||
@@ -790,6 +805,121 @@ class PluginManager:
|
||||
del self._header_widgets[widget_id]
|
||||
print(f"Expired widget removed: {widget_id}")
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# Plugin Update Checking
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def _extract_github_repo(homepage: Optional[str]) -> Optional[str]:
|
||||
"""Extract 'owner/repo' from a GitHub URL.
|
||||
|
||||
Args:
|
||||
homepage: Plugin homepage URL
|
||||
|
||||
Returns:
|
||||
'owner/repo' string or None
|
||||
"""
|
||||
if not homepage:
|
||||
return None
|
||||
m = re.match(r"https?://github\.com/([^/]+/[^/]+?)(?:\.git)?/?$", homepage)
|
||||
return m.group(1) if m else None
|
||||
|
||||
async def check_plugin_updates(self) -> List[PluginUpdateInfo]:
|
||||
"""Check all installed plugins for available updates.
|
||||
|
||||
Queries each plugin's GitHub repo (derived from homepage) for
|
||||
newer releases. Skips plugins without a GitHub homepage.
|
||||
|
||||
Returns:
|
||||
List of PluginUpdateInfo for plugins with updates available
|
||||
"""
|
||||
updates: List[PluginUpdateInfo] = []
|
||||
|
||||
plugins_to_check = []
|
||||
for plugin_id, info in self._plugins.items():
|
||||
repo = self._extract_github_repo(info.metadata.homepage)
|
||||
if repo:
|
||||
plugins_to_check.append((plugin_id, info, repo))
|
||||
|
||||
if not plugins_to_check:
|
||||
return updates
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
for plugin_id, info, repo in plugins_to_check:
|
||||
try:
|
||||
update = await self._check_single_plugin_update(
|
||||
session, plugin_id, info, repo
|
||||
)
|
||||
if update:
|
||||
updates.append(update)
|
||||
except Exception as e:
|
||||
print(f"Error checking updates for plugin {plugin_id}: {e}")
|
||||
|
||||
return updates
|
||||
|
||||
async def _check_single_plugin_update(
|
||||
self,
|
||||
session: aiohttp.ClientSession,
|
||||
plugin_id: str,
|
||||
info: PluginInfo,
|
||||
repo: str,
|
||||
) -> Optional[PluginUpdateInfo]:
|
||||
"""Check a single plugin for available update.
|
||||
|
||||
Args:
|
||||
session: aiohttp session
|
||||
plugin_id: Plugin identifier
|
||||
info: Current plugin info
|
||||
repo: GitHub 'owner/repo' string
|
||||
|
||||
Returns:
|
||||
PluginUpdateInfo if update available, None otherwise
|
||||
"""
|
||||
url = f"https://api.github.com/repos/{repo}/releases/latest"
|
||||
async with session.get(url) as resp:
|
||||
if resp.status != 200:
|
||||
return None
|
||||
data = await resp.json()
|
||||
|
||||
latest_tag = data.get("tag_name", "").lstrip("v")
|
||||
current_ver = info.metadata.version
|
||||
|
||||
if not self._is_newer_plugin_version(latest_tag, current_ver):
|
||||
return None
|
||||
|
||||
# Find a suitable download asset
|
||||
download_url = ""
|
||||
download_size = None
|
||||
for asset in data.get("assets", []):
|
||||
if asset["name"].endswith((".tar.gz", ".zip")):
|
||||
download_url = asset["browser_download_url"]
|
||||
download_size = asset.get("size")
|
||||
break
|
||||
|
||||
# Fall back to source tarball
|
||||
if not download_url:
|
||||
download_url = data.get("tarball_url", "")
|
||||
|
||||
return PluginUpdateInfo(
|
||||
plugin_id=plugin_id,
|
||||
current_version=current_ver,
|
||||
available_version=latest_tag,
|
||||
changelog=data.get("body", ""),
|
||||
download_url=download_url,
|
||||
download_size=download_size,
|
||||
source_repo=repo,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _is_newer_plugin_version(latest: str, current: str) -> bool:
|
||||
"""Compare two version strings, returning True if latest > current."""
|
||||
def parse(v: str) -> tuple:
|
||||
return tuple(int(x) for x in re.split(r'[-+]', v)[0].split('.'))
|
||||
try:
|
||||
return parse(latest) > parse(current)
|
||||
except (ValueError, AttributeError):
|
||||
return latest != current
|
||||
|
||||
|
||||
# Global plugin manager instance
|
||||
_plugin_manager: Optional[PluginManager] = None
|
||||
|
||||
Reference in New Issue
Block a user