Files
tp/tools/tp.py
T

1693 lines
53 KiB
Python
Raw Normal View History

2021-03-28 22:49:05 +02:00
"""
2021-12-02 23:38:37 +01:00
tp.py - Various tools used for the zeldaret/tp project.
2021-03-28 22:49:05 +02:00
"""
import hashlib
import io
2021-03-28 22:49:05 +02:00
import os
2021-12-02 23:38:37 +01:00
import sys
2021-03-28 22:49:05 +02:00
import time
2021-12-02 23:38:37 +01:00
import json
import subprocess
import logging
2021-12-02 23:38:37 +01:00
import multiprocessing as mp
import shutil
2023-01-14 08:18:31 +02:00
import platform
import stat
import zipfile
2021-03-28 22:49:05 +02:00
2021-04-10 22:32:02 +02:00
from dataclasses import dataclass, field
2022-05-07 11:38:20 -07:00
from typing import Dict, List, Set, Tuple
2021-12-02 23:38:37 +01:00
from pathlib import Path
2023-01-30 09:19:24 -08:00
def _handle_import_error(ex: ImportError):
2021-12-02 23:38:37 +01:00
MISSING_PREREQUISITES = (
2023-05-31 05:54:08 -06:00
f"Missing prerequisite python module {ex}.\n"
2021-12-02 23:38:37 +01:00
f"Run `python3 -m pip install --user -r tools/requirements.txt` to install prerequisites."
)
print(MISSING_PREREQUISITES, file=sys.stderr)
sys.exit(1)
2023-01-30 09:19:24 -08:00
try:
import click
import libdol
2023-08-09 16:27:37 -06:00
import libgithub
2023-01-30 09:19:24 -08:00
import requests
import glob
2023-01-30 09:19:24 -08:00
from rich.logging import RichHandler
from rich.console import Console
from rich.progress import Progress
from rich.text import Text
from rich.table import Table
2023-06-27 11:16:48 -06:00
from typing import Optional
2023-01-30 09:19:24 -08:00
except ImportError as ex:
_handle_import_error(ex)
2021-12-02 23:38:37 +01:00
class PathPath(click.Path):
def convert(self, value, param, ctx):
return Path(super().convert(value, param, ctx))
2021-03-28 22:49:05 +02:00
VERSION = "1.0"
CONSOLE = Console()
logging.basicConfig(
level="NOTSET",
format="%(message)s",
datefmt="[%X]",
2021-12-02 23:38:37 +01:00
handlers=[RichHandler(console=CONSOLE, rich_tracebacks=True)],
2021-03-28 22:49:05 +02:00
)
LOG = logging.getLogger("rich")
LOG.setLevel(logging.INFO)
2021-04-01 02:07:58 +02:00
loggers = [logging.getLogger(name) for name in logging.root.manager.loggerDict]
for logger in loggers:
logger.setLevel(logging.INFO)
2023-08-11 00:51:32 -06:00
if sys.version_info < (3, 10):
LOG.error("This script requires Python 3.10 or newer!")
sys.exit(1)
2021-12-02 23:38:37 +01:00
DEFAULT_GAME_PATH = "game"
DEFAULT_TOOLS_PATH = "tools"
DEFAULT_BUILD_PATH = "build/dolzel2"
DEFAULT_EXPECTED_PATH = "expected/build/dolzel2"
2021-04-06 18:00:35 +02:00
2021-03-28 22:49:05 +02:00
@click.group()
@click.version_option(VERSION)
def tp():
2021-12-02 23:38:37 +01:00
"""Tools to help the decompilation of "The Legend of Zelda: Twilight Princess" """
2021-03-28 22:49:05 +02:00
pass
2021-12-02 23:38:37 +01:00
@tp.command(name="expected")
@click.option("--debug/--no-debug")
@click.option(
"--build-path",
type=PathPath(file_okay=False, dir_okay=True),
default=DEFAULT_BUILD_PATH,
required=True,
)
@click.option(
"--expected-path",
type=PathPath(file_okay=False, dir_okay=True),
default=DEFAULT_EXPECTED_PATH,
required=True,
)
2022-05-07 11:38:20 -07:00
def expected_copy(debug: bool, build_path: Path, expected_path: Path):
2021-12-02 23:38:37 +01:00
"""Copy the current build folder to the expected folder"""
if debug:
LOG.setLevel(logging.DEBUG)
if not build_path.exists() or not build_path.is_dir():
LOG.error(
f"You need to successfully build the project before copy into expected folder."
)
sys.exit(1)
shutil.rmtree(expected_path, ignore_errors=True)
expected_path.mkdir(exist_ok=True, parents=True)
CONSOLE.log(f"src: '{build_path}'")
CONSOLE.log(f"dst: '{expected_path}'")
shutil.copytree(build_path, expected_path, dirs_exist_ok=True)
@tp.command(name="setup")
@click.option("--debug/--no-debug")
@click.option(
"--game-path",
type=PathPath(file_okay=False, dir_okay=True),
default=DEFAULT_GAME_PATH,
required=True,
)
@click.option(
"--tools-path",
type=PathPath(file_okay=False, dir_okay=True),
default=DEFAULT_TOOLS_PATH,
required=True,
)
@click.option(
"--yaz0-encoder",
type=str,
default="native",
required=False,
)
2023-07-17 07:21:18 +03:00
@click.option("--force-download/--no-force-download")
2023-07-17 00:42:22 -06:00
@click.option("--skip-iso/--no-skip-iso", default=False)
def setup(debug: bool, game_path: Path, tools_path: Path, yaz0_encoder: str, force_download: bool, skip_iso: bool):
2021-12-02 23:38:37 +01:00
"""Setup project"""
if debug:
LOG.setLevel(logging.DEBUG)
#
text = Text("--- Creating directories")
text.stylize("bold magenta")
CONSOLE.print(text)
if not game_path.exists():
game_path.mkdir(parents=True, exist_ok=True)
if not tools_path.exists() or not tools_path.is_dir():
LOG.error(f"Tools directory missing '{tools_path}'")
sys.exit(1)
#
2023-01-24 22:50:19 -05:00
text = Text("--- Fetching compiler")
2021-12-02 23:38:37 +01:00
text.stylize("bold magenta")
CONSOLE.print(text)
compilers = tools_path.joinpath("mwcc_compiler")
2023-07-17 07:21:18 +03:00
if force_download:
shutil.rmtree(compilers)
2021-12-02 23:38:37 +01:00
if not compilers.exists() or not compilers.is_dir():
2023-01-24 22:50:19 -05:00
os.mkdir(compilers)
2023-07-17 07:21:18 +03:00
r = requests.get('https://cdn.discordapp.com/attachments/727918646525165659/1129759991696457728/GC_WII_COMPILERS.zip')
2023-01-24 22:50:19 -05:00
z = zipfile.ZipFile(io.BytesIO(r.content))
z.extractall(compilers)
gc_path = compilers.joinpath("GC")
allfiles = os.listdir(gc_path)
for f in allfiles:
src_path = os.path.join(gc_path, f)
dst_path = os.path.join(compilers, f)
shutil.move(src_path, dst_path)
os.rmdir(gc_path)
#
text = Text("--- Patching compiler")
text.stylize("bold magenta")
CONSOLE.print(text)
2021-12-02 23:38:37 +01:00
c27 = compilers.joinpath("2.7")
if not c27.exists() or not c27.is_dir():
LOG.error(
(
f"Unable to find MWCC compiler version 2.7: missing directory '{c27}'\n"
f"Check the README for instructions on how to obtain the compilers"
)
)
sys.exit(1)
2023-01-14 08:18:31 +02:00
c125 = compilers.joinpath("1.2.5")
if not c125.exists() or not c125.is_dir():
LOG.error(
(
f"Unable to find MWCC compiler version 1.2.5: missing directory '{c125}'\n"
f"Check the README for instructions on how to obtain the compilers"
)
)
sys.exit(1)
2023-07-17 07:21:18 +03:00
c125n = compilers.joinpath("1.2.5n")
if not c125n.exists() or not c125n.is_dir():
LOG.error(
(
2023-07-17 07:21:18 +03:00
f"Unable to find patched MWCC compiler version 1.2.5n: missing directory '{c125n}'\n"
f"Check the README for instructions on how to obtain the compilers"
)
)
sys.exit(1)
c27_lmgr326b = None
for name in os.listdir(c27):
if name.lower() == "lmgr326b.dll":
c27_lmgr326b = c27.joinpath(name)
break
if not c27_lmgr326b or not c27_lmgr326b.is_file():
2021-12-02 23:38:37 +01:00
LOG.error(
(
f"Unable to find 'lmgr326b.dll' in '{c27}': missing file '{c27_lmgr326b}'\n"
f"Check the README for instructions on how to obtain the compilers"
)
)
sys.exit(1)
def copy_lmgr326b(path: Path):
lmgr326b_cc = path.joinpath("LMGR326B.dll")
if not lmgr326b_cc.is_file():
LOG.debug(f"copy: '{c27_lmgr326b}', to: '{lmgr326b_cc}'")
shutil.copy(c27_lmgr326b, lmgr326b_cc)
2021-12-02 23:38:37 +01:00
copy_lmgr326b(c27)
copy_lmgr326b(c125)
2023-07-17 07:21:18 +03:00
copy_lmgr326b(c125n)
2023-01-14 08:18:31 +02:00
2021-12-02 23:38:37 +01:00
c27_mwcceppc = c27.joinpath("mwcceppc.exe")
if not c27_mwcceppc.exists() or not c27_mwcceppc.is_file():
LOG.error(
(
f"Unable to find 'mwcceppc.exe' in '{c27}': missing file '{c27_mwcceppc}'\n"
f"Check the README for instructions on how to obtain the compilers"
)
)
sys.exit(1)
c27_mwldeppc = c27.joinpath("mwldeppc.exe")
if not c27_mwldeppc.exists() or not c27_mwldeppc.is_file():
LOG.error(
(
f"Unable to find 'mwldeppc.exe' in '{c27}': missing file '{c27_mwldeppc}'\n"
f"Check the README for instructions on how to obtain the compilers"
)
)
sys.exit(1)
MWCCEPPC_SHA1 = "100DD3A2898A1ECE55462801D42FF5DE8B42E29F"
MWCCEPPC_PATCHED_SHA1 = "22C7DCFBAD7FE0710C84EA714B81C5220D8E2996"
MWLDEPPC_SHA1 = "8225A47FF099A3AECB32CD307A95CB69B30A6A84"
with c27_mwcceppc.open("rb") as file:
mwcceppc_sha1 = sha1_from_data(bytearray(file.read()))
with c27_mwldeppc.open("rb") as file:
mwldeppc_sha1 = sha1_from_data(bytearray(file.read()))
c27_mwcceppc_old = c27.joinpath("mwcceppc.old.exe")
c27_mwcceppc_orignal = c27.joinpath("mwcceppc.exe")
c27_mwcceppc_patched = c27.joinpath("mwcceppc_modded.exe")
2021-12-02 23:38:37 +01:00
2022-05-07 11:38:20 -07:00
def patch_compiler(src: Path, dst: Path, apply: bool):
2021-12-02 23:38:37 +01:00
with src.open("rb") as src_file:
with dst.open("wb") as dst_file:
data = bytearray(src_file.read())
if apply:
data[0x001C6A54] = 0x6D
else:
data[0x001C6A54] = 0x69
dst_file.write(data)
2023-01-14 08:18:31 +02:00
if platform.system() == "Linux":
os.chmod(dst, stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP | stat.S_IROTH | stat.S_IXOTH)
2021-12-02 23:38:37 +01:00
if mwcceppc_sha1 == MWCCEPPC_SHA1:
LOG.debug(f"found original compiler: '{c27_mwcceppc}' ('{mwcceppc_sha1}')")
c27_mwcceppc_old.unlink(missing_ok=True)
shutil.move(c27_mwcceppc, c27_mwcceppc_old)
shutil.copy(c27_mwcceppc_old, c27_mwcceppc_orignal)
patch_compiler(c27_mwcceppc_old, c27_mwcceppc_patched, apply=True) # patch
elif mwcceppc_sha1 == MWCCEPPC_PATCHED_SHA1:
LOG.debug(f"found patched compiler: '{c27_mwcceppc}' ('{mwcceppc_sha1}')")
c27_mwcceppc_old.unlink(missing_ok=True)
shutil.move(c27_mwcceppc, c27_mwcceppc_old)
shutil.copy(c27_mwcceppc_old, c27_mwcceppc_patched)
patch_compiler(
c27_mwcceppc_old, c27_mwcceppc_orignal, apply=False
) # revert patch
else:
LOG.error(
(
f"Invalid 'mwcceppc.exe' checksum of '{mwcceppc_sha1}'\n"
f"Check the README for instructions on how to obtain the compilers"
)
)
sys.exit(1)
# add execute flag to compilers for WSL
if os.name == 'posix':
subprocess.run(['chmod', '+x'] + list(compilers.glob("*/*.exe")))
2023-01-30 09:19:24 -08:00
#
text = Text("--- Building tools")
text.stylize("bold magenta")
CONSOLE.print(text)
if subprocess.run(["make", "tools"]).returncode != 0:
LOG.error("An error occurred while running 'make tools'")
exit(1)
2023-07-17 00:42:22 -06:00
if skip_iso is False:
text = Text("--- Extracting game assets")
text.stylize("bold magenta")
CONSOLE.print(text)
2021-12-02 23:38:37 +01:00
2023-07-17 00:42:22 -06:00
iso = Path("gz2e01.iso")
if not iso.exists() or not iso.is_file():
LOG.error(
(
f"Missing file '{iso}'.\n"
f"Did you forget to copy the NTSC-U version in the root directory?"
)
2021-12-02 23:38:37 +01:00
)
2023-07-17 00:42:22 -06:00
sys.exit(1)
2021-12-02 23:38:37 +01:00
2023-07-17 00:42:22 -06:00
try:
import extract_game_assets
extract_game_assets.extract(iso, game_path, yaz0_encoder)
2023-07-17 00:42:22 -06:00
except ImportError as ex:
_handle_import_error(ex)
except Exception as e:
LOG.error(f"failure:")
LOG.error(e)
sys.exit(1)
2021-12-02 23:38:37 +01:00
text = Text("--- Complete")
text.stylize("bold magenta")
CONSOLE.print(text)
CONSOLE.print("You should now be able to build the project 👍")
CONSOLE.print("Check the README for instructions for further instructions")
#
# Progress
#
2021-03-28 22:49:05 +02:00
@tp.command(name="progress")
2021-12-02 23:38:37 +01:00
@click.option("--debug/--no-debug")
@click.option("--matching/--no-matching", default=True, is_flag=True)
@click.option("--print-rels/--no-print-rels", default=True, is_flag=True)
2021-12-02 23:38:37 +01:00
@click.option(
"--format",
"-f",
default="FANCY",
type=click.Choice(["FANCY", "CSV", "JSON-SHIELD", "JSON"], case_sensitive=False),
)
@click.option(
"--build-path",
type=PathPath(file_okay=False, dir_okay=True),
default=DEFAULT_BUILD_PATH,
required=True,
)
2022-05-07 11:38:20 -07:00
def progress(debug: bool, matching: bool, format: str, print_rels: bool, build_path: Path):
2021-12-02 23:38:37 +01:00
"""Calculate decompilation progress"""
2021-03-28 22:49:05 +02:00
if debug:
LOG.setLevel(logging.DEBUG)
2021-04-10 22:32:02 +02:00
if format == "FANCY":
text = Text("--- Progress")
text.stylize("bold magenta")
CONSOLE.print(text)
2021-04-06 18:00:35 +02:00
2021-12-02 23:38:37 +01:00
calculate_progress(build_path, matching, format, print_rels)
2021-03-28 22:49:05 +02:00
2021-12-02 23:38:37 +01:00
#
# Check
#
2021-04-06 18:00:35 +02:00
@tp.command(name="check")
2021-12-02 23:38:37 +01:00
@click.option("--debug/--no-debug")
@click.option("--rels", default=False, is_flag=True)
@click.option(
"--game-path",
type=PathPath(file_okay=False, dir_okay=True),
default=DEFAULT_GAME_PATH,
required=True,
)
@click.option(
"--build-path",
type=PathPath(file_okay=False, dir_okay=True),
default=DEFAULT_BUILD_PATH,
required=True,
)
2022-05-07 11:38:20 -07:00
def check(debug: bool, rels: bool, game_path: Path, build_path: Path):
2021-12-02 23:38:37 +01:00
"""Compare SHA1 Checksums"""
2021-04-06 18:00:35 +02:00
if debug:
LOG.setLevel(logging.DEBUG)
text = Text("--- Check")
text.stylize("bold magenta")
CONSOLE.print(text)
try:
2022-01-15 17:59:59 -07:00
check_sha1(game_path/"files", build_path, rels)
2021-04-06 18:00:35 +02:00
text = Text(" OK")
text.stylize("bold green")
CONSOLE.print(text)
except CheckException as e:
LOG.error(e)
text = Text(" ERR")
text.stylize("bold red")
CONSOLE.print(text)
sys.exit(1)
2021-12-02 23:38:37 +01:00
2021-04-10 22:32:02 +02:00
@dataclass
class ProgressSection:
name: str
addr: int
2021-04-10 22:32:02 +02:00
size: int
decompiled: int
2021-03-28 22:49:05 +02:00
2021-04-10 22:32:02 +02:00
@property
def percentage(self):
return 100 * (self.decompiled / self.size)
2021-12-02 23:38:37 +01:00
2021-04-10 22:32:02 +02:00
@dataclass
class ProgressGroup:
name: str
size: int
decompiled: int
sections: Dict[str, ProgressSection] = field(default_factory=dict)
@property
def percentage(self):
return 100 * (self.decompiled / self.size)
2022-05-07 11:38:20 -07:00
def calculate_rel_progress(build_path: Path, matching: bool, format: str, asm_files: Set[Path], ranges: List[Tuple[int, int]]):
2023-01-30 23:34:45 -08:00
results: List[ProgressGroup] = []
2021-12-02 23:38:37 +01:00
start = time.time()
rel_paths = get_files_with_ext(build_path.joinpath("rel"), ".rel")
end = time.time()
LOG.debug(f"get_files_with_ext: {(end - start)*1000} ms")
start = time.time()
from collections import defaultdict
str_asm_rel = f"asm{os.path.sep}rel{os.path.sep}"
2021-12-02 23:38:37 +01:00
range_dict = defaultdict(list)
for file, range in zip(asm_files, ranges):
str_file = str(file)
if not str_file.startswith(str_asm_rel):
2021-12-02 23:38:37 +01:00
continue
rel = str_file.split(os.path.sep)[-3]
2021-12-02 23:38:37 +01:00
range_dict[rel].append(range[1] - range[0])
end = time.time()
LOG.debug(f"range_dict: {(end - start)*1000} ms")
2021-04-10 22:32:02 +02:00
for rel_path in rel_paths:
2021-12-02 23:38:37 +01:00
start = time.time()
2021-04-10 22:32:02 +02:00
2021-12-02 23:38:37 +01:00
size = rel_path.stat().st_size
2021-04-10 22:32:02 +02:00
name = rel_path.name.replace(".rel", "")
2021-12-02 23:38:37 +01:00
end = time.time()
2021-04-10 22:32:02 +02:00
2021-12-02 23:38:37 +01:00
rel_ranges = range_dict[name]
decompiled = size - sum(rel_ranges)
2021-04-10 22:32:02 +02:00
results.append(ProgressGroup(name, size, decompiled, {}))
2023-01-30 23:34:45 -08:00
results.sort(key=lambda prog: prog.name)
2021-04-10 22:32:02 +02:00
return results
2021-12-02 23:38:37 +01:00
2022-05-07 11:38:20 -07:00
def calculate_dol_progress(build_path: Path, matching: bool, format: str, asm_files: Set[Path], ranges: List[Tuple[int, int]]):
2021-03-28 22:49:05 +02:00
# read .dol file
2021-12-02 23:38:37 +01:00
dol_path = build_path.joinpath("main.dol")
2021-03-28 22:49:05 +02:00
if not dol_path.exists():
LOG.error(f"Unable to read '{dol_path}'")
sys.exit(1)
with dol_path.open("rb") as file:
data = file.read()
dol = libdol.read(data)
# get section sizes
total_size = len(data)
format_size = 0x100
2021-04-01 02:07:58 +02:00
# assume everything is decompiled
2021-12-02 23:38:37 +01:00
sections = dict(
[
(
section.name,
ProgressSection(
section.name,
section.addr,
section.aligned_size,
section.aligned_size,
),
)
for section in dol.sections
if section.data
]
)
2021-04-01 02:07:58 +02:00
2021-03-28 22:49:05 +02:00
init = dol.get_named_section(".init")
assert init
text = dol.get_named_section(".text")
assert text
LOG.debug(f"init {init.addr:08X}-{init.addr + init.size:08X}")
LOG.debug(f"text {text.addr:08X}-{text.addr + text.size:08X}")
# substract the size of each asm function
for function_range in ranges:
if function_range[0] >= init.addr and function_range[1] < init.addr + init.size:
2021-12-02 23:38:37 +01:00
sections[".init"].decompiled -= function_range[1] - function_range[0]
elif (
function_range[0] >= text.addr and function_range[1] < text.addr + text.size
):
sections[".text"].decompiled -= function_range[1] - function_range[0]
total_decompiled_size = format_size + sum(
[section.decompiled for section in sections.values()]
)
2021-04-10 22:32:02 +02:00
return ProgressGroup("main.dol", total_size, total_decompiled_size, sections)
2021-12-02 23:38:37 +01:00
2022-05-07 11:38:20 -07:00
def calculate_progress(build_path: Path, matching: bool, format: str, print_rels: bool):
2021-04-10 22:32:02 +02:00
if not matching:
LOG.error("non-matching progress is not support yet.")
sys.exit(1)
2021-12-02 23:38:37 +01:00
start = time.time()
# find all _used_ asm files
asm_files = find_used_asm_files(not matching, use_progress_bar=(format == "FANCY"))
end = time.time()
LOG.debug(f"find_used_asm_files: {(end - start)*1000} ms")
start = time.time()
# calculate the range each asm file occupies
ranges = find_function_ranges(asm_files)
end = time.time()
LOG.debug(f"find_function_ranges: {(end - start)*1000} ms")
start = time.time()
dol_progress = calculate_dol_progress(
build_path, matching, format, asm_files, ranges
)
end = time.time()
LOG.debug(f"calculate_dol_progress: {(end - start)*1000} ms")
2021-04-10 22:32:02 +02:00
rel_size = 0
rel_decompiled = 0
2021-12-02 23:38:37 +01:00
rels_progress = []
if print_rels:
start = time.time()
rels_progress = calculate_rel_progress(
build_path, matching, format, asm_files, ranges
)
end = time.time()
LOG.debug(f"calculate_rel_progress: {(end - start)*1000} ms")
for rel in rels_progress:
rel_size += rel.size
rel_decompiled += rel.decompiled
2021-04-10 22:32:02 +02:00
total_size = dol_progress.size + rel_size
decompiled_size = dol_progress.decompiled + rel_decompiled
2021-03-28 22:49:05 +02:00
if format == "FANCY":
2022-04-10 16:29:58 -06:00
tableString = "# Twilight Princess Decompilation Progress\n\n## Dol\n\nSection | Percentage | Decompiled (bytes) | Total (bytes)\n---|---|---|---\n"
2021-03-28 22:49:05 +02:00
table = Table(title="main.dol")
2021-12-02 23:38:37 +01:00
table.add_column("Section", justify="right", style="cyan", no_wrap=True)
2021-03-28 22:49:05 +02:00
table.add_column("Percentage", style="green")
2021-12-02 23:38:37 +01:00
table.add_column("Decompiled (bytes)", justify="right", style="bright_yellow")
table.add_column("Total (bytes)", justify="right", style="bright_magenta")
2021-03-28 22:49:05 +02:00
2021-04-10 22:32:02 +02:00
for name, section in dol_progress.sections.items():
2021-12-02 23:38:37 +01:00
table.add_row(
name,
f"{section.percentage:10.6f}%",
f"{section.decompiled}",
f"{section.size}",
)
2022-04-10 16:29:58 -06:00
tableString = tableString+name+" | "+f"{section.percentage:10.6f}%"+" | "+f"{section.decompiled}"+" | "+f"{section.size}"+"\n"
2021-04-01 02:07:58 +02:00
table.add_row("", "", "", "")
2021-12-02 23:38:37 +01:00
table.add_row(
"total",
f"{dol_progress.percentage:10.6f}%",
f"{dol_progress.decompiled}",
f"{dol_progress.size}",
)
2022-04-10 16:29:58 -06:00
tableString = tableString+"Total | "+f"{dol_progress.percentage:10.6f}%"+" | "+f"{dol_progress.decompiled}"+" | "+f"{dol_progress.size}"+"\n\n"
2021-04-10 22:32:02 +02:00
CONSOLE.print(table)
2022-04-10 16:29:58 -06:00
tableString = tableString+"## Total\n\nSection | Percentage | Decompiled (bytes) | Total (bytes)\n---|---|---|---\n"
tableString = tableString+"main.dol | "+f"{dol_progress.percentage:10.6f}%"+" | "+f"{dol_progress.decompiled}"+" | "+f"{dol_progress.size}"+"\n"
2021-04-10 22:32:02 +02:00
if print_rels:
2022-04-10 16:29:58 -06:00
tableString = tableString+"RELs | "+f"{100 * (rel_decompiled / rel_size):10.6f}%"+" | "+f"{rel_decompiled}"+" | "+f"{rel_size}"+"\n"
tableString = tableString+"Total | "+f"{100 * (decompiled_size / total_size):10.6f}%"+" | "+f"{decompiled_size}"+" | "+f"{total_size}"+"\n\n"
if print_rels:
tableString = tableString+"## RELs\n\nSection | Percentage | Decompiled (bytes) | Total (bytes)\n---|---|---|---\n"
table = Table(title="RELs")
2021-12-02 23:38:37 +01:00
table.add_column("Section", justify="right", style="cyan", no_wrap=True)
table.add_column("Percentage", style="green")
2021-12-02 23:38:37 +01:00
table.add_column(
"Decompiled (bytes)", justify="right", style="bright_yellow"
)
table.add_column("Total (bytes)", justify="right", style="bright_magenta")
2021-04-10 22:32:02 +02:00
for rel in rels_progress:
2021-12-02 23:38:37 +01:00
table.add_row(
rel.name,
f"{rel.percentage:10.6f}%",
f"{rel.decompiled}",
f"{rel.size}",
)
2022-04-10 16:29:58 -06:00
tableString = tableString+rel.name+" | "+f"{rel.percentage:10.6f}%"+" | "+f"{rel.decompiled}"+" | "+f"{rel.size}"+"\n"
2021-04-10 22:32:02 +02:00
table.add_row("", "", "", "")
2021-12-02 23:38:37 +01:00
table.add_row(
"total",
f"{100 * (rel_decompiled / rel_size):10.6f}%",
f"{rel_decompiled}",
f"{rel_size}",
)
2022-04-10 16:29:58 -06:00
tableString = tableString+"Total | "+f"{100 * (rel_decompiled / rel_size):10.6f}%"+" | "+f"{rel_decompiled}"+" | "+f"{rel_size}"+"\n"
2021-04-10 22:32:02 +02:00
CONSOLE.print(table)
2021-04-10 22:32:02 +02:00
table = Table(title="Total")
2021-12-02 23:38:37 +01:00
table.add_column("Section", justify="right", style="cyan", no_wrap=True)
2021-04-10 22:32:02 +02:00
table.add_column("Percentage", style="green")
2021-12-02 23:38:37 +01:00
table.add_column("Decompiled (bytes)", justify="right", style="bright_yellow")
table.add_column("Total (bytes)", justify="right", style="bright_magenta")
2021-04-10 22:32:02 +02:00
2021-12-02 23:38:37 +01:00
table.add_row(
"main.dol",
f"{dol_progress.percentage:10.6f}%",
f"{dol_progress.decompiled}",
f"{dol_progress.size}",
)
if rels_progress:
2021-12-02 23:38:37 +01:00
table.add_row(
"RELs",
f"{100 * (rel_decompiled / rel_size):10.6f}%",
f"{rel_decompiled}",
f"{rel_size}",
)
else:
# if we don't have any rel progress, just indicate N/A
table.add_row("RELs", "".center(11), f"", f"")
2021-04-10 22:32:02 +02:00
table.add_row("", "", "", "")
2021-12-02 23:38:37 +01:00
table.add_row(
"total",
f"{100 * (decompiled_size / total_size):10.6f}%",
f"{decompiled_size}",
f"{total_size}",
)
2021-03-28 22:49:05 +02:00
CONSOLE.print(table)
2022-04-10 16:29:58 -06:00
open("Progress.md","w").write(tableString)
2021-03-28 22:49:05 +02:00
elif format == "CSV":
version = 1
2022-07-01 02:01:37 +02:00
try:
import git
git_object = git.Repo().head.object
timestamp = str(git_object.committed_date)
git_hash = git_object.hexsha
except ImportError:
LOG.warning("can't import git, some fields will be missing!")
timestamp = ""
git_hash = ""
2021-04-10 22:32:02 +02:00
2021-03-28 22:49:05 +02:00
data = [
2021-12-02 23:38:37 +01:00
str(version),
timestamp,
git_hash,
str(dol_progress.decompiled),
str(dol_progress.size),
str(rel_decompiled),
str(rel_size),
str(decompiled_size),
str(total_size),
2021-03-28 22:49:05 +02:00
]
print(",".join(data))
elif format == "JSON-SHIELD":
# https://shields.io/endpoint
2021-12-02 23:38:37 +01:00
print(
json.dumps(
{
"schemaVersion": 1,
"label": "progress",
"message": f"{100 * (decompiled_size / total_size):.3g}%",
"color": "yellow", # TODO: color
}
)
)
elif format == "JSON":
2023-06-20 02:21:29 +02:00
# TODO: add dol sections instead of total dol.
data = {
"code": decompiled_size,
"code/total": total_size,
"dol": dol_progress.decompiled,
"dol/total": dol_progress.size,
"rels": rel_decompiled,
"rels/total": rel_size,
}
print(json.dumps(data))
2021-03-28 22:49:05 +02:00
else:
2021-04-10 22:32:02 +02:00
print(dol_progress.percentage)
print(100 * (rel_decompiled / rel_size))
print(100 * (decompiled_size / total_size))
2021-03-28 22:49:05 +02:00
LOG.error("unknown format: '{format}'")
2021-12-02 23:38:37 +01:00
2022-05-07 11:38:20 -07:00
def find_function_range(asm: Path) -> Tuple[int, int]:
with asm.open("r", encoding="utf-8") as file:
2021-12-02 23:38:37 +01:00
lines = file.readlines()
for line in lines:
if line.startswith("/* "):
fast_first = int(line[3:11], 16)
break
for line in lines[::-1]:
if line.startswith("/* "):
fast_last = int(line[3:11], 16) + 4
break
return (fast_first, fast_last)
2022-05-07 11:38:20 -07:00
def find_function_ranges(asm_files: Set[Path]):
2021-12-02 23:38:37 +01:00
if len(asm_files) < 128:
return [find_function_range(x) for x in asm_files]
2021-03-28 22:49:05 +02:00
2021-12-02 23:38:37 +01:00
thread_count = 4
with mp.Pool(processes=2 * thread_count) as pool:
jobs_left = len(asm_files)
result = pool.map_async(find_function_range, asm_files)
while result._number_left > 0:
time.sleep(1 / 20)
2021-03-28 22:49:05 +02:00
2021-12-02 23:38:37 +01:00
function_ranges = result.get()
return function_ranges
2021-03-28 22:49:05 +02:00
2021-08-17 02:59:00 +02:00
2021-12-02 23:38:37 +01:00
#
# Remove ASM
#
@tp.command(
name="remove-unused-asm",
help="Remove all of the asm that is decompiled and not used anymore",
)
@click.option("--check", default=False, is_flag=True)
2022-05-07 11:38:20 -07:00
def remove_unused_asm_cmd(check: bool):
2021-12-02 23:38:37 +01:00
result = remove_unused_asm(check)
if check:
if result == 0:
print("OK")
sys.exit(0)
else:
sys.exit(1)
2021-08-17 02:59:00 +02:00
2022-05-07 11:38:20 -07:00
def remove_unused_asm(check: bool):
2021-12-02 23:38:37 +01:00
unused_files, error_files = find_unused_asm_files(False, use_progress_bar=not check)
2021-08-17 02:59:00 +02:00
for unused_file in unused_files:
if not check:
2021-12-02 23:38:37 +01:00
unused_file.unlink()
CONSOLE.print(f"removed '{unused_file}'")
2021-12-02 23:38:37 +01:00
text = Text(" OK")
text.stylize("bold green")
CONSOLE.print(text)
if check:
text = Text(" NOTE: The was just a check run. No files were actually removed.")
2021-12-02 23:38:37 +01:00
text.stylize("bold green")
CONSOLE.print(text)
return len(unused_files)
#
# Pull-Request
#
2021-03-28 22:49:05 +02:00
@tp.command(name="pull-request")
2021-12-02 23:38:37 +01:00
@click.option("--debug/--no-debug")
@click.option(
"--rels",
default=True,
2021-12-02 23:38:37 +01:00
is_flag=True,
help="RELs will also be build and checked",
)
@click.option(
"--thread-count",
"-j",
"thread_count",
help="This option is passed forward to all 'make' commands.",
default=4,
)
@click.option(
"--game-path",
type=PathPath(file_okay=False, dir_okay=True),
default=DEFAULT_GAME_PATH,
required=True,
)
@click.option(
"--build-path",
type=PathPath(file_okay=False, dir_okay=True),
default=DEFAULT_BUILD_PATH,
required=True,
)
2022-05-07 11:38:20 -07:00
def pull_request(debug: bool, rels: bool, thread_count: int, game_path: Path, build_path: Path):
2021-12-02 23:38:37 +01:00
"""Verify that everything is OK before pull-request"""
2021-03-28 22:49:05 +02:00
if debug:
LOG.setLevel(logging.DEBUG)
text = Text("Pull-Request Checklist:")
text.stylize("bold")
CONSOLE.print(text)
2021-12-02 23:38:37 +01:00
#
text = Text("--- Remove unused .s files")
text.stylize("bold magenta")
CONSOLE.print(text)
remove_unused_asm(False)
2021-03-28 22:49:05 +02:00
#
text = Text("--- Full Rebuild")
text.stylize("bold magenta")
CONSOLE.print(text)
2021-12-02 23:38:37 +01:00
if rebuild(thread_count, rels):
2021-03-28 22:49:05 +02:00
text = Text(" OK")
text.stylize("bold green")
CONSOLE.print(text)
else:
text = Text(" ERR")
text.stylize("bold red")
CONSOLE.print(text)
sys.exit(1)
2021-04-06 18:00:35 +02:00
#
text = Text("--- Check")
text.stylize("bold magenta")
CONSOLE.print(text)
try:
2021-12-02 23:38:37 +01:00
check_sha1(game_path, build_path, rels)
2021-04-06 18:00:35 +02:00
text = Text(" OK")
text.stylize("bold green")
CONSOLE.print(text)
except CheckException as e:
LOG.error(e)
text = Text(" ERR")
text.stylize("bold red")
CONSOLE.print(text)
sys.exit(1)
2021-03-28 22:49:05 +02:00
#
text = Text("--- Calculate Progress")
text.stylize("bold magenta")
CONSOLE.print(text)
2021-12-02 23:38:37 +01:00
calculate_progress(build_path, True, "FANCY", rels)
2021-03-28 22:49:05 +02:00
2022-05-07 11:38:20 -07:00
def find_all_asm_files() -> Tuple[Set[Path], Set[Path]]:
2021-12-02 23:38:37 +01:00
"""Recursivly find all files in the 'asm/' folder"""
2021-03-28 22:49:05 +02:00
files = set()
errors = set()
2022-05-07 11:38:20 -07:00
def recursive(parent: Path):
2021-03-28 22:49:05 +02:00
paths = sorted(
parent.iterdir(),
key=lambda path: (path.is_file(), path.name.lower()),
)
for path in paths:
if path.name.startswith("."):
continue
if path.is_dir():
recursive(path)
else:
if path.suffix == ".s":
2021-03-28 22:49:05 +02:00
files.add(path)
else:
errors.add(path)
# Check for all .s files in ./asm/
2021-03-28 22:49:05 +02:00
root = Path("./asm/")
assert root.exists()
recursive(root)
2021-03-28 22:49:05 +02:00
# check for .inc files in all directories
root = Path("./")
assert root.exists()
2021-03-28 22:49:05 +02:00
recursive(root)
LOG.debug(
2021-12-02 23:38:37 +01:00
f"find_all_asm_files: found {len(files)} .s files and {len(errors)} bad files"
)
2021-03-28 22:49:05 +02:00
return files, errors
2022-05-07 11:38:20 -07:00
def find_unused_asm_files(non_matching: bool, use_progress_bar: bool = True):
2021-12-02 23:38:37 +01:00
"""Search for unused asm function files."""
2021-03-28 22:49:05 +02:00
asm_files, error_files = find_all_asm_files()
2021-12-02 23:38:37 +01:00
included_asm_files = find_used_asm_files(
non_matching, use_progress_bar=use_progress_bar
)
2021-03-28 22:49:05 +02:00
unused_asm_files = asm_files - included_asm_files
2021-12-02 23:38:37 +01:00
LOG.debug(f"find_unused_asm_files: found {len(unused_asm_files)} unused .s files")
2021-03-28 22:49:05 +02:00
return unused_asm_files, error_files
2022-05-07 11:38:20 -07:00
def find_all_header_files() -> Set[Path]:
2021-12-02 23:38:37 +01:00
"""Recursivly find all files in the 'include/' folder"""
2021-03-28 22:49:05 +02:00
files = set()
2022-05-07 11:38:20 -07:00
def recursive(parent: Path):
2021-03-28 22:49:05 +02:00
paths = sorted(
parent.iterdir(),
key=lambda path: (path.is_file(), path.name.lower()),
)
for path in paths:
# Remove hidden files
if path.name.startswith("."):
continue
if path.is_dir():
recursive(path)
else:
2021-12-02 23:38:37 +01:00
if path.suffix == ".h":
2021-03-28 22:49:05 +02:00
files.add(path)
root = Path("./include/")
assert root.exists()
recursive(root)
LOG.debug(f"find_all_header_files: found {len(files)} .h files")
return files
2022-05-07 11:38:20 -07:00
def find_all_files() -> Set[Path]:
2022-04-24 04:02:50 -07:00
"""Recursively find all c/cpp files in '/src/', '/libs/', and '/rel/' """
2021-03-28 22:49:05 +02:00
files = set()
2022-05-07 11:38:20 -07:00
def recursive(parent: Path):
2021-03-28 22:49:05 +02:00
paths = sorted(
parent.iterdir(),
key=lambda path: (path.is_file(), path.name.lower()),
)
for path in paths:
# Remove hidden files
if path.name.startswith("."):
continue
if path.is_dir():
recursive(path)
else:
if path.suffix == ".cpp" or path.suffix == ".c" or path.suffix == ".inc":
2021-03-28 22:49:05 +02:00
files.add(path)
src_root = Path("./src/")
libs_root = Path("./libs/")
rel_root = Path("./rel/")
assert src_root.exists()
assert libs_root.exists()
assert rel_root.exists()
recursive(src_root)
recursive(libs_root)
recursive(rel_root)
2022-04-24 04:02:50 -07:00
LOG.debug(f"find_all_files: found {len(files)} .c/.cpp files")
2021-03-28 22:49:05 +02:00
return files
def find_includes(lines: List[str], non_matching: bool, ext: str = ".s") -> Set[Path]:
2021-03-28 22:49:05 +02:00
includes = set()
for line in lines:
key = '#include "'
start = line.find(key)
if start < 0:
continue
start += len(key)
end = line.find('"', start)
if end < 0:
continue
include_path = line[start:end]
if include_path.endswith(ext):
includes.add(Path(include_path))
return includes
2022-05-07 11:38:20 -07:00
def find_used_asm_files(non_matching: bool, use_progress_bar: bool = True) -> Set[Path]:
2022-04-24 04:02:50 -07:00
cpp_files = find_all_files()
2021-03-28 22:49:05 +02:00
includes = set()
2021-04-01 02:07:58 +02:00
if use_progress_bar:
2021-12-02 23:38:37 +01:00
with Progress(
console=CONSOLE, transient=True, refresh_per_second=1
) as progress:
2021-04-01 02:07:58 +02:00
task = progress.add_task(f"preprocessing...", total=len(cpp_files))
2021-03-28 22:49:05 +02:00
2021-04-01 02:07:58 +02:00
for cpp_file in cpp_files:
with cpp_file.open("r", encoding="utf-8") as file:
2021-04-01 02:07:58 +02:00
includes.update(find_includes(file.readlines(), non_matching))
progress.update(task, advance=1)
else:
2021-03-28 22:49:05 +02:00
for cpp_file in cpp_files:
with cpp_file.open("r", encoding="utf-8") as file:
2021-03-28 22:49:05 +02:00
includes.update(find_includes(file.readlines(), non_matching))
# TODO: NON_MATCHING
LOG.debug(f"find_used_asm_files: found {len(includes)} included .s or .inc files")
2021-03-28 22:49:05 +02:00
return includes
2022-05-07 11:38:20 -07:00
def rebuild(thread_count: int, include_rels: bool):
2021-03-28 22:49:05 +02:00
LOG.debug("make clean")
with Progress(console=CONSOLE, transient=True, refresh_per_second=5) as progress:
task = progress.add_task(f"make clean", total=1000, start=False)
cmd = ["make", f"-j{thread_count}", "clean"]
2021-12-02 23:38:37 +01:00
result = subprocess.run(
args=cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
2021-03-28 22:49:05 +02:00
LOG.debug("make clean complete")
2021-04-06 18:00:35 +02:00
if result.returncode != 0:
return False
2021-03-28 22:49:05 +02:00
2021-12-02 23:38:37 +01:00
if include_rels:
LOG.debug("make clean_rels")
with Progress(
console=CONSOLE, transient=True, refresh_per_second=5
) as progress:
task = progress.add_task(f"make clean_rels", total=1000, start=False)
cmd = ["make", f"-j{thread_count}", "clean_rels"]
result = subprocess.run(
args=cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
LOG.debug("make clean_rels complete")
if result.returncode != 0:
return False
2021-03-28 22:49:05 +02:00
LOG.debug("make main.dol")
with Progress(console=CONSOLE, transient=True, refresh_per_second=5) as progress:
task = progress.add_task(f"make", total=1000, start=False)
cmd = ["make", f"-j{thread_count}", "build/dolzel2/main.dol"]
2021-12-02 23:38:37 +01:00
result = subprocess.run(
args=cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
2021-03-28 22:49:05 +02:00
LOG.debug("make main.dol complete")
2021-04-06 18:00:35 +02:00
if result.returncode != 0:
return False
2021-03-28 22:49:05 +02:00
2021-12-02 23:38:37 +01:00
if include_rels:
LOG.debug("make RELs")
with Progress(
console=CONSOLE, transient=True, refresh_per_second=5
) as progress:
task = progress.add_task(f"make rels", total=1000, start=False)
2021-03-28 22:49:05 +02:00
2021-12-02 23:38:37 +01:00
cmd = ["make", f"-j{thread_count}", "rels"]
result = subprocess.run(
args=cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
LOG.debug("make RELs complete")
if result.returncode != 0:
return False
2021-03-28 22:49:05 +02:00
2021-04-06 18:00:35 +02:00
return True
2021-03-28 22:49:05 +02:00
2021-04-06 18:00:35 +02:00
def sha1_from_data(data):
2021-03-28 22:49:05 +02:00
sha1 = hashlib.sha1()
sha1.update(data)
2021-04-06 18:00:35 +02:00
return sha1.hexdigest().upper()
2021-03-28 22:49:05 +02:00
2021-12-02 23:38:37 +01:00
2022-05-07 11:38:20 -07:00
def get_files_with_ext(path: Path, ext: str):
2021-04-06 18:00:35 +02:00
return [x for x in path.glob(f"**/*{ext}") if x.is_file()]
2021-12-02 23:38:37 +01:00
2021-04-06 18:00:35 +02:00
class CheckException(Exception):
...
2022-05-07 11:38:20 -07:00
def check_sha1(game_path: Path, build_path: Path, include_rels: bool):
2023-01-30 09:19:24 -08:00
try:
import librel
except ImportError as ex:
_handle_import_error(ex)
2021-04-06 18:00:35 +02:00
EXPECTED = {}
2021-12-02 23:38:37 +01:00
EXPECTED[0] = (
"",
"4997D93B9692620C40E90374A0F1DBF0E4889395",
)
2021-12-02 23:38:37 +01:00
if include_rels:
2022-07-01 01:09:48 +02:00
with open('sha1sums.json') as f:
rel_shas = json.load(f)
for num, (name, sha) in enumerate(rel_shas.items()):
if num == 0:
continue
EXPECTED[num] = (
name,
sha
)
2021-04-06 18:00:35 +02:00
if not build_path.exists():
raise CheckException(f"Path not found: '{build_path}'")
build_dol_path = build_path.joinpath("main.dol")
if not build_dol_path.exists():
raise CheckException(f"File not found: '{build_dol_path}'")
CURRENT = {}
2021-12-02 23:38:37 +01:00
with build_dol_path.open("rb") as file:
2021-04-06 18:00:35 +02:00
data = file.read()
2021-12-02 23:38:37 +01:00
CURRENT[0] = (
str(build_dol_path),
sha1_from_data(data),
)
2021-04-06 18:00:35 +02:00
2021-12-02 23:38:37 +01:00
if include_rels:
build_rels_path = get_files_with_ext(build_path/"rel", ".rel")
2021-12-02 23:38:37 +01:00
for rel_filepath in build_rels_path:
with rel_filepath.open("rb") as file:
data = bytearray(file.read())
rel = librel.read(data)
CURRENT[rel.index] = (
str(rel_filepath),
sha1_from_data(data),
)
2021-04-06 18:00:35 +02:00
expected_keys = set(EXPECTED.keys())
current_keys = set(CURRENT.keys())
match = expected_keys - current_keys
if len(match) > 0:
2021-12-02 23:38:37 +01:00
raise CheckException(
f"Missing main.dol or RELs (expected: {len(expected_keys)}, found: {len(current_keys)})"
)
2021-04-06 18:00:35 +02:00
errors = 0
for key in expected_keys:
if key in current_keys:
expected = EXPECTED[key]
current = CURRENT[key]
2022-07-01 01:09:48 +02:00
if current[1] != expected[1]:
2021-04-06 18:00:35 +02:00
errors += 1
2022-07-01 01:09:48 +02:00
LOG.error(f"{current[1]} {expected[1]} {current[0]} ({expected[0]})")
2021-04-06 18:00:35 +02:00
if errors > 0:
raise CheckException("NO MATCH!")
2021-03-28 22:49:05 +02:00
return True
2023-08-09 16:27:37 -06:00
#
# Github Command Helpers
#
import functools
def common_github_options(func):
@click.option("--debug/--no-debug")
@click.option(
"--personal-access-token",
help="Github Personal Access Token for authorizing API calls.",
required=False,
default=os.environ.get('GITHUB_TOKEN')
)
@click.option(
"--owner",
help="Github repo owner",
required=False,
default="zeldaret"
)
@click.option(
"--repo",
help="Github repository name",
required=False,
default="tp"
)
2023-08-11 00:51:32 -06:00
@click.option(
"--state-file",
help="File to store the state of the issues in. Defaults to tools/projects.yml",
required=False,
default="tools/pjstate.yml"
)
2023-08-09 16:27:37 -06:00
@functools.wraps(func)
def wrapper(*args, **kwargs):
return func(*args, **kwargs)
return wrapper
2023-08-11 00:51:32 -06:00
def prereqs(owner: str, repo: str, personal_access_token: str, state_file: str):
2023-08-09 16:27:37 -06:00
# Setup GraphQL client singleton
libgithub.GraphQLClient.setup(personal_access_token)
# Setup RepoInfo classvars
libgithub.RepoInfo.owner = libgithub.OwnerInfo()
libgithub.RepoInfo.owner.name = owner
libgithub.RepoInfo.name = repo
libgithub.RepoInfo.set_ids()
# Load in the project state
2023-08-11 00:51:32 -06:00
libgithub.StateFile.load(state_file)
2023-08-09 16:27:37 -06:00
2023-08-11 00:51:32 -06:00
def load_from_yaml(type: str, project_name: str) -> any:
2023-08-09 16:27:37 -06:00
with open("./tools/projects.yml", 'r') as stream:
try:
import yaml
projects_data = yaml.safe_load(stream)
LOG.debug(f"Loaded projects.yml data: {projects_data}")
match type:
case "labels":
2023-08-11 00:51:32 -06:00
ret_data = libgithub.Label.get_all_from_yaml(projects_data, project_name)
2023-08-09 16:27:37 -06:00
case "issues":
2023-08-11 00:51:32 -06:00
ret_data = libgithub.Issue.get_all_from_yaml(projects_data, project_name)
2023-08-09 16:27:37 -06:00
case "projects":
2023-08-11 00:51:32 -06:00
ret_data = libgithub.Project.get_all_from_yaml(projects_data, project_name)
2023-08-09 16:27:37 -06:00
case _:
LOG.error(f"Invalid type: {type}")
sys.exit(1)
return ret_data
except ImportError:
LOG.error("Can't import yaml, exiting.")
sys.exit(1)
except yaml.YAMLError as error:
LOG.error(f"Error loading YAML: {error}")
sys.exit(1)
#
# Github Sync Commands
#
@tp.command(name="github-sync-labels", help="Creates all labels based on tools/projects.yml")
@common_github_options
2023-08-11 00:51:32 -06:00
@click.option(
"--project",
help="Only sync labels for a specific project",
required=False,
default=None
)
def github_sync_labels(debug: bool, personal_access_token: str, owner: str, repo: str, project: str, state_file: str):
2023-08-09 16:27:37 -06:00
if debug:
LOG.setLevel(logging.DEBUG)
2023-08-11 00:51:32 -06:00
prereqs(owner, repo, personal_access_token, state_file)
yaml_labels = load_from_yaml("labels", project)
2023-08-09 16:27:37 -06:00
LOG.info("Syncing up labels")
for label in yaml_labels:
label.check_and_create()
@tp.command(name="github-sync-issues", help="Creates all issues and labels based on tools/projects.yml")
@common_github_options
2023-08-11 00:51:32 -06:00
@click.option(
"--project",
help="Only sync labels for a specific project",
required=False,
default=None
)
def github_sync_issues(debug: bool, personal_access_token: str, owner: str, repo: str, project: str, state_file: str):
2023-08-09 16:27:37 -06:00
if debug:
LOG.setLevel(logging.DEBUG)
2023-08-11 00:51:32 -06:00
prereqs(owner,repo,personal_access_token, state_file)
yaml_issues = load_from_yaml("issues", project)
2023-08-09 16:27:37 -06:00
LOG.info("Syncing up issues")
for issue in yaml_issues:
issue.check_and_create()
@tp.command(name="github-sync-projects", help="Creates all projects, issues and labels based on tools/projects.yml")
@common_github_options
2023-08-11 00:51:32 -06:00
@click.option(
"--project",
help="Only sync labels for a specific project",
required=False,
default=None
)
def github_sync_projects(debug: bool, personal_access_token: str, owner: str, repo: str, project: str, state_file: str):
2023-08-09 16:27:37 -06:00
if debug:
LOG.setLevel(logging.DEBUG)
2023-08-11 00:51:32 -06:00
prereqs(owner, repo, personal_access_token, state_file)
yaml_projects = load_from_yaml("projects", project)
2023-08-09 16:27:37 -06:00
LOG.info("Syncing up projects")
for project in yaml_projects:
project.check_and_create()
2023-08-11 00:51:32 -06:00
@tp.command(name="github-update-issues", help="Checks all issues and updates their status and assigness.")
2023-08-09 16:27:37 -06:00
@common_github_options
@click.option(
'--filename','filenames',
2023-08-11 00:51:32 -06:00
help="Filename(s) used to look for and update issues.",
2023-08-09 16:27:37 -06:00
multiple=True,
type=click.Path(exists=True)
)
2023-08-11 00:51:32 -06:00
@click.option(
'--author',
multiple=True,
help="Author(s) to assign issues to.",
default=None
)
2023-08-09 16:27:37 -06:00
@click.option(
'--all',
help="Check all items in every project and update their status.",
is_flag=True,
default=False
)
@click.option(
'--clang-lib-path',
help="Path to libclang.so",
default="/usr/lib/x86_64-linux-gnu/libclang-16.so"
)
2023-08-11 00:51:32 -06:00
def github_update_issues(debug: bool, personal_access_token: str, owner: str, repo: str, filenames: Tuple[click.Path], all: bool, author: str, clang_lib_path: str, state_file: str):
2023-08-09 16:27:37 -06:00
if debug:
LOG.setLevel("DEBUG")
2023-08-11 00:51:32 -06:00
if author == () and all == False:
LOG.error("Author is required when --all is not set. Please set it using the --author argument.")
sys.exit(1)
prereqs(owner, repo, personal_access_token, state_file)
2023-08-09 16:27:37 -06:00
issues = libgithub.StateFile.data.get('issues')
projects = libgithub.StateFile.data.get('projects')
filenames_list = list(filenames)
2023-08-11 00:51:32 -06:00
author_list = list(author)
if len(author_list) == 0:
author_list = [""] * len(filenames_list)
2023-08-09 16:27:37 -06:00
# If all flag is set, check all issue file paths in state file
if all:
for issue in issues:
filenames_list.append(issue["file_path"])
2023-08-11 00:51:32 -06:00
import classify_tu, clang, itertools
2023-08-09 16:27:37 -06:00
# Set the clang library file
clang.cindex.Config.set_library_file(clang_lib_path)
2023-08-11 00:51:32 -06:00
for filename,author in itertools.zip_longest(filenames_list,author_list):
2023-08-09 16:27:37 -06:00
LOG.info(f"Classifying TU {filename}")
status = classify_tu.run(filename)
LOG.debug(f"Classification result: {status}")
if status == "error":
LOG.error(f"Error classifying TU {filename}")
sys.exit(1)
# Find the matching issue_id for the filename
issue_id = None
for issue in issues:
if issue["file_path"] == filename:
issue_id = issue["id"]
2023-08-11 00:51:32 -06:00
issue_title = issue["title"]
2023-08-09 16:27:37 -06:00
break
if issue_id is None:
LOG.error(f"Couldn't find issue_id for {filename}. Run github-sync-issues first.")
sys.exit(1)
# Find the matching project_id, item_id and status_field for the issue_id
project_id = None
for project in projects:
for item in project["items"]:
if item["issue_id"] == issue_id:
project_id = project["id"]
item_id = item["item_id"]
status_field = project["status_field"]
break
if project_id is None:
LOG.error(f"Couldn't find project_id associated with {filename}. Run github-sync-projects first.")
sys.exit(1)
libgithub.Project(id=project_id,status_field=status_field).set_status_for_item(item_id, status)
2023-08-11 00:51:32 -06:00
github_issue = libgithub.Issue(id=issue_id,title=issue_title)
# Add the author as an assignee if it was passed in
if author is not None:
# Find the matching author
author_user = libgithub.User(name=author)
author_user.get_id()
# Add the author as an assignee
assignees = github_issue.get_all_assignees()
assignees.append(author_user)
github_issue.add_assignees(assignees)
# Close the issue if status is done
2023-08-09 16:27:37 -06:00
if status == "done":
2023-08-11 00:51:32 -06:00
github_issue.set_closed()
2023-08-09 16:27:37 -06:00
#
# Github Clean Commands
#
@tp.command(name="github-clean-labels", help="Delete all labels for a given owner/repository.")
@common_github_options
2023-08-11 00:51:32 -06:00
def github_clean_labels(debug: bool, personal_access_token: str, owner: str, repo: str, state_file: str) -> None:
2023-08-09 16:27:37 -06:00
if debug:
LOG.setLevel("DEBUG")
LOG.warning(f"This command will completely delete all labels for {owner}/{repo}. Are you sure you want to do this? (y/n)")
confirmation = input().lower()
if confirmation == 'y':
2023-08-11 00:51:32 -06:00
prereqs(owner,repo,personal_access_token, state_file)
2023-08-09 16:27:37 -06:00
libgithub.Label.delete_all()
else:
sys.exit(0)
@tp.command(name="github-clean-issues", help="Delete all issues for a given owner/repository.")
@common_github_options
2023-08-11 00:51:32 -06:00
def github_clean_issues(debug: bool, personal_access_token: str, owner: str, repo: str, state_file: str) -> None:
2023-08-09 16:27:37 -06:00
if debug:
LOG.setLevel("DEBUG")
LOG.warning(f"This command will completely delete all issues for {owner}/{repo}. Are you sure you want to do this? (y/n)")
confirmation = input().lower()
if confirmation == 'y':
2023-08-11 00:51:32 -06:00
prereqs(owner,repo,personal_access_token, state_file)
2023-08-09 16:27:37 -06:00
libgithub.Issue.delete_all()
else:
sys.exit(0)
@tp.command(name="github-clean-projects", help="Delete all projects for a given owner/repository.")
@common_github_options
2023-08-11 00:51:32 -06:00
def github_clean_projects(debug: bool, personal_access_token: str, owner: str, repo: str, state_file: str) -> None:
2023-08-09 16:27:37 -06:00
if debug:
LOG.setLevel("DEBUG")
LOG.warning(f"This command will completely delete all projects for {owner}/{repo}. Are you sure you want to do this? (y/n)")
confirmation = input().lower()
if confirmation == 'y':
2023-08-11 00:51:32 -06:00
prereqs(owner,repo,personal_access_token, state_file)
2023-08-09 16:27:37 -06:00
libgithub.Project.delete_all()
else:
sys.exit(0)
#
# Progress Command Helpers
#
2023-06-27 11:16:48 -06:00
def copy_progress_script() -> None:
file_path = './tools/tp.py'
destination_path = './tools/tp_copy.py'
if not os.path.exists(destination_path):
shutil.copyfile(file_path, destination_path)
2023-06-27 11:16:48 -06:00
def make_progress_dir() -> None:
progress_dir = './progress'
if not os.path.exists(progress_dir):
os.mkdir(progress_dir)
2023-06-27 11:16:48 -06:00
def generate_progress(commit: str, wibo_path: Optional[str] = None) -> None:
git_show_output = subprocess.check_output(['git', 'show', '-s', '--format=%ct', commit]).decode('ascii').strip()
commit_timestamp = git_show_output
commit_string = f'progress/{commit_timestamp}_{commit}.json'
if os.path.exists(commit_string):
2023-06-27 11:16:48 -06:00
LOG.info(f"File {commit_string} already exists, skipping.")
return
process = subprocess.Popen(["make", "clean_all"], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = process.communicate()
if process.returncode != 0:
2023-06-27 11:16:48 -06:00
LOG.error(f"Error during make clean_all: {stderr.decode()}")
return
2023-06-27 11:16:48 -06:00
LOG.debug(f"stdout: {stdout.decode()}")
2023-06-27 11:16:48 -06:00
make_command = ["make", "all", "rels", f"-j{os.cpu_count()}"]
if wibo_path:
make_command.append(f"WINE={wibo_path}")
process = subprocess.Popen(make_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = process.communicate()
2023-06-27 11:16:48 -06:00
if process.returncode != 0:
2023-06-27 11:16:48 -06:00
LOG.error(f"Error during make all rels: {stderr.decode()}")
return
2023-06-27 11:16:48 -06:00
LOG.debug(f"stdout: {stdout.decode()}")
command = ["python", "./tools/tp_copy.py", "progress", "-f", "JSON"]
with open(commit_string, 'w') as outfile:
process = subprocess.Popen(command, stdout=outfile, stderr=subprocess.PIPE)
2023-06-27 11:16:48 -06:00
stdout, stderr = process.communicate()
if process.returncode != 0:
2023-06-27 11:16:48 -06:00
LOG.error(f"Error: {stderr.decode()}")
LOG.debug(f"stdout: {stdout.decode()}")
2023-06-27 11:16:48 -06:00
def checkout_and_run(repo_path: str, start_commit_hash: str, wibo_path: Optional[str] = None) -> None:
2023-08-09 16:27:37 -06:00
try:
import git
repo = git.Repo(repo_path)
head_commit = repo.head.commit
except ImportError:
LOG.error("Can't import git, exiting.")
sys.exit(1)
copy_progress_script()
make_progress_dir()
try:
commits = list(repo.iter_commits(f'{start_commit_hash}..{head_commit.hexsha}'))
commits.append(repo.commit(start_commit_hash))
for commit in commits[::-1]:
2023-06-27 11:16:48 -06:00
LOG.info(f"Checking out commit {commit.hexsha}")
repo.git.checkout(commit.hexsha)
2023-06-27 11:16:48 -06:00
generate_progress(commit.hexsha, wibo_path)
except Exception as e:
2023-06-27 11:16:48 -06:00
LOG.error(f"Error occurred: {e}")
finally:
2023-06-27 11:16:48 -06:00
LOG.debug(f"Checking out origin head commit: {head_commit.hexsha}")
repo.git.checkout(head_commit.hexsha)
2023-08-09 16:27:37 -06:00
#
# Progress Commands
#
@tp.command(name="progress-history")
@click.option("--debug/--no-debug", default=False)
@click.option("--repo-path", default=".", required=False, help="Path to your git repository. Defaults to current directory.")
@click.option("--start-commit", default="bc428f7f65b97cc9035aed1dc1b71c54ff2e6c3d", required=False, help="Start commit hash. If none supplied, will start at the commit where Julgodis added the progress script.")
2023-06-27 11:16:48 -06:00
@click.option("--wibo-path", default=None, required=False, help="Path to wibo build. If none supplied, the default Wine will be used.")
def progress_history(debug, repo_path, start_commit, wibo_path):
if debug:
LOG.setLevel(logging.DEBUG)
LOG.warning(f"This command will generate the progress for every commit since {start_commit}. This could take many hours to complete. Are you sure you want to do this? (y/n)")
confirmation = input().lower()
if confirmation == 'y':
2023-06-27 11:16:48 -06:00
checkout_and_run(repo_path, start_commit, wibo_path)
else:
sys.exit(0)
def get_git_commit_timestamp() -> int:
return int(subprocess.check_output(['git', 'show', '-s', '--format=%ct']).decode('ascii').rstrip())
def get_git_commit_sha() -> str:
return subprocess.check_output(['git', 'rev-parse', 'HEAD']).decode('ascii').strip()
def generate_url(base_url: str, project: str, version: str) -> str:
url_components = [base_url.rstrip('/'), 'data']
for arg in [project, version.replace('.', '-')]:
if arg != "":
url_components.append(arg)
return str.join('/', url_components) + '/'
@tp.command(name="upload-progress")
@click.option("--debug/--no-debug")
@click.option('-b', '--base_url', required=True, help='API base URL')
@click.option('-a', '--api_key', required=False, default=os.environ.get('PROGRESS_API_KEY'), help='API key (env var PROGRESS_API_KEY)')
@click.option('-p', '--project', required=True, help='Project slug')
@click.option('-v', '--version', required=True, help='Version slug')
@click.argument('input', type=click.Path(exists=True))
2023-06-27 11:16:48 -06:00
def upload_progress(debug: bool, base_url: str, api_key: str, project: str, version: str, input: str) -> None:
if debug:
LOG.setLevel(logging.DEBUG)
if not api_key:
raise click.UsageError("API key required")
url = generate_url(base_url, project, version)
entries = []
# Check if input is a directory
if os.path.isdir(input):
LOG.debug(f'Loading all JSON files in directory {input}')
# Read all JSON files in the directory
json_files = glob.glob(os.path.join(input, "*.json"))
for json_file in json_files:
# Extract timestamp and commit SHA from filename
filename = Path(json_file).stem
parts = filename.split('_')
if len(parts) != 2 or not parts[0].isdigit() or len(parts[1]) != 40:
LOG.error(f"Filename '{filename}' is not in the correct format. When supplying an entire directory with JSON files in it, the filenames need to be in the format: '<unix_timestamp>_<git_sha>.json' in order for Frogress to properly understand the data.")
sys.exit(1)
timestamp, git_hash = parts
with open(json_file, "r") as f:
data = json.load(f)
entries.append({
"timestamp": int(timestamp),
"git_hash": git_hash,
"categories": {
"default": data,
},
})
else:
# Process a single JSON file
with open(input, "r") as f:
LOG.debug(f'Loading single JSON file {f.name}')
data = json.load(f)
entries.append({
"timestamp": get_git_commit_timestamp(),
"git_hash": get_git_commit_sha(),
"categories": {
"default": data,
},
})
for entry in entries:
LOG.info(f"Publishing entry to {url}")
LOG.debug(f"Entry: {entry}")
data = {
"api_key": api_key,
"entries": [entry], # only send current entry
}
try:
r = requests.post(url, json=data)
r.raise_for_status()
except requests.exceptions.HTTPError as err:
LOG.error(f"HTTP request failed: {err}")
exit(1)
2021-03-28 22:49:05 +02:00
if __name__ == "__main__":
tp()