From b086012292dc70d44354a1b734124bc562039510 Mon Sep 17 00:00:00 2001 From: oxmc7769 Date: Fri, 20 Mar 2026 05:16:13 -0700 Subject: [PATCH] Fix patching --- environ.py | 241 ++++++++++++++++++++++++++-------------- repo.yml | 11 +- scripts/better_patch.py | 57 +++++++--- 3 files changed, 206 insertions(+), 103 deletions(-) diff --git a/environ.py b/environ.py index 12fea7b..0acac3a 100644 --- a/environ.py +++ b/environ.py @@ -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 -> /frameworks/base/ + 0001-root-level.patch -> / + + With mappings from repo.yml: + + patches/ + android/36/android_frameworks_base-*.patch -> /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 /, 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 ''}") + + 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 ) diff --git a/repo.yml b/repo.yml index 85c2895..d3eda22 100644 --- a/repo.yml +++ b/repo.yml @@ -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" \ No newline at end of file + 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 \ No newline at end of file diff --git a/scripts/better_patch.py b/scripts/better_patch.py index 432a400..d99ab12 100644 --- a/scripts/better_patch.py +++ b/scripts/better_patch.py @@ -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() \ No newline at end of file