mirror of
https://github.com/browseros-ai/BrowserOS.git
synced 2026-05-13 23:53:25 +00:00
352 lines
11 KiB
Python
352 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Patch management module for Nxtscape build system
|
|
"""
|
|
|
|
import sys
|
|
import shutil
|
|
import subprocess
|
|
from pathlib import Path
|
|
from typing import Iterator, List, Tuple, Optional
|
|
from context import BuildContext
|
|
from utils import (
|
|
log_info,
|
|
log_error,
|
|
log_success,
|
|
log_warning,
|
|
IS_WINDOWS,
|
|
IS_LINUX,
|
|
IS_MACOS,
|
|
)
|
|
|
|
# Switch to new patching system using dev CLI
|
|
NEW_PATCHING = True
|
|
|
|
|
|
def apply_patches_with_dev_cli(
|
|
ctx: BuildContext, interactive: bool = False, commit_each: bool = False
|
|
) -> bool:
|
|
"""Apply patches using the new dev CLI system"""
|
|
if not ctx.apply_patches:
|
|
log_info("\n⏭️ Skipping patches")
|
|
return True
|
|
|
|
log_info("\n🩹 Applying patches using new dev CLI system...")
|
|
|
|
# Check if git is available
|
|
if not shutil.which("git"):
|
|
log_error("Git is not available in PATH")
|
|
log_error("Please install Git to apply patches")
|
|
raise RuntimeError("Git not found in PATH")
|
|
|
|
# Import dev CLI module
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
from modules.dev_cli.apply import apply_all_patches
|
|
|
|
# Call the dev CLI function directly
|
|
applied, failed = apply_all_patches(
|
|
build_ctx=ctx,
|
|
commit_each=commit_each,
|
|
dry_run=False,
|
|
interactive=interactive,
|
|
)
|
|
|
|
# Handle results
|
|
if failed and not interactive:
|
|
# In non-interactive mode, fail if any patches failed
|
|
raise RuntimeError(f"Failed to apply {len(failed)} patches")
|
|
|
|
return True
|
|
|
|
|
|
def apply_patches(
|
|
ctx: BuildContext, interactive: bool = False, commit_each: bool = False
|
|
) -> bool:
|
|
"""Apply Nxtscape patches"""
|
|
# Use new patching system if enabled
|
|
if NEW_PATCHING:
|
|
return apply_patches_with_dev_cli(ctx, interactive, commit_each)
|
|
|
|
# Otherwise, use the legacy patching system
|
|
if not ctx.apply_patches:
|
|
log_info("\n⏭️ Skipping patches")
|
|
return True
|
|
|
|
log_info("\n🩹 Applying patches...")
|
|
|
|
# Check if git is available
|
|
if not shutil.which("git"):
|
|
log_error("Git is not available in PATH")
|
|
log_error("Please install Git to apply patches")
|
|
raise RuntimeError("Git not found in PATH")
|
|
|
|
# Get list of patches
|
|
root_patches_dir = ctx.get_patches_dir()
|
|
nxtscape_patches_dir = ctx.get_nxtscape_patches_dir()
|
|
|
|
if not nxtscape_patches_dir.exists():
|
|
log_error(f"Patches directory not found: {nxtscape_patches_dir}")
|
|
raise FileNotFoundError(f"Patches directory not found: {nxtscape_patches_dir}")
|
|
|
|
# get all patches in nxtscape_patches_dir
|
|
all_patches = list(parse_series_file(root_patches_dir))
|
|
|
|
# Filter out patches that should be skipped on this platform
|
|
patches = []
|
|
skipped_count = 0
|
|
for patch_path, skip_platforms in all_patches:
|
|
if should_skip_patch(skip_platforms):
|
|
log_info(
|
|
f"⏭️ Skipping {patch_path.name} (not for {get_current_platform()})"
|
|
)
|
|
skipped_count += 1
|
|
else:
|
|
patches.append((patch_path, skip_platforms))
|
|
|
|
if not patches:
|
|
if skipped_count > 0:
|
|
log_info(
|
|
f"⚠️ All {skipped_count} patches were skipped for {get_current_platform()}"
|
|
)
|
|
else:
|
|
log_info("⚠️ No patches found to apply")
|
|
return True
|
|
|
|
log_info(
|
|
f"Found {len(patches)} patches to apply ({skipped_count} skipped for {get_current_platform()})"
|
|
)
|
|
|
|
if interactive:
|
|
log_info(
|
|
"🔍 Interactive mode enabled - will ask for confirmation before each patch"
|
|
)
|
|
|
|
if commit_each:
|
|
log_info("📝 Git commit mode enabled - will create a commit after each patch")
|
|
|
|
# Apply each patch
|
|
for i, (patch_path, _) in enumerate(patches, 1):
|
|
if not patch_path.exists():
|
|
log_info(f"⚠️ Patch file not found: {patch_path}")
|
|
continue
|
|
|
|
if interactive:
|
|
# Show patch info and ask for confirmation
|
|
log_info(f"\n{'='*60}")
|
|
log_info(f"Patch {i}/{len(patches)}: {patch_path.name}")
|
|
log_info(f"{'='*60}")
|
|
|
|
while True:
|
|
choice = input(
|
|
"\nOptions:\n 1) Apply this patch\n 2) Skip this patch\n 3) Stop patching here\nEnter your choice (1-3): "
|
|
).strip()
|
|
|
|
if choice == "1":
|
|
apply_single_patch(
|
|
patch_path, ctx.chromium_src, i, len(patches), commit_each
|
|
)
|
|
break
|
|
elif choice == "2":
|
|
log_warning(f"⏭️ Skipping patch {patch_path.name}")
|
|
break
|
|
elif choice == "3":
|
|
log_info("Stopping patch process as requested")
|
|
return True
|
|
else:
|
|
log_error("Invalid choice. Please enter 1, 2, or 3.")
|
|
else:
|
|
apply_single_patch(
|
|
patch_path, ctx.chromium_src, i, len(patches), commit_each
|
|
)
|
|
|
|
log_success("Patches applied")
|
|
return True
|
|
|
|
|
|
def get_current_platform() -> str:
|
|
"""Get the current platform name for skip checking"""
|
|
if IS_WINDOWS:
|
|
return "windows"
|
|
elif IS_LINUX:
|
|
return "linux"
|
|
elif IS_MACOS:
|
|
return "darwin"
|
|
else:
|
|
return "unknown"
|
|
|
|
|
|
def should_skip_patch(skip_platforms: Optional[List[str]]) -> bool:
|
|
"""Check if a patch should be skipped on the current platform"""
|
|
if skip_platforms is None:
|
|
return False
|
|
|
|
current_platform = get_current_platform()
|
|
|
|
# Also check for common aliases
|
|
platform_aliases = {
|
|
"darwin": ["darwin", "macos", "mac", "osx"],
|
|
"linux": ["linux"],
|
|
"windows": ["windows", "win32", "win"],
|
|
}
|
|
|
|
current_aliases = platform_aliases.get(current_platform, [current_platform])
|
|
|
|
# Check if any skip platform matches our current platform or its aliases
|
|
for skip_platform in skip_platforms:
|
|
if skip_platform in current_aliases:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def parse_series_file(patches_dir: Path) -> Iterator[Tuple[Path, Optional[List[str]]]]:
|
|
"""Parse the series file to get list of patches with skip directives
|
|
|
|
Returns tuples of (patch_path, skip_platforms) where skip_platforms
|
|
is None if no platforms should be skipped, or a list of platform names
|
|
"""
|
|
series_file = patches_dir / "series"
|
|
|
|
# Read series file
|
|
with series_file.open("r") as f:
|
|
lines = f.read().splitlines()
|
|
|
|
patches = []
|
|
for line in lines:
|
|
# Skip empty lines and comments
|
|
line = line.strip()
|
|
if not line or line.startswith("#"):
|
|
continue
|
|
|
|
skip_platforms = None
|
|
|
|
# Check for #skip directive
|
|
if " #skip:" in line:
|
|
parts = line.split(" #skip:")
|
|
line = parts[0].strip()
|
|
# Parse platforms to skip
|
|
skip_platforms = [p.strip().lower() for p in parts[1].split(",")]
|
|
elif " #" in line:
|
|
# Remove other inline comments
|
|
line = line.split(" #")[0].strip()
|
|
|
|
patches.append((patches_dir / line, skip_platforms))
|
|
|
|
return patches
|
|
|
|
|
|
def apply_single_patch(
|
|
patch_path: Path,
|
|
tree_path: Path,
|
|
current_num: int,
|
|
total: int,
|
|
commit_each: bool = False,
|
|
) -> bool:
|
|
"""Apply a single patch using git apply"""
|
|
# Use git apply which is cross-platform and handles patch format better
|
|
cmd = [
|
|
"git",
|
|
"apply",
|
|
"--ignore-whitespace",
|
|
"--whitespace=nowarn",
|
|
"-p1",
|
|
str(patch_path),
|
|
]
|
|
|
|
log_info(f" * Applying {patch_path.name} ({current_num}/{total})")
|
|
|
|
# Run from the tree_path directory
|
|
result = subprocess.run(cmd, text=True, capture_output=True, cwd=tree_path)
|
|
|
|
if result.returncode == 0:
|
|
if commit_each:
|
|
commit_patch(patch_path, tree_path)
|
|
return True
|
|
|
|
# Patch failed - try with --3way for better conflict resolution
|
|
log_warning(f"Standard apply failed, trying 3-way merge for {patch_path.name}")
|
|
cmd.append("--3way")
|
|
result = subprocess.run(
|
|
cmd[:-1] + ["--3way", str(patch_path)],
|
|
text=True,
|
|
capture_output=True,
|
|
cwd=tree_path,
|
|
)
|
|
|
|
if result.returncode == 0:
|
|
log_info(f"✓ Applied {patch_path.name} with 3-way merge")
|
|
if commit_each:
|
|
commit_patch(patch_path, tree_path)
|
|
return True
|
|
|
|
# Patch still failed
|
|
log_error(f"Failed to apply patch: {patch_path.name}")
|
|
if result.stderr:
|
|
log_error(f"Error: {result.stderr}")
|
|
|
|
# Interactive prompt for handling failure
|
|
log_error("\n============================================")
|
|
log_error(f"Patch {patch_path.name} failed to apply.")
|
|
log_info("Options:")
|
|
log_info(" 1) Skip this patch and continue")
|
|
log_info(" 2) Retry this patch")
|
|
log_info(" 3) Abort patching")
|
|
log_info(" 4) Interactive mode - Fix manually and continue")
|
|
|
|
while True:
|
|
choice = input("Enter your choice (1-4): ").strip()
|
|
|
|
if choice == "1":
|
|
log_warning(f"⏭️ Skipping patch {patch_path.name}")
|
|
return True # Continue with next patch
|
|
elif choice == "2":
|
|
return apply_single_patch(
|
|
patch_path, tree_path, current_num, total, commit_each
|
|
)
|
|
elif choice == "3":
|
|
log_error("Aborting patch process")
|
|
raise RuntimeError("Patch process aborted by user")
|
|
elif choice == "4":
|
|
log_info("\nPlease fix the issue manually, then press Enter to continue...")
|
|
input("Press Enter when ready: ")
|
|
# Retry after manual fix
|
|
return apply_single_patch(
|
|
patch_path, tree_path, current_num, total, commit_each
|
|
)
|
|
|
|
|
|
def commit_patch(patch_path: Path, tree_path: Path) -> bool:
|
|
"""Create a git commit for the applied patch"""
|
|
try:
|
|
# Stage all changes
|
|
cmd_add = ["git", "add", "-A"]
|
|
result = subprocess.run(cmd_add, capture_output=True, text=True, cwd=tree_path)
|
|
if result.returncode != 0:
|
|
log_warning(f"Failed to stage changes for patch {patch_path.name}")
|
|
if result.stderr:
|
|
log_warning(f"Error: {result.stderr}")
|
|
return False
|
|
|
|
# Create commit message
|
|
patch_name = patch_path.stem # Remove .patch extension
|
|
commit_message = f"patch: {patch_name}"
|
|
|
|
# Create the commit
|
|
cmd_commit = ["git", "commit", "-m", commit_message]
|
|
result = subprocess.run(
|
|
cmd_commit, capture_output=True, text=True, cwd=tree_path
|
|
)
|
|
|
|
if result.returncode == 0:
|
|
log_success(f"📝 Created commit for patch: {patch_name}")
|
|
return True
|
|
else:
|
|
log_warning(f"Failed to commit patch {patch_path.name}")
|
|
if result.stderr:
|
|
log_warning(f"Error: {result.stderr}")
|
|
return False
|
|
|
|
except Exception as e:
|
|
log_warning(f"Error creating commit for patch {patch_path.name}: {e}")
|
|
return False
|