Files
PawletOS-Build/environ.py
T
2025-11-05 17:22:24 +00:00

426 lines
16 KiB
Python

import os
import sys
import subprocess
import argparse
from pathlib import Path
import yaml
import requests
# Import the patch handler
from scripts.better_patch import GitPatchHandler
# Helper functions
def run_command(command, cwd=None, shell=False):
"""Run a shell command in a subprocess."""
print(f"Running command: {' '.join(command) if isinstance(command, list) else command}")
result = subprocess.run(command, cwd=cwd, capture_output=True, text=True, shell=shell)
if result.returncode != 0:
print(f"Error running command: {' '.join(command) if isinstance(command, list) else command}")
print(result.stderr)
raise subprocess.CalledProcessError(result.returncode, command)
print(result.stdout)
return result
def run_command_realtime(command, cwd=None, shell=False):
"""Run a shell command in a subprocess with real-time output."""
print(f"Running command: {' '.join(command) if isinstance(command, list) else command}")
# For repo commands, use direct terminal output to preserve progress bars
if isinstance(command, list) and "repo" in command[0]:
# Direct passthrough to terminal for repo commands
result = subprocess.run(command, cwd=cwd, shell=shell)
if result.returncode != 0:
raise subprocess.CalledProcessError(result.returncode, command)
return result.returncode
else:
# For other commands, use the buffered approach
process = subprocess.Popen(
command,
cwd=cwd,
shell=shell,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
universal_newlines=True
)
# Print output in real-time
for line in process.stdout:
print(line, end='', flush=True)
# Wait for process to complete and get return code
return_code = process.wait()
if return_code != 0:
raise subprocess.CalledProcessError(return_code, command)
return return_code
def load_repo_config(config_file="repo.yml"):
"""Load repository configuration from YAML file."""
if not os.path.exists(config_file):
print(f"Warning: Repo config file {config_file} not found")
return None
with open(config_file, 'r') as f:
config = yaml.safe_load(f)
return config
def repo_init(source_dir, config):
"""Initialize repo with configuration."""
if not config or 'repo' not in config:
print("No repo configuration found")
sys.exit(1)
repo_config = config['repo']
# Build repo init command
init_cmd = ["repo", "init"]
# Add repo URL
if 'url' in repo_config:
init_cmd.extend(["-u", repo_config['url']])
else:
print("Error: No repo URL specified in config")
return
# Add branch if specified
if 'branch' in repo_config:
init_cmd.extend(["-b", repo_config['branch']])
# Add other options
if 'options' in repo_config:
for option in repo_config['options']:
init_cmd.append(option)
print("Initializing repo...")
run_command_realtime(init_cmd, cwd=source_dir)
# Download local manifests if specified
if 'local_manifests' in repo_config:
download_local_manifests(source_dir, repo_config['local_manifests'])
def download_local_manifests(source_dir, manifests):
"""Download local manifest files using requests."""
manifests_dir = os.path.join(source_dir, ".repo", "local_manifests")
os.makedirs(manifests_dir, exist_ok=True)
for manifest_name, manifest_url in manifests.items():
manifest_path = os.path.join(manifests_dir, f"{manifest_name}.xml")
print(f"Downloading local manifest: {manifest_name} from {manifest_url}")
try:
response = requests.get(manifest_url, timeout=30)
response.raise_for_status()
with open(manifest_path, 'w') as f:
f.write(response.text)
print(f"Successfully downloaded {manifest_name}.xml")
except requests.exceptions.RequestException as e:
print(f"Failed to download {manifest_name}.xml: {e}")
def setup_git_lfs(source_dir):
"""Set up Git LFS in the repository."""
print("Setting up Git LFS...")
try:
# Check if git-lfs is installed
run_command(["git", "lfs", "version"], cwd=source_dir)
except (subprocess.CalledProcessError, FileNotFoundError):
print("Warning: git-lfs is not installed. Skipping Git LFS setup.")
return False
try:
# Initialize Git LFS in the repo
run_command(["git", "lfs", "install"], cwd=source_dir)
# Pull LFS objects for all projects
print("Pulling Git LFS objects...")
run_command_realtime(["repo", "forall", "-c", "git lfs pull"], cwd=source_dir)
print("Git LFS setup completed successfully!")
return True
except subprocess.CalledProcessError as e:
print(f"Warning: Git LFS setup failed: {e}")
return False
def find_patches(patch_dirs):
"""Find all patch files in the patch directories."""
patches = []
for patch_dir in patch_dirs:
if not os.path.exists(patch_dir):
print(f"Warning: Patch directory does not exist: {patch_dir}")
continue
patch_files = list(Path(patch_dir).glob("*.patch"))
print(f"Found {len(patch_files)} patches in {patch_dir}")
patches.extend(patch_files)
# Remove duplicates (in case same patch appears in multiple directories)
unique_patches = []
seen_names = set()
for patch in patches:
if patch.name not in seen_names:
unique_patches.append(patch)
seen_names.add(patch.name)
return unique_patches
def apply_patch_legacy(patch_file, source_dir, patch_level=1, force=False):
"""Apply a single patch file using legacy patch command."""
print(f"Applying patch: {patch_file} to {source_dir} with -p{patch_level}")
try:
run_command(["patch", f"-p{patch_level}", "-i", str(patch_file)], cwd=source_dir)
except subprocess.CalledProcessError as e:
if force:
print(f"Patch failed but forcing continue: {patch_file}")
# Try to reverse any partially applied patch
try:
run_command(["patch", f"-p{patch_level}", "-R", "-i", str(patch_file)], cwd=source_dir)
except:
print(f"Could not reverse failed patch: {patch_file}")
else:
raise
def apply_patches_robust(patches, source_dir, patch_level=1, force=False, ignore_patches=None, verbose=False):
"""Apply patches using the robust patch handler."""
if ignore_patches is None:
ignore_patches = []
# Filter out ignored patches
patches_to_apply = [p for p in patches if p.name not in ignore_patches]
if not patches_to_apply:
print("No patches to apply after filtering")
return True, []
print(f"Using robust patch handler for {len(patches_to_apply)} patches")
# Initialize the patch handler
handler = GitPatchHandler(repo_path=source_dir, verbose=verbose)
# Analyze patches first
analysis = handler.analyze_patch_series([str(p) for p in patches_to_apply])
print("Patch analysis completed:")
print(f" - Total patches: {analysis['summary']['total_patches']}")
print(f" - Files modified: {analysis['summary']['total_files_modified']}")
print(f" - Potential conflicts: {len(analysis['summary']['file_conflicts_detected'])}")
# Apply patches
result = handler.apply_patch_series(
[str(p) for p in patches_to_apply],
target_dir=".", # Already in source_dir
stop_on_conflict=not force,
rollback_on_failure=True
)
# Print results
print("\nPatch application results:")
print(f" - Overall success: {result['success']}")
print(f" - Patches applied: {len(result['patches_applied'])}/{len(patches_to_apply)}")
# Print details for failed patches
failed_patches = []
for patch_result in result['results']:
patch_name = Path(patch_result['patch']).name
if not patch_result['success']:
failed_patches.append(patch_name)
print(f" - FAILED: {patch_name} - {patch_result['message']}")
if patch_result.get('conflicts'):
print(f" Conflicts: {', '.join(patch_result['conflicts'])}")
return result['success'], failed_patches
def apply_patches_legacy(patches, source_dir, patch_level=1, force=False, ignore_patches=None):
"""Legacy patch application for fallback."""
if ignore_patches is None:
ignore_patches = []
applied_patches = 0
failed_patches = []
for patch_file in patches:
patch_name = patch_file.name
if patch_name in ignore_patches:
print(f"Ignoring patch: {patch_name}")
continue
try:
apply_patch_legacy(patch_file, source_dir, patch_level, force)
applied_patches += 1
except Exception as e:
print(f"Failed to apply patch {patch_name}: {e}")
failed_patches.append(patch_name)
if not force:
print("Stopping due to patch failure. Use --forceApplyPatches to continue.")
return False, failed_patches
print(f"Successfully applied {applied_patches} patches")
return True, failed_patches
def repo_sync(force=False, jobs=None, current_dir="."):
"""Sync Android source using repo with real-time output."""
print("Syncing Android source...")
sync_cmd = ["repo", "sync"]
if force:
sync_cmd.extend(["--force-sync"])
if jobs:
sync_cmd.extend([f"-j{jobs}"])
# Use real-time output for repo sync
run_command_realtime(sync_cmd, cwd=current_dir)
def parse_arguments():
"""Parse command line arguments."""
parser = argparse.ArgumentParser(description="Android build environment setup script")
parser.add_argument("--noSync", action="store_true",
help="Skip repo sync (sync is enabled by default)")
parser.add_argument("--forceResync", action="store_true",
help="Force sync source code with repo sync --force-sync")
parser.add_argument("--applyPatches", action="store_true",
help="Apply patches from patches directory")
parser.add_argument("--forceApplyPatches", action="store_true",
help="Force apply patches even if some fail")
parser.add_argument("--patchLevel", type=int, default=1,
help="Patch level for -p option (default: 1)")
parser.add_argument("--sourceDir", type=str, default="AOSP-source",
help="Source directory (default: AOSP-source)")
parser.add_argument("--patchDir", type=str, default=["patches"], nargs="+",
help="Patch directory (default: patches). Can specify multiple directories.")
parser.add_argument("--syncJobs", type=int, default=None,
help="Number of parallel jobs for repo sync (default: CPU count)")
parser.add_argument("--ignorePatches", type=str, nargs="*", default=[],
help="List of patches to ignore")
parser.add_argument("--useLegacyPatch", action="store_true",
help="Use legacy patch command instead of robust handler")
parser.add_argument("--verbosePatches", action="store_true",
help="Verbose output for patch application")
parser.add_argument("--config", type=str, default="repo.yml",
help="Path to repo configuration file (default: repo.yml)")
parser.add_argument("--no-git-lfs", action="store_true",
help="Disable Git LFS setup (enabled by default)")
return parser.parse_args()
def main():
# Parse command line arguments
args = parse_arguments()
# Use command line arguments with defaults
source_dir = args.sourceDir
patch_dirs = args.patchDir
patch_level = args.patchLevel
sync_jobs = args.syncJobs if args.syncJobs is not None else os.cpu_count()
ignore_patches = args.ignorePatches
print("Android Build Environment Setup")
print(f"Source directory: {source_dir}")
print(f"Patch directories: {', '.join(patch_dirs)}")
print(f"Patch level: -p{patch_level}")
print(f"Sync jobs: {sync_jobs}")
if ignore_patches:
print(f"Ignoring patches: {', '.join(ignore_patches)}")
print(f"Git LFS: {'Disabled' if args.no_git_lfs else 'Enabled'}")
# Get absolute path to prevent any relative path confusion
current_dir = os.path.abspath(".")
absolute_source_dir = os.path.abspath(source_dir)
print(f"Current directory: {current_dir}")
print(f"Absolute source directory: {absolute_source_dir}")
# Check if we're trying to create a nested directory
if absolute_source_dir.startswith(current_dir) and absolute_source_dir != current_dir:
# We're creating a subdirectory, which is fine
working_dir = absolute_source_dir
if not os.path.exists(working_dir):
os.makedirs(working_dir)
print(f"Created source directory: {working_dir}")
else:
print(f"Using existing source directory: {working_dir}")
else:
# Use the specified directory directly
working_dir = absolute_source_dir
if not os.path.exists(working_dir):
os.makedirs(working_dir)
print(f"Created source directory: {working_dir}")
else:
print(f"Using existing source directory: {working_dir}")
# Load repo configuration
config = load_repo_config(args.config)
# Initialize repo if config exists and .repo directory doesn't exist
repo_dir = os.path.join(working_dir, ".repo")
if config and not os.path.exists(repo_dir):
repo_init(working_dir, config)
elif config and os.path.exists(repo_dir):
print("Repo already initialized, skipping init")
elif not config:
print("No repo configuration found, skipping repo init")
# Handle repo sync - always sync unless --noSync is specified
if not args.noSync:
repo_sync(force=args.forceResync, jobs=sync_jobs, current_dir=working_dir)
else:
print("Skipping repo sync as requested")
# Setup Git LFS after repo sync (enabled by default unless --no-git-lfs is specified)
if not args.no_git_lfs:
setup_git_lfs(working_dir)
else:
print("Skipping Git LFS setup as requested")
# Apply patches
if args.applyPatches or args.forceApplyPatches:
# Handle relative paths - if working_dir is not ".", prepend it to patch directories
processed_patch_dirs = []
for patch_dir in patch_dirs:
if working_dir != "." and not os.path.isabs(patch_dir):
processed_patch_dirs.append(os.path.join(working_dir, patch_dir))
else:
processed_patch_dirs.append(patch_dir)
patches = find_patches(processed_patch_dirs)
if patches:
if not args.useLegacyPatch:
# Use robust patch handler
success, failed_patches = apply_patches_robust(
patches,
working_dir,
patch_level,
args.forceApplyPatches,
ignore_patches,
args.verbosePatches
)
else:
# Use legacy patch command
success, failed_patches = apply_patches_legacy(
patches,
working_dir,
patch_level,
args.forceApplyPatches,
ignore_patches
)
if not success and not args.forceApplyPatches:
print(f"Patch application failed. Failed patches: {', '.join(failed_patches)}")
sys.exit(1)
elif failed_patches:
print(f"Some patches failed but continuing (force mode): {', '.join(failed_patches)}")
else:
print("All patches applied successfully!")
else:
print(f"No patches found in {', '.join(processed_patch_dirs)}")
print("Environment setup completed!")
if __name__ == "__main__":
main()