Fix patching
This commit is contained in:
+157
-84
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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
@@ -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()
|
||||
Reference in New Issue
Block a user