omegafox/scripts/developer.py
2025-04-21 20:41:38 -07:00

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)