1# Copyright 2024 The Pigweed Authors 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); you may not 4# use this file except in compliance with the License. You may obtain a copy of 5# the License at 6# 7# https://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12# License for the specific language governing permissions and limitations under 13# the License. 14"""Utilities to apply a patch to a file. 15 16The patching is done entirely in python and doesn't shell out any other tools. 17""" 18 19import argparse 20import logging 21from pathlib import Path 22import os 23import sys 24import shutil 25import tempfile 26 27import patch # type: ignore 28 29logging.basicConfig(stream=sys.stdout, level=logging.WARN) 30_LOG = logging.getLogger(__name__) 31 32 33def _parse_args() -> argparse.Namespace: 34 """Registers the script's arguments on an argument parser.""" 35 36 parser = argparse.ArgumentParser(description=__doc__) 37 38 parser.add_argument( 39 '--patch-file', 40 type=Path, 41 required=True, 42 help='Location of the patch file to apply.', 43 ) 44 parser.add_argument( 45 '--src', 46 type=Path, 47 required=True, 48 help='Location of the source file to be patched.', 49 ) 50 parser.add_argument( 51 '--dst', 52 type=Path, 53 required=True, 54 help='Destination to copy the --src file to.', 55 ) 56 parser.add_argument( 57 '--root', 58 type=Path, 59 default=None, 60 help='Root directory for applying the patch.', 61 ) 62 63 return parser.parse_args() 64 65 66def _longest_common_suffix(str1: str, str2: str) -> str: 67 if not str1 or not str2: 68 return "" 69 70 i = len(str1) - 1 71 j = len(str2) - 1 72 suffix = "" 73 while i >= 0 and j >= 0 and str1[i] == str2[j]: 74 suffix = str1[i] + suffix 75 i -= 1 76 j -= 1 77 78 return suffix 79 80 81def _get_temp_path_for_src(src: Path, root: Path) -> str: 82 # Calculate the temp directory structure which matches the path of 83 # the src. 84 # Note in bazel the paths passed are all children of the sandbox, 85 # but in GN, the paths are peers of the build directory, ie "../" 86 # To handle these differences, first expand all paths to absolute 87 # paths, and then make the src relative to the root. 88 89 absolute_src = src.absolute() 90 _LOG.debug("absolute_src = %s", absolute_src) 91 absolute_root = Path.cwd() 92 if root: 93 absolute_root = root.absolute() 94 _LOG.debug("absolute_root = %s", absolute_root) 95 96 relative_src = absolute_src.relative_to(absolute_root) 97 _LOG.debug("relative_src = %s", relative_src) 98 99 return os.path.dirname(relative_src) 100 101 102def copy_and_apply_patch( 103 patch_file: Path, src: Path, dst: Path, root: Path 104) -> int: 105 """Copy then apply the diff""" 106 107 # create a temp directory which contains the entire 108 # path tree of the file to ensure the diff applies. 109 tmp_dir = tempfile.TemporaryDirectory() 110 full_tmp_dir = Path(tmp_dir.name, _get_temp_path_for_src(src, root)) 111 _LOG.debug("full_tmp_dir = %s", full_tmp_dir) 112 os.makedirs(full_tmp_dir) 113 114 tmp_src = Path(full_tmp_dir, src.name) 115 shutil.copyfile(src, tmp_src, follow_symlinks=False) 116 117 p = patch.fromfile(patch_file) 118 if not p.apply(root=tmp_dir.name): 119 return 1 120 121 shutil.copyfile(tmp_src, dst, follow_symlinks=False) 122 return 0 123 124 125if __name__ == '__main__': 126 sys.exit(copy_and_apply_patch(**vars(_parse_args()))) 127