Add link_order_callback feature

See comments in configure.py for feature documentation.

Resolves #6
This commit is contained in:
Luke Street 2024-12-30 17:49:45 -07:00
parent ca32d3f429
commit 57f5777025
2 changed files with 95 additions and 19 deletions

View File

@ -277,6 +277,24 @@ config.libs = [
}, },
] ]
# Optional callback to adjust link order. This can be used to add, remove, or reorder objects.
# This is called once per module, with the module ID and the current link order.
#
# For example, this adds "dummy.c" to the end of the DOL link order if configured with --non-matching.
# "dummy.c" *must* be configured as a Matching (or Equivalent) object in order to be linked.
def link_order_callback(module_id: int, objects: List[str]) -> List[str]:
# Don't modify the link order for matching builds
if not config.non_matching:
return objects
if module_id == 0: # DOL
return objects + ["dummy.c"]
return objects
# Uncomment to enable the link order callback.
# config.link_order_callback = link_order_callback
# Optional extra categories for progress tracking # Optional extra categories for progress tracking
# Adjust as desired for your project # Adjust as desired for your project
config.progress_categories = [ config.progress_categories = [

View File

@ -17,7 +17,20 @@ import os
import platform import platform
import sys import sys
from pathlib import Path from pathlib import Path
from typing import IO, Any, Dict, Iterable, List, Optional, Set, Tuple, Union, cast from typing import (
Any,
Callable,
cast,
Dict,
IO,
Iterable,
List,
Optional,
Set,
Tuple,
TypedDict,
Union,
)
from . import ninja_syntax from . import ninja_syntax
from .ninja_syntax import serialize_path from .ninja_syntax import serialize_path
@ -179,6 +192,9 @@ class ProjectConfig:
self.scratch_preset_id: Optional[int] = ( self.scratch_preset_id: Optional[int] = (
None # Default decomp.me preset ID for scratches None # Default decomp.me preset ID for scratches
) )
self.link_order_callback: Optional[Callable[[int, List[str]], List[str]]] = (
None # Callback to add/remove/reorder units within a module
)
# Progress output, progress.json and report.json config # Progress output, progress.json and report.json config
self.progress = True # Enable report.json generation and CLI progress output self.progress = True # Enable report.json generation and CLI progress output
@ -294,10 +310,38 @@ def make_flags_str(flags: Optional[List[str]]) -> str:
return " ".join(flags) return " ".join(flags)
# Unit configuration
class BuildConfigUnit(TypedDict):
object: Optional[str]
name: str
autogenerated: bool
# Module configuration
class BuildConfigModule(TypedDict):
name: str
module_id: int
ldscript: str
entry: str
units: List[BuildConfigUnit]
# Module link configuration
class BuildConfigLink(TypedDict):
modules: List[str]
# Build configuration generated by decomp-toolkit
class BuildConfig(BuildConfigModule):
version: str
modules: List[BuildConfigModule]
links: List[BuildConfigLink]
# Load decomp-toolkit generated config.json # Load decomp-toolkit generated config.json
def load_build_config( def load_build_config(
config: ProjectConfig, build_config_path: Path config: ProjectConfig, build_config_path: Path
) -> Optional[Dict[str, Any]]: ) -> Optional[BuildConfig]:
if not build_config_path.is_file(): if not build_config_path.is_file():
return None return None
@ -305,7 +349,7 @@ def load_build_config(
return tuple(map(int, (v.split(".")))) return tuple(map(int, (v.split("."))))
f = open(build_config_path, "r", encoding="utf-8") f = open(build_config_path, "r", encoding="utf-8")
build_config: Dict[str, Any] = json.load(f) build_config: BuildConfig = json.load(f)
config_version = build_config.get("version") config_version = build_config.get("version")
if config_version is None: if config_version is None:
print("Invalid config.json, regenerating...") print("Invalid config.json, regenerating...")
@ -321,6 +365,24 @@ def load_build_config(
return None return None
f.close() f.close()
# Apply link order callback
if config.link_order_callback:
modules: List[BuildConfigModule] = [build_config, *build_config["modules"]]
for module in modules:
unit_names = list(map(lambda u: u["name"], module["units"]))
unit_names = config.link_order_callback(module["module_id"], unit_names)
units: List[BuildConfigUnit] = []
for unit_name in unit_names:
units.append(
# Find existing unit or create a new one
next(
(u for u in module["units"] if u["name"] == unit_name),
{"object": None, "name": unit_name, "autogenerated": False},
)
)
module["units"] = units
return build_config return build_config
@ -338,7 +400,7 @@ def generate_build(config: ProjectConfig) -> None:
def generate_build_ninja( def generate_build_ninja(
config: ProjectConfig, config: ProjectConfig,
objects: Dict[str, Object], objects: Dict[str, Object],
build_config: Optional[Dict[str, Any]], build_config: Optional[BuildConfig],
) -> None: ) -> None:
out = io.StringIO() out = io.StringIO()
n = ninja_syntax.Writer(out) n = ninja_syntax.Writer(out)
@ -699,9 +761,9 @@ def generate_build_ninja(
return path.parent / (path.name + ".MAP") return path.parent / (path.name + ".MAP")
class LinkStep: class LinkStep:
def __init__(self, config: Dict[str, Any]) -> None: def __init__(self, config: BuildConfigModule) -> None:
self.name: str = config["name"] self.name = config["name"]
self.module_id: int = config["module_id"] self.module_id = config["module_id"]
self.ldscript: Optional[Path] = Path(config["ldscript"]) self.ldscript: Optional[Path] = Path(config["ldscript"])
self.entry = config["entry"] self.entry = config["entry"]
self.inputs: List[str] = [] self.inputs: List[str] = []
@ -907,13 +969,14 @@ def generate_build_ninja(
return obj_path return obj_path
def add_unit(build_obj, link_step: LinkStep): def add_unit(build_obj: BuildConfigUnit, link_step: LinkStep):
obj_path, obj_name = build_obj["object"], build_obj["name"] obj_path, obj_name = build_obj["object"], build_obj["name"]
obj = objects.get(obj_name) obj = objects.get(obj_name)
if obj is None: if obj is None:
if config.warn_missing_config and not build_obj["autogenerated"]: if config.warn_missing_config and not build_obj["autogenerated"]:
print(f"Missing configuration for {obj_name}") print(f"Missing configuration for {obj_name}")
link_step.add(obj_path) if obj_path is not None:
link_step.add(Path(obj_path))
return return
link_built_obj = obj.completed link_built_obj = obj.completed
@ -942,12 +1005,7 @@ def generate_build_ninja(
link_step.add(built_obj_path) link_step.add(built_obj_path)
elif obj_path is not None: elif obj_path is not None:
# Use the original (extracted) object # Use the original (extracted) object
link_step.add(obj_path) link_step.add(Path(obj_path))
else:
lib_name = obj.options["lib"]
sys.exit(
f"Missing object for {obj_name}: {obj.src_path} {lib_name} {obj}"
)
# Add DOL link step # Add DOL link step
link_step = LinkStep(build_config) link_step = LinkStep(build_config)
@ -1268,7 +1326,7 @@ def generate_build_ninja(
def generate_objdiff_config( def generate_objdiff_config(
config: ProjectConfig, config: ProjectConfig,
objects: Dict[str, Object], objects: Dict[str, Object],
build_config: Optional[Dict[str, Any]], build_config: Optional[BuildConfig],
) -> None: ) -> None:
if build_config is None: if build_config is None:
return return
@ -1333,7 +1391,7 @@ def generate_objdiff_config(
} }
def add_unit( def add_unit(
build_obj: Dict[str, Any], module_name: str, progress_categories: List[str] build_obj: BuildConfigUnit, module_name: str, progress_categories: List[str]
) -> None: ) -> None:
obj_path, obj_name = build_obj["object"], build_obj["name"] obj_path, obj_name = build_obj["object"], build_obj["name"]
base_object = Path(obj_name).with_suffix("") base_object = Path(obj_name).with_suffix("")
@ -1481,7 +1539,7 @@ def generate_objdiff_config(
def generate_compile_commands( def generate_compile_commands(
config: ProjectConfig, config: ProjectConfig,
objects: Dict[str, Object], objects: Dict[str, Object],
build_config: Optional[Dict[str, Any]], build_config: Optional[BuildConfig],
) -> None: ) -> None:
if build_config is None or not config.generate_compile_commands: if build_config is None or not config.generate_compile_commands:
return return
@ -1570,7 +1628,7 @@ def generate_compile_commands(
clangd_config = [] clangd_config = []
def add_unit(build_obj: Dict[str, Any]) -> None: def add_unit(build_obj: BuildConfigUnit) -> None:
obj = objects.get(build_obj["name"]) obj = objects.get(build_obj["name"])
if obj is None: if obj is None:
return return