362 lines
12 KiB
Python
362 lines
12 KiB
Python
#!/usr/bin/env python3
|
|
|
|
"""
|
|
GUI for managing Omegafox patches.
|
|
"""
|
|
import os
|
|
import re
|
|
import sys
|
|
import easygui
|
|
|
|
from _mixin import find_src_dir, is_bootstrap_patch, list_patches, patch, run, temp_cd
|
|
|
|
|
|
def into_omegafox_dir():
|
|
"""Cd to the omegafox-* folder"""
|
|
this_script = os.path.dirname(os.path.abspath(__file__))
|
|
# Go one directory up from the current script path
|
|
os.chdir(os.path.dirname(this_script))
|
|
os.chdir(find_src_dir('.', version=sys.argv[1], release=sys.argv[2]))
|
|
|
|
|
|
def reset_omegafox():
|
|
"""Reset the Omegafox source"""
|
|
with temp_cd('..'):
|
|
run('make revert')
|
|
run('touch _READY')
|
|
|
|
|
|
def run_patches(reverse=False):
|
|
"""Apply patches"""
|
|
patch_files = list_patches()
|
|
|
|
# Create a display list with status labels and a reverse lookup mapping
|
|
display_choices = []
|
|
mapping = {}
|
|
for patch_file in patch_files:
|
|
# If the patch is a bootstrap patch, mark it with the appropriate label
|
|
if is_bootstrap_patch(patch_file):
|
|
status = "BOOTSTRAP"
|
|
else:
|
|
can_apply, can_reverse, broken = check_patch(patch_file)
|
|
if broken:
|
|
status = "BROKEN"
|
|
elif can_reverse:
|
|
status = "APPLIED"
|
|
elif can_apply:
|
|
status = "NOT APPLIED"
|
|
else:
|
|
status = "UNKNOWN"
|
|
# Format the display string (remove the '../patches/' prefix)
|
|
display_name = f"[{status}] {patch_file[len('../patches/'):].strip()}"
|
|
display_choices.append(display_name)
|
|
mapping[display_name] = patch_file
|
|
|
|
title = "Unpatch files" if reverse else "Patch files"
|
|
selected_display = easygui.multchoicebox(title, "Patches", display_choices, preselect=[])
|
|
if not selected_display:
|
|
return False
|
|
|
|
# Convert the selected items back to filenames
|
|
for display_name in selected_display:
|
|
patch_file = mapping[display_name]
|
|
patch(patch_file, reverse=reverse)
|
|
return True
|
|
|
|
def open_patch_workspace(selected_patch, stop_at_patch=False):
|
|
"""
|
|
Resets a workspace for editing a patch.
|
|
|
|
Process:
|
|
1. Resets Omegafox
|
|
2. Patches all except the selected patch
|
|
3. Sets checkpoint
|
|
4. Reruns the selected patch, but reads rejects similar to "Find broken patches"
|
|
"""
|
|
# Prepare UI
|
|
patch_files = list_patches()
|
|
|
|
# Reset workspace
|
|
reset_omegafox()
|
|
|
|
skipped_patches = []
|
|
applied_patches = []
|
|
# Patch all except the selected patch
|
|
for patch_file in patch_files:
|
|
if patch_file == selected_patch:
|
|
if stop_at_patch:
|
|
break
|
|
continue
|
|
if is_broken(patch_file):
|
|
print(f'Skipping broken patch: {patch_file}')
|
|
skipped_patches.append(patch_file)
|
|
continue
|
|
patch(patch_file, silent=True)
|
|
applied_patches.append(patch_file)
|
|
|
|
# Set checkpoint
|
|
if applied_patches:
|
|
with temp_cd('..'):
|
|
run('make checkpoint')
|
|
|
|
# Set message for patch result
|
|
patch_broken = is_broken(selected_patch)
|
|
if patch_broken:
|
|
message = "Broken patch has been applied to the workspace.\n\nPLEASE FIX THE FOLLOWING:\n"
|
|
else:
|
|
message = "Successfully applied patch to the workspace.\n"
|
|
|
|
# Run the selected patch
|
|
patch_result = os.popen(f'patch -p1 -i "{selected_patch}"').read()
|
|
|
|
# Find any line containing a file .rej
|
|
if patch_broken:
|
|
for line in patch_result.splitlines():
|
|
if file := re.search(r'[^\s]+\.rej', line):
|
|
message += f'> {file[0]}' + '\n'
|
|
|
|
def msg_format_paths(file_list):
|
|
message = ''
|
|
for patch_file in file_list:
|
|
message += '> ' + patch_file[len('../patches/') :] + '\n'
|
|
return message
|
|
|
|
# Show which patches were applied if not all patches were allowed
|
|
if stop_at_patch and applied_patches:
|
|
message += f'\n{"-" * 22} Applied patches {"-" * 22}\n'
|
|
message += msg_format_paths(applied_patches)
|
|
|
|
if skipped_patches:
|
|
message += f'\n{"-" * 17} Skipped patches (broken!) {"-" * 17}\n'
|
|
message += msg_format_paths(skipped_patches)
|
|
|
|
message += f'\n{"-" * 24} Full output {"-" * 24}\n{patch_result}'
|
|
easygui.textbox("Patch Result", "Patch Result", message)
|
|
|
|
|
|
def check_patch(patch_file):
|
|
"""
|
|
Checks if the patch can be applied or can be reversed
|
|
Returns (can_apply, can_reverse, is_broken)
|
|
"""
|
|
can_apply = not bool(
|
|
os.system(f'patch -p1 --dry-run --force -i "{patch_file}" > /dev/null 2>&1')
|
|
)
|
|
can_reverse = not bool(
|
|
os.system(f'patch -p1 -R --dry-run --force -i "{patch_file}" > /dev/null 2>&1')
|
|
)
|
|
return can_apply, can_reverse, not (can_apply or can_reverse)
|
|
|
|
|
|
def is_broken(patch_file):
|
|
"""Check if a patch file is broken"""
|
|
_, _, is_broken = check_patch(patch_file)
|
|
return is_broken
|
|
|
|
|
|
def get_rejects(patch_file):
|
|
"""Get rejects from a patch file"""
|
|
cmd = f'patch -p1 -i "{patch_file}" | tee /dev/stderr | sed -n -E \'s/^.*saving rejects to file (.*\\.rej)$/\\1/p\''
|
|
result = os.popen(cmd).read().strip()
|
|
return result.split('\n') if result else []
|
|
|
|
|
|
# GUI Choicebox with options
|
|
choices = [
|
|
"Reset workspace",
|
|
"Edit a patch",
|
|
"Create new patch",
|
|
"\u2014" * 44,
|
|
"List patches currently applied",
|
|
"Select patches",
|
|
"Reverse patches",
|
|
"Find broken patches (resets workspace)",
|
|
"\u2014" * 44,
|
|
"See current workspace",
|
|
"Write workspace to patch",
|
|
"Set checkpoint",
|
|
]
|
|
|
|
"""
|
|
GUI Choicebox
|
|
"""
|
|
|
|
|
|
def handle_choice(choice):
|
|
"""Handle UI choice"""
|
|
match choice:
|
|
case "Reset workspace":
|
|
reset_omegafox()
|
|
easygui.msgbox(
|
|
"Reset. All patches & changes have been removed.",
|
|
"Reset Complete",
|
|
)
|
|
|
|
case "Create new patch":
|
|
# Reset omegafox, apply all patches, then create a checkpoint
|
|
reset_omegafox()
|
|
with temp_cd('..'):
|
|
run('make dir')
|
|
run('make checkpoint')
|
|
easygui.msgbox(
|
|
"Created new patch workspace. You can test Omegafox with 'make run'.\n\n"
|
|
"When you are finished, write your workspace back to a new patch.",
|
|
"New Patch Workspace",
|
|
)
|
|
|
|
case "List patches currently applied":
|
|
# Produces a list of patches that are applied
|
|
apply_dict = {}
|
|
for patch_file in list_patches():
|
|
print(f'FILE: {patch_file}')
|
|
# Ignore bootstrap files, these will always break.
|
|
if is_bootstrap_patch(patch_file):
|
|
apply_dict[patch_file] = 'IGNORED'
|
|
continue
|
|
# Check if the patch can be applied or reversed
|
|
can_apply, can_reverse, broken = check_patch(patch_file)
|
|
if broken:
|
|
apply_dict[patch_file] = 'BROKEN'
|
|
elif can_reverse:
|
|
apply_dict[patch_file] = 'APPLIED'
|
|
elif can_apply:
|
|
apply_dict[patch_file] = 'NOT APPLIED'
|
|
else:
|
|
apply_dict[patch_file] = 'UNKNOWN (broken .patch?)'
|
|
easygui.textbox(
|
|
"Patching Result",
|
|
"Patching Result",
|
|
'\n'.join(
|
|
sorted(
|
|
(
|
|
f'{v}\t{k[len("../patches/"):-len(".patch")]}'
|
|
for k, v in apply_dict.items()
|
|
),
|
|
reverse=True,
|
|
key=lambda x: x[0],
|
|
)
|
|
),
|
|
)
|
|
|
|
case "Set checkpoint":
|
|
with temp_cd('..'):
|
|
run('make checkpoint')
|
|
easygui.msgbox("Checkpoint set.", "Checkpoint Set")
|
|
|
|
case "Select patches":
|
|
result = run_patches(reverse=False)
|
|
if result:
|
|
easygui.msgbox("Patching completed.", "Patching Complete")
|
|
|
|
case "Reverse patches":
|
|
result = run_patches(reverse=True)
|
|
if result:
|
|
easygui.msgbox("Unpatching completed.", "Unpatching Complete")
|
|
|
|
case "Find broken patches (resets workspace)":
|
|
reset_omegafox()
|
|
|
|
get_all = None
|
|
|
|
broken_patches = []
|
|
for patch_file in list_patches():
|
|
print(f'Testing: {patch_file}')
|
|
if reject_files := get_rejects(patch_file):
|
|
# Add the patch to the list
|
|
broken_patches.append((patch_file, reject_files))
|
|
# If get_all is None, ask the user if they want to get all the rejects
|
|
if get_all is None:
|
|
get_all = easygui.ynbox(
|
|
f"Reject was found: {patch_file}.\nGet the rest of them?",
|
|
"Get All Rejects",
|
|
choices=["Yes", "No"],
|
|
)
|
|
# If the user closed the dialog, return
|
|
if get_all is None:
|
|
return
|
|
# If the user said no, break the patch loop
|
|
if not get_all:
|
|
break
|
|
|
|
if not broken_patches:
|
|
easygui.msgbox("All patches applied successfully.", "Patching Result")
|
|
return
|
|
|
|
# Display message
|
|
message = "Some patches failed to apply:\n\n"
|
|
for patch_file, rejects in broken_patches:
|
|
message += '> ' + patch_file[len('../patches/') :] + '\n'
|
|
message += '\n\n\n'
|
|
|
|
# Show file contents
|
|
for patch_file, rejects in broken_patches:
|
|
message += f"Patch: {patch_file[len('../patches/'):]}\nRejects:\n"
|
|
for reject in rejects:
|
|
with open(reject, 'r') as f:
|
|
count = len(re.findall('^@@.*', f.read(), re.MULTILINE))
|
|
message += f"{count} reject(s) > {reject}\n"
|
|
message += '\n'
|
|
easygui.textbox("Patching Result", "Failed Patches", message)
|
|
|
|
case "Edit a patch":
|
|
patch_files = list_patches()
|
|
ui_choices = []
|
|
for n, file_name in enumerate(patch_files):
|
|
# If the patch is bootstrap, label it
|
|
if is_bootstrap_patch(file_name):
|
|
status = "BOOTSTRAP"
|
|
else:
|
|
can_apply, can_reverse, broken = check_patch(file_name)
|
|
if broken:
|
|
status = "BROKEN"
|
|
elif can_reverse:
|
|
status = "APPLIED"
|
|
elif can_apply:
|
|
status = "NOT APPLIED"
|
|
else:
|
|
status = "UNKNOWN"
|
|
display_name = f"{n+1}. [{status}] {file_name[len('../patches/'):]}".strip()
|
|
ui_choices.append(display_name)
|
|
|
|
selected_patch = easygui.choicebox(
|
|
"Select patch to open in workspace",
|
|
"Patches",
|
|
ui_choices,
|
|
)
|
|
# Return if user cancelled
|
|
if not selected_patch:
|
|
return
|
|
# Get file path of selected patch
|
|
selected_patch = patch_files[ui_choices.index(selected_patch)]
|
|
open_patch_workspace(
|
|
selected_patch,
|
|
# Patches starting with 0- rely on being ran first.
|
|
stop_at_patch=is_bootstrap_patch(selected_patch),
|
|
)
|
|
|
|
case "See current workspace":
|
|
result = os.popen('git diff').read()
|
|
easygui.textbox("Diff", "Diff", result)
|
|
|
|
case "Write workspace to patch":
|
|
# Open a file dialog to select a file to write the diff to
|
|
with temp_cd('../patches'):
|
|
file_path = easygui.filesavebox(
|
|
"Select a file to write the patch to",
|
|
"Write Patch",
|
|
filetypes="*.patch",
|
|
)
|
|
if not file_path:
|
|
exit()
|
|
run(f'git diff > {file_path}')
|
|
easygui.msgbox(f"Patch has been written to {file_path}.", "Patch Written")
|
|
|
|
case _:
|
|
print('No choice selected')
|
|
|
|
|
|
if __name__ == "__main__":
|
|
into_omegafox_dir()
|
|
|
|
while choice := easygui.choicebox("Select an option:", "Omegafox Dev Tools", choices):
|
|
handle_choice(choice)
|