mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-19 19:41:06 +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>
715 lines
23 KiB
Python
Generated
715 lines
23 KiB
Python
Generated
#!/usr/bin/env python3
|
|
"""Linux packaging module for BrowserOS (AppImage and .deb)"""
|
|
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
from typing import List, Optional
|
|
|
|
from ...common.module import CommandModule, ValidationError
|
|
from ...common.context import Context
|
|
from ...common.utils import (
|
|
log_info,
|
|
log_error,
|
|
log_warning,
|
|
log_success,
|
|
run_command,
|
|
safe_rmtree,
|
|
join_paths,
|
|
IS_LINUX,
|
|
)
|
|
from ...common.notify import get_notifier, COLOR_GREEN
|
|
|
|
|
|
class LinuxPackageModule(CommandModule):
|
|
produces = ["appimage", "deb"]
|
|
requires = []
|
|
description = "Create AppImage and .deb packages for Linux"
|
|
|
|
def validate(self, ctx: Context) -> None:
|
|
if not IS_LINUX():
|
|
raise ValidationError("Linux packaging requires Linux")
|
|
|
|
out_dir = join_paths(ctx.chromium_src, ctx.out_dir)
|
|
chrome_binary = join_paths(out_dir, ctx.BROWSEROS_APP_NAME)
|
|
|
|
if not chrome_binary.exists():
|
|
raise ValidationError(f"Chrome binary not found: {chrome_binary}")
|
|
|
|
def execute(self, ctx: Context) -> None:
|
|
log_info(
|
|
f"\n📦 Packaging {ctx.BROWSEROS_APP_BASE_NAME} {ctx.get_browseros_chromium_version()} for Linux ({ctx.architecture})"
|
|
)
|
|
|
|
package_dir = ctx.get_dist_dir()
|
|
package_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
appimage_path = self._package_appimage(ctx, package_dir)
|
|
deb_path = self._package_deb(ctx, package_dir)
|
|
|
|
if appimage_path:
|
|
ctx.artifact_registry.add("appimage", appimage_path)
|
|
if deb_path:
|
|
ctx.artifact_registry.add("deb", deb_path)
|
|
|
|
if not (appimage_path or deb_path):
|
|
raise RuntimeError("Both AppImage and .deb packaging failed")
|
|
|
|
log_success("✅ Linux packaging complete!")
|
|
if appimage_path and deb_path:
|
|
log_info(" Both AppImage and .deb created successfully")
|
|
elif appimage_path:
|
|
log_warning(" Only AppImage created (.deb failed)")
|
|
elif deb_path:
|
|
log_warning(" Only .deb created (AppImage failed)")
|
|
|
|
# Send Slack notification
|
|
notifier = get_notifier()
|
|
artifacts = []
|
|
if appimage_path:
|
|
artifacts.append(appimage_path.name)
|
|
if deb_path:
|
|
artifacts.append(deb_path.name)
|
|
notifier.notify(
|
|
"📦 Package Created",
|
|
f"Linux packages created successfully",
|
|
{
|
|
"Artifacts": ", ".join(artifacts),
|
|
"Version": ctx.semantic_version,
|
|
},
|
|
color=COLOR_GREEN,
|
|
)
|
|
|
|
def _package_appimage(self, ctx: Context, package_dir: Path) -> Optional[Path]:
|
|
return package_appimage(ctx, package_dir)
|
|
|
|
def _package_deb(self, ctx: Context, package_dir: Path) -> Optional[Path]:
|
|
return package_deb(ctx, package_dir)
|
|
|
|
|
|
# =============================================================================
|
|
# Shared Helper Functions (used by both AppImage and .deb)
|
|
# =============================================================================
|
|
|
|
|
|
def copy_browser_files(
|
|
ctx: Context, target_dir: Path, set_sandbox_suid: bool = True
|
|
) -> bool:
|
|
"""Copy browser binaries, libraries, and resources to target directory.
|
|
|
|
Args:
|
|
ctx: Build context
|
|
target_dir: Destination directory for browser files
|
|
set_sandbox_suid: If True, set SUID bit on chrome_sandbox (AppImage only)
|
|
|
|
Returns:
|
|
True if successful, False otherwise
|
|
"""
|
|
target_dir.mkdir(parents=True, exist_ok=True)
|
|
out_dir = join_paths(ctx.chromium_src, ctx.out_dir)
|
|
|
|
files_to_copy = [
|
|
ctx.BROWSEROS_APP_NAME,
|
|
"chrome_crashpad_handler",
|
|
"chrome_sandbox",
|
|
"chromedriver",
|
|
"libEGL.so",
|
|
"libGLESv2.so",
|
|
"libvk_swiftshader.so",
|
|
"libvulkan.so.1",
|
|
"libqt5_shim.so",
|
|
"libqt6_shim.so",
|
|
"vk_swiftshader_icd.json",
|
|
"icudtl.dat",
|
|
"snapshot_blob.bin",
|
|
"v8_context_snapshot.bin",
|
|
"chrome_100_percent.pak",
|
|
"chrome_200_percent.pak",
|
|
"resources.pak",
|
|
]
|
|
|
|
for file in files_to_copy:
|
|
src = join_paths(out_dir, file)
|
|
if Path(src).exists():
|
|
shutil.copy2(src, join_paths(target_dir, file))
|
|
log_info(f" ✓ Copied {file}")
|
|
else:
|
|
log_warning(f" ⚠ File not found: {file}")
|
|
|
|
dirs_to_copy = ["locales", "MEIPreload", "BrowserOSServer"]
|
|
for dir_name in dirs_to_copy:
|
|
src = join_paths(out_dir, dir_name)
|
|
if Path(src).exists():
|
|
shutil.copytree(src, join_paths(target_dir, dir_name), dirs_exist_ok=True)
|
|
log_info(f" ✓ Copied {dir_name}/")
|
|
|
|
browseros_path = Path(join_paths(target_dir, ctx.BROWSEROS_APP_NAME))
|
|
if browseros_path.exists():
|
|
browseros_path.chmod(0o755)
|
|
|
|
sandbox_path = Path(join_paths(target_dir, "chrome_sandbox"))
|
|
if sandbox_path.exists():
|
|
if set_sandbox_suid:
|
|
sandbox_path.chmod(0o4755)
|
|
else:
|
|
sandbox_path.chmod(0o755)
|
|
|
|
crashpad_path = Path(join_paths(target_dir, "chrome_crashpad_handler"))
|
|
if crashpad_path.exists():
|
|
crashpad_path.chmod(0o755)
|
|
|
|
return True
|
|
|
|
|
|
def create_desktop_file(apps_dir: Path, exec_path: str) -> Path:
|
|
"""Create .desktop file with specified Exec path.
|
|
|
|
Args:
|
|
apps_dir: Directory where .desktop file should be created
|
|
exec_path: Full path for Exec= line in desktop file
|
|
|
|
Returns:
|
|
Path to created .desktop file
|
|
"""
|
|
apps_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
desktop_content = f"""[Desktop Entry]
|
|
Version=1.0
|
|
Name=BrowserOS
|
|
GenericName=Web Browser
|
|
Comment=Browse the World Wide Web
|
|
Exec={exec_path} %U
|
|
Terminal=false
|
|
Type=Application
|
|
Categories=Network;WebBrowser;
|
|
MimeType=text/html;text/xml;application/xhtml+xml;application/xml;application/vnd.mozilla.xul+xml;application/rss+xml;application/rdf+xml;image/gif;image/jpeg;image/png;x-scheme-handler/http;x-scheme-handler/https;x-scheme-handler/ftp;x-scheme-handler/chrome;video/webm;application/x-xpinstall;
|
|
Icon=browseros
|
|
StartupWMClass=chromium-browser
|
|
"""
|
|
|
|
desktop_file = Path(join_paths(apps_dir, "browseros.desktop"))
|
|
desktop_file.write_text(desktop_content)
|
|
log_info(" ✓ Created desktop file")
|
|
return desktop_file
|
|
|
|
|
|
def copy_icon(ctx: Context, icons_dir: Path) -> bool:
|
|
"""Copy product icons at multiple sizes to hicolor icon directory.
|
|
|
|
Args:
|
|
ctx: Build context
|
|
icons_dir: Base icons directory (usr/share/icons/hicolor)
|
|
|
|
Returns:
|
|
True if at least one icon was copied, False if none found
|
|
"""
|
|
icons_base = Path(join_paths(ctx.root_dir, "resources", "icons"))
|
|
copied = False
|
|
|
|
for size in [16, 22, 24, 32, 48, 64, 128, 256]:
|
|
icon_src = Path(join_paths(icons_base, f"product_logo_{size}.png"))
|
|
if icon_src.exists():
|
|
icon_dest = Path(join_paths(icons_dir, f"{size}x{size}", "apps", "browseros.png"))
|
|
icon_dest.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(icon_src, icon_dest)
|
|
copied = True
|
|
|
|
if copied:
|
|
log_info(" ✓ Copied icons (multiple sizes)")
|
|
else:
|
|
log_warning(" ⚠ No icon files found in resources/icons/")
|
|
|
|
return copied
|
|
|
|
|
|
# =============================================================================
|
|
# AppImage Packaging Functions
|
|
# =============================================================================
|
|
|
|
|
|
def prepare_appdir(ctx: Context, appdir: Path) -> bool:
|
|
"""Prepare the AppDir structure for AppImage"""
|
|
log_info("📁 Preparing AppDir structure...")
|
|
|
|
app_root = join_paths(appdir, "opt", "browseros")
|
|
usr_share = join_paths(appdir, "usr", "share")
|
|
icons_dir = join_paths(usr_share, "icons", "hicolor")
|
|
apps_dir = join_paths(usr_share, "applications")
|
|
|
|
# Copy browser files (with SUID on chrome_sandbox for AppImage)
|
|
if not copy_browser_files(ctx, app_root, set_sandbox_suid=True):
|
|
return False
|
|
|
|
# Create desktop file
|
|
desktop_file = create_desktop_file(
|
|
apps_dir, f"/opt/browseros/{ctx.BROWSEROS_APP_NAME}"
|
|
)
|
|
|
|
# Copy icons (multiple sizes)
|
|
copy_icon(ctx, icons_dir)
|
|
|
|
# AppImage-specific: Copy desktop file to root and update Exec line
|
|
appdir_desktop = Path(join_paths(appdir, "browseros.desktop"))
|
|
shutil.copy2(desktop_file, appdir_desktop)
|
|
desktop_content = appdir_desktop.read_text()
|
|
desktop_content = desktop_content.replace(
|
|
f"Exec=/opt/browseros/{ctx.BROWSEROS_APP_NAME} %U", "Exec=AppRun %U"
|
|
)
|
|
appdir_desktop.write_text(desktop_content)
|
|
|
|
# AppImage-specific: Copy icon to root (256px for best quality)
|
|
icon_src = Path(join_paths(ctx.root_dir, "resources", "icons", "product_logo_256.png"))
|
|
if not icon_src.exists():
|
|
icon_src = Path(join_paths(ctx.root_dir, "resources", "icons", "product_logo.png"))
|
|
if icon_src.exists():
|
|
appdir_icon = Path(join_paths(appdir, "browseros.png"))
|
|
shutil.copy2(icon_src, appdir_icon)
|
|
|
|
# AppImage-specific: Create AppRun script
|
|
apprun_content = f"""#!/bin/sh
|
|
THIS="$(readlink -f "${{0}}")"
|
|
HERE="$(dirname "${{THIS}}")"
|
|
export LD_LIBRARY_PATH="${{HERE}}"/opt/browseros:$LD_LIBRARY_PATH
|
|
export CHROME_WRAPPER="${{THIS}}"
|
|
"${{HERE}}"/opt/browseros/{ctx.BROWSEROS_APP_NAME} "$@"
|
|
"""
|
|
|
|
apprun_file = Path(join_paths(appdir, "AppRun"))
|
|
apprun_file.write_text(apprun_content)
|
|
apprun_file.chmod(0o755)
|
|
log_info(" ✓ Created AppRun script")
|
|
|
|
return True
|
|
|
|
|
|
def download_appimagetool(ctx: Context) -> Optional[Path]:
|
|
"""Download appimagetool if not available"""
|
|
tool_dir = Path(join_paths(ctx.root_dir, "build", "tools"))
|
|
tool_dir.mkdir(exist_ok=True)
|
|
|
|
tool_path = Path(join_paths(tool_dir, "appimagetool-x86_64.AppImage"))
|
|
|
|
if tool_path.exists():
|
|
log_info("✓ appimagetool already available")
|
|
return tool_path
|
|
|
|
log_info("📥 Downloading appimagetool...")
|
|
url = "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
|
|
|
|
cmd = ["wget", "-O", str(tool_path), url]
|
|
result = run_command(cmd, check=False)
|
|
|
|
if result.returncode == 0:
|
|
tool_path.chmod(0o755)
|
|
log_success("✓ Downloaded appimagetool")
|
|
return tool_path
|
|
else:
|
|
log_error("Failed to download appimagetool")
|
|
return None
|
|
|
|
|
|
def create_appimage(ctx: Context, appdir: Path, output_path: Path) -> bool:
|
|
"""Create AppImage from AppDir"""
|
|
log_info("📦 Creating AppImage...")
|
|
|
|
# Download appimagetool if needed
|
|
appimagetool = download_appimagetool(ctx)
|
|
if not appimagetool:
|
|
return False
|
|
|
|
# Set architecture environment variable (required by appimagetool)
|
|
arch = "x86_64" if ctx.architecture == "x64" else "aarch64"
|
|
|
|
# Create AppImage with ARCH env var set for this command only
|
|
cmd = [
|
|
str(appimagetool),
|
|
"--comp",
|
|
"gzip", # Use gzip compression
|
|
str(appdir),
|
|
str(output_path),
|
|
]
|
|
|
|
# Pass ARCH as environment variable to the subprocess
|
|
env = os.environ.copy()
|
|
env["ARCH"] = arch
|
|
|
|
result = subprocess.run(
|
|
cmd,
|
|
capture_output=True,
|
|
text=True,
|
|
env=env,
|
|
check=False
|
|
)
|
|
|
|
if result.returncode == 0:
|
|
log_success(f"✓ Created AppImage: {output_path}")
|
|
# Make executable
|
|
output_path.chmod(0o755)
|
|
return True
|
|
else:
|
|
log_error("Failed to create AppImage")
|
|
if result.stderr:
|
|
log_error(result.stderr)
|
|
return False
|
|
|
|
|
|
# =============================================================================
|
|
# Debian Package (.deb) Functions
|
|
# =============================================================================
|
|
|
|
|
|
def create_launcher_script(ctx: Context, bin_dir: Path) -> None:
|
|
"""Create launcher script in /usr/bin/browseros."""
|
|
bin_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
launcher_content = f"""#!/bin/sh
|
|
# BrowserOS launcher script
|
|
export LD_LIBRARY_PATH=/usr/lib/browseros:$LD_LIBRARY_PATH
|
|
exec /usr/lib/browseros/{ctx.BROWSEROS_APP_NAME} "$@"
|
|
"""
|
|
|
|
launcher_path = Path(join_paths(bin_dir, "browseros"))
|
|
launcher_path.write_text(launcher_content)
|
|
launcher_path.chmod(0o755)
|
|
log_info(" ✓ Created launcher script")
|
|
|
|
|
|
def create_control_file(ctx: Context, debian_dir: Path) -> None:
|
|
"""Create DEBIAN/control file with package metadata."""
|
|
debian_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
# Version formatting: strip 'v' prefix and spaces, ensure numeric
|
|
version = ctx.get_browseros_chromium_version()
|
|
version = version.lstrip("v").replace(" ", "").replace("_", ".")
|
|
|
|
# Architecture mapping
|
|
deb_arch = "amd64" if ctx.architecture == "x64" else "arm64"
|
|
|
|
control_content = f"""Package: browseros
|
|
Version: {version}
|
|
Section: web
|
|
Priority: optional
|
|
Architecture: {deb_arch}
|
|
Depends: libc6 (>= 2.31), libglib2.0-0, libnss3, libnspr4, libx11-6, libatk1.0-0, libatk-bridge2.0-0, libcups2, libasound2, libdrm2, libgbm1, libpango-1.0-0, libcairo2, libudev1, libxcomposite1, libxdamage1, libxrandr2, libxkbcommon0, libgtk-3-0
|
|
Provides: www-browser, gnome-www-browser
|
|
Recommends: apparmor
|
|
Maintainer: BrowserOS Team <support@browseros.com>
|
|
Homepage: https://www.browseros.com/
|
|
Description: BrowserOS - The open source agentic browser
|
|
BrowserOS is a privacy-focused web browser built on Chromium,
|
|
designed for modern web browsing with AI capabilities.
|
|
"""
|
|
|
|
control_path = Path(join_paths(debian_dir, "control"))
|
|
control_path.write_text(control_content)
|
|
log_info(" ✓ Created DEBIAN/control")
|
|
|
|
|
|
def create_postinst_script(debian_dir: Path) -> None:
|
|
"""Create DEBIAN/postinst script for sandbox, AppArmor, and alternatives."""
|
|
postinst_content = """#!/bin/sh
|
|
# Post-installation script for BrowserOS
|
|
set -e
|
|
|
|
# Set SUID bit on chrome_sandbox for sandboxing support
|
|
if [ -f /usr/lib/browseros/chrome_sandbox ]; then
|
|
chmod 4755 /usr/lib/browseros/chrome_sandbox
|
|
fi
|
|
|
|
# Load AppArmor profile (required for Ubuntu 23.10+ user namespace restrictions)
|
|
if [ -d /etc/apparmor.d ] && command -v apparmor_parser >/dev/null 2>&1; then
|
|
apparmor_parser -r -T -W /etc/apparmor.d/browseros 2>/dev/null || true
|
|
fi
|
|
|
|
# Register as a selectable default browser
|
|
if [ "$1" = "configure" ]; then
|
|
update-alternatives --install /usr/bin/x-www-browser x-www-browser /usr/bin/browseros 40
|
|
update-alternatives --install /usr/bin/gnome-www-browser gnome-www-browser /usr/bin/browseros 40
|
|
fi
|
|
|
|
exit 0
|
|
"""
|
|
|
|
postinst_path = Path(join_paths(debian_dir, "postinst"))
|
|
postinst_path.write_text(postinst_content)
|
|
postinst_path.chmod(0o755)
|
|
log_info(" ✓ Created DEBIAN/postinst")
|
|
|
|
|
|
def create_prerm_script(debian_dir: Path) -> None:
|
|
"""Create DEBIAN/prerm script to clean up on removal."""
|
|
prerm_content = """#!/bin/sh
|
|
# Pre-removal script for BrowserOS
|
|
set -e
|
|
|
|
# Unregister as default browser
|
|
if [ "$1" = "remove" ] || [ "$1" = "deconfigure" ]; then
|
|
update-alternatives --remove x-www-browser /usr/bin/browseros 2>/dev/null || true
|
|
update-alternatives --remove gnome-www-browser /usr/bin/browseros 2>/dev/null || true
|
|
fi
|
|
|
|
# Unload AppArmor profile before files are removed
|
|
if command -v apparmor_parser >/dev/null 2>&1 && [ -f /etc/apparmor.d/browseros ]; then
|
|
apparmor_parser -R /etc/apparmor.d/browseros 2>/dev/null || true
|
|
fi
|
|
|
|
exit 0
|
|
"""
|
|
|
|
prerm_path = Path(join_paths(debian_dir, "prerm"))
|
|
prerm_path.write_text(prerm_content)
|
|
prerm_path.chmod(0o755)
|
|
log_info(" ✓ Created DEBIAN/prerm")
|
|
|
|
|
|
def create_apparmor_profile(ctx: Context, apparmor_dir: Path) -> None:
|
|
"""Create AppArmor profile that permits unprivileged user namespaces.
|
|
|
|
Ubuntu 23.10+ restricts unprivileged user namespaces via AppArmor.
|
|
Without this profile, the Chromium sandbox cannot create namespaces
|
|
and the browser fatally crashes on launch (see GitHub issue #165).
|
|
"""
|
|
apparmor_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
profile_content = f"""# AppArmor profile for BrowserOS
|
|
# This profile allows everything and only exists to give the application
|
|
# a name instead of having the label "unconfined", and to grant permission
|
|
# to create unprivileged user namespaces (required for Chromium sandbox on
|
|
# Ubuntu 23.10+ and other distros that restrict userns via AppArmor).
|
|
|
|
abi <abi/4.0>,
|
|
include <tunables/global>
|
|
|
|
profile browseros /usr/lib/browseros/{ctx.BROWSEROS_APP_NAME} flags=(unconfined) {{
|
|
userns,
|
|
|
|
include if exists <local/browseros>
|
|
}}
|
|
"""
|
|
|
|
profile_path = Path(join_paths(apparmor_dir, "browseros"))
|
|
profile_path.write_text(profile_content)
|
|
log_info(" ✓ Created AppArmor profile")
|
|
|
|
|
|
def create_metainfo_file(ctx: Context, metainfo_dir: Path) -> None:
|
|
"""Create AppStream metainfo file for software center discoverability.
|
|
|
|
Installs to /usr/share/metainfo/ so GNOME Software, KDE Discover,
|
|
and other AppStream-aware tools can display BrowserOS in their catalogs.
|
|
"""
|
|
metainfo_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
version = ctx.get_browseros_chromium_version()
|
|
version = version.lstrip("v").replace(" ", "").replace("_", ".")
|
|
|
|
metainfo_content = f"""<?xml version="1.0" encoding="UTF-8"?>
|
|
<component type="desktop-application">
|
|
<id>browseros.desktop</id>
|
|
<launchable type="desktop-id">browseros.desktop</launchable>
|
|
<name>BrowserOS</name>
|
|
<developer id="com.browseros">
|
|
<name>BrowserOS Team</name>
|
|
</developer>
|
|
<summary>The open source agentic browser</summary>
|
|
<metadata_license>CC0-1.0</metadata_license>
|
|
<project_license>BSD-3-Clause and LGPL-2.1+ and Apache-2.0 and IJG and MIT and GPL-2.0+ and ISC and OpenSSL and (MPL-1.1 or GPL-2.0 or LGPL-2.0)</project_license>
|
|
<url type="homepage">https://www.browseros.com/</url>
|
|
<url type="bugtracker">https://github.com/browseros-ai/BrowserOS/issues</url>
|
|
<url type="help">https://docs.browseros.com/</url>
|
|
<description>
|
|
<p>
|
|
BrowserOS is a privacy-focused web browser built on Chromium,
|
|
designed for modern web browsing with AI capabilities.
|
|
</p>
|
|
<p>
|
|
Browse the web with built-in agentic AI features that help you
|
|
automate tasks and interact with web pages intelligently.
|
|
</p>
|
|
</description>
|
|
<categories>
|
|
<category>Network</category>
|
|
<category>WebBrowser</category>
|
|
</categories>
|
|
<keywords>
|
|
<keyword>web browser</keyword>
|
|
<keyword>chromium</keyword>
|
|
<keyword>ai</keyword>
|
|
<keyword>agentic</keyword>
|
|
<keyword>privacy</keyword>
|
|
</keywords>
|
|
<releases>
|
|
<release version="{version}" />
|
|
</releases>
|
|
<content_rating type="oars-1.1" />
|
|
</component>
|
|
"""
|
|
|
|
metainfo_path = Path(join_paths(metainfo_dir, "browseros.metainfo.xml"))
|
|
metainfo_path.write_text(metainfo_content)
|
|
log_info(" ✓ Created AppStream metainfo")
|
|
|
|
|
|
def prepare_debdir(ctx: Context, debdir: Path) -> bool:
|
|
"""Prepare directory structure for .deb package.
|
|
|
|
Structure:
|
|
debdir/
|
|
├── DEBIAN/
|
|
│ ├── control
|
|
│ ├── postinst
|
|
│ └── prerm
|
|
├── etc/
|
|
│ └── apparmor.d/
|
|
│ └── browseros
|
|
├── usr/
|
|
│ ├── bin/
|
|
│ │ └── browseros (launcher script)
|
|
│ ├── lib/browseros/
|
|
│ │ └── [all browser files]
|
|
│ └── share/
|
|
│ ├── applications/browseros.desktop
|
|
│ ├── icons/hicolor/{16..256}x{16..256}/apps/browseros.png
|
|
│ └── metainfo/browseros.metainfo.xml
|
|
"""
|
|
log_info("📁 Preparing .deb directory structure...")
|
|
|
|
lib_dir = join_paths(debdir, "usr", "lib", "browseros")
|
|
bin_dir = join_paths(debdir, "usr", "bin")
|
|
share_dir = join_paths(debdir, "usr", "share")
|
|
apps_dir = join_paths(share_dir, "applications")
|
|
icons_dir = join_paths(share_dir, "icons", "hicolor")
|
|
metainfo_dir = join_paths(share_dir, "metainfo")
|
|
debian_dir = join_paths(debdir, "DEBIAN")
|
|
apparmor_dir = join_paths(debdir, "etc", "apparmor.d")
|
|
|
|
# Copy browser files (without SUID, will be set in postinst)
|
|
if not copy_browser_files(ctx, lib_dir, set_sandbox_suid=False):
|
|
return False
|
|
|
|
# Create launcher script in /usr/bin/
|
|
create_launcher_script(ctx, bin_dir)
|
|
|
|
# Create desktop file
|
|
create_desktop_file(apps_dir, "/usr/bin/browseros")
|
|
|
|
# Copy icons (multiple sizes for hicolor theme)
|
|
copy_icon(ctx, icons_dir)
|
|
|
|
# Create AppStream metainfo for software center discoverability
|
|
create_metainfo_file(ctx, metainfo_dir)
|
|
|
|
# Install AppArmor profile (fixes crash on Ubuntu 23.10+)
|
|
create_apparmor_profile(ctx, apparmor_dir)
|
|
|
|
# Create DEBIAN metadata files
|
|
create_control_file(ctx, debian_dir)
|
|
create_postinst_script(debian_dir)
|
|
create_prerm_script(debian_dir)
|
|
|
|
log_success("✓ .deb directory prepared")
|
|
return True
|
|
|
|
|
|
def create_deb(ctx: Context, debdir: Path, output_path: Path) -> bool:
|
|
"""Build .deb package using dpkg-deb."""
|
|
log_info("📦 Creating .deb package...")
|
|
|
|
# Verify dpkg-deb is available
|
|
if not shutil.which("dpkg-deb"):
|
|
log_error("dpkg-deb not found. Install with: sudo apt install dpkg")
|
|
return False
|
|
|
|
cmd = [
|
|
"dpkg-deb",
|
|
"--build",
|
|
"--root-owner-group", # Ensure files owned by root:root
|
|
str(debdir),
|
|
str(output_path),
|
|
]
|
|
|
|
result = run_command(cmd, check=False)
|
|
|
|
if result.returncode == 0:
|
|
log_success(f"✓ Created .deb package: {output_path}")
|
|
output_path.chmod(0o644) # Standard package permissions
|
|
return True
|
|
else:
|
|
log_error("Failed to create .deb package")
|
|
return False
|
|
|
|
|
|
# =============================================================================
|
|
# Main Packaging Entry Points
|
|
# =============================================================================
|
|
|
|
|
|
def package_appimage(ctx: Context, package_dir: Path) -> Optional[Path]:
|
|
"""Create AppImage package.
|
|
|
|
Returns:
|
|
Path to created AppImage, or None if failed
|
|
"""
|
|
log_info("🖼️ Building AppImage...")
|
|
|
|
appdir = Path(join_paths(package_dir, f"{ctx.BROWSEROS_APP_BASE_NAME}.AppDir"))
|
|
if appdir.exists():
|
|
safe_rmtree(appdir)
|
|
|
|
if not prepare_appdir(ctx, appdir):
|
|
safe_rmtree(appdir)
|
|
return None
|
|
|
|
filename = ctx.get_artifact_name("appimage")
|
|
output_path = Path(join_paths(package_dir, filename))
|
|
|
|
success = create_appimage(ctx, appdir, output_path)
|
|
safe_rmtree(appdir)
|
|
|
|
if success:
|
|
log_success(f"✅ AppImage created: {output_path.name}")
|
|
log_info(f" Size: {output_path.stat().st_size / 1024 / 1024:.1f} MB")
|
|
return output_path
|
|
|
|
return None
|
|
|
|
|
|
def package_deb(ctx: Context, package_dir: Path) -> Optional[Path]:
|
|
"""Create .deb package.
|
|
|
|
Returns:
|
|
Path to created .deb, or None if failed
|
|
"""
|
|
log_info("📦 Building .deb package...")
|
|
|
|
debdir = Path(join_paths(package_dir, f"{ctx.BROWSEROS_APP_BASE_NAME}_deb"))
|
|
if debdir.exists():
|
|
safe_rmtree(debdir)
|
|
|
|
if not prepare_debdir(ctx, debdir):
|
|
safe_rmtree(debdir)
|
|
return None
|
|
|
|
filename = ctx.get_artifact_name("deb")
|
|
output_path = Path(join_paths(package_dir, filename))
|
|
|
|
success = create_deb(ctx, debdir, output_path)
|
|
safe_rmtree(debdir)
|
|
|
|
if success:
|
|
log_success(f"✅ .deb package created: {output_path.name}")
|
|
log_info(f" Size: {output_path.stat().st_size / 1024 / 1024:.1f} MB")
|
|
return output_path
|
|
|
|
return None
|
|
def package_universal(contexts: List[Context]) -> bool:
|
|
"""Linux doesn't support universal binaries"""
|
|
log_warning("Universal binaries are not supported on Linux")
|
|
return False
|
|
|
|
|
|
# Sign functions moved to sign/linux.py
|
|
# - sign_binaries()
|
|
# These are now in modules/sign/linux.py
|