mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-20 12:30:04 +00:00
* chore: bump PATCH and OFFSET * fix: add AppArmor profile and improve .deb packaging for Ubuntu 23.10+ Ship an AppArmor profile with the .deb package that grants the `userns` permission, fixing the fatal sandbox crash on Ubuntu 23.10+ and other distros that restrict unprivileged user namespaces via AppArmor (closes #165). Also adds: Qt5/Qt6 shim libraries for native file dialogs on KDE, update-alternatives registration for default browser selection, prerm cleanup script, and Provides/Recommends metadata. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: correct CDN download path for .deb and add multi-size icons Update .deb download path from lowercase "browseros.deb" to "BrowserOS.deb" to match the URL advertised in README (cdn.browseros.com/download/BrowserOS.deb). Also install icons at all available sizes instead of only 256x256. Closes #368 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: add multi-size icons and AppStream metainfo to .deb package Install product icons at all standard hicolor sizes (16, 22, 24, 32, 48, 64, 128, 256) instead of only 256px, so desktop environments can pick the appropriate resolution for panels, menus, and task switchers. Ship AppStream metainfo at /usr/share/metainfo/browseros.metainfo.xml so GNOME Software, KDE Discover, and other software centers can discover and display BrowserOS in their catalogs. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: guard postinst update-alternatives with $1=configure check Matches prerm's pattern — only register alternatives during normal configure, not during dpkg error-recovery paths (abort-upgrade, etc.) where /usr/bin/browseros may not exist yet. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
208 lines
5.8 KiB
Python
Generated
208 lines
5.8 KiB
Python
Generated
#!/usr/bin/env python3
|
|
"""Common utilities for release modules"""
|
|
|
|
import subprocess
|
|
from datetime import datetime
|
|
from typing import Dict, List, Optional
|
|
|
|
from ...common.env import EnvConfig
|
|
from ...common.utils import log_warning
|
|
from ..storage import get_release_json, get_r2_client, BOTO3_AVAILABLE
|
|
|
|
PLATFORMS = ["macos", "win", "linux"]
|
|
PLATFORM_DISPLAY_NAMES = {"macos": "macOS", "win": "Windows", "linux": "Linux"}
|
|
|
|
DOWNLOAD_PATH_MAPPING = {
|
|
"macos": {
|
|
"arm64": "download/BrowserOS-arm64.dmg",
|
|
"x64": "download/BrowserOS-x86_64.dmg",
|
|
"universal": "download/BrowserOS.dmg",
|
|
},
|
|
"win": {
|
|
"x64_installer": "download/BrowserOS_installer.exe",
|
|
},
|
|
"linux": {
|
|
"x64_appimage": "download/BrowserOS.AppImage",
|
|
"x64_deb": "download/BrowserOS.deb",
|
|
},
|
|
}
|
|
|
|
|
|
def fetch_all_release_metadata(
|
|
version: str, env: Optional[EnvConfig] = None
|
|
) -> Dict[str, Dict]:
|
|
"""Fetch release.json from all platforms for a version"""
|
|
if env is None:
|
|
env = EnvConfig()
|
|
|
|
metadata = {}
|
|
for platform in PLATFORMS:
|
|
release_data = get_release_json(version, platform, env)
|
|
if release_data:
|
|
metadata[platform] = release_data
|
|
|
|
return metadata
|
|
|
|
|
|
def list_all_versions(env: Optional[EnvConfig] = None) -> List[str]:
|
|
"""List all available release versions from R2.
|
|
|
|
Returns versions sorted in descending order (newest first).
|
|
"""
|
|
if not BOTO3_AVAILABLE:
|
|
return []
|
|
|
|
if env is None:
|
|
env = EnvConfig()
|
|
|
|
if not env.has_r2_config():
|
|
return []
|
|
|
|
client = get_r2_client(env)
|
|
if not client:
|
|
return []
|
|
|
|
versions = []
|
|
continuation_token = None
|
|
|
|
while True:
|
|
kwargs = {
|
|
"Bucket": env.r2_bucket,
|
|
"Prefix": "releases/",
|
|
"Delimiter": "/",
|
|
}
|
|
if continuation_token:
|
|
kwargs["ContinuationToken"] = continuation_token
|
|
|
|
try:
|
|
response = client.list_objects_v2(**kwargs)
|
|
except Exception:
|
|
break
|
|
|
|
for prefix in response.get("CommonPrefixes", []):
|
|
# prefix looks like "releases/0.31.0/"
|
|
version = prefix["Prefix"].replace("releases/", "").rstrip("/")
|
|
if version:
|
|
versions.append(version)
|
|
|
|
if not response.get("IsTruncated"):
|
|
break
|
|
continuation_token = response.get("NextContinuationToken")
|
|
|
|
# Sort versions descending (newest first) using version tuple comparison
|
|
def version_key(v: str) -> tuple:
|
|
parts = []
|
|
for part in v.split("."):
|
|
try:
|
|
parts.append(int(part))
|
|
except ValueError:
|
|
parts.append(0)
|
|
return tuple(parts)
|
|
|
|
versions.sort(key=version_key, reverse=True)
|
|
return versions
|
|
|
|
|
|
def format_size(size_bytes: int) -> str:
|
|
"""Format bytes as human-readable size"""
|
|
if size_bytes >= 1024 * 1024 * 1024:
|
|
return f"{size_bytes / (1024 * 1024 * 1024):.1f} GB"
|
|
elif size_bytes >= 1024 * 1024:
|
|
return f"{size_bytes / (1024 * 1024):.0f} MB"
|
|
elif size_bytes >= 1024:
|
|
return f"{size_bytes / 1024:.0f} KB"
|
|
return f"{size_bytes} B"
|
|
|
|
|
|
def generate_appcast_item(
|
|
artifact: Dict,
|
|
version: str,
|
|
sparkle_version: str,
|
|
build_date: str,
|
|
) -> str:
|
|
"""Generate Sparkle <item> XML for an artifact"""
|
|
try:
|
|
dt = datetime.fromisoformat(build_date.replace("Z", "+00:00"))
|
|
pub_date = dt.strftime("%a, %d %b %Y %H:%M:%S %z")
|
|
except Exception:
|
|
pub_date = build_date
|
|
|
|
signature = artifact.get("sparkle_signature", "")
|
|
length = artifact.get("sparkle_length", artifact.get("size", 0))
|
|
|
|
return f"""<item>
|
|
<title>BrowserOS - {version}</title>
|
|
<description sparkle:format="plain-text">
|
|
</description>
|
|
<sparkle:version>{sparkle_version}</sparkle:version>
|
|
<sparkle:shortVersionString>{version}</sparkle:shortVersionString>
|
|
<pubDate>{pub_date}</pubDate>
|
|
<link>https://browseros.com</link>
|
|
<enclosure
|
|
url="{artifact['url']}"
|
|
sparkle:edSignature="{signature}"
|
|
length="{length}"
|
|
type="application/octet-stream" />
|
|
<sparkle:minimumSystemVersion>10.15</sparkle:minimumSystemVersion>
|
|
</item>"""
|
|
|
|
|
|
def generate_release_notes(version: str, metadata: Dict[str, Dict]) -> str:
|
|
"""Generate markdown release notes from metadata"""
|
|
chromium_version = "unknown"
|
|
for platform in PLATFORMS:
|
|
if platform in metadata:
|
|
chromium_version = metadata[platform].get("chromium_version", "unknown")
|
|
break
|
|
|
|
notes = f"""## BrowserOS v{version}
|
|
|
|
Chromium version: {chromium_version}
|
|
|
|
### Downloads
|
|
|
|
"""
|
|
for platform in PLATFORMS:
|
|
if platform not in metadata:
|
|
continue
|
|
|
|
platform_name = PLATFORM_DISPLAY_NAMES[platform]
|
|
notes += f"**{platform_name}:**\n"
|
|
|
|
for key, artifact in metadata[platform].get("artifacts", {}).items():
|
|
notes += f"- [{artifact['filename']}]({artifact['url']})\n"
|
|
notes += "\n"
|
|
|
|
return notes
|
|
|
|
|
|
def get_repo_from_git() -> Optional[str]:
|
|
"""Get GitHub repo (owner/name) from git remote"""
|
|
try:
|
|
result = subprocess.run(
|
|
["git", "remote", "get-url", "origin"],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
remote_url = result.stdout.strip()
|
|
|
|
if "github.com" not in remote_url:
|
|
return None
|
|
|
|
if remote_url.startswith("git@"):
|
|
return remote_url.split(":")[-1].replace(".git", "")
|
|
else:
|
|
return "/".join(remote_url.split("/")[-2:]).replace(".git", "")
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def check_gh_cli() -> bool:
|
|
"""Check if gh CLI is available"""
|
|
try:
|
|
subprocess.run(["gh", "--version"], capture_output=True, check=True)
|
|
return True
|
|
except Exception:
|
|
return False
|