summaryrefslogtreecommitdiff
path: root/tools
diff options
context:
space:
mode:
authorAetias <aetias@outlook.com>2025-05-22 22:05:31 +0200
committerAetias <aetias@outlook.com>2025-05-22 22:05:31 +0200
commit0ae9b8c14902a59341ec84730d417eabe595f2de (patch)
tree03ce8d268435e7165017374eaee15fbc37b1ace6 /tools
parent4a764f9d58cb16343c02afb3592aad7aabf66d6d (diff)
parent3f6a81d03dec2bd7c8394b508dbe4f017a5244b9 (diff)
Merge remote-tracking branch 'zeldaret/main' into decomp-PlayerControl
Diffstat (limited to 'tools')
-rwxr-xr-x[-rw-r--r--]tools/configure.py542
-rw-r--r--tools/download_tool.py53
-rw-r--r--tools/get_platform.py31
-rwxr-xr-xtools/m2ctx.py2
-rw-r--r--tools/ninja_syntax.py2
-rw-r--r--tools/setup.py54
-rwxr-xr-x[-rw-r--r--]tools/sha1.py2
7 files changed, 522 insertions, 164 deletions
diff --git a/tools/configure.py b/tools/configure.py
index f3c34311..0662b332 100644..100755
--- a/tools/configure.py
+++ b/tools/configure.py
@@ -1,22 +1,34 @@
-#!/usr/bin/python3
+#!/usr/bin/env python3
+import json
import os
from pathlib import Path
-import platform
import argparse
import sys
+import subprocess
+from typing import Any
import ninja_syntax
+from get_platform import Platform, get_platform
+
+
+DEFAULT_WIBO_PATH = "./wibo"
parser = argparse.ArgumentParser(description="Generates build.ninja")
-parser.add_argument('-w', type=str, default="./wibo", dest="wine", required=False, help="Path to Wine/Wibo (linux only)")
+parser.add_argument('-w', type=str, default=DEFAULT_WIBO_PATH, dest="wine", required=False, help="Path to Wine/Wibo (linux only)")
+parser.add_argument("--compiler", type=Path, required=False, help="Path to pre-installed compiler root directory")
+parser.add_argument("--no-extract", action="store_true", help="Skip extract step")
+parser.add_argument("--dsd", type=Path, required=False, help="Path to pre-installed dsd CLI")
parser.add_argument('version', help='Game version')
args = parser.parse_args()
# Config
GAME = "ph"
+DSD_VERSION = 'v0.9.1'
+WIBO_VERSION = '0.6.16'
+OBJDIFF_VERSION = 'v2.7.1'
MWCC_VERSION = "2.0/sp1p5"
DECOMP_ME_COMPILER = "mwcc_30_131"
CC_FLAGS = " ".join([
@@ -38,20 +50,32 @@ CC_FLAGS = " ".join([
"-nolink", # Do not link
"-msgstyle gcc", # Use GCC-like messages (some IDEs will make file names clickable)
])
+# Passed to all modules and final arm9.o link
LD_FLAGS = " ".join([
"-proc arm946e", # Target processor
+ "-dead", # Strip unused code
"-nostdlib", # No C/C++ standard library
"-interworking", # Enable ARM/Thumb interworking
- "-m Entry", # Set entry function
"-map closure,unused", # Generate map file
"-msgstyle gcc", # Use GCC-like messages (some IDEs will make file names clickable)
])
+# Only passed to the module links
+MODULE_LD_FLAGS = " ".join([
+ "-library", # Link as a static library
+])
+# Only passed to the final arm9.o link
+ARM9_LD_FLAGS = " ".join([
+ "-m Entry", # Set entry function
+])
DSD_OBJDIFF_ARGS = " ".join([
"--scratch", # Metadata for creating decomp.me scratches
f"--compiler {DECOMP_ME_COMPILER}", # decomp.me compiler name
f'--c-flags "{CC_FLAGS} -lang=c++"',# decomp.me compiler flags
"--custom-make ninja", # Command for rebuilding files
])
+DSD_BASE_FLAGS = " ".join([
+ "--force-color", # Force color output
+])
# Paths
@@ -65,12 +89,13 @@ src_path = root_path / "src"
libs_path = root_path / "libs"
extract_path = root_path / "extract"
tools_path = root_path / "tools"
-mwcc_path = tools_path / "mwccarm" / MWCC_VERSION
+mwcc_root = args.compiler or tools_path / "mwccarm"
+mwcc_path = mwcc_root / MWCC_VERSION
# Includes
includes = [
- str(root_path / "include")
+ root_path / "include"
]
for root, dirs, _ in os.walk(libs_path):
for dir in dirs:
@@ -80,38 +105,141 @@ CC_INCLUDES = " ".join(f"-i {include}" for include in includes)
# Platform info
-EXE = ""
-WINE = ""
-system = platform.system()
-if system == "Windows":
- system = "windows"
- EXE = ".exe"
-elif system == "Linux":
- system = "linux"
- WINE = args.wine
-else:
- print(f"Unknown system '{system}'")
+platform = get_platform()
+if platform is None:
exit(1)
-match platform.machine().lower():
- case "amd64" | "x86_64": machine = "x86_64"
- case machine:
- print(f"Unknown machine: {machine}")
- exit(1)
+EXE = platform.exe
+WINE = args.wine if platform.system != "windows" else ""
+DSD = str(args.dsd or os.path.join('.', str(root_path / f"dsd{EXE}")))
+OBJDIFF = os.path.join('.', str(root_path / f"objdiff-cli{EXE}"))
+CC = os.path.join('.', str(mwcc_path / "mwccarm.exe"))
+LD = os.path.join('.', str(mwcc_path / "mwldarm.exe"))
+PYTHON = sys.executable
-PYTHON = sys.executable
+class Project:
+ def __init__(self, game_version: str, *, platform: Platform, delinks_json: Any | None):
+ self.game_version = game_version
+ '''Version of the game'''
+ self.game_config = config_path / game_version
+ '''Root directory for dsd configs'''
+
+ if not self.game_config.is_dir():
+ print(f"Version '{game_version}' not recognized")
+ exit(1)
+
+ self.platform = platform
+ '''Host platform information'''
+ self.delinks_json = delinks_json
+ '''Delinks JSON data from dsd'''
+
+ self.game_build = build_path / game_version
+ '''Path to build directory'''
+ self.game_extract = extract_path / game_version
+ '''Path to extract directory'''
+
+ self.delinks_files = get_config_files(self.game_config, "delinks.txt")
+ '''Paths to every delinks.txt file'''
+ self.relocs_files = get_config_files(self.game_config, "relocs.txt")
+ '''Paths to every relocs.txt file'''
+ self.symbols_files = get_config_files(self.game_config, "symbols.txt")
+ '''Paths to every symbols.txt file'''
+
+ def dsd_configs(self) -> list[str]:
+ return self.delinks_files + self.relocs_files + self.symbols_files
+
+ def arm9_config_yaml(self) -> Path:
+ return self.game_config / "arm9" / "config.yaml"
+
+ def baserom(self) -> Path:
+ return extract_path / f'baserom_{GAME}_{self.game_version}.nds'
+
+ def build_rom(self) -> str:
+ return f"{GAME}_{self.game_version}.nds"
+
+ def baserom_config(self) -> Path:
+ return self.game_extract / 'config.yaml'
+
+ def build_rom_config(self) -> Path:
+ return self.game_build / "build" / "rom_config.yaml"
+
+ def source_object_files(self) -> list[str]:
+ return [
+ str(self.game_build / source_file.with_suffix(".o"))
+ for source_file in get_c_cpp_files([src_path, libs_path])
+ ]
+
+ def arm9_o(self) -> Path:
+ return self.game_build / "arm9.o"
+
+ def arm9_disassembly_dir(self) -> Path:
+ return self.game_build / "asm"
+
+ def objdiff_report(self) -> Path:
+ return self.game_build / "report.json"
+
+ def modules(self) -> list[Any]:
+ if self.delinks_json is None:
+ return []
+ return self.delinks_json['modules']
+
+ def delink_files(self) -> list[str]:
+ if self.delinks_json is None:
+ return []
+ return [file['delink_file'] for module in self.delinks_json['modules'] for file in module['files']]
+
+ def arm9_lcf_file(self) -> str:
+ if self.delinks_json is None:
+ return ""
+ return self.delinks_json['arm9_lcf_file']
+
+ def module_lcf_files(self) -> list[str]:
+ if self.delinks_json is None:
+ return []
+ return [module['lcf_file'] for module in self.delinks_json['modules']]
+
+
+def can_run_dsd() -> bool:
+ try:
+ output = subprocess.run([DSD, "--version"], capture_output=True, text=True, check=True)
+ version = output.stdout.strip().split(" ")[-1]
+ if not version.startswith("v"):
+ version = "v" + version
+
+ # If it's not the correct version, Ninja will download it and then rerun this script
+ return version == DSD_VERSION
+ except subprocess.CalledProcessError:
+ return False
+ except FileNotFoundError:
+ return False
def main():
- game_version: str = args.version
- game_config = config_path / game_version
- if not game_config.is_dir():
- print(f"Version '{game_version}' not recognized")
- return
+ if platform is None: return
+
+ delinks_json = None
+ if can_run_dsd():
+ out = subprocess.run([
+ DSD,
+ "--force-color",
+ "json",
+ "delinks",
+ "--config-path", config_path / args.version / "arm9" / "config.yaml"
+ ], capture_output=True, text=True, check=True)
+ delinks_json = json.loads(out.stdout)
+
+ project = Project(args.version, platform=platform, delinks_json=delinks_json)
+
with build_ninja_path.open("w") as file:
n = ninja_syntax.Writer(file)
+ n.rule(
+ name="download_tool",
+ command=f'{PYTHON} tools/download_tool.py $tool $tag --path $path'
+ )
+ n.newline()
+
if arm7_bios_path.is_file():
n.variable("arm7_bios_flag", f"--arm7-bios {arm7_bios_path.relative_to(root_path)}")
else:
@@ -120,20 +248,26 @@ def main():
n.rule(
name="extract",
- command="./dsd rom extract --rom $in --output-path $output_path $arm7_bios_flag"
+ command=f"{DSD} {DSD_BASE_FLAGS} rom extract --rom $in --output-path $output_path $arm7_bios_flag"
)
n.newline()
n.rule(
name="delink",
- command="./dsd delink --config-path $config_path"
+ command=f"{DSD} {DSD_BASE_FLAGS} delink --config-path $config_path"
+ )
+ n.newline()
+
+ n.rule(
+ name="disassemble",
+ command=f"{DSD} {DSD_BASE_FLAGS} dis --config-path $config_path --asm-path $output_path --ual"
)
n.newline()
# -MMD excludes all includes instead of just system includes for some reason, so use -MD instead.
- mwcc_cmd = f'{WINE} "{mwcc_path}/mwccarm.exe" {CC_FLAGS} {CC_INCLUDES} $cc_flags -d $game_version -MD -c $in -o $basedir'
- mwcc_implicit = []
- if system != "windows":
+ mwcc_cmd = f'{WINE} "{CC}" {CC_FLAGS} {CC_INCLUDES} $cc_flags -d $game_version -MD -c $in -o $basedir'
+ mwcc_implicit = [CC]
+ if platform.system != "windows":
transform_dep = "tools/transform_dep.py"
mwcc_cmd += f" && $python {transform_dep} $basefile.d $basefile.d"
mwcc_implicit.append(transform_dep)
@@ -146,31 +280,37 @@ def main():
n.rule(
name="lcf",
- command="./dsd lcf -c $config_path --lcf-file $lcf_file --objects-file $objects_file"
+ command=f"{DSD} {DSD_BASE_FLAGS} lcf -c $config_path"
)
n.newline()
n.rule(
name="mwld",
- command=f'{WINE} "{mwcc_path}/mwldarm.exe" {LD_FLAGS} @$objects_file $lcf_file -o $out'
+ command=f'{WINE} "{LD}" {LD_FLAGS} $extra_ld_flags $in -o $out'
)
n.newline()
n.rule(
name="rom_config",
- command="./dsd rom config --elf $in --config $config_path"
+ command=f"{DSD} {DSD_BASE_FLAGS} rom config --elf $in --config $config_path"
)
n.newline()
n.rule(
name="rom_build",
- command="./dsd rom build --config $in --rom $out $arm7_bios_flag"
+ command=f"{DSD} {DSD_BASE_FLAGS} rom build --config $in --rom $out $arm7_bios_flag"
)
n.newline()
n.rule(
name="objdiff",
- command=f"./dsd objdiff --config-path $config_path {DSD_OBJDIFF_ARGS}"
+ command=f"{DSD} {DSD_BASE_FLAGS} objdiff --config-path $config_path {DSD_OBJDIFF_ARGS}"
+ )
+ n.newline()
+
+ n.rule(
+ name="objdiff_report",
+ command=f"{OBJDIFF} report generate -o $out"
)
n.newline()
@@ -181,69 +321,170 @@ def main():
n.newline()
n.rule(
+ name="check_modules",
+ command=f"{DSD} {DSD_BASE_FLAGS} check modules --config-path $config_path --fail"
+ )
+ n.newline()
+
+ n.rule(
+ name="check_symbols",
+ command=f"{DSD} {DSD_BASE_FLAGS} check symbols --config-path $config_path --elf-path $elf_path --fail --max-lines 20"
+ )
+ n.newline()
+
+ n.rule(
+ name="apply",
+ command=f"{DSD} {DSD_BASE_FLAGS} apply --config-path $config_path --elf-path $elf_path"
+ )
+ n.newline()
+
+ n.rule(
name="sha1",
command=f"{PYTHON} tools/sha1.py $in -c $sha1_file"
)
n.newline()
- game_build = build_path / game_version
- game_extract = extract_path / game_version
+ configure_cmdline = subprocess.list2cmdline(sys.argv[1:])
+ n.rule(
+ name="configure",
+ command=f"{PYTHON} tools/configure.py {configure_cmdline}",
+ generator=True
+ )
+ n.newline()
+
+ add_download_tool_builds(n, project)
+ add_extract_build(n, project)
+ add_delink_and_lcf_builds(n, project)
+ add_disassemble_builds(n, project)
+ add_mwcc_builds(n, project, mwcc_implicit)
+ add_mwld_and_rom_builds(n, project)
+ add_check_builds(n, project)
+ add_objdiff_builds(n, project)
+ add_configure_build(n, project)
+ add_apply_build(n, project)
+
+ n.default(["objdiff", "check", "sha1"])
- add_extract_build(n, game_extract, game_version)
- add_delink_and_lcf_builds(n, game_config, game_build, game_extract)
- add_mwcc_builds(n, game_version, game_build, mwcc_implicit)
- add_mwld_and_rom_builds(n, game_build, game_config, game_version)
+def add_download_tool_builds(n: ninja_syntax.Writer, project: Project):
+ if args.dsd is None:
+ n.build(
+ rule="download_tool",
+ outputs=DSD,
+ variables={
+ "tool": "dsd",
+ "tag": DSD_VERSION,
+ "path": DSD,
+ },
+ )
+ n.newline()
-def add_extract_build(n: ninja_syntax.Writer, game_extract: Path, game_version: str):
- rom_path = extract_path / f'baserom_{GAME}_{game_version}.nds'
- rom_config = game_extract / 'config.yaml'
n.build(
- inputs=str(rom_path),
- rule="extract",
- outputs=str(rom_config),
+ rule="download_tool",
+ outputs=OBJDIFF,
variables={
- "output_path": str(game_extract)
+ "tool": "objdiff",
+ "tag": OBJDIFF_VERSION,
+ "path": OBJDIFF,
}
)
n.newline()
+ if args.compiler is None:
+ n.build(
+ rule="download_tool",
+ outputs=[CC, LD],
+ variables={
+ "tool": "mwccarm",
+ "tag": "latest",
+ "path": str(tools_path),
+ },
+ )
+ n.newline()
+
+ if project.platform.system != "windows" and WINE == DEFAULT_WIBO_PATH:
+ n.build(
+ rule="download_tool",
+ outputs=WINE,
+ variables={
+ "tool": "wibo",
+ "tag": WIBO_VERSION,
+ "path": WINE,
+ },
+ )
+ n.newline()
+
+
+def add_extract_build(n: ninja_syntax.Writer, project: Project):
+ if not args.no_extract:
+ n.build(
+ inputs=str(project.baserom()),
+ implicit=DSD,
+ rule="extract",
+ outputs=str(project.baserom_config()),
+ variables={
+ "output_path": str(project.game_extract)
+ }
+ )
+ n.newline()
+
+
+def add_mwld_and_rom_builds(n: ninja_syntax.Writer, project: Project):
+ n.comment("Link each module separately")
+ for module in project.modules():
+ lcf_file = module['lcf_file']
+ objects_to_link = [file['object_to_link'] for file in module['files']]
+ elf_file = module['elf_file']
+ n.build(
+ inputs=objects_to_link + [lcf_file],
+ implicit=LD,
+ rule="mwld",
+ outputs=elf_file,
+ variables={
+ 'extra_ld_flags': MODULE_LD_FLAGS,
+ }
+ )
+ n.newline()
+
+ n.comment("Link all modules together")
+ module_elf_files = [module['elf_file'] for module in project.modules()]
+ elf_file = str(project.arm9_o())
+ lcf_file = project.arm9_lcf_file()
+ if len(module_elf_files) > 0:
+ n.build(
+ inputs=module_elf_files + [lcf_file],
+ implicit=LD,
+ rule="mwld",
+ outputs=elf_file,
+ variables={
+ 'extra_ld_flags': ARM9_LD_FLAGS,
+ }
+ )
+ n.newline()
-def add_mwld_and_rom_builds(n: ninja_syntax.Writer, game_build: Path, game_config: Path, game_version: str):
- source_object_files = [
- str(game_build / source_file.with_suffix(".o"))
- for source_file in get_c_cpp_files([src_path, libs_path])
- ]
- lcf_file = str(game_build / "linker_script.lcf")
- objects_file = str(game_build / "objects.txt")
- delink_file = str(game_build / "delinks" / "delink.yaml")
- elf_file = str(game_build / "arm9.o")
n.build(
- inputs=source_object_files + [lcf_file, objects_file, delink_file],
- rule="mwld",
- outputs=elf_file,
- variables={
- "target_dir": game_build,
- "objects_file": objects_file,
- "lcf_file": lcf_file,
- }
+ inputs=elf_file,
+ rule="phony",
+ outputs="arm9",
)
n.newline()
- rom_config_file = str(game_build / "build" / "rom_config.yaml")
+ rom_config_file = str(project.build_rom_config())
n.build(
inputs=elf_file,
+ implicit=DSD,
rule="rom_config",
outputs=rom_config_file,
variables={
- "config_path": game_config / "arm9" / "config.yaml",
+ "config_path": str(project.arm9_config_yaml()),
}
)
n.newline()
- rom_file = f"{GAME}_{game_version}.nds"
+ rom_file = project.build_rom()
n.build(
- inputs=[rom_config_file],
+ inputs=rom_config_file,
+ implicit=DSD,
rule="rom_build",
outputs=rom_file,
)
@@ -267,28 +508,28 @@ def add_mwld_and_rom_builds(n: ninja_syntax.Writer, game_build: Path, game_confi
n.newline()
-def add_mwcc_builds(n: ninja_syntax.Writer, game_version: str, game_build: Path, mwcc_implicit: list[Path]):
+def add_mwcc_builds(n: ninja_syntax.Writer, project: Project, mwcc_implicit: list[str]):
for source_file in get_c_cpp_files([src_path, libs_path]):
- src_obj_path = game_build / source_file
- cc_flags = []
+ src_obj_path = project.game_build / source_file
+ cc_flags: list[str] = []
if is_cpp(source_file): cc_flags.append("-lang=c++")
elif is_c(source_file): cc_flags.append("-lang=c")
n.build(
inputs=str(source_file),
+ implicit=mwcc_implicit,
rule="mwcc",
outputs=str(src_obj_path.with_suffix(".o")),
variables={
- "game_version": game_version,
+ "game_version": project.game_version,
"cc_flags": " ".join(cc_flags),
"basedir": os.path.dirname(src_obj_path),
"basefile": str(src_obj_path.with_suffix("")),
},
- implicit=mwcc_implicit,
)
n.newline()
extension = source_file.suffix
- ctx_file = str(game_build / source_file.with_suffix(f".ctx{extension}"))
+ ctx_file = str(project.game_build / source_file.with_suffix(f".ctx{extension}"))
n.build(
inputs=str(source_file),
rule="m2ctx",
@@ -306,58 +547,103 @@ def get_c_cpp_files(dirs: list[Path]):
yield root / file
-def is_cpp(name: str):
+def is_cpp(name: str | Path):
return Path(name).suffix in [".cpp"]
-def is_c(name: str):
+def is_c(name: str | Path):
return Path(name).suffix in [".c"]
-def add_delink_and_lcf_builds(n: ninja_syntax.Writer, game_config: Path, game_build: Path, game_extract: Path):
- n.comment("Delink ELF binaries when any delinks.txt file is modified")
- delinks_files = get_config_files(game_config, "delinks.txt")
- relocs_files = get_config_files(game_config, "relocs.txt")
- symbols_files = get_config_files(game_config, "symbols.txt")
- rom_config = str(game_extract / 'config.yaml')
- delinks_path = game_build / "delinks"
+def add_delink_and_lcf_builds(n: ninja_syntax.Writer, project: Project):
+ rom_config = str(project.baserom_config())
+ delink_files = project.delink_files()
+ if len(delink_files) > 0:
+ n.comment("Delink ELF binaries when any delinks.txt file is modified")
+ n.build(
+ inputs=project.dsd_configs() + [rom_config],
+ implicit=DSD,
+ rule="delink",
+ outputs=delink_files,
+ variables={
+ "config_path": str(project.arm9_config_yaml()),
+ }
+ )
+ n.newline()
+
+ n.build(
+ inputs=delink_files,
+ rule="phony",
+ outputs="delink"
+ )
+ n.newline()
+
+ lcf_files = project.module_lcf_files() + [project.arm9_lcf_file()]
+ if len(lcf_files) > 1:
+ n.build(
+ inputs=project.delinks_files + [str(rom_config)],
+ implicit=DSD,
+ rule="lcf",
+ outputs=lcf_files,
+ variables={
+ "config_path": str(project.arm9_config_yaml()),
+ }
+ )
+ n.newline()
+
+
+def add_disassemble_builds(n: ninja_syntax.Writer, project: Project):
n.build(
- inputs=delinks_files + relocs_files + symbols_files + [rom_config],
- rule="delink",
- outputs=str(delinks_path / "delink.yaml"),
+ inputs=project.dsd_configs(),
+ implicit=DSD,
+ rule="disassemble",
+ outputs="dis",
variables={
- "config_path": game_config / "arm9" / "config.yaml",
+ "config_path": str(project.arm9_config_yaml()),
+ "output_path": str(project.arm9_disassembly_dir()),
}
)
n.newline()
+
+def add_check_builds(n: ninja_syntax.Writer, project: Project):
n.build(
- inputs=str(delinks_path / "delink.yaml"),
- rule="phony",
- outputs="delink"
+ inputs=str(project.arm9_o()),
+ rule="check_modules",
+ outputs="check_modules",
+ variables={
+ "config_path": str(project.arm9_config_yaml()),
+ },
)
n.newline()
- lcf_file = game_build / "linker_script.lcf"
- objects_file = game_build / "objects.txt"
n.build(
- inputs=delinks_files + [str(rom_config)],
- rule="lcf",
- outputs=[str(lcf_file), str(objects_file)],
+ inputs=str(project.arm9_o()),
+ rule="check_symbols",
+ outputs="check_symbols",
variables={
- "config_path": game_config / "arm9" / "config.yaml",
- "lcf_file": lcf_file,
- "objects_file": objects_file,
- }
+ "config_path": str(project.arm9_config_yaml()),
+ "elf_path": str(project.arm9_o()),
+ },
)
n.newline()
n.build(
- inputs=delinks_files + relocs_files + symbols_files,
+ inputs=["check_modules", "check_symbols"],
+ rule="phony",
+ outputs="check",
+ )
+ n.newline()
+
+
+def add_objdiff_builds(n: ninja_syntax.Writer, project: Project):
+ n.build(
+ inputs=project.dsd_configs(),
+ implicit=DSD,
rule="objdiff",
outputs="objdiff.json",
variables={
- "config_path": game_config / "arm9" / "config.yaml",
+ "config_path": str(project.arm9_config_yaml()),
}
)
n.newline()
@@ -369,8 +655,52 @@ def add_delink_and_lcf_builds(n: ninja_syntax.Writer, game_config: Path, game_bu
)
n.newline()
+ delink_files = [file['delink_file'] for module in project.modules() for file in module['files']]
+ n.build(
+ inputs=["objdiff.json"],
+ implicit=[OBJDIFF] + delink_files + project.source_object_files(),
+ rule="objdiff_report",
+ outputs=str(project.objdiff_report()),
+ )
+ n.newline()
+
+ n.build(
+ inputs=str(project.objdiff_report()),
+ rule="phony",
+ outputs="report",
+ )
+ n.newline()
+
+
+def add_configure_build(n: ninja_syntax.Writer, project: Project):
+ this_file = str(Path(__file__).resolve())
+ n.build(
+ outputs="build.ninja",
+ rule="configure",
+ implicit=[
+ this_file,
+ # Require dsd to exist when rerunning configure.py
+ DSD,
+ *project.dsd_configs(),
+ ]
+ )
+
+
+def add_apply_build(n: ninja_syntax.Writer, project: Project):
+ n.build(
+ inputs=project.dsd_configs() + [str(project.arm9_o())],
+ implicit=DSD,
+ rule="apply",
+ outputs="apply",
+ variables={
+ "config_path": str(project.arm9_config_yaml()),
+ "elf_path": str(project.arm9_o()),
+ }
+ )
+ n.newline()
+
-def get_config_files(game_config: Path, name: str):
+def get_config_files(game_config: Path, name: str) -> list[str]:
return [
f"{root}/{file}"
for root, _, files in os.walk(game_config)
diff --git a/tools/download_tool.py b/tools/download_tool.py
new file mode 100644
index 00000000..755acd8f
--- /dev/null
+++ b/tools/download_tool.py
@@ -0,0 +1,53 @@
+import argparse
+from pathlib import Path
+from get_platform import get_platform
+import zipfile
+import io
+import requests
+import stat
+
+root_path = Path(__file__).parent.parent
+
+
+platform = get_platform()
+if platform is None:
+ exit(1)
+
+parser = argparse.ArgumentParser()
+parser.add_argument("tool")
+parser.add_argument("tag")
+parser.add_argument("--path", type=Path, required=True)
+args = parser.parse_args()
+
+
+def dsd_url(tag: str) -> str:
+ return f'https://github.com/AetiasHax/ds-decomp/releases/download/{tag}/dsd-{platform.system}-{platform.machine}{platform.exe}'
+
+def mwccarm_url(tag: str) -> str:
+ return 'http://decomp.aetias.com/files/mwccarm.zip'
+
+def wibo_url(tag: str) -> str:
+ return f'https://github.com/decompals/wibo/releases/download/{tag}/wibo'
+
+def objdiff_url(tag: str) -> str:
+ return f'https://github.com/encounter/objdiff/releases/download/{tag}/objdiff-cli-{platform.system}-{platform.machine}{platform.exe}'
+
+TOOLS = {
+ "dsd": dsd_url,
+ "mwccarm": mwccarm_url,
+ "wibo": wibo_url,
+ "objdiff": objdiff_url,
+}
+
+
+download_url = TOOLS[args.tool](args.tag)
+print(f'\nDownloading {args.tool} {args.tag}...')
+response = requests.get(download_url)
+if download_url.endswith('.zip'):
+ zip_file = zipfile.ZipFile(io.BytesIO(response.content))
+ zip_file.extractall(args.path)
+else:
+ out_path: Path = args.path
+ with out_path.open('wb') as f:
+ f.write(response.content)
+ out_path.chmod(out_path.stat().st_mode | stat.S_IEXEC)
diff --git a/tools/get_platform.py b/tools/get_platform.py
new file mode 100644
index 00000000..a4fe40d8
--- /dev/null
+++ b/tools/get_platform.py
@@ -0,0 +1,31 @@
+import platform
+
+
+class Platform:
+ def __init__(self, *, system: str, machine: str, exe: str):
+ self.system = system
+ '''Name of operating system: "windows" or "linux"'''
+ self.machine = machine
+ '''Name of machine architecture: "x86_64"'''
+ self.exe = exe
+ '''Executable file extension: ".exe" for Windows, "" otherwise'''
+
+
+def get_platform() -> Platform | None:
+ exe = ""
+ system = platform.system()
+ if system == "Windows":
+ system = "windows"
+ exe = ".exe"
+ elif system == "Linux":
+ system = "linux"
+ else:
+ print(f"Unknown system '{system}'")
+ return None
+ match platform.machine().lower():
+ case "amd64" | "x86_64": machine = "x86_64"
+ case machine:
+ print(f"Unknown machine: {machine}")
+ return None
+
+ return Platform(system=system, machine=machine, exe=exe)
diff --git a/tools/m2ctx.py b/tools/m2ctx.py
index 7eef9bd7..9684424f 100755
--- a/tools/m2ctx.py
+++ b/tools/m2ctx.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
import sys
import pyperclip
diff --git a/tools/ninja_syntax.py b/tools/ninja_syntax.py
index 2aa8456e..e0716e8a 100644
--- a/tools/ninja_syntax.py
+++ b/tools/ninja_syntax.py
@@ -1,5 +1,3 @@
-#!/usr/bin/python
-
# Copyright 2011 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/tools/setup.py b/tools/setup.py
deleted file mode 100644
index f81ca25d..00000000
--- a/tools/setup.py
+++ /dev/null
@@ -1,54 +0,0 @@
-import requests
-import zipfile
-import io
-from pathlib import Path
-import platform
-import stat
-
-DSD_VERSION = 'v0.4.0'
-WIBO_VERSION = '0.6.16'
-
-
-tools_path = Path(__file__).parent
-root_path = tools_path.parent
-
-
-EXE = ""
-system = platform.system()
-if system == "Windows":
- system = "windows"
- EXE = ".exe"
-elif system == "Linux":
- system = "linux"
-else:
- print(f"Unknown system '{system}'")
- exit(1)
-match platform.machine().lower():
- case 'amd64' | 'x86_64': machine = 'x86_64'
- case machine:
- print(f'Unknown machine: {machine}')
- exit(1)
-
-
-print('\nInstalling dsd...')
-response = requests.get(f'https://github.com/AetiasHax/ds-decomp/releases/download/{DSD_VERSION}/dsd-{system}-{machine}{EXE}')
-dsd_path = root_path / f'dsd{EXE}'
-with open(dsd_path, 'wb') as f:
- f.write(response.content)
-dsd_path.chmod(dsd_path.stat().st_mode | stat.S_IEXEC)
-
-
-print('\nInstalling toolchain...')
-response = requests.get('http://decomp.aetias.com/files/mwccarm.zip')
-zip_file = zipfile.ZipFile(io.BytesIO(response.content))
-zip_file.extractall(tools_path)
-
-
-if system == "linux":
- print('\nInstalling wibo...')
- response = requests.get(f'https://github.com/decompals/wibo/releases/download/{WIBO_VERSION}/wibo')
- wibo_path = root_path / 'wibo'
- with open(wibo_path, 'wb') as f:
- f.write(response.content)
- wibo_path.chmod(wibo_path.stat().st_mode | stat.S_IEXEC)
-
diff --git a/tools/sha1.py b/tools/sha1.py
index 8cd6edd8..fd1cbda8 100644..100755
--- a/tools/sha1.py
+++ b/tools/sha1.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python
+#!/usr/bin/env python3
from pathlib import Path
import argparse