From 11e3aaad47b69d44d0f65c5faeafd8e251e3652b Mon Sep 17 00:00:00 2001 From: Nicolas 'Pixel' Noble Date: Wed, 16 Oct 2024 12:46:48 -0700 Subject: [PATCH 1/5] Fixing Windows build Implementing an ersatz of the patch command in pure Python. --- CMakeLists.txt | 2 +- tools/patch.py | 208 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+), 1 deletion(-) create mode 100644 tools/patch.py diff --git a/CMakeLists.txt b/CMakeLists.txt index 2c8b3250..c2b096b0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -119,7 +119,7 @@ ExternalProject_Add(grpc-repo CMAKE_CACHE_ARGS -DCMAKE_CXX_STANDARD:STRING=${TRITON_MIN_CXX_STANDARD} # TODO(nnoble): remove this patch when the fix for https://github.com/abseil/abseil-cpp/issues/1769 is integrated within our dependencies - PATCH_COMMAND patch -N -i ${CMAKE_CURRENT_SOURCE_DIR}/tools/abseil_no_rebuild.patch -d ${CMAKE_CURRENT_BINARY_DIR}/grpc-repo/src/grpc/third_party/abseil-cpp || true + PATCH_COMMAND python3 ${CMAKE_CURRENT_SOURCE_DIR}/tools/patch.py apply -i -d ${CMAKE_CURRENT_BINARY_DIR}/grpc-repo/src/grpc/third_party/abseil-cpp ${CMAKE_CURRENT_SOURCE_DIR}/tools/abseil_no_rebuild.patch COMMAND python3 ${CMAKE_CURRENT_SOURCE_DIR}/tools/install_src.py --src ${INSTALL_SRC_DEST_ARG} --dest-basename=grpc_1.54.3 ) # diff --git a/tools/patch.py b/tools/patch.py new file mode 100644 index 00000000..ac5ef9a0 --- /dev/null +++ b/tools/patch.py @@ -0,0 +1,208 @@ +#!/usr/bin/env python3 +# Copyright (c) 2024, NVIDIA CORPORATION. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions +# are met: +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of NVIDIA CORPORATION nor the names of its +# contributors may be used to endorse or promote products derived +# from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS ``AS IS'' AND ANY +# EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY +# OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +""" +This is a basic patch tool that can parse unified diff format patches +and apply them to files. It is not a full-featured patch tool and +only supports a subset of the unified diff format, just enough to +apply the patches we need. It won't properly validate the patch file +format, so it's up to the user to ensure the patch file is correct. +It's also not optimized for performance, so it may be slow for large +patches or files. +""" + +import os +import sys +from enum import Enum + +class FileAlreadyPatchedError(Exception): + pass + +class FileNotMatchingError(Exception): + pass + +class PatchMalformedError(Exception): + pass + +class LineType(Enum): + MATCH = ' ' + ADD = '+' + REMOVE = '-' + +class PatchLine: + def __init__(self, line, line_type): + self.line = line + self.line_type = line_type + + def __str__(self): + return f"{self.line_type.value} {self.line}" + +class Hunk: + def __init__(self, filename, location, count): + self.filename = filename + self.location = int(location) + self.count = int(count) + self.lines = [] + + def apply(self): + with open(self.filename, 'r') as file: + lines = file.readlines() + + output = [] + current_line = 1 + relative_line = 0 + for line in lines: + line = line.rstrip('\r\n') + # Pass through the lines not inside the hunk + if current_line < int(self.location) or current_line >= int(self.location + self.count): + output.append(line) + else: + if self.lines[relative_line].line_type == LineType.MATCH: + if self.lines[relative_line].line != line: + raise FileNotMatchingError(f"Patch failed: {self.filename}:{current_line} does not match expected line:\nSource: {line}\nTarget: {self.lines[relative_line].line}") + output.append(line) + relative_line += 1 + elif self.lines[relative_line].line_type == LineType.ADD: + # Verify we're not already patched + if self.lines[relative_line].line == line: + raise FileAlreadyPatchedError(f"Patch failed: {self.filename}:{current_line} already patched") + # Write lines as long as we have a streak of ADD lines + while self.lines[relative_line].line_type == LineType.ADD: + output.append(self.lines[relative_line].line) + relative_line += 1 + # A streak of ADD lines is supposed to be followed by a MATCH line + if self.lines[relative_line].line_type == LineType.MATCH: + # We still want to verify the line matches + if self.lines[relative_line].line != line: + raise FileNotMatchingError(f"Patch failed: {self.filename}:{current_line} does not match expected line:\nSource: {line}\nTarget: {self.lines[relative_line].line}") + output.append(line) + relative_line += 1 + else: + raise PatchMalformedError(f"Unexpected line type: {self.lines[relative_line].line_type}") + elif self.lines[relative_line].line_type == LineType.REMOVE: + relative_line += 1 + current_line += 1 + with open(self.filename, 'w') as file: + file.write('\n'.join(output)) + + def __str__(self): + return f"Hunk: {self.filename} {self.location},{self.count}\n" + '\n'.join([str(line) for line in self.lines]) + +class Patch: + def __init__(self, filename): + self.filename = filename + self.hunks = [] + + def parse(self): + with open(self.filename, 'r') as file: + lines = file.readlines() + + hunk = None + current_filename = None + for line in lines: + line = line.rstrip('\r\n') + # Lines will start with '---', '+++', '@@', '-', ' ', or '+'. We + # will not support patch files coming from git, with lines starting + # with 'diff' or 'index'. + if line.startswith('---'): + # Ignore "old" filename + continue + elif line.startswith('+++'): + # Store "new" filename + current_filename = line[4:].lstrip(' ').split()[0] + elif line.startswith('@@') and line.endswith('@@'): + # Parse the hunk header's line numbers. + # We only care about the new location, and old count. I know + # this looks odd, but it makes sense when you look at how the + # hunk is applied. + old, new = line[3:-3].split() + location = new.split(',')[0][1:] + count = old.split(',')[1] + if hunk: + self.hunks.append(hunk) + hunk = Hunk(current_filename, location, count) + elif line.startswith('-'): + hunk.lines.append(PatchLine(line[1:], LineType.REMOVE)) + elif line.startswith('+'): + hunk.lines.append(PatchLine(line[1:], LineType.ADD)) + elif line.startswith(' '): + hunk.lines.append(PatchLine(line[1:], LineType.MATCH)) + else: + # Throw an error if we encounter an unexpected line + raise Exception(f"Unexpected line: {line}") + + if hunk: + self.hunks.append(hunk) + + def apply(self): + for hunk in self.hunks: + hunk.apply() + + def __str__(self): + return f"Patch: {self.filename}\n" + '\n'.join([str(hunk) for hunk in self.hunks]) + + +def usage(): + print("Usage: patch.py [options] ") + print("Commands:") + print(" apply - Apply the patch") + print(" -i, --ignore-already-patched - Ignore already patched files") + print(" -d, --directory - Directory to apply the patch to") + print(" parse - Parse the patch file") + print(" help - Show this help message") + sys.exit(1) + +if __name__ == "__main__": + if len(sys.argv) < 2: + usage() + + command = sys.argv[1] + if command == 'help': + usage() + elif command == 'parse': + patch = Patch(sys.argv[2]) + patch.parse() + print(patch) + elif command == 'apply': + ignore_already_patched = False + # Parse options + for i in range(2, len(sys.argv) - 1): + if sys.argv[i] in ['-i', '--ignore-already-patched']: + ignore_already_patched = True + elif sys.argv[i] in ['-d', '--directory']: + os.chdir(sys.argv[i + 1]) + patch = Patch(sys.argv[-1]) + patch.parse() + try: + patch.apply() + except FileAlreadyPatchedError as e: + if ignore_already_patched: + print(f"Ignoring already patched file: {e}") + else: + raise + else: + usage() From 217080c6e5b07cd79da443a6164ccdb68c755987 Mon Sep 17 00:00:00 2001 From: Nicolas 'Pixel' Noble Date: Wed, 16 Oct 2024 14:19:38 -0700 Subject: [PATCH 2/5] Rewrite main using argparse. --- tools/patch.py | 57 +++++++++++++++++++++++++------------------------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/tools/patch.py b/tools/patch.py index ac5ef9a0..c06cc9b3 100644 --- a/tools/patch.py +++ b/tools/patch.py @@ -38,6 +38,7 @@ import os import sys from enum import Enum +import argparse class FileAlreadyPatchedError(Exception): pass @@ -165,44 +166,42 @@ def apply(self): def __str__(self): return f"Patch: {self.filename}\n" + '\n'.join([str(hunk) for hunk in self.hunks]) +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="A basic patch tool.") + subparsers = parser.add_subparsers(dest="command", help="Commands") -def usage(): - print("Usage: patch.py [options] ") - print("Commands:") - print(" apply - Apply the patch") - print(" -i, --ignore-already-patched - Ignore already patched files") - print(" -d, --directory - Directory to apply the patch to") - print(" parse - Parse the patch file") - print(" help - Show this help message") - sys.exit(1) + # Apply command + apply_parser = subparsers.add_parser("apply", help="Apply the patch") + apply_parser.add_argument("-i", "--ignore-already-patched", action="store_true", help="Ignore already patched files") + apply_parser.add_argument("-d", "--directory", type=str, help="Directory to apply the patch to") + apply_parser.add_argument("patchfile", type=str, help="Patch file to apply") -if __name__ == "__main__": - if len(sys.argv) < 2: - usage() - - command = sys.argv[1] - if command == 'help': - usage() - elif command == 'parse': - patch = Patch(sys.argv[2]) + # Parse command + parse_parser = subparsers.add_parser("parse", help="Parse the patch file") + parse_parser.add_argument("patchfile", type=str, help="Patch file to parse") + + # Help command + subparsers.add_parser("help", help="Show this help message") + + args = parser.parse_args() + + if args.command == "help": + parser.print_help() + elif args.command == "parse": + patch = Patch(args.patchfile) patch.parse() print(patch) - elif command == 'apply': - ignore_already_patched = False - # Parse options - for i in range(2, len(sys.argv) - 1): - if sys.argv[i] in ['-i', '--ignore-already-patched']: - ignore_already_patched = True - elif sys.argv[i] in ['-d', '--directory']: - os.chdir(sys.argv[i + 1]) - patch = Patch(sys.argv[-1]) + elif args.command == "apply": + if args.directory: + os.chdir(args.directory) + patch = Patch(args.patchfile) patch.parse() try: patch.apply() except FileAlreadyPatchedError as e: - if ignore_already_patched: + if args.ignore_already_patched: print(f"Ignoring already patched file: {e}") else: raise else: - usage() + parser.print_help() From d41b2b142c3c552de192ec44775eff6d65545f12 Mon Sep 17 00:00:00 2001 From: Nicolas 'Pixel' Noble Date: Wed, 16 Oct 2024 14:23:14 -0700 Subject: [PATCH 3/5] Make range check a bit more readable. --- tools/patch.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tools/patch.py b/tools/patch.py index c06cc9b3..c420cc52 100644 --- a/tools/patch.py +++ b/tools/patch.py @@ -78,10 +78,8 @@ def apply(self): relative_line = 0 for line in lines: line = line.rstrip('\r\n') - # Pass through the lines not inside the hunk - if current_line < int(self.location) or current_line >= int(self.location + self.count): - output.append(line) - else: + # Only consider lines within the hunk's range + if int(self.location) <= current_line and current_line < int(self.location + self.count): if self.lines[relative_line].line_type == LineType.MATCH: if self.lines[relative_line].line != line: raise FileNotMatchingError(f"Patch failed: {self.filename}:{current_line} does not match expected line:\nSource: {line}\nTarget: {self.lines[relative_line].line}") @@ -106,6 +104,8 @@ def apply(self): raise PatchMalformedError(f"Unexpected line type: {self.lines[relative_line].line_type}") elif self.lines[relative_line].line_type == LineType.REMOVE: relative_line += 1 + else: + output.append(line) current_line += 1 with open(self.filename, 'w') as file: file.write('\n'.join(output)) From b8fd97759dbcd8e054a6a6d8b48e080cb9af8bde Mon Sep 17 00:00:00 2001 From: Nicolas 'Pixel' Noble Date: Wed, 16 Oct 2024 14:27:28 -0700 Subject: [PATCH 4/5] Simplifying further range check. --- tools/patch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/patch.py b/tools/patch.py index c420cc52..99fead08 100644 --- a/tools/patch.py +++ b/tools/patch.py @@ -79,7 +79,7 @@ def apply(self): for line in lines: line = line.rstrip('\r\n') # Only consider lines within the hunk's range - if int(self.location) <= current_line and current_line < int(self.location + self.count): + if self.location <= current_line and current_line < self.location + self.count: if self.lines[relative_line].line_type == LineType.MATCH: if self.lines[relative_line].line != line: raise FileNotMatchingError(f"Patch failed: {self.filename}:{current_line} does not match expected line:\nSource: {line}\nTarget: {self.lines[relative_line].line}") From 32c7389d070652e90a3a1f89cf2fb6561af2e9b4 Mon Sep 17 00:00:00 2001 From: Nicolas 'Pixel' Noble Date: Wed, 16 Oct 2024 14:35:16 -0700 Subject: [PATCH 5/5] Removing `sys` import that's no longer used. --- tools/patch.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/patch.py b/tools/patch.py index 99fead08..a3d10adb 100644 --- a/tools/patch.py +++ b/tools/patch.py @@ -36,7 +36,6 @@ """ import os -import sys from enum import Enum import argparse