"""Rebuilds the vendored MoonSharp package from the source archive in 3rdParty\\Moonsharp. Run by hand when the vendored source or its patch changes; the output is checked in, so builds never run this. It unpacks the source into Junk\\MoonSharpBuild, applies source\\moonsharp.patch, builds it with dotnet, and replaces the package snapshot that build.py extracts into Junk\\NuGet. To change the patch, run with --prepare, edit the source under Junk\\MoonSharpBuild\\src, then run with --make-patch to write the difference from the pristine source back to source\\moonsharp.patch. """ import argparse import base64 import difflib import hashlib import os import re import shutil import subprocess import sys import tempfile import unittest REPO_ROOT = os.path.abspath(os.path.join(os.path.abspath(__file__), "..", "..")) MOONSHARP_DIRECTORY = os.path.join(REPO_ROOT, "3rdParty", "Moonsharp") SOURCE_ARCHIVE = os.path.join(MOONSHARP_DIRECTORY, "source", "moonsharp-upstream-source.7z") PATCH_FILE = os.path.join(MOONSHARP_DIRECTORY, "source", "moonsharp.patch") BUILD_FILES_DIRECTORY = os.path.join(MOONSHARP_DIRECTORY, "build") PACKAGE_ARCHIVE = os.path.join(MOONSHARP_DIRECTORY, "packages", "moonsharp.2.0.0.7z") SEVEN_ZIP = os.path.join(REPO_ROOT, "3rdParty", "7-Zip", "7zr.exe") BUILD_DIRECTORY = os.path.join(REPO_ROOT, "Junk", "MoonSharpBuild") PRISTINE_DIRECTORY = os.path.join(REPO_ROOT, "Junk", "MoonSharpPristine") EXTRACTED_PACKAGE_DIRECTORY = os.path.join(REPO_ROOT, "Junk", "NuGet", "moonsharp") # Patches touch only this folder of the unpacked tree. PATCHED_SUBDIRECTORY = "src" NO_FILE = "/dev/null" HUNK_HEADER = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@") TARGET_FRAMEWORK = "netstandard2.0" NUSPEC = """ MoonSharp 2.0.0.0 Marco.Mastropaolo MoonSharp 2.0.0, built from the source vendored in 3rdParty\\Moonsharp. """ class PatchError(Exception): pass class Hunk: def __init__(self, old_start, old_count, lines): self.old_start = old_start self.old_count = old_count # Each line is a (kind, text) pair. Kind is " " for context, "-" for removed and "+" for added. self.lines = lines class FilePatch: def __init__(self, old_path, new_path, hunks): self.old_path = old_path self.new_path = new_path self.hunks = hunks class TextFile: """A text file's lines, with each line's own ending kept so a patch can leave untouched lines alone.""" def __init__(self, has_byte_order_mark, lines): self.has_byte_order_mark = has_byte_order_mark # Each line is a (content, ending) pair. Only the last line's ending can be empty. self.lines = lines def ends_with_newline(self): return not self.lines or bool(self.lines[-1][1]) def split_into_lines(text): return [(match.group(1), match.group(2)) for match in re.finditer(r"([^\r\n]*)(\r\n|\n|\r|$)", text) if match.group(1) or match.group(2)] def read_text_file(path): with open(path, "rb") as file: data = file.read() try: text = data.decode("utf-8") except UnicodeDecodeError: raise PatchError("{} isn't UTF-8 text.".format(path)) has_byte_order_mark = text.startswith("") return TextFile(has_byte_order_mark, split_into_lines(text[1:] if has_byte_order_mark else text)) def read_patchable_file(path): text_file = read_text_file(path) if not text_file.ends_with_newline(): raise PatchError("{} doesn't end in a newline, which a patch can't describe.".format(path)) return text_file def write_text_file(path, text_file): os.makedirs(os.path.dirname(path), exist_ok=True) text = "".join(content + ending for content, ending in text_file.lines) with open(path, "wb") as file: file.write((("" if text_file.has_byte_order_mark else "") + text).encode("utf-8")) def parse_patch(patch_text): """Reads a unified diff. A line's own ending is dropped, since the patch file's endings are the checkout's.""" patch_lines = patch_text.replace("\r\n", "\n").split("\n") if patch_lines[-1] == "": patch_lines.pop() file_patches = [] index = 0 while index < len(patch_lines): if not patch_lines[index].startswith("--- ") or index + 1 >= len(patch_lines) \ or not patch_lines[index + 1].startswith("+++ "): raise PatchError("Patch line {} should start a file with '--- path', but is: {}".format( index + 1, patch_lines[index])) file_patch = FilePatch(patch_lines[index][4:], patch_lines[index + 1][4:], []) index += 2 while index < len(patch_lines) and patch_lines[index].startswith("@@"): match = HUNK_HEADER.match(patch_lines[index]) if not match: raise PatchError("Patch line {} isn't a hunk header: {}".format(index + 1, patch_lines[index])) old_start = int(match.group(1)) old_count = int(match.group(2)) if match.group(2) is not None else 1 new_count = int(match.group(4)) if match.group(4) is not None else 1 index += 1 lines = [] remaining_old = old_count remaining_new = new_count while remaining_old > 0 or remaining_new > 0: if index >= len(patch_lines): raise PatchError("The hunk at line {} of {} ends early.".format(old_start, file_patch.new_path)) line = patch_lines[index] kind = line[:1] if line else " " if kind == "\\": index += 1 continue if kind not in " -+": raise PatchError("Patch line {} isn't part of the hunk: {}".format(index + 1, line)) if kind != "+": remaining_old -= 1 if kind != "-": remaining_new -= 1 lines.append((kind, line[1:])) index += 1 if remaining_old < 0 or remaining_new < 0: raise PatchError("The hunk at line {} of {} has more lines than its header says.".format( old_start, file_patch.new_path)) file_patch.hunks.append(Hunk(old_start, old_count, lines)) file_patches.append(file_patch) return file_patches def checked_relative_path(path): parts = path.replace("\\", "/").split("/") if os.path.isabs(path) or path.startswith("/") or ".." in parts: raise PatchError("A patch may only name files inside the source tree, not '{}'.".format(path)) return os.path.join(*parts) def apply_hunks(path, original_lines, hunks): """Returns the lines that result from applying the hunks, each of which must match the original exactly.""" default_ending = original_lines[0][1] if original_lines else "\n" result = [] position = 0 for hunk in hunks: start = hunk.old_start if hunk.old_count == 0 else hunk.old_start - 1 if start < position: raise PatchError("The hunks for {} overlap or are out of order, at line {}.".format(path, hunk.old_start)) result.extend(original_lines[position:start]) ending = original_lines[start - 1][1] if start > 0 else default_ending for kind, content in hunk.lines: if kind == "+": result.append((content, ending)) continue if start >= len(original_lines) or original_lines[start][0] != content: found = original_lines[start][0] if start < len(original_lines) else "(the end of the file)" raise PatchError("The patch doesn't match {} at line {}.\nExpected: {}\nFound: {}".format( path, start + 1, content, found)) ending = original_lines[start][1] if kind == " ": result.append(original_lines[start]) start += 1 position = start result.extend(original_lines[position:]) return result def apply_patch(patch_text, root): """Applies a unified diff whose paths are relative to root. It changes nothing unless every hunk matches.""" changes = [] named_paths = set() for file_patch in parse_patch(patch_text): named_path = file_patch.new_path if file_patch.old_path == NO_FILE else file_patch.old_path if named_path in named_paths: raise PatchError("The patch names {} twice.".format(named_path)) named_paths.add(named_path) if file_patch.old_path == NO_FILE: path = os.path.join(root, checked_relative_path(file_patch.new_path)) if os.path.exists(path): raise PatchError("The patch creates {}, which already exists.".format(file_patch.new_path)) lines = apply_hunks(file_patch.new_path, [], file_patch.hunks) changes.append((path, TextFile(False, lines))) continue path = os.path.join(root, checked_relative_path(file_patch.old_path)) if not os.path.exists(path): raise PatchError("The patch changes {}, which doesn't exist.".format(file_patch.old_path)) text_file = read_patchable_file(path) lines = apply_hunks(file_patch.old_path, text_file.lines, file_patch.hunks) if file_patch.new_path == NO_FILE: if lines: raise PatchError("The patch deletes {}, but doesn't remove all of it.".format(file_patch.old_path)) changes.append((path, None)) else: changes.append((path, TextFile(text_file.has_byte_order_mark, lines))) for path, text_file in changes: if text_file is None: os.remove(path) else: write_text_file(path, text_file) def relative_files(root, subdirectory): returnMe = set() for current, _, names in os.walk(os.path.join(root, subdirectory)): for name in names: returnMe.add(os.path.relpath(os.path.join(current, name), root).replace("\\", "/")) return returnMe def make_patch(pristine_root, edited_root, subdirectory): """Returns a unified diff that turns the pristine files under the subdirectory into the edited ones. Only the text of each line counts, so a file that differs only in its line endings or byte order mark is skipped. """ pristine_files = relative_files(pristine_root, subdirectory) edited_files = relative_files(edited_root, subdirectory) returnMe = [] for relative_path in sorted(pristine_files | edited_files): old_file = TextFile(False, []) new_file = TextFile(False, []) if relative_path in pristine_files: old_file = read_text_file(os.path.join(pristine_root, relative_path)) if relative_path in edited_files: new_file = read_text_file(os.path.join(edited_root, relative_path)) old_lines = [content for content, _ in old_file.lines] new_lines = [content for content, _ in new_file.lines] old_name = relative_path if relative_path in pristine_files else NO_FILE new_name = relative_path if relative_path in edited_files else NO_FILE differences = list(difflib.unified_diff(old_lines, new_lines, old_name, new_name, lineterm="")) if differences and not (old_file.ends_with_newline() and new_file.ends_with_newline()): raise PatchError("{} changes, but doesn't end in a newline, which a patch can't describe.".format( relative_path)) if not differences and old_name != new_name: # An empty file has no lines to differ by, but it still appears or disappears. differences = ["--- " + old_name, "+++ " + new_name] returnMe.extend(differences) return "".join(line + "\n" for line in returnMe) def remove_tree(path): # The \\?\\ prefix lets Windows delete paths past 260 characters. if os.path.exists(path): shutil.rmtree("\\\\?\\" + path) def extract(archive, directory): os.makedirs(directory, exist_ok=True) subprocess.check_call([SEVEN_ZIP, "x", archive, "-o" + directory, "-y"], stdout=subprocess.DEVNULL) def build(build_directory): # The project restores NETStandard.Library from Junk\NuGet, which build.py fills from the vendored archives. sys.path.insert(0, os.path.join(REPO_ROOT, "Scripts")) import build as build_script build_script.ensure_vendored_nuget_packages_extracted(quiet=True) subprocess.check_call(["dotnet", "build", "-c", "Release", "--nologo", "-v", "q"], cwd=build_directory) return os.path.join(build_directory, "bin", "Release", TARGET_FRAMEWORK) def write_package_tree(built_directory, package_directory): library_directory = os.path.join(package_directory, "lib", TARGET_FRAMEWORK) os.makedirs(library_directory) for extension in ("dll", "pdb", "xml"): shutil.copy(os.path.join(built_directory, "MoonSharp.Interpreter." + extension), library_directory) with open(os.path.join(package_directory, "moonsharp.nuspec"), "w", encoding="utf-8", newline="\n") as file: file.write(NUSPEC) with open(os.path.join(library_directory, "MoonSharp.Interpreter.dll"), "rb") as file: content_hash = base64.b64encode(hashlib.sha512(file.read()).digest()).decode("ascii") metadata = '{{\n "version": 2,\n "contentHash": "{}",\n "source": "3rdParty\\\\Moonsharp"\n}}\n'.format( content_hash) with open(os.path.join(package_directory, ".nupkg.metadata"), "w", encoding="utf-8", newline="\n") as file: file.write(metadata) def replace_package_archive(package_directory): if os.path.exists(PACKAGE_ARCHIVE): os.remove(PACKAGE_ARCHIVE) subprocess.check_call([SEVEN_ZIP, "a", "-mx=9", PACKAGE_ARCHIVE, "."], cwd=package_directory, stdout=subprocess.DEVNULL) def remove_stale_restore_results(): """Deletes the cached restore results that name MoonSharp and returns the ones that couldn't be deleted.""" # NuGet's no-op restore doesn't notice a package's contents changed, so a project keeps the old lib\ path. returnMe = [] junk_directory = os.path.join(REPO_ROOT, "Junk") for name in os.listdir(junk_directory): assets_path = os.path.join(junk_directory, name, "project.assets.json") if not os.path.isfile(assets_path): continue with open(assets_path, encoding="utf-8") as file: if '"moonsharp/' not in file.read().lower(): continue try: os.remove(assets_path) except PermissionError: returnMe.append(assets_path) return returnMe def prepare_build_directory(): remove_tree(BUILD_DIRECTORY) extract(SOURCE_ARCHIVE, BUILD_DIRECTORY) for name in os.listdir(BUILD_FILES_DIRECTORY): shutil.copy(os.path.join(BUILD_FILES_DIRECTORY, name), BUILD_DIRECTORY) if os.path.exists(PATCH_FILE): with open(PATCH_FILE, encoding="utf-8") as file: apply_patch(file.read(), BUILD_DIRECTORY) def write_patch_file(): remove_tree(PRISTINE_DIRECTORY) extract(SOURCE_ARCHIVE, PRISTINE_DIRECTORY) patch_text = make_patch(PRISTINE_DIRECTORY, BUILD_DIRECTORY, PATCHED_SUBDIRECTORY) with open(PATCH_FILE, "w", encoding="utf-8", newline="\n") as file: file.write(patch_text) print("Wrote {}.".format(PATCH_FILE)) def rebuild_package(): prepare_build_directory() built_directory = build(BUILD_DIRECTORY) package_directory = os.path.join(BUILD_DIRECTORY, "package") write_package_tree(built_directory, package_directory) replace_package_archive(package_directory) # build.py skips an extraction whose folder already exists, so the old copy has to go. remove_tree(EXTRACTED_PACKAGE_DIRECTORY) locked_paths = remove_stale_restore_results() print("Wrote {}. Other working copies need Scripts\\CleanTemporaryFiles.bat.".format(PACKAGE_ARCHIVE)) if locked_paths: print("These are in use, probably by Visual Studio. Delete them before the next build:") for path in locked_paths: print(" " + path) return 1 return 0 class PatchTests(unittest.TestCase): def setUp(self): self.temporary_directory = tempfile.TemporaryDirectory() self.addCleanup(self.temporary_directory.cleanup) self.root = self.temporary_directory.name def write(self, name, text, root=None): path = os.path.join(root or self.root, *name.split("/")) os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "wb") as file: file.write(text.encode("utf-8")) def read(self, name, root=None): with open(os.path.join(root or self.root, *name.split("/")), "rb") as file: return file.read().decode("utf-8") def test_apply_replaces_a_line(self): self.write("a.cs", "one\ntwo\nthree\n") apply_patch("--- a.cs\n+++ a.cs\n@@ -1,3 +1,3 @@\n one\n-two\n+TWO\n three\n", self.root) self.assertEqual("one\nTWO\nthree\n", self.read("a.cs")) def test_apply_inserts_after_the_line_a_zero_length_hunk_names(self): self.write("a.cs", "one\ntwo\n") apply_patch("--- a.cs\n+++ a.cs\n@@ -1,0 +2,2 @@\n+x\n+y\n", self.root) self.assertEqual("one\nx\ny\ntwo\n", self.read("a.cs")) def test_apply_deletes_lines(self): self.write("a.cs", "one\ntwo\nthree\n") apply_patch("--- a.cs\n+++ a.cs\n@@ -1,3 +1,2 @@\n one\n-two\n three\n", self.root) self.assertEqual("one\nthree\n", self.read("a.cs")) def test_apply_finds_a_later_hunk_in_the_original_after_an_earlier_one_grows_the_file(self): self.write("a.cs", "1\n2\n3\n4\n5\n6\n7\n8\n") patch = "--- a.cs\n+++ a.cs\n@@ -1,1 +1,3 @@\n 1\n+a\n+b\n@@ -7,2 +9,2 @@\n 7\n-8\n+z\n" apply_patch(patch, self.root) self.assertEqual("1\na\nb\n2\n3\n4\n5\n6\n7\nz\n", self.read("a.cs")) def test_apply_keeps_the_byte_order_mark_and_every_untouched_lines_ending(self): self.write("a.cs", "one\r\ntwo\nthree\r\n") apply_patch("--- a.cs\n+++ a.cs\n@@ -1,1 +1,1 @@\n-one\n+ONE\n", self.root) self.assertEqual("ONE\r\ntwo\nthree\r\n", self.read("a.cs")) def test_apply_gives_added_lines_the_ending_of_the_line_before_them(self): self.write("a.cs", "one\ntwo\r\nthree\n") apply_patch("--- a.cs\n+++ a.cs\n@@ -2,1 +2,2 @@\n two\n+added\n", self.root) self.assertEqual("one\ntwo\r\nadded\r\nthree\n", self.read("a.cs")) def test_apply_gives_a_pure_insertion_the_ending_of_the_line_it_follows(self): self.write("a.cs", "one\ntwo\r\nthree\n") apply_patch("--- a.cs\n+++ a.cs\n@@ -2,0 +3,1 @@\n+added\n", self.root) self.assertEqual("one\ntwo\r\nadded\r\nthree\n", self.read("a.cs")) def test_apply_reads_a_patch_whose_own_lines_end_in_crlf(self): self.write("a.cs", "one\ntwo\n") apply_patch("--- a.cs\r\n+++ a.cs\r\n@@ -2,1 +2,1 @@\r\n-two\r\n+TWO\r\n", self.root) self.assertEqual("one\nTWO\n", self.read("a.cs")) def test_apply_reads_a_removed_line_that_looks_like_a_file_header(self): self.write("a.cs", "one\n-- two\n") apply_patch("--- a.cs\n+++ a.cs\n@@ -2,1 +2,1 @@\n--- two\n+++ two\n", self.root) self.assertEqual("one\n++ two\n", self.read("a.cs")) def test_apply_that_does_not_match_names_the_line_and_changes_no_file(self): self.write("a.cs", "one\ntwo\n") self.write("b.cs", "red\ngreen\n") patch = ("--- a.cs\n+++ a.cs\n@@ -1,1 +1,1 @@\n-one\n+ONE\n" "--- b.cs\n+++ b.cs\n@@ -2,1 +2,1 @@\n-blue\n+BLUE\n") with self.assertRaises(PatchError) as raised: apply_patch(patch, self.root) self.assertIn("b.cs at line 2", str(raised.exception)) self.assertIn("Expected: blue", str(raised.exception)) self.assertIn("Found: green", str(raised.exception)) self.assertEqual("one\ntwo\n", self.read("a.cs")) def test_apply_that_runs_past_the_end_of_the_file_does_not_match(self): self.write("a.cs", "one\n") with self.assertRaises(PatchError) as raised: apply_patch("--- a.cs\n+++ a.cs\n@@ -2,1 +2,1 @@\n-two\n+TWO\n", self.root) self.assertIn("(the end of the file)", str(raised.exception)) def test_apply_creates_a_file(self): apply_patch("--- /dev/null\n+++ new/b.cs\n@@ -0,0 +1,2 @@\n+one\n+two\n", self.root) self.assertEqual("one\ntwo\n", self.read("new/b.cs")) def test_apply_refuses_to_create_a_file_that_exists(self): self.write("b.cs", "x\n") with self.assertRaises(PatchError): apply_patch("--- /dev/null\n+++ b.cs\n@@ -0,0 +1,1 @@\n+one\n", self.root) def test_apply_deletes_a_file(self): self.write("a.cs", "one\ntwo\n") apply_patch("--- a.cs\n+++ /dev/null\n@@ -1,2 +0,0 @@\n-one\n-two\n", self.root) self.assertFalse(os.path.exists(os.path.join(self.root, "a.cs"))) def test_apply_refuses_to_delete_a_file_it_does_not_remove_all_of(self): self.write("a.cs", "one\ntwo\n") with self.assertRaises(PatchError): apply_patch("--- a.cs\n+++ /dev/null\n@@ -1,1 +0,0 @@\n-one\n", self.root) def test_apply_refuses_a_file_that_does_not_exist(self): with self.assertRaises(PatchError): apply_patch("--- a.cs\n+++ a.cs\n@@ -1,1 +1,1 @@\n-one\n+ONE\n", self.root) def test_apply_refuses_a_path_outside_the_root(self): for path in ("../a.cs", "x/../../a.cs", "/a.cs"): with self.assertRaises(PatchError): apply_patch("--- /dev/null\n+++ {}\n@@ -0,0 +1,1 @@\n+one\n".format(path), self.root) def test_apply_refuses_a_patch_that_names_a_file_twice(self): self.write("a.cs", "one\n") patch = "--- a.cs\n+++ a.cs\n@@ -1,1 +1,1 @@\n-one\n+two\n" * 2 with self.assertRaises(PatchError): apply_patch(patch, self.root) def test_apply_refuses_hunks_that_overlap(self): self.write("a.cs", "1\n2\n3\n") patch = "--- a.cs\n+++ a.cs\n@@ -1,2 +1,2 @@\n-1\n+a\n 2\n@@ -2,2 +2,2 @@\n-2\n+b\n 3\n" with self.assertRaises(PatchError): apply_patch(patch, self.root) def test_apply_refuses_a_hunk_shorter_than_its_header(self): self.write("a.cs", "one\ntwo\n") with self.assertRaises(PatchError): apply_patch("--- a.cs\n+++ a.cs\n@@ -1,2 +1,2 @@\n-one\n", self.root) def test_apply_refuses_text_that_is_not_a_patch(self): with self.assertRaises(PatchError): apply_patch("hello\n", self.root) def test_apply_refuses_a_file_without_a_final_newline(self): self.write("a.cs", "one\ntwo") with self.assertRaises(PatchError): apply_patch("--- a.cs\n+++ a.cs\n@@ -1,1 +1,1 @@\n-one\n+ONE\n", self.root) def test_apply_refuses_a_file_that_is_not_utf8(self): with open(os.path.join(self.root, "a.cs"), "wb") as file: file.write(b"one\xff\n") with self.assertRaises(PatchError): apply_patch("--- a.cs\n+++ a.cs\n@@ -1,1 +1,1 @@\n-one\n+ONE\n", self.root) def test_make_patch_of_identical_trees_is_empty(self): self.write("pristine/src/a.cs", "one\n") self.write("edited/src/a.cs", "one\n") self.assertEqual("", make_patch(os.path.join(self.root, "pristine"), os.path.join(self.root, "edited"), "src")) def test_make_patch_ignores_a_change_of_line_endings_and_byte_order_mark(self): self.write("pristine/src/a.cs", "one\r\ntwo\r\n") self.write("edited/src/a.cs", "one\ntwo\n") self.assertEqual("", make_patch(os.path.join(self.root, "pristine"), os.path.join(self.root, "edited"), "src")) def test_make_patch_then_apply_adds_and_removes_empty_files(self): self.write("pristine/src/removed.cs", "") self.write("edited/src/added.cs", "") pristine_root = os.path.join(self.root, "pristine") apply_patch(make_patch(pristine_root, os.path.join(self.root, "edited"), "src"), pristine_root) self.assertEqual("", self.read("pristine/src/added.cs")) self.assertFalse(os.path.exists(os.path.join(pristine_root, "src", "removed.cs"))) def test_make_patch_skips_an_unchanged_file_with_no_final_newline(self): self.write("pristine/src/a.cs", "one\ntwo") self.write("edited/src/a.cs", "one\ntwo") self.assertEqual("", make_patch(os.path.join(self.root, "pristine"), os.path.join(self.root, "edited"), "src")) def test_make_patch_refuses_a_changed_file_with_no_final_newline(self): self.write("pristine/src/a.cs", "one\ntwo") self.write("edited/src/a.cs", "one\nTWO") with self.assertRaises(PatchError): make_patch(os.path.join(self.root, "pristine"), os.path.join(self.root, "edited"), "src") def test_make_patch_ignores_files_outside_the_subdirectory(self): self.write("pristine/src/a.cs", "one\n") self.write("edited/src/a.cs", "one\n") self.write("edited/obj/b.cs", "built\n") self.assertEqual("", make_patch(os.path.join(self.root, "pristine"), os.path.join(self.root, "edited"), "src")) def test_make_patch_then_apply_turns_the_pristine_tree_into_the_edited_one(self): self.write("pristine/src/changed.cs", "" + "".join("line{}\r\n".format(number) for number in range(30))) self.write("pristine/src/removed.cs", "gone\nalso gone\n") self.write("pristine/src/same.cs", "same\n") edited = "".join("line{}\n".format(number) for number in range(30)) edited = edited.replace("line3\n", "line3\ninserted\n").replace("line25\n", "") self.write("edited/src/changed.cs", edited) self.write("edited/src/added/new.cs", "brand\nnew\n") self.write("edited/src/same.cs", "same\n") pristine_root = os.path.join(self.root, "pristine") patch = make_patch(pristine_root, os.path.join(self.root, "edited"), "src") apply_patch(patch, pristine_root) self.assertEqual("" + edited.replace("\n", "\r\n"), self.read("pristine/src/changed.cs")) self.assertFalse(os.path.exists(os.path.join(pristine_root, "src", "removed.cs"))) self.assertEqual("brand\nnew\n", self.read("pristine/src/added/new.cs")) self.assertEqual("same\n", self.read("pristine/src/same.cs")) def main(): parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) mode = parser.add_mutually_exclusive_group() mode.add_argument("--prepare", action="store_true", help="Unpack and patch the source into Junk\\MoonSharpBuild, then stop so it can be edited.") mode.add_argument("--make-patch", action="store_true", help="Write the difference between the pristine source and Junk\\MoonSharpBuild\\src.") mode.add_argument("--test", action="store_true", help="Run this script's own tests.") arguments = parser.parse_args() if arguments.test: suite = unittest.TestLoader().loadTestsFromTestCase(PatchTests) return 0 if unittest.TextTestRunner(verbosity=2).run(suite).wasSuccessful() else 1 if arguments.prepare: prepare_build_directory() print("Unpacked into {}.".format(BUILD_DIRECTORY)) elif arguments.make_patch: write_patch_file() else: return rebuild_package() return 0 if __name__ == "__main__": sys.exit(main())