• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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