diff --git a/tools/delink.py b/tools/delink.py new file mode 100755 index 00000000..94f48090 --- /dev/null +++ b/tools/delink.py @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 + +# this script attempts to run `dsd delink` for modules that were updated +# `python delink.py dsd_path [optional version]` + +import glob +import json +import subprocess +import sys + +from pathlib import Path + + +CONFIG_DIR = Path("config").resolve() +DSD_PATH = Path("./dsd").resolve() # only used when debugging + +TIMESTAMP_FILE = Path("build").resolve() / "delink_timestamps.json" +TIMESTAMP_FILE.parent.mkdir(exist_ok=True) + +DEBUG = False + + +def version_exists(version: str) -> bool: + return glob.glob(str(Path("extract") / f"baserom_st_{version}.nds")) != [] + + +def main(): + # get supported versions + GAME_VERSIONS = [ + "eur", + "eur1", + "usa", + "jp", + ] + + versions = [version for version in GAME_VERSIONS if version_exists(version)] + + # or if we want a specific one + if len(sys.argv) > 2: + versions = [sys.argv[2]] + + # should we write TIMESTAMP_FILE + do_write = False + + # initialize dicts + file_paths_map: dict[str, list[Path]] = {} + current_timestamp_map: dict[str, dict[str, float]] = {} + for version in versions: + file_paths_map[version] = [] + current_timestamp_map[version] = {} + + # fetch config paths + file_kinds = ["symbols", "relocs", "delinks"] + for version in versions: + for file_path in (CONFIG_DIR / version).rglob("**/*.txt"): + for file_kind in file_kinds: + if file_path.stem != file_kind: + continue + + file_paths_map[version].append(file_path) + + # fetch current timestamps + for version, file_paths in file_paths_map.items(): + for path in file_paths: + current_timestamp_map[version][str(path.relative_to(Path.cwd()))] = path.stat().st_mtime + + if TIMESTAMP_FILE.exists(): + # if the cache exists + + # load the cache + saved_timestamp_map: dict[str, dict[str, float]] = json.loads(TIMESTAMP_FILE.read_text()) + + for version, saved_map in saved_timestamp_map.items(): + do_delink = False + + for saved_path_str, saved_timestamp in saved_map.items(): + timestamp = current_timestamp_map[version].get(saved_path_str) + + # if current timestamp not found or current timestamp is different than the saved timestamp + # we make it delink when not found because it might mean we added a new version + # and haven't cached it yet + if timestamp is None or timestamp != saved_timestamp: + do_delink = True + break + + if do_delink: + dsd_p = DSD_PATH if DEBUG else sys.argv[1] + + # ideally we'd only delink the necessary modules + # however because of how dsd works we can't do that otherwise modules with + # external calls won't be delinked again + command = [str(dsd_p), "delink", "--config-path", str(CONFIG_DIR / version / "arm9" / "config.yaml")] + subprocess.run(command, check=True) + + if DEBUG: + print(f"[DEBUG]: execution completed for '{' '.join(command)}'") + + do_write = True + else: + # if the cache doesn't exist simply create it + do_write = True + + if do_write: + with TIMESTAMP_FILE.open("w") as file: + json.dump(current_timestamp_map, file, indent=4) + + +if __name__ == "__main__": + main() diff --git a/tools/project.py b/tools/project.py index 9c541451..445cf7ab 100644 --- a/tools/project.py +++ b/tools/project.py @@ -154,6 +154,7 @@ class ProjectConfig: self.cc_path = (self.mwcc_path / "mwccarm.exe").resolve() self.as_path = (self.mwcc_path / "mwasmarm.exe").resolve() self.ld_path = (self.mwcc_path / "mwldarm.exe").resolve() + self.delinkpy_path = (self.root_path / "tools" / "delink.py").resolve() self.python_path = Path(sys.executable) self.dsd_base_flags = [ @@ -246,11 +247,11 @@ class ProjectConfig: return self.mwcc_tag @property - def current_path(self): + def current_path(self) -> Path: return Path(__name__) @property - def root_path(self): + def root_path(self) -> Path: return self.current_path.parent @property @@ -290,7 +291,7 @@ class ProjectConfig: return self.root_path / "tools" @property - def mwcc_path(self): + def mwcc_path(self) -> Path: return self.mwcc_root / self.mwcc_version @property @@ -422,7 +423,7 @@ def add_download_tool_builds(cfg: ProjectConfig, n: ninja_syntax.Writer, args: A variables={ "tool": "objdiff", "tag": cfg.objdiff_version, - "path": cfg.objdiff_path, + "path": str(cfg.objdiff_path), } ) n.newline() @@ -609,11 +610,10 @@ def add_mwcc_build(cfg: ProjectConfig, version: str, n: ninja_syntax.Writer, sou src_obj_path = cfg.get_game_build(version) / source_file cc_flags: list[str] = object.options["cflags"] or [] + object.options["extra_cflags"] or [] - if "-lang=c++" not in cc_flags or "-lang=c" not in cc_flags: - if is_cpp(source_file): - cc_flags.append("-lang=c++") - elif is_c(source_file): - cc_flags.append("-lang=c") + if "-lang=c++" not in cc_flags and is_cpp(source_file): + cc_flags.append("-lang=c++") + elif "-lang=c" not in cc_flags and is_c(source_file): + cc_flags.append("-lang=c") n.build( inputs=str(source_file), @@ -1152,7 +1152,7 @@ def process_project(cfg: ProjectConfig, args: Any): ]) n.rule( name="objdiff", - command=f"touch {cfg.dsd_path} && {cfg.dsd_path} {cfg.dsd_flags} objdiff --config-path $config_path --output-path $out_path {dsd_objdiff_args}" + command=f"{cfg.dsd_path} {cfg.dsd_flags} objdiff --config-path $config_path --output-path $out_path {dsd_objdiff_args}" ) n.newline() @@ -1195,7 +1195,7 @@ def process_project(cfg: ProjectConfig, args: Any): configure_cmdline = subprocess.list2cmdline(sys.argv[1:]) n.rule( name="configure", - command=f"{cfg.python_path} tools/configure.py {configure_cmdline}", + command=f"{cfg.python_path} tools/configure.py {configure_cmdline} && {cfg.python_path} {cfg.delinkpy_path} {cfg.dsd_path}", generator=True ) n.newline() @@ -1275,7 +1275,6 @@ def process_project(cfg: ProjectConfig, args: Any): rule="post_objdiff", implicit=[f"objdiff_{version}.json" for version in cfg.game_versions], outputs="objdiff", - order_only=cmds_map["delink"], ) n.newline() @@ -1298,7 +1297,6 @@ def process_project(cfg: ProjectConfig, args: Any): implicit=version_to_cmds[version], ) - # n.default(["format", "objdiff", *defaults]) - n.default(["objdiff", *defaults]) + n.default(["format", "objdiff", *defaults]) else: n.default(["download_tools"])