Run dsd delink automatically on symbols/delinks/relocs update (#145)

* automatically run delink when config is changed

* fix issue with -lang flag

* add back the intended defaults

* raise error on failed run

* remove ninja objdiff running ninja delink

* delink all modules I guess (fixes an issue)
This commit is contained in:
Yanis
2026-08-24 17:56:17 +02:00
committed by GitHub
parent 721bb0efd8
commit 05612891ad
2 changed files with 121 additions and 14 deletions
+109
View File
@@ -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()
+12 -14
View File
@@ -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"])