Fix patching

This commit is contained in:
2026-03-20 05:16:13 -07:00
parent 5b82916a29
commit b086012292
3 changed files with 206 additions and 103 deletions
+157 -84
View File
@@ -147,26 +147,57 @@ def setup_git_lfs(source_dir):
print(f"Warning: Git LFS setup failed: {e}")
return False
def find_patches(patch_dirs):
"""Find all patch files in the patch directories."""
def find_patches(patch_dirs, mappings=None):
"""Find all patch files recursively in the patch directories.
Returns a list of (patch_path, target_subdir) tuples where target_subdir
is the AOSP project path the patch should be applied into.
mappings (optional dict) translates patch subdirectory paths to AOSP paths,
e.g. {"android/36": "frameworks/base"}. Without a mapping the subdirectory
structure must mirror the AOSP project layout directly:
patches/
frameworks/base/0001-sig-spoof.patch -> <source>/frameworks/base/
0001-root-level.patch -> <source>/
With mappings from repo.yml:
patches/
android/36/android_frameworks_base-*.patch -> <source>/frameworks/base/
"""
if mappings is None:
mappings = {}
patches = []
seen_names = set()
for patch_dir in patch_dirs:
if not os.path.exists(patch_dir):
patch_dir_path = Path(patch_dir)
if not patch_dir_path.exists():
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
found = sorted(patch_dir_path.rglob("*.patch"))
print(f"Found {len(found)} patches in {patch_dir} (recursive)")
for patch_file in found:
if patch_file.name in seen_names:
print(f" Skipping duplicate patch name: {patch_file.name}")
continue
seen_names.add(patch_file.name)
rel_parent = patch_file.parent.relative_to(patch_dir_path)
rel_str = str(rel_parent).replace("\\", "/") # normalise on Windows
target_subdir = rel_str if rel_str != "." else "."
# Apply mapping if one is configured for this subdirectory
if target_subdir in mappings:
target_subdir = mappings[target_subdir]
patches.append((patch_file, target_subdir))
return patches
def apply_patch_legacy(patch_file, source_dir, patch_level=1, force=False):
"""Apply a single patch file using legacy patch command."""
@@ -185,71 +216,100 @@ def apply_patch_legacy(patch_file, source_dir, patch_level=1, force=False):
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."""
"""Apply patches using the robust patch handler.
patches is a list of (patch_path, target_subdir) tuples produced by find_patches().
Each patch is applied inside <source_dir>/<target_subdir>, so patches can be
organised into subdirectories that mirror the AOSP project layout.
"""
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]
# Filter ignored patches (match by filename)
patches_to_apply = [(p, sub) for p, sub 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
# Group patches by target subdir so we can run analysis per project
from collections import defaultdict
groups = defaultdict(list)
for patch_path, subdir in patches_to_apply:
groups[subdir].append(patch_path)
all_failed = []
overall_success = True
for subdir, group_patches in sorted(groups.items()):
target_dir = os.path.join(source_dir, subdir) if subdir != "." else source_dir
if not os.path.exists(target_dir):
print(f"Warning: Target directory does not exist, skipping: {target_dir}")
all_failed.extend([p.name for p in group_patches])
overall_success = False
continue
print(f"\nApplying {len(group_patches)} patch(es) to: {subdir if subdir != '.' else '<source root>'}")
handler = GitPatchHandler(repo_path=target_dir, verbose=verbose)
analysis = handler.analyze_patch_series([str(p) for p in group_patches])
print(f" Analysis: {analysis['summary']['total_patches']} patch(es), "
f"{analysis['summary']['total_files_modified']} file(s) modified, "
f"{len(analysis['summary']['file_conflicts_detected'])} potential conflict(s)")
result = handler.apply_patch_series(
[str(p) for p in group_patches],
target_dir=".",
stop_on_conflict=not force,
rollback_on_failure=True
)
print(f" Result: {'SUCCESS' if result['success'] else 'FAILED'} "
f"({len(result['patches_applied'])}/{len(group_patches)} applied)")
for patch_result in result['results']:
patch_name = Path(patch_result['patch']).name
if not patch_result['success']:
all_failed.append(patch_name)
print(f" FAILED: {patch_name}{patch_result['message']}")
if patch_result.get('conflicts'):
print(f" Conflicts: {', '.join(patch_result['conflicts'])}")
if patch_result.get('rejected_files'):
print(f" Rejected hunks in: {', '.join(patch_result['rejected_files'])}")
if not result['success']:
overall_success = False
if not force:
break
print(f"\nOverall patch result: {'SUCCESS' if overall_success else 'FAILED'}")
return overall_success, all_failed
def apply_patches_legacy(patches, source_dir, patch_level=1, force=False, ignore_patches=None):
"""Legacy patch application for fallback."""
"""Legacy patch application for fallback.
patches is a list of (patch_path, target_subdir) tuples.
"""
if ignore_patches is None:
ignore_patches = []
applied_patches = 0
failed_patches = []
for patch_file in patches:
for patch_file, subdir in patches:
patch_name = patch_file.name
if patch_name in ignore_patches:
print(f"Ignoring patch: {patch_name}")
continue
target_dir = os.path.join(source_dir, subdir) if subdir != "." else source_dir
try:
apply_patch_legacy(patch_file, source_dir, patch_level, force)
apply_patch_legacy(patch_file, target_dir, patch_level, force)
applied_patches += 1
except Exception as e:
print(f"Failed to apply patch {patch_name}: {e}")
@@ -257,7 +317,7 @@ def apply_patches_legacy(patches, source_dir, patch_level=1, force=False, ignore
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
@@ -291,8 +351,8 @@ def parse_arguments():
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("--patchDir", type=str, default=None, nargs="+",
help="Patch directories to search (overrides repo.yml patches.dirs). Can specify multiple.")
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=[],
@@ -314,7 +374,7 @@ def main():
# Use command line arguments with defaults
source_dir = args.sourceDir
patch_dirs = args.patchDir
patch_dirs = args.patchDir # may be None if not explicitly passed
patch_level = args.patchLevel
sync_jobs = args.syncJobs if args.syncJobs is not None else os.cpu_count()
ignore_patches = args.ignorePatches
@@ -379,34 +439,47 @@ def main():
# Apply patches
if args.applyPatches or args.forceApplyPatches:
# Handle relative paths - if working_dir is not ".", prepend it to patch directories
# Resolve patch dirs: CLI --patchDir > repo.yml patches.dirs > fallback "patches"
patch_config = config.get('patches', {}) if config else {}
if patch_dirs is not None:
effective_patch_dirs = patch_dirs
elif 'dirs' in patch_config:
effective_patch_dirs = patch_config['dirs']
print(f"Using patch directories from repo.yml: {effective_patch_dirs}")
else:
effective_patch_dirs = ['patches']
# Load directory mappings from repo.yml (e.g. "android/36" -> "frameworks/base")
mappings = patch_config.get('mappings', {})
if mappings:
print(f"Patch directory mappings: {mappings}")
# Resolve relative paths against working_dir
processed_patch_dirs = []
for patch_dir in patch_dirs:
if working_dir != "." and not os.path.isabs(patch_dir):
for patch_dir in effective_patch_dirs:
if 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)
patches = find_patches(processed_patch_dirs, mappings=mappings)
if patches:
if not args.useLegacyPatch:
# Use robust patch handler
success, failed_patches = apply_patches_robust(
patches,
working_dir,
patch_level,
args.forceApplyPatches,
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,
patches,
working_dir,
patch_level,
args.forceApplyPatches,
ignore_patches
)
+10 -1
View File
@@ -7,4 +7,13 @@ repo:
manifest_brcm_rpi: "https://git.oxmc.me/PawletOS/android_local_manifest/raw/branch/android-16.0/vendors/brcm_rpi.xml"
manifest_lineageos: "https://git.oxmc.me/PawletOS/android_local_manifest/raw/branch/android-16.0/vendors/lineageos.xml"
manifest_pawletos: "https://git.oxmc.me/PawletOS/android_local_manifest/raw/branch/android-16.0/pawletos.xml"
remove_projects: "https://git.oxmc.me/PawletOS/android_local_manifest/raw/branch/android-16.0/remove_projects.xml"
remove_projects: "https://git.oxmc.me/PawletOS/android_local_manifest/raw/branch/android-16.0/remove_projects.xml"
patches:
# Directories (relative to source root) to search for patches.
# Each entry is resolved inside the AOSP source directory after repo sync.
# Subdirectory layout mirrors AOSP project paths:
# vendor/pawlet/patches/frameworks/base/*.patch -> applied at frameworks/base/
# vendor/pawlet/patches/packages/apps/Settings/*.patch -> applied at packages/apps/Settings/
dirs:
- vendor/pawlet/patches
+39 -18
View File
@@ -11,6 +11,7 @@ import os
import sys
import re
import shutil
import difflib
from pathlib import Path
from typing import List, Dict, Optional, Any
import argparse
@@ -204,18 +205,18 @@ class GitPatchHandler:
}
def _try_fuzzy_apply(self, patch_file: str, target_dir: str, previous_result: subprocess.CompletedProcess) -> Dict[str, Any]:
"""Try applying patch with fuzzy matching"""
self._log("Attempting fuzzy patch application")
"""Try applying patch with --reject to apply what fits, then check for leftover .rej files"""
self._log("Attempting fuzzy patch application with --reject")
try:
cmd = [
'git', 'apply',
'--3way',
'--allow-overlap',
'--verbose',
'--reject', # Apply parts that work, reject others
'--reject', # Apply hunks that work, write .rej for those that don't
str(patch_file)
]
result = subprocess.run(
cmd,
cwd=target_dir,
@@ -223,15 +224,34 @@ class GitPatchHandler:
text=True,
timeout=30
)
if result.returncode == 0 or 'applied' in result.stdout.lower():
# Check for leftover .rej files — these mean hunks were skipped
rej_files = list(Path(target_dir).rglob('*.rej'))
if rej_files:
rej_names = [str(r.relative_to(target_dir)) for r in rej_files]
self._log(f"Found {len(rej_files)} reject file(s): {rej_names}")
# Clean up .rej files so they don't pollute the tree
for rej in rej_files:
try:
rej.unlink()
except OSError:
pass
return {
'success': True,
'message': 'Patch applied with rejects',
'success': False,
'message': f'Patch partially applied — {len(rej_files)} hunk(s) rejected',
'output': result.stdout,
'error': result.stderr,
'method': 'git-fuzzy',
'had_rejects': 'reject' in result.stdout.lower()
'rejected_files': rej_names
}
if result.returncode == 0:
return {
'success': True,
'message': 'Patch applied cleanly (fuzzy)',
'output': result.stdout,
'error': result.stderr,
'method': 'git-fuzzy'
}
else:
return {
@@ -242,7 +262,7 @@ class GitPatchHandler:
'error': result.stderr,
'method': 'git-fuzzy'
}
except Exception as e:
return {
'success': False,
@@ -357,11 +377,15 @@ class GitPatchHandler:
files = []
for line in patch_content.split('\n'):
if line.startswith('--- ') or line.startswith('+++ '):
file_path = line.split(' ')[1]
if file_path != '/dev/null' and not file_path.startswith('a/') and not file_path.startswith('b/'):
# Clean up git-style prefixes
clean_path = re.sub(r'^(a|b)/', '', file_path)
files.append(clean_path)
parts = line.split(' ', 1)
if len(parts) < 2:
continue
file_path = parts[1].split('\t')[0].strip() # handle timestamps after tab
if file_path == '/dev/null':
continue
# Strip git-style a/ or b/ prefixes (standard in git format-patch output)
clean_path = re.sub(r'^[ab]/', '', file_path)
files.append(clean_path)
return list(set(files))
def _count_lines_changed(self, patch_content: str) -> int:
@@ -606,8 +630,5 @@ def print_patch_results(result: Dict[str, Any]) -> None:
print(f" [ROLLED BACK]")
# Add difflib for the create_patch method
import difflib
if __name__ == '__main__':
main()