From 5b8fbc6cd5f96ee77ec4c4369e492371656f279b Mon Sep 17 00:00:00 2001 From: Luke Street Date: Wed, 4 Oct 2023 23:26:02 -0400 Subject: [PATCH] Initial commit --- .gitattributes | 13 + .gitignore | 11 + .vscode.example/c_cpp_properties.json | 22 + .vscode.example/settings.json | 26 + LICENSE | 21 + README.example.md | 114 +++ README.md | 65 ++ assets/dolphin-extract.png | Bin 0 -> 11843 bytes assets/objdiff.png | Bin 0 -> 66571 bytes config/GAMEID/build.sha1 | 2 + config/GAMEID/config.example.yml | 100 +++ config/GAMEID/config.yml | 12 + config/GAMEID/splits.txt | 1 + config/GAMEID/symbols.txt | 1 + configure.py | 233 ++++++ docs/comment_section.md | 106 +++ docs/common_bss.md | 69 ++ docs/dependencies.md | 33 + docs/getting_started.md | 124 ++++ docs/splits.md | 42 ++ docs/symbols.md | 35 + orig/GAMEID/.gitkeep | 0 tools/__init__.py | 0 tools/decompctx.py | 87 +++ tools/download_tool.py | 93 +++ tools/ninja_syntax.py | 223 ++++++ tools/project.py | 982 ++++++++++++++++++++++++++ tools/transform_dep.py | 84 +++ tools/upload_progress.py | 76 ++ 29 files changed, 2575 insertions(+) create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 .vscode.example/c_cpp_properties.json create mode 100644 .vscode.example/settings.json create mode 100644 LICENSE create mode 100644 README.example.md create mode 100644 README.md create mode 100644 assets/dolphin-extract.png create mode 100644 assets/objdiff.png create mode 100644 config/GAMEID/build.sha1 create mode 100644 config/GAMEID/config.example.yml create mode 100644 config/GAMEID/config.yml create mode 100644 config/GAMEID/splits.txt create mode 100644 config/GAMEID/symbols.txt create mode 100644 configure.py create mode 100644 docs/comment_section.md create mode 100644 docs/common_bss.md create mode 100644 docs/dependencies.md create mode 100644 docs/getting_started.md create mode 100644 docs/splits.md create mode 100644 docs/symbols.md create mode 100644 orig/GAMEID/.gitkeep create mode 100644 tools/__init__.py create mode 100644 tools/decompctx.py create mode 100644 tools/download_tool.py create mode 100644 tools/ninja_syntax.py create mode 100644 tools/project.py create mode 100644 tools/transform_dep.py create mode 100644 tools/upload_progress.py diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..9545db5 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,13 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Explicitly declare text files +*.py text + +# Enforce platform-specific encodings +*.bat text eol=crlf +*.sh text eol=lf +*.sha1 text eol=lf + +# DTK keeps these files with LF +config/**/*.txt text eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d37c144 --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +__pycache__ +.idea +.vscode +.ninja_* +*.exe +build +build.ninja +objdiff.json +orig/*/* +!orig/*/.gitkeep +/*.txt diff --git a/.vscode.example/c_cpp_properties.json b/.vscode.example/c_cpp_properties.json new file mode 100644 index 0000000..60e51b8 --- /dev/null +++ b/.vscode.example/c_cpp_properties.json @@ -0,0 +1,22 @@ +{ + "configurations": [ + { + "name": "Linux", + "includePath": [ + "${workspaceFolder}/include/**" + ], + "cStandard": "c99", + "cppStandard": "c++98", + "intelliSenseMode": "linux-clang-x86", + "compilerPath": "", + "configurationProvider": "ms-vscode.makefile-tools", + "browse": { + "path": [ + "${workspaceFolder}/include" + ], + "limitSymbolsToIncludedHeaders": true + } + } + ], + "version": 4 +} diff --git a/.vscode.example/settings.json b/.vscode.example/settings.json new file mode 100644 index 0000000..60e7e27 --- /dev/null +++ b/.vscode.example/settings.json @@ -0,0 +1,26 @@ +{ + "[c]": { + "files.encoding": "utf8", + "editor.defaultFormatter": "xaver.clang-format" + }, + "[cpp]": { + "files.encoding": "utf8", + "editor.defaultFormatter": "xaver.clang-format" + }, + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter" + }, + "files.insertFinalNewline": true, + "files.trimFinalNewlines": true, + "files.associations": { + "*.inc": "cpp" + }, + "search.useIgnoreFiles": false, + "search.exclude": { + "build/*/config.json": true, + "build/**/*.MAP": true, + "build.ninja": true, + ".ninja_*": true, + "objdiff.json": true + } +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..af8690a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Luke Street + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.example.md b/README.example.md new file mode 100644 index 0000000..af8f9a1 --- /dev/null +++ b/README.example.md @@ -0,0 +1,114 @@ +Some Game +[![Build Status]][actions] ![Progress] ![DOL Progress] ![RELs Progress] [![Discord Badge]][discord] +============= + + +[Build Status]: https://github.com/zeldaret/tww/actions/workflows/build.yml/badge.svg +[actions]: https://github.com/zeldaret/tww/actions/workflows/build.yml + +[Progress]: https://img.shields.io/endpoint?label=Code&url=https%3A%2F%2Fprogress.decomp.club%2Fdata%2Ftww%2FGZLE01%2Fall%2F%3Fmode%3Dshield%26measure%3Dcode + +[DOL Progress]: https://img.shields.io/endpoint?label=DOL&url=https%3A%2F%2Fprogress.decomp.club%2Fdata%2Ftww%2FGZLE01%2Fdol%2F%3Fmode%3Dshield%26measure%3Dcode + +[RELs Progress]: https://img.shields.io/endpoint?label=RELs&url=https%3A%2F%2Fprogress.decomp.club%2Fdata%2Ftww%2FGZLE01%2Fmodules%2F%3Fmode%3Dshield%26measure%3Dcode + +[Discord Badge]: https://img.shields.io/discord/727908905392275526?color=%237289DA&logo=discord&logoColor=%23FFFFFF +[discord]: https://discord.gg/hKx3FJJgrV + +A work-in-progress decompilation of Some Game. + +This repository does **not** contain any game assets or assembly whatsoever. An existing copy of the game is required. + +Supported versions: + +- `GAMEID`: Rev 0 (USA) + +Dependencies +============ + +Windows: +-------- + +On Windows, it's **highly recommended** to use native tooling. WSL or msys2 are **not** required. +When running under WSL, [objdiff](#diffing) is unable to get filesystem notifications for automatic rebuilds. + +- Install [Python](https://www.python.org/downloads/) and add it to `%PATH%`. + - Also available from the [Windows Store](https://apps.microsoft.com/store/detail/python-311/9NRWMJP3717K). +- Download [ninja](https://github.com/ninja-build/ninja/releases) and add it to `%PATH%`. + - Quick install via pip: `pip install ninja` + +macOS: +------ +- Install [ninja](https://github.com/ninja-build/ninja/wiki/Pre-built-Ninja-packages): + ``` + brew install ninja + ``` +- Install [wine-crossover](https://github.com/Gcenx/homebrew-wine): + ``` + brew install --cask --no-quarantine gcenx/wine/wine-crossover + ``` + +After OS upgrades, if macOS complains about `Wine Crossover.app` being unverified, you can unquarantine it using: +```sh +sudo xattr -rd com.apple.quarantine '/Applications/Wine Crossover.app' +``` + +Linux: +------ +- Install [ninja](https://github.com/ninja-build/ninja/wiki/Pre-built-Ninja-packages). +- For non-x86(_64) platforms: Install wine from your package manager. + - For x86(_64), [WiBo](https://github.com/decompals/WiBo), a minimal 32-bit Windows binary wrapper, will be automatically downloaded and used. + +Building +======== + +- Clone the repository: + ``` + git clone https://github.com/my/repo.git + ``` +- Using [Dolphin Emulator](https://dolphin-emu.org/), extract your game to `orig/GAMEID`. +![](assets/dolphin-extract.png) + - To save space, the only necessary files are the following. Any others can be deleted. + - `sys/main.dol` + - `files/rels/*.rel` +- Configure: + ``` + python configure.py + ``` + To use a version other than `GAMEID` (USA), specify it with `--version`. +- Build: + ``` + ninja + ``` + +Visual Studio Code +================== + +If desired, use the recommended Visual Studio Code settings by renaming the `.vscode.example` directory to `.vscode`. + +Diffing +======= + +Once the initial build succeeds, an `objdiff.json` should exist in the project root. + +Download the latest release from [encounter/objdiff](https://github.com/encounter/objdiff). Under project settings, set `Project directory`. The configuration should be loaded automatically. + +Select an object from the left sidebar to begin diffing. Changes to the project will rebuild automatically: changes to source files, headers, `configure.py`, `splits.txt` or `symbols.txt`. + +![](assets/objdiff.png) diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d77cac --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +decomp-toolkit Project Template +=============================== + +If starting a new GameCube / Wii decompilation project, this repository can be used as a scaffold. + +See [decomp-toolkit](https://github.com/encounter/decomp-toolkit) for background on the concept and more information on the tooling used. + +Documentation +------------- + +- [Dependencies](docs/dependencies.md) +- [Getting Started](docs/getting_started.md) +- [`symbols.txt`](docs/symbols.md) +- [`splits.txt`](docs/splits.md) + +General: +- [Common BSS](docs/common_bss.md) +- [`.comment` section](docs/comment_section.md) + +References +-------- + +- [Discord: GC/Wii Decompilation](https://discord.gg/hKx3FJJgrV) (Come to `#dtk` for help!) +- [objdiff](https://github.com/encounter/objdiff) (Local diffing tool) +- [decomp.me](https://decomp.me) (Collaborate on matches) +- [frogress](https://github.com/decompals/frogress) (Decompilation progress API) +- [wibo](https://github.com/decompals/wibo) (Minimal Win32 wrapper for Linux) +- [sjiswrap](https://github.com/encounter/sjiswrap) (UTF-8 to Shift JIS wrapper) + +Projects using this structure: +- [zeldaret/tww](https://github.com/zeldaret/tww) +- [PrimeDecomp/echoes](https://github.com/PrimeDecomp/echoes) +- [DarkRTA/rb3](https://github.com/DarkRTA/rb3) +- [InputEvelution/wp](https://github.com/InputEvelution/wp) +- [Rainchus/ttyd_dtk](https://github.com/Rainchus/ttyd_dtk) + +Features +-------- +- Few external dependencies: Just `python` for the generator and `ninja` for the build system. See [Dependencies](docs/dependencies.md). +- Simple configuration: Everything lives in `config.yml`, `symbols.txt`, and `splits.txt`. +- Multi-version support: Separate configurations for each game version, and a `configure.py --version` flag to switch between them. +- Feature-rich analyzer: Many time-consuming tasks are automated, allowing you to focus on the decompilation itself. See [Analyzer features](https://github.com/encounter/decomp-toolkit#analyzer-features). +- REL support: RELs each have their own `symbols.txt` and `splits.txt`, and will automatically be built and linked against the main binary. +- No manual assembly: decomp-toolkit handles splitting the DOL into relocatable objects based on the configuration. No game assets are committed to the repository. +- Progress calculation and upload script for [frogress](https://github.com/decompals/frogress). +- Integration with [objdiff](https://github.com/encounter/objdiff) for a diffing workflow. +- (TODO) CI workflow template for GitHub Actions. + +Project structure +----------------- + +- `configure.py` - Project configuration and generator script. +- `config/[GAMEID]` - Configuration files for each game version. +- `config/[GAMEID]/build.sha1` - SHA-1 hashes for each built artifact, for final verification. +- `build/` - Build artifacts generated by the the build process. Ignored by `.gitignore`. +- `orig/[GAMEID]` - Original game files, extracted from the disc. Ignored by `.gitignore`. +- `orig/[GAMEID]/.gitkeep` - Empty checked-in file to ensure the directory is created on clone. +- `src/` - C/C++ source files. +- `include/` - C/C++ header files. +- `tools/` - Scripts shared between projects. + +Temporary, delete when done: +- `config/GAMEID/config.example.yml` - Example configuration file and documentation. +- `docs/` - Documentation for decomp-toolkit configuration. +- `README.md` - This file, replace with your own. For a template, see [`README.example.md`](README.example.md). \ No newline at end of file diff --git a/assets/dolphin-extract.png b/assets/dolphin-extract.png new file mode 100644 index 0000000000000000000000000000000000000000..67a3aa3bc1b5716ab472e24c3807daa1b426a18c GIT binary patch literal 11843 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 literal 0 HcmV?d00001 diff --git a/assets/objdiff.png b/assets/objdiff.png new file mode 100644 index 0000000000000000000000000000000000000000..080e335ba0921822df1a5425753bd26670f9cdea GIT binary patch literal 66571 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˷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

fAX?tZn~(m;Htc9Jk_WuJj34~|2{}e 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 literal 0 HcmV?d00001 diff --git a/config/GAMEID/build.sha1 b/config/GAMEID/build.sha1 new file mode 100644 index 0000000..8bcde68 --- /dev/null +++ b/config/GAMEID/build.sha1 @@ -0,0 +1,2 @@ +0123456789abcdef0123456789abcdef01234567 build/GAMEID/main.dol +0123456789abcdef0123456789abcdef01234567 build/GAMEID/module/module.rel diff --git a/config/GAMEID/config.example.yml b/config/GAMEID/config.example.yml new file mode 100644 index 0000000..1f0022d --- /dev/null +++ b/config/GAMEID/config.example.yml @@ -0,0 +1,100 @@ +# Path to the main.dol file. +object: orig/GAMEID/sys/main.dol +# (optional) SHA-1 hash of the main.dol file for verification. +hash: 0123456789abcdef0123456789abcdef01234567 +# (optional) Name override. Defaults to "main". +name: main + +# (optional) Path to the symbols.txt file. +# This file will be created if it does not exist. +# See docs/symbols.md for more information. +symbols: config/GAMEID/symbols.txt +# (optional) Path to the splits.txt file. +# This file will be created if it does not exist. +# See docs/splits.md for more information. +splits: config/GAMEID/splits.txt + +# (optional) Path to the DOL's .map file. +# This should only used for initial analysis, and generating the symbols and splits files. +# Once those files are generated, remove this to avoid conflicts. +map: orig/GAMEID/files/main.MAP +# (optional) Start address of common BSS symbols, if any. +# Useful along with `map`, but not required otherwise, since common BSS +# is marked in the splits file. +common_start: 0x80001234 + +# (optional) Version used to generate `.comment` sections in the split objects. +# If not specified, no `.comment` sections will be generated. +# See docs/comment_section.md for more information. +mw_comment_version: 8 + +# (optional) Path to `selfile.sel` for Wii games with RSO files. +selfile: orig/GAMEID/files/selfile.sel +# (optional) SHA-1 hash of the `selfile.sel` file for verification. +selfile_hash: 0123456789abcdef0123456789abcdef01234567 + +# (optional) When enabled, function boundary analysis will be skipped. +# Only valid _after_ initial analysis has been performed and +# the symbols and splits files have been generated. +quick_analysis: false + +# (optional) When enabled, the analyzer will attempt to detect sizes +# and data types of objects based on code usage and alignment. +detect_objects: true + +# (optional) When enabled, the analyzer will attempt to detect strings, +# wide strings, and string tables. +detect_strings: true + +# (optional) Whether to write disassembly to the split output directory. +# While not used in the build process, the disassembly is useful +# for reading and usage with other tools, like decomp.me. +write_asm: true + +# (optional) If symbols are _fully_ known (e.g. from a complete map file), +# this can be set to true to skip most analysis steps, and ensure new +# symbols are not created by the analyzer. +# If you're not sure, leave this false. +symbols_known: false + +# (optional) Whether to create `gap_` symbols to prevent the linker from +# adjusting the alignment / address of symbols. +# When alignments are fully known (e.g. from a complete map file), +# this can be set to false. +fill_gaps: true + +# (optional) Custom template for `ldscript.lcf`. Avoid unless necessary. +# See https://github.com/encounter/decomp-toolkit/blob/main/assets/ldscript.lcf +ldscript_template: config/GAMEID/module/ldscript.tpl + +# (optional) Configuration for modules. +modules: + +- # Path to the module. + object: orig/GAMEID/files/module.rel + + # (optional) SHA-1 hash of the module for verification. + hash: 0123456789abcdef0123456789abcdef01234567 + + # (optional) Name of the module. Defaults to the module's filename. + name: module + + # (optional) Path to the module's symbols.txt file. + # This file will be created if it does not exist. + # See docs/symbols.md for more information. + symbols: config/GAMEID/module/symbols.txt + # (optional) Path to the module's splits.txt file. + # This file will be created if it does not exist. + # See docs/splits.md for more information. + splits: config/GAMEID/module/splits.txt + + # (optional) Path to the module's .map file. + # See `map` above for more information. + map: orig/GAMEID/files/module.MAP + + # (optional) Mark symbols as "force active" / "exported". + force_active: [] + + # (optional) Custom template for `ldscript.lcf`, if needed. + # See https://github.com/encounter/decomp-toolkit/blob/main/assets/ldscript_partial.lcf + ldscript_template: config/GAMEID/module/ldscript.tpl diff --git a/config/GAMEID/config.yml b/config/GAMEID/config.yml new file mode 100644 index 0000000..e48180d --- /dev/null +++ b/config/GAMEID/config.yml @@ -0,0 +1,12 @@ +# See config.example.yml for documentation. +object: orig/GAMEID/sys/main.dol +hash: 0123456789abcdef0123456789abcdef01234567 +symbols: config/GAMEID/symbols.txt +splits: config/GAMEID/splits.txt +mw_comment_version: 8 + +modules: +- object: orig/GAMEID/files/module.rel + hash: 0123456789abcdef0123456789abcdef01234567 + symbols: config/GAMEID/module/symbols.txt + splits: config/GAMEID/module/splits.txt diff --git a/config/GAMEID/splits.txt b/config/GAMEID/splits.txt new file mode 100644 index 0000000..a2e4ff3 --- /dev/null +++ b/config/GAMEID/splits.txt @@ -0,0 +1 @@ +// Intentionally empty. Initial analysis will generate this automatically. \ No newline at end of file diff --git a/config/GAMEID/symbols.txt b/config/GAMEID/symbols.txt new file mode 100644 index 0000000..a2e4ff3 --- /dev/null +++ b/config/GAMEID/symbols.txt @@ -0,0 +1 @@ +// Intentionally empty. Initial analysis will generate this automatically. \ No newline at end of file diff --git a/configure.py b/configure.py new file mode 100644 index 0000000..9dd3bda --- /dev/null +++ b/configure.py @@ -0,0 +1,233 @@ +#!/usr/bin/env python3 + +### +# Generates build files for the project. +# This file also includes the project configuration, +# such as compiler flags and the object matching status. +# +# Usage: +# python3 configure.py +# ninja +# +# Append --help to see available options. +### + +import sys +import argparse + +from pathlib import Path +from tools.project import ( + Object, + ProjectConfig, + calculate_progress, + generate_build, + is_windows, +) + +# Game versions +DEFAULT_VERSION = 0 +VERSIONS = [ + "GAMEID", # 0 +] + +if len(VERSIONS) > 1: + versions_str = ", ".join(VERSIONS[:-1]) + f" or {VERSIONS[-1]}" +else: + versions_str = VERSIONS[0] + +parser = argparse.ArgumentParser() +parser.add_argument( + "mode", + default="configure", + help="configure or progress (default: configure)", + nargs="?", +) +parser.add_argument( + "--version", + dest="version", + default=VERSIONS[DEFAULT_VERSION], + help=f"version to build ({versions_str})", +) +parser.add_argument( + "--build-dir", + dest="build_dir", + type=Path, + default=Path("build"), + help="base build directory (default: build)", +) +parser.add_argument( + "--compilers", + dest="compilers", + type=Path, + help="path to compilers (optional)", +) +parser.add_argument( + "--map", + dest="map", + action="store_true", + help="generate map file(s)", +) +parser.add_argument( + "--debug", + dest="debug", + action="store_true", + help="build with debug info (non-matching)", +) +if not is_windows(): + parser.add_argument( + "--wrapper", + dest="wrapper", + type=Path, + help="path to wibo or wine (optional)", + ) +parser.add_argument( + "--build-dtk", + dest="build_dtk", + type=Path, + help="path to decomp-toolkit source (optional)", +) +parser.add_argument( + "--sjiswrap", + dest="sjiswrap", + type=Path, + help="path to sjiswrap.exe (optional)", +) +parser.add_argument( + "--verbose", + dest="verbose", + action="store_true", + help="print verbose output", +) +args = parser.parse_args() + +config = ProjectConfig() +config.version = args.version.upper() +if config.version not in VERSIONS: + sys.exit(f"Invalid version '{config.version}', expected {versions_str}") +version_num = VERSIONS.index(config.version) + +# Apply arguments +config.build_dir = args.build_dir +config.build_dtk_path = args.build_dtk +config.compilers_path = args.compilers +config.debug = args.debug +config.generate_map = args.map +config.sjiswrap_path = args.sjiswrap +if not is_windows(): + config.wrapper = args.wrapper + +# Tool versions +config.compilers_tag = "1" +config.dtk_tag = "v0.5.5" +config.sjiswrap_tag = "v1.1.1" +config.wibo_tag = "0.6.3" + +# Project +config.config_path = Path("config") / config.version / "config.yml" +config.check_sha_path = Path("config") / config.version / "build.sha1" +config.ldflags = [ + "-fp hardware", + "-nodefaults", + "-listclosure", +] + +# Base flags, common to most GC/Wii games. +# Generally leave untouched, with overrides added below. +cflags_base = [ + "-nodefaults", + "-proc gekko", + "-align powerpc", + "-enum int", + "-fp hardware", + "-Cpp_exceptions off", + # "-W all", + "-O4,p", + "-inline auto", + '-pragma "cats off"', + '-pragma "warn_notinlined off"', + "-maxerrors 1", + "-nosyspath", + "-RTTI off", + "-fp_contract on", + "-str reuse", + "-i include", + "-i libc", + "-enc SJIS", + f"-DVERSION={version_num}", +] + +# Debug flags +if config.debug: + cflags_base.extend(["-sym on", "-DDEBUG=1"]) +else: + cflags_base.append("-DNDEBUG=1") + +# Metrowerks library flags +cflags_runtime = [ + *cflags_base, + "-use_lmw_stmw on", + "-str reuse,pool,readonly", + "-gccinc", + "-common off", + "-inline auto", +] + +# REL flags +cflags_rel = [ + *cflags_base, + "-sdata 0", + "-sdata2 0", +] + +config.linker_version = "Wii/1.3" + + +# Helper function for Dolphin libraries +def DolphinLib(lib_name, objects): + return { + "lib": lib_name, + "mw_version": "Wii/1.1", + "cflags": cflags_base, + "host": False, + "objects": objects, + } + + +# Helper function for REL script objects +def Rel(lib_name, objects): + return { + "lib": lib_name, + "mw_version": "Wii/1.3", + "cflags": cflags_rel, + "host": True, + "objects": objects, + } + + +Matching = True +NonMatching = False + +config.warn_missing_config = True +config.warn_missing_source = False +config.libs = [ + { + "lib": "Runtime.PPCEABI.H", + "mw_version": config.linker_version, + "cflags": cflags_runtime, + "host": False, + "objects": [ + Object(NonMatching, "Runtime.PPCEABI.H/global_destructor_chain.c"), + Object(NonMatching, "Runtime.PPCEABI.H/__init_cpp_exceptions.cpp"), + ], + }, +] + +if args.mode == "configure": + # Write build.ninja and objdiff.json + generate_build(config) +elif args.mode == "progress": + # Print progress and write progress.json + config.progress_each_module = args.verbose + calculate_progress(config) +else: + sys.exit("Unknown mode: " + args.mode) diff --git a/docs/comment_section.md b/docs/comment_section.md new file mode 100644 index 0000000..2e63b2e --- /dev/null +++ b/docs/comment_section.md @@ -0,0 +1,106 @@ +# CodeWarrior `.comment` section + +Files built with `mwcc` contain a `.comment` section: + +``` +$ powerpc-eabi-readelf -We object.o + +Section Headers: + [Nr] Name Type Addr Off Size ES Flg Lk Inf Al + [ 0] NULL 00000000 000000 000000 00 0 0 0 + [ 1] .text PROGBITS 00000000 000034 000708 00 AX 0 0 4 + ... + [16] .comment PROGBITS 00000000 00153b 0001b4 01 0 0 1 +``` + +The `.comment` section contains information that `mwld` uses during linking, primarily symbol alignment and a "force active" / export flag. + +If missing, `mwld` will **not** adjust the alignment of symbols or remove any unused symbols. + +This behavior is quite useful in some cases. When we split our program into objects, we're working from the final post-aligned, post-stripped result, and don't want the linker to make any changes. Most decompilation projects rely on this behavior unintentionally, since their generated objects don't contain a `.comment` section. (For example, objects built with `powerpc-eabi-as`.) + +However, we need the `.comment` section for some purposes: +- Reproducing the [common BSS inflation bug](common_bss.md#inflation-bug) requires the `.comment` section present, due to the above. The linker inflates the size of the first common BSS symbol in a TU, but won't actually move any data around unless the `.comment` section is present. +- In newer versions of the linker, using common BSS at all _without_ a valid `.comment` section will cause an internal linker error. + +When the `.comment` section is generated, decomp-toolkit will mark all global symbols as "exported" to prevent any deadstripping, since the presence of the `.comment` section itself enables deadstripping. + +Generating the `.comment` section and setting the "export" flag is also useful to prevent the linker from removing entire objects. A missing `.comment` section will prevent the removal of unused symbols _inside_ of an object, but the linker will still remove the entire object itself if it thinks it's unused. + +## Contents + +The contents of this section follow a very simple format: + +### Header + +`[0x0 size: 0xB]` Magic: `43 6F 64 65 57 61 72 72 69 6F 72` ("CodeWarrior") + +`[0xB size: 0x1]` Version(?): `XX` + +It's not known whether this field actually affects `mwld` in any way, but it's configurable for completeness sake. (See `mw_comment_version` in [`config.example.yml`](/config/GAMEID/config.example.yml).) + +Known values: +- `08` - CodeWarrior for GameCube 1.0+ +- `0A` - CodeWarrior for GameCube 1.3.2+ +- `0B`, `0C` - CodeWarrior for GameCube 2.7+ (difference unknown) +- `0E`, `0F` - CodeWarrior for GameCube 3.0a3+ (difference unknown) + +`[0xC size: 0x4]` Compiler version: `XX XX XX XX` + +First 3 bytes are major, minor, and patch version numbers. +4th byte is unknown, but is always `01`. + +Example: `Version 2.3.3 build 144` -> `02 03 00 01` +Often the `.exe`'s properties (which `--help` reads from) and the internal version number (here) will differ. + +`[0x10 size: 1]` Pool data: `XX` + +- `00` - Data pooling disabled +- `01` - Data pooling enabled + +`[0x11 size: 1]` Float type: `XX` + +- `00` - Floating point disabled +- `01` - Software floating point +- `02` - Hardware floating point + +`[0x12 size: 2]` Processor type: `00 16` (Gekko) + +`[0x14 size: 1]` Unknown, always `2C`. Possibly the start of symbol entries. + +`[0x15 size: 1]` "Quirk" flags: `XX` + +Bitfield of miscellaneous flags. Known flags: +- `01` - "Incompatible return small structs" +- `02` - "Incompatible SFPE double params" +- `04` - "Unsafe global reg vars" + +`[0x16 size: 22]` Padding until `0x2C` + +### Symbol entry + +At `0x2C` is the first symbol entry. There is one 8 byte entry per ELF symbol. + +This includes the "null" ELF symbol, so the first entry will be all 0's. + +`[0x0 size: 4]` Alignment: `XX XX XX XX` + +`[0x4 size: 1]` Visibility flags(?): `XX` + +Known values: +- `00` - Default +- `0D` - Weak +- `0E` - Unknown, also weak? + +`[0x5 size: 1]` Active flags(?): `XX` + +Known values: +- `00` - Default +- `08` - Force active / export. Prevents the symbol from being deadstripped. + When applied on a section symbol, the entire section is kept as-is. This is used + by `mwcc` when data pooling is triggered (indicated by a symbol like `...data.0`), likely to prevent the hard-coded section-relative offsets from breaking. + Can also be set using `#pragma force_active on` or `__declspec(export)`. +- `10` - Unknown +- `20` - Unknown + +`[0x6 size: 2]` Padding(?): `00 00` \ No newline at end of file diff --git a/docs/common_bss.md b/docs/common_bss.md new file mode 100644 index 0000000..948da8a --- /dev/null +++ b/docs/common_bss.md @@ -0,0 +1,69 @@ +# Common BSS + +When passed the `-common on` flag, `mwcc` will generate global BSS symbols as **common**. The linker deduplicates common symbols with the same name, and allocates an area at the **end** of `.bss` for them. + +This is a legacy feature, allowing uninitialized global variables to be defined in headers without linker errors: + +```c +// foo.h +int foo; +``` + +With `-common on`, any TU that includes `foo.h` will define `foo` as a **common** symbol. The linker will deduplicate `foo` across TUs, similar to weak symbols. Common symbols are then generated at the **end** of `.bss`, after all other `.bss` symbols. + +With `-common off`, `foo` would be defined as a **global** symbol, and the linker would error out with a duplicate symbol error if `foo.h` was included in multiple TUs. + +In `splits.txt`, common BSS can be defined with the `common` attribute: + +``` +foo.cpp: + .text start:0x80047E5C end:0x8004875C + .ctors start:0x803A54C4 end:0x803A54C8 + .data start:0x803B1B40 end:0x803B1B60 + .bss start:0x803DF828 end:0x803DFA8C + .bss start:0x8040D4AC end:0x8040D4D8 common +``` + +As shown above, a file can contain both regular `.bss` and common `.bss`. Marking common `.bss` appropriately is important for determining the final link order. + +## Detection + +Example from Pikmin 2: +``` +00016e60 00000c 805069c0 1 .bss utilityU.a PSMainSide_CreaturePrm.cpp +00016e60 00000c 805069c0 4 @3464 utilityU.a PSMainSide_CreaturePrm.cpp +00016e6c 000048 805069cc 4 saoVVOutput_direction___Q214JStudio_JStage14TAdaptor_light JSystem.a object-light.cpp +00016eb4 0000d0 80506a14 4 saoVVOutput___Q214JStudio_JStage14TAdaptor_actor JSystem.a object-actor.cpp +``` + +In this example, we see a symbol from `utilityU.a PSMainSide_CreaturePrm.cpp`. We know that this file is very close to the _end_ of the link order. Afterwards, there's a symbol from `JSystem.a object-light.cpp`, which is very close to the _beginning_ of the link order. + +A file can't be both at the beginning and end of the link order, so it's a strong indication that `saoVVOutput_direction___Q214JStudio_JStage14TAdaptor_light` marks the beginning of the common BSS section. + +One other indication from this example is the lack of a `.bss` section symbol from `JSystem.a object-actor.cpp` and any following files in the link order. Section symbols aren't generated for common BSS. + +Without a map, it's harder to tell if there's a common BSS section, but guesses can be made. When looking at XREFs in Ghidra, if a symbol is close to the _end_ of `.bss`, but has XREFs from various addresses close to the _beginning_ of `.text`, it could be an indication of common BSS. + +For games built with older versions of the linker, the inflation bug (described below) can also be used to detect common BSS. + +## Inflation bug + +In older versions of the linker (<= GC 2.6?), when calculating the size of common symbols, the linker will accidentally set the size of the first common symbol in a TU to the size of the _entire_ common section in that TU. + +Example from Pikmin 2: + +``` +# Section Addr | Size | Addr | Alignment | Name | File +00017260 000188 80506dc0 4 mPadList__10JUTGamePad JSystem.a JUTGamePad.cpp +000173e8 000030 80506f48 4 mPadStatus__10JUTGamePad JSystem.a JUTGamePad.cpp +00017418 0000c0 80506f78 4 mPadButton__10JUTGamePad JSystem.a JUTGamePad.cpp +000174d8 000040 80507038 4 mPadMStick__10JUTGamePad JSystem.a JUTGamePad.cpp +00017518 000040 80507078 4 mPadSStick__10JUTGamePad JSystem.a JUTGamePad.cpp +00017558 00000c 805070b8 4 sPatternList__19JUTGamePadLongPress JSystem.a JUTGamePad.cpp +``` + +In this example, `mPadList__10JUTGamePad` is the first common symbol in the TU, and was inflated to include the size of all other common symbols in the TU. In reality, it's only supposed to be `0xC` bytes, given `0x188 - 0x30 - 0xC0 - 0x40 - 0x40 - 0xC`. + +This can be useful to determine if symbols are in the same TU without a map: if a `.bss` symbol is much larger than expected, it could be the first common symbol in a TU. One can subtract the sizes of following symbols to find the true size of the symbol, along with the end of the TU's common symbols. + +To reproduce this behavior, the `.comment` section must be present in the object. See [`.comment` section](comment_section.md) for more details. diff --git a/docs/dependencies.md b/docs/dependencies.md new file mode 100644 index 0000000..b8b531b --- /dev/null +++ b/docs/dependencies.md @@ -0,0 +1,33 @@ +# Dependencies + +## Windows: + +On Windows, it's **highly recommended** to use native tooling. WSL or msys2 are **not** required. +When running under WSL, [objdiff](#diffing) is unable to get filesystem notifications for automatic rebuilds. + +- Install [Python](https://www.python.org/downloads/) and add it to `%PATH%`. + - Also available from the [Windows Store](https://apps.microsoft.com/store/detail/python-311/9NRWMJP3717K). +- Download [ninja](https://github.com/ninja-build/ninja/releases) and add it to `%PATH%`. + - Quick install via pip: `pip install ninja` + +## macOS: + +- Install [ninja](https://github.com/ninja-build/ninja/wiki/Pre-built-Ninja-packages): + ``` + brew install ninja + ``` +- Install [wine-crossover](https://github.com/Gcenx/homebrew-wine): + ``` + brew install --cask --no-quarantine gcenx/wine/wine-crossover + ``` + +After OS upgrades, if macOS complains about `Wine Crossover.app` being unverified, you can unquarantine it using: +```sh +sudo xattr -rd com.apple.quarantine '/Applications/Wine Crossover.app' +``` + +## Linux: + +- Install [ninja](https://github.com/ninja-build/ninja/wiki/Pre-built-Ninja-packages). +- For non-x86(_64) platforms: Install wine from your package manager. + - For x86(_64), [WiBo](https://github.com/decompals/WiBo), a minimal 32-bit Windows binary wrapper, will be automatically downloaded and used. diff --git a/docs/getting_started.md b/docs/getting_started.md new file mode 100644 index 0000000..b084c5e --- /dev/null +++ b/docs/getting_started.md @@ -0,0 +1,124 @@ +# Getting Started + +See [Dependencies](dependencies.md) first. + +Clone this template repository. + +Rename `orig/GAMEID` to the game's ID. (For example, `GLZE01` for _The Legend of Zelda: The Wind Waker_.) + +Extract your game to `orig/[GAMEID]`. In Dolphin, use "Extract Entire Disc" for GameCube games, or use "Data Partition" -> "Extract Entire Partition" for Wii games. + +Rename `config/GAMEID` to the game's ID and modify `config/[GAMEID]/config.yml` appropriately, using [`config.example.yml`](/config/GAMEID/config.example.yml) as a reference. If the game doesn't use RELs, the `modules` list in `config.yml` can be removed. + +Generate a `config/[GAMEID]/build.sha1` file for verification. This file is a list of SHA-1 hashes for each build artifact. One possible way: + +```shell +$ dtk shasum orig/[GAMEID]/sys/main.dol orig/[GAMEID]/files/*.rel > config/[GAMEID]/build.sha1 +``` + +Then, modify the paths in `config/[GAMEID]/build.sha1` to point to the `build` directory instead of `orig`. The DOL will be built at `build/[GAMEID]/main.dol`, and modules will be built at `build/[GAMEID]/[module_name]/[module_name].rel`. + +Update `VERSIONS` in [`configure.py`](/configure.py) with the game ID. + +Run `python configure.py` to generate the initial `build.ninja`. + +Run `ninja` to perform initial analysis. + +If all goes well, the initial `symbols.txt` and `splits.txt` should be automatically generated. Though it's likely it won't build yet. See [Post-analysis](#post-analysis) for next steps. + +## Using a `.map` + +If the game has `.map` files matching the DOL (and RELs, if applicable), they can be used to fill out `symbols.txt` and `splits.txt` automatically during the initial analysis. + +Add the `map` key to `config.yml`, pointing to the `.map` file from the game disc. (For example, `orig/[GAMEID]/files/main.map`.) For RELs, add a `map` key to each module in `config.yml`. + +If the game uses [common BSS](common_bss.md), be sure to set `common_start` as well. (See [`config.example.yml`](/config/GAMEID/config.example.yml).) Otherwise, the final link order may fail to be determined. + +Once the initial analysis is completed, `symbols.txt` and `splits.txt` will be generated from the map information. **Remove** the `map` fields from `config.yml` to avoid conflicts. + +## Post-analysis + +After the initial analysis, `symbols.txt` and `splits.txt` will be generated. These files can be modified to adjust symbols and split points. + +If the game uses C++ exceptions, it's required to set up a split for the `__init_cpp_exceptions.cpp` file. This differs between linker versions. + +Often indicated by the following error: + +``` +# runtime sources 'global_destructor_chain.c' and +# '__init_cpp_exceptions.cpp' both need to be updated to latest version. +``` + +### GC 1.0 - 2.6 linkers: + +```yaml +# splits.txt +Runtime.PPCEABI.H/__init_cpp_exceptions.cpp: + .text start:0x803294EC end:0x80329568 + .ctors start:0x80338680 end:0x80338684 + .dtors start:0x80338820 end:0x80338828 + .sdata start:0x803F67F0 end:0x803F67F8 +``` + +`.text`: +Find the following symbols in `symbols.txt`: +``` +GetR2__Fv = .text:0x803294EC; // type:function size:0x8 scope:local align:4 +__fini_cpp_exceptions = .text:0x803294F4; // type:function size:0x34 scope:global align:4 +__init_cpp_exceptions = .text:0x80329528; // type:function size:0x40 scope:global align:4 +``` +The split end is the address of `__init_cpp_exceptions` + size. + +`.ctors`: +Find the address of `__init_cpp_exception_reference` or `_ctors` in symbols.txt. +Always size 4. + +`.dtors`: +Look for the address of `__destroy_global_chain_reference` or `_dtors` in symbols.txt. +If `__fini_cpp_exceptions_reference` is present, it's size 8, otherwise size 4 + +`.sdata`: +Find the following symbol in `symbols.txt`: +``` +fragmentID = .sdata:0x803F67F0; // type:object size:0x4 scope:local align:4 data:4byte +``` +The split end includes any inter-TU padding, so it's usually size 8. + +### GC 2.7+ and Wii linkers: + +```yaml +# splits.txt +Runtime.PPCEABI.H/__init_cpp_exceptions.cpp: + .text start:0x80345C34 end:0x80345CA4 + .ctors start:0x803A54A0 end:0x803A54A4 rename:.ctors$10 + .dtors start:0x803A56A0 end:0x803A56A4 rename:.dtors$10 + .dtors start:0x803A56A4 end:0x803A56A8 rename:.dtors$15 + .sdata start:0x80418CA8 end:0x80418CB0 +``` + +`.text`: +Find the following symbols in `symbols.txt`: +``` +__fini_cpp_exceptions = .text:0x80345C34; // type:function size:0x34 scope:global +__init_cpp_exceptions = .text:0x80345C68; // type:function size:0x3C scope:global +``` +The split end is the address of `__init_cpp_exceptions` + size. + +`.ctors$10`: +Find the address of `__init_cpp_exception_reference` or `_ctors` in symbols.txt. +Always size 4. + +`.dtors$10`: +Look for the address of `__destroy_global_chain_reference` or `_dtors` in symbols.txt. +Always size 4. + +`.dtors$15`: +Look for the address of `__fini_cpp_exceptions_reference` in symbols.txt. +Always size 4. + +`.sdata`: +Find the following symbol in `symbols.txt`: +``` +fragmentID = .sdata:0x80418CA8; // type:object size:0x4 scope:local data:4byte +``` +The split end includes any inter-TU padding, so it's usually size 8. \ No newline at end of file diff --git a/docs/splits.md b/docs/splits.md new file mode 100644 index 0000000..2fa4739 --- /dev/null +++ b/docs/splits.md @@ -0,0 +1,42 @@ +# `splits.txt` + +This file contains file splits for a module. + +Example: + +``` +path/to/file.cpp: + .text start:0x80047E5C end:0x8004875C + .ctors start:0x803A54C4 end:0x803A54C8 + .data start:0x803B1B40 end:0x803B1B60 + .bss start:0x803DF828 end:0x803DFA8C + .bss start:0x8040D4AC end:0x8040D4D8 common +``` + +## Format + +``` +path/to/file.cpp: [file attributes] + section [section attributes] + ... +``` + +- `path/to/file.cpp` The name of the source file, usually relative to `src`. The file does **not** need to exist to start. + This corresponds to an entry in `configure.py` for specifying compiler flags and other options. + +### File attributes + +- `comment:` Overrides the `mw_comment_version` setting in [`config.yml`](/config/GAMEID/config.example.yml) for this file. See [Comment section](comment_section.md). + +`comment:0` is used to disable `.comment` section generation for a file that wasn't compiled with `mwcc`. +Example: `TRK_MINNOW_DOLPHIN/ppc/Export/targsupp.s: comment:0` +This file was assembled and only contains label symbols. Generating a `.comment` section for it will crash `mwld`. + +### Section attributes + +- `start:` The start address of the section within the file. For DOLs, this is the absolute address (e.g. `0x80001234`). For RELs, this is the section-relative address (e.g. `0x1234`). +- `end:` The end address of the section within the file. +- `align:` Specifies the alignment of the section. If not specified, the default alignment for the section is used. +- `rename:` Writes this section under a different name when generating the split object. Used for `.ctors$10`, etc. +- `common` Only valid for `.bss`. See [Common BSS](common_bss.md). +- `skip` Skips this data when writing the object file. Used for ignoring data that's linker-generated. diff --git a/docs/symbols.md b/docs/symbols.md new file mode 100644 index 0000000..cfcffff --- /dev/null +++ b/docs/symbols.md @@ -0,0 +1,35 @@ +# `symbols.txt` + +This file contains all symbols for a module, one per line. + +Example line: +``` +__dt__13mDoExt_bckAnmFv = .text:0x800DD2EC; // type:function size:0x5C scope:global align:4 +``` + +## Format + +Numbers can be written as decimal or hexadecimal. Hexadecimal numbers must be prefixed with `0x`. + +Comment lines starting with `//` or `#` are permitted, but are currently **not** preserved when updating the file. + +``` +symbol_name = section:address; // [attributes] +``` + +- `symbol_name` - The name of the symbol. (For C++, this is the mangled name, e.g. `__dt__13mDoExt_bckAnmFv`) +- `section` - The section the symbol is in. +- `address` - The address of the symbol. For DOLs, this is the absolute address (e.g. `0x80001234`). For RELs, this is the section-relative address (e.g. `0x1234`). + +### Attributes + +All attributes are optional, and are separated by spaces. + +- `type:` The symbol type. `function`, `object`, or `label`. +- `size:` The size of the symbol. +- `scope:` The symbol's visibility. `global` (default), `local` or `weak`. +- `align:` The symbol's alignment. +- `data:` The data type used when writing disassembly. `byte`, `2byte`, `4byte`, `8byte`, `float`, `double`, `string`, `wstring`, `string_table`, or `wstring_table`. +- `hidden` Marked as "hidden" in the generated object. (Only used for extab) +- `force_active` Marked as ["exported"](comment_section.md) in the generated object, and added to `FORCEACTIVE` in the generated `ldscript.lcf`. Prevents the symbol from being deadstripped. +- `noreloc` Prevents the _contents_ of the symbol from being interpreted as addresses. Used for objects containing data that look like pointers, but aren't. \ No newline at end of file diff --git a/orig/GAMEID/.gitkeep b/orig/GAMEID/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/tools/__init__.py b/tools/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/decompctx.py b/tools/decompctx.py new file mode 100644 index 0000000..c42b182 --- /dev/null +++ b/tools/decompctx.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 + +### +# Generates a ctx.c file, usable for "Context" on https://decomp.me. +# +# Usage: +# python3 tools/decompctx.py src/file.cpp +# +# If changes are made, please submit a PR to +# https://github.com/encounter/dtk-template +### + +import argparse +import os +import re + +script_dir = os.path.dirname(os.path.realpath(__file__)) +root_dir = os.path.abspath(os.path.join(script_dir, "..")) +src_dir = os.path.join(root_dir, "src") +include_dir = os.path.join(root_dir, "include") + +include_pattern = re.compile(r'^#include\s*[<"](.+?)[>"]$') +guard_pattern = re.compile(r'^#ifndef\s+(.*)$') + +defines = set() + +def import_h_file(in_file: str, r_path: str) -> str: + rel_path = os.path.join(root_dir, r_path, in_file) + inc_path = os.path.join(include_dir, in_file) + if os.path.exists(rel_path): + return import_c_file(rel_path) + elif os.path.exists(inc_path): + return import_c_file(inc_path) + else: + print("Failed to locate", in_file) + exit(1) + +def import_c_file(in_file) -> str: + in_file = os.path.relpath(in_file, root_dir) + out_text = '' + + try: + with open(in_file, encoding="utf-8") as file: + out_text += process_file(in_file, list(file)) + except Exception: + with open(in_file) as file: + out_text += process_file(in_file, list(file)) + return out_text + +def process_file(in_file: str, lines) -> str: + out_text = '' + for idx, line in enumerate(lines): + guard_match = guard_pattern.match(line.strip()) + if idx == 0: + if guard_match: + if guard_match[1] in defines: + break + defines.add(guard_match[1]) + print("Processing file", in_file) + include_match = include_pattern.match(line.strip()) + if include_match and not include_match[1].endswith(".s"): + out_text += f"/* \"{in_file}\" line {idx} \"{include_match[1]}\" */\n" + out_text += import_h_file(include_match[1], os.path.dirname(in_file)) + out_text += f"/* end \"{include_match[1]}\" */\n" + else: + out_text += line + + return out_text + +def main(): + parser = argparse.ArgumentParser( + description="""Create a context file which can be used for decomp.me""" + ) + parser.add_argument( + "c_file", + help="""File from which to create context""", + ) + args = parser.parse_args() + + output = import_c_file(args.c_file) + + with open(os.path.join(root_dir, "ctx.c"), "w", encoding="utf-8") as f: + f.write(output) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tools/download_tool.py b/tools/download_tool.py new file mode 100644 index 0000000..2709966 --- /dev/null +++ b/tools/download_tool.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 + +### +# Downloads various tools from GitHub releases. +# +# Usage: +# python3 tools/download_tool.py wibo build/tools/wibo --tag 1.0.0 +# +# If changes are made, please submit a PR to +# https://github.com/encounter/dtk-template +### + +import argparse +import io +import os +import platform +import shutil +import stat +import sys +import urllib.request +import zipfile + +from pathlib import Path + + +def dtk_url(tag): + uname = platform.uname() + suffix = "" + system = uname.system.lower() + if system == "darwin": + system = "macos" + elif system == "windows": + suffix = ".exe" + arch = uname.machine.lower() + if arch == "amd64": + arch = "x86_64" + + repo = "https://github.com/encounter/decomp-toolkit" + return f"{repo}/releases/download/{tag}/dtk-{system}-{arch}{suffix}" + + +def sjiswrap_url(tag): + repo = "https://github.com/encounter/sjiswrap" + return f"{repo}/releases/download/{tag}/sjiswrap-windows-x86.exe" + + +def wibo_url(tag): + repo = "https://github.com/decompals/wibo" + return f"{repo}/releases/download/{tag}/wibo" + + +def compilers_url(tag): + if tag == "1": + return "https://cdn.discordapp.com/attachments/727918646525165659/1129759991696457728/GC_WII_COMPILERS.zip" + else: + sys.exit("Unknown compilers tag %s" % tag) + + +TOOLS = { + "dtk": dtk_url, + "sjiswrap": sjiswrap_url, + "wibo": wibo_url, + "compilers": compilers_url, +} + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("tool", help="Tool name") + parser.add_argument("output", type=Path, help="output file path") + parser.add_argument("--tag", help="GitHub tag", required=True) + args = parser.parse_args() + + url = TOOLS[args.tool](args.tag) + output = Path(args.output) + + print(f"Downloading {url} to {output}") + req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) + with urllib.request.urlopen(req) as response: + if url.endswith(".zip"): + data = io.BytesIO(response.read()) + with zipfile.ZipFile(data) as f: + f.extractall(output) + output.touch(mode=0o755) + else: + with open(output, "wb") as f: + shutil.copyfileobj(response, f) + st = os.stat(output) + os.chmod(output, st.st_mode | stat.S_IEXEC) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tools/ninja_syntax.py b/tools/ninja_syntax.py new file mode 100644 index 0000000..ffd88a0 --- /dev/null +++ b/tools/ninja_syntax.py @@ -0,0 +1,223 @@ +# Copyright 2011 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Python module for generating .ninja files. + +Note that this is emphatically not a required piece of Ninja; it's +just a helpful utility for build-file-generation systems that already +use Python. +""" + +import re +import textwrap + + +def escape_path(word): + return word.replace("$ ", "$$ ").replace(" ", "$ ").replace(":", "$:") + + +class Writer(object): + def __init__(self, output, width=78): + self.output = output + self.width = width + + def newline(self): + self.output.write("\n") + + def comment(self, text): + for line in textwrap.wrap( + text, self.width - 2, break_long_words=False, break_on_hyphens=False + ): + self.output.write("# " + line + "\n") + + def variable(self, key, value, indent=0): + if value is None: + return + if isinstance(value, list): + value = " ".join(filter(None, value)) # Filter out empty strings. + self._line("%s = %s" % (key, value), indent) + + def pool(self, name, depth): + self._line("pool %s" % name) + self.variable("depth", depth, indent=1) + + def rule( + self, + name, + command, + description=None, + depfile=None, + generator=False, + pool=None, + restat=False, + rspfile=None, + rspfile_content=None, + deps=None, + ): + self._line("rule %s" % name) + self.variable("command", command, indent=1) + if description: + self.variable("description", description, indent=1) + if depfile: + self.variable("depfile", depfile, indent=1) + if generator: + self.variable("generator", "1", indent=1) + if pool: + self.variable("pool", pool, indent=1) + if restat: + self.variable("restat", "1", indent=1) + if rspfile: + self.variable("rspfile", rspfile, indent=1) + if rspfile_content: + self.variable("rspfile_content", rspfile_content, indent=1) + if deps: + self.variable("deps", deps, indent=1) + + def build( + self, + outputs, + rule, + inputs=None, + implicit=None, + order_only=None, + variables=None, + implicit_outputs=None, + pool=None, + dyndep=None, + ): + outputs = as_list(outputs) + out_outputs = [escape_path(x) for x in outputs] + all_inputs = [escape_path(x) for x in as_list(inputs)] + + if implicit: + implicit = [escape_path(x) for x in as_list(implicit)] + all_inputs.append("|") + all_inputs.extend(implicit) + if order_only: + order_only = [escape_path(x) for x in as_list(order_only)] + all_inputs.append("||") + all_inputs.extend(order_only) + if implicit_outputs: + implicit_outputs = [escape_path(x) for x in as_list(implicit_outputs)] + out_outputs.append("|") + out_outputs.extend(implicit_outputs) + + self._line( + "build %s: %s" % (" ".join(out_outputs), " ".join([rule] + all_inputs)) + ) + if pool is not None: + self._line(" pool = %s" % pool) + if dyndep is not None: + self._line(" dyndep = %s" % dyndep) + + if variables: + if isinstance(variables, dict): + iterator = iter(variables.items()) + else: + iterator = iter(variables) + + for key, val in iterator: + self.variable(key, val, indent=1) + + return outputs + + def include(self, path): + self._line("include %s" % path) + + def subninja(self, path): + self._line("subninja %s" % path) + + def default(self, paths): + self._line("default %s" % " ".join(as_list(paths))) + + def _count_dollars_before_index(self, s, i): + """Returns the number of '$' characters right in front of s[i].""" + dollar_count = 0 + dollar_index = i - 1 + while dollar_index > 0 and s[dollar_index] == "$": + dollar_count += 1 + dollar_index -= 1 + return dollar_count + + def _line(self, text, indent=0): + """Write 'text' word-wrapped at self.width characters.""" + leading_space = " " * indent + while len(leading_space) + len(text) > self.width: + # The text is too wide; wrap if possible. + + # Find the rightmost space that would obey our width constraint and + # that's not an escaped space. + available_space = self.width - len(leading_space) - len(" $") + space = available_space + while True: + space = text.rfind(" ", 0, space) + if space < 0 or self._count_dollars_before_index(text, space) % 2 == 0: + break + + if space < 0: + # No such space; just use the first unescaped space we can find. + space = available_space - 1 + while True: + space = text.find(" ", space + 1) + if ( + space < 0 + or self._count_dollars_before_index(text, space) % 2 == 0 + ): + break + if space < 0: + # Give up on breaking. + break + + self.output.write(leading_space + text[0:space] + " $\n") + text = text[space + 1 :] + + # Subsequent lines are continuations, so indent them. + leading_space = " " * (indent + 2) + + self.output.write(leading_space + text + "\n") + + def close(self): + self.output.close() + + +def as_list(input): + if input is None: + return [] + if isinstance(input, list): + return input + return [input] + + +def escape(string): + """Escape a string such that it can be embedded into a Ninja file without + further interpretation.""" + assert "\n" not in string, "Ninja syntax does not allow newlines" + # We only have one special metacharacter: '$'. + return string.replace("$", "$$") + + +def expand(string, vars, local_vars={}): + """Expand a string containing $vars as Ninja would. + + Note: doesn't handle the full Ninja variable syntax, but it's enough + to make configure.py's use of it work. + """ + + def exp(m): + var = m.group(1) + if var == "$": + return "$" + return local_vars.get(var, vars.get(var, "")) + + return re.sub(r"\$(\$|\w*)", exp, string) \ No newline at end of file diff --git a/tools/project.py b/tools/project.py new file mode 100644 index 0000000..28a8d32 --- /dev/null +++ b/tools/project.py @@ -0,0 +1,982 @@ +### +# decomp-toolkit project generator +# Generates build.ninja and objdiff.json. +# +# This generator is intentionally project-agnostic +# and shared between multiple projects. Any configuration +# specific to a project should be added to `configure.py`. +# +# If changes are made, please submit a PR to +# https://github.com/encounter/dtk-template +### + +import io +import json +import os +import platform +import sys + +from pathlib import Path +from . import ninja_syntax + +if sys.platform == "cygwin": + sys.exit( + f"Cygwin/MSYS2 is not supported." + f"\nPlease use native Windows Python instead." + f"\n(Current path: {sys.executable})" + ) + + +class ProjectConfig: + def __init__(self): + # Paths + self.build_dir = Path("build") + self.src_dir = Path("src") + self.tools_dir = Path("tools") + + # Tooling + self.dtk_tag = None # Git tag + self.build_dtk_path = None # If None, download + self.compilers_tag = None # 1 + self.compilers_path = None # If None, download + self.wibo_tag = None # Git tag + self.wrapper = None # If None, download wibo on Linux + self.sjiswrap_tag = None # Git tag + self.sjiswrap_path = None # If None, download + + # Project config + self.build_rels = True # Build REL files + self.check_sha_path = None # Path to version.sha1 + self.config_path = None # Path to config.yml + self.debug = False # Build with debug info + self.generate_map = False # Generate map file(s) + self.ldflags = None # Linker flags + self.libs = None # List of libraries + self.linker_version = None # mwld version + self.version = None # Version name + self.warn_missing_config = False # Warn on missing unit configuration + self.warn_missing_source = False # Warn on missing source file + + # Progress output and progress.json config + self.progress_all = True # Include combined "all" category + self.progress_modules = True # Include combined "modules" category + self.progress_each_module = ( + True # Include individual modules, disable for large numbers of modules + ) + + def validate(self): + required_attrs = [ + "build_dir", + "src_dir", + "tools_dir", + "check_sha_path", + "config_path", + "ldflags", + "linker_version", + "libs", + "version", + ] + for attr in required_attrs: + if getattr(self, attr) is None: + sys.exit(f"ProjectConfig.{attr} missing") + + def find_object(self, name): + for lib in self.libs: + for obj in lib["objects"]: + if obj.name == name: + return [lib, obj] + return None + + def out_path(self): + return self.build_dir / self.version + + +class Object: + def __init__(self, completed, name, **options): + self.name = name + self.completed = completed + self.options = { + "add_to_all": True, + "cflags": None, + "mw_version": None, + "shiftjis": True, + "source": name, + } + self.options.update(options) + + +def is_windows(): + return os.name == "nt" + + +# On Windows, we need this to use && in commands +CHAIN = "cmd /c " if is_windows() else "" +# Native executable extension +EXE = ".exe" if is_windows() else "" + + +# Replace forward slashes with backslashes on Windows +def os_str(value): + return str(value).replace("/", os.sep) + + +# Replace backslashes with forward slashes on Windows +def unix_str(value): + return str(value).replace(os.sep, "/") + + +# Stringify paths for ninja_syntax +def path(value): + if value is None: + return None + elif isinstance(value, list): + return list(map(os_str, filter(lambda x: x is not None, value))) + else: + return [os_str(value)] + + +# Load decomp-toolkit generated config.json +def load_build_config(config, build_config_path): + if not build_config_path.is_file(): + return None + + def versiontuple(v): + return tuple(map(int, (v.split(".")))) + + f = open(build_config_path, "r", encoding="utf-8") + build_config = json.load(f) + config_version = build_config.get("version") + if not config_version: + # Invalid config.json + f.close() + os.remove(build_config_path) + return None + + dtk_version = config.dtk_tag[1:] # Strip v + if versiontuple(config_version) < versiontuple(dtk_version): + # Outdated config.json + f.close() + os.remove(build_config_path) + return None + + f.close() + return build_config + + +# Generate build.ninja and objdiff.json +def generate_build(config): + build_config = load_build_config(config, config.out_path() / "config.json") + generate_build_ninja(config, build_config) + generate_objdiff_config(config, build_config) + + +# Generate build.ninja +def generate_build_ninja(config, build_config): + config.validate() + + out = io.StringIO() + n = ninja_syntax.Writer(out) + n.variable("ninja_required_version", "1.3") + n.newline() + + configure_script = os.path.relpath(os.path.abspath(sys.argv[0])) + python_lib = os.path.relpath(__file__) + python_lib_dir = os.path.dirname(python_lib) + n.comment("The arguments passed to configure.py, for rerunning it.") + n.variable("configure_args", sys.argv[1:]) + n.variable("python", f'"{sys.executable}"') + n.newline() + + ### + # Variables + ### + n.comment("Variables") + ldflags = " ".join(config.ldflags) + if config.generate_map: + ldflags += " -mapunused" + if config.debug: + ldflags += " -g" + n.variable("ldflags", ldflags) + n.variable("mw_version", config.linker_version) + n.newline() + + ### + # Tooling + ### + n.comment("Tooling") + + build_path = config.out_path() + build_tools_path = config.build_dir / "tools" + download_tool = config.tools_dir / "download_tool.py" + n.rule( + name="download_tool", + command=f"$python {download_tool} $tool $out --tag $tag", + description="TOOL $out", + ) + + if config.build_dtk_path: + dtk = build_tools_path / "release" / f"dtk{EXE}" + n.rule( + name="cargo", + command="cargo build --release --manifest-path $in --bin $bin --target-dir $target", + description="CARGO $bin", + depfile=path(Path("$target") / "release" / "$bin.d"), + deps="gcc", + ) + n.build( + outputs=path(dtk), + rule="cargo", + inputs=path(config.build_dtk_path / "Cargo.toml"), + implicit=path(config.build_dtk_path / "Cargo.lock"), + variables={ + "bin": "dtk", + "target": build_tools_path, + }, + ) + elif config.dtk_tag: + dtk = build_tools_path / f"dtk{EXE}" + n.build( + outputs=path(dtk), + rule="download_tool", + implicit=path(download_tool), + variables={ + "tool": "dtk", + "tag": config.dtk_tag, + }, + ) + else: + sys.exit("ProjectConfig.dtk_tag missing") + + if config.sjiswrap_path: + sjiswrap = config.sjiswrap_path + elif config.sjiswrap_tag: + sjiswrap = build_tools_path / "sjiswrap.exe" + n.build( + outputs=path(sjiswrap), + rule="download_tool", + implicit=path(download_tool), + variables={ + "tool": "sjiswrap", + "tag": config.sjiswrap_tag, + }, + ) + else: + sys.exit("ProjectConfig.sjiswrap_tag missing") + + # Only add an implicit dependency on wibo if we download it + wrapper = config.wrapper + wrapper_implicit = None + if ( + config.wibo_tag is not None + and sys.platform == "linux" + and platform.machine() in ("i386", "x86_64") + and config.wrapper is None + ): + wrapper = build_tools_path / "wibo" + wrapper_implicit = wrapper + n.build( + outputs=path(wrapper), + rule="download_tool", + implicit=path(download_tool), + variables={ + "tool": "wibo", + "tag": config.wibo_tag, + }, + ) + if not is_windows() and wrapper is None: + wrapper = "wine" + wrapper_cmd = f"{wrapper} " if wrapper else "" + + compilers_implicit = None + if config.compilers_path: + compilers = config.compilers_path + elif config.compilers_tag: + compilers = config.build_dir / "compilers" + compilers_implicit = compilers + n.build( + outputs=path(compilers), + rule="download_tool", + implicit=path(download_tool), + variables={ + "tool": "compilers", + "tag": config.compilers_tag, + }, + ) + + n.newline() + + ### + # Build rules + ### + compiler_path = compilers / "$mw_version" + + # MWCC + mwcc = compiler_path / "mwcceppc.exe" + mwcc_cmd = f"{wrapper_cmd}{mwcc} $cflags -MMD -c $in -o $basedir" + mwcc_implicit = [compilers_implicit or mwcc, wrapper_implicit] + + # MWCC with UTF-8 to Shift JIS wrapper + mwcc_sjis_cmd = f"{wrapper_cmd}{sjiswrap} {mwcc} $cflags -MMD -c $in -o $basedir" + mwcc_sjis_implicit = [*mwcc_implicit, sjiswrap] + + # MWLD + mwld = compiler_path / "mwldeppc.exe" + mwld_cmd = f"{wrapper_cmd}{mwld} $ldflags -o $out @$out.rsp" + mwld_implicit = [compilers_implicit or mwld, wrapper_implicit] + + if os.name != "nt": + transform_dep = config.tools_dir / "transform_dep.py" + mwcc_cmd += f" && $python {transform_dep} $basefile.d $basefile.d" + mwcc_sjis_cmd += f" && $python {transform_dep} $basefile.d $basefile.d" + mwcc_implicit.append(transform_dep) + mwcc_sjis_implicit.append(transform_dep) + + n.comment("Link ELF file") + n.rule( + name="link", + command=mwld_cmd, + description="LINK $out", + rspfile="$out.rsp", + rspfile_content="$in_newline", + ) + n.newline() + + n.comment("Generate DOL") + n.rule( + name="elf2dol", + command=f"{dtk} elf2dol $in $out", + description="DOL $out", + ) + n.newline() + + n.comment("Generate REL(s)") + makerel_rsp = build_path / "makerel.rsp" + n.rule( + name="makerel", + command=f"{dtk} rel make -w -c $config @{makerel_rsp}", + description="REL", + rspfile=path(makerel_rsp), + rspfile_content="$in_newline", + ) + n.newline() + + n.comment("MWCC build") + n.rule( + name="mwcc", + command=mwcc_cmd, + description="MWCC $out", + depfile="$basefile.d", + deps="gcc", + ) + n.newline() + + n.comment("MWCC build (with UTF-8 to Shift JIS wrapper)") + n.rule( + name="mwcc_sjis", + command=mwcc_sjis_cmd, + description="MWCC $out", + depfile="$basefile.d", + deps="gcc", + ) + n.newline() + + n.comment("Host build") + n.variable("host_cflags", "-I include -Wno-trigraphs") + n.variable( + "host_cppflags", + "-std=c++98 -I include -fno-exceptions -fno-rtti -D_CRT_SECURE_NO_WARNINGS -Wno-trigraphs -Wno-c++11-extensions", + ) + n.rule( + name="host_cc", + command="clang $host_cflags -c -o $out $in", + description="CC $out", + ) + n.rule( + name="host_cpp", + command="clang++ $host_cppflags -c -o $out $in", + description="CXX $out", + ) + n.newline() + + ### + # Source files + ### + n.comment("Source files") + build_src_path = build_path / "src" + build_host_path = build_path / "host" + build_config_path = build_path / "config.json" + + def map_path(path): + return path.parent / (path.name + ".MAP") + + class LinkStep: + def __init__(self, config): + self.name = config["name"] + self.module_id = config["module_id"] + self.ldscript = config["ldscript"] + self.entry = config["entry"] + self.inputs = [] + + def add(self, obj): + self.inputs.append(obj) + + def output(self): + if self.module_id == 0: + return build_path / f"{self.name}.dol" + else: + return build_path / self.name / f"{self.name}.rel" + + def partial_output(self): + if self.module_id == 0: + return build_path / f"{self.name}.elf" + else: + return build_path / self.name / f"{self.name}.plf" + + def write(self, n): + n.comment(f"Link {self.name}") + if self.module_id == 0: + elf_path = build_path / f"{self.name}.elf" + dol_path = build_path / f"{self.name}.dol" + elf_ldflags = f"$ldflags -lcf {self.ldscript}" + if config.generate_map: + elf_map = map_path(elf_path) + elf_ldflags += f" -map {elf_map}" + else: + elf_map = None + n.build( + outputs=path(elf_path), + rule="link", + inputs=path(self.inputs), + implicit=path([self.ldscript, *mwld_implicit]), + implicit_outputs=path(elf_map), + variables={"ldflags": elf_ldflags}, + ) + n.build( + outputs=path(dol_path), + rule="elf2dol", + inputs=path(elf_path), + implicit=path(dtk), + ) + else: + preplf_path = build_path / self.name / f"{self.name}.preplf" + plf_path = build_path / self.name / f"{self.name}.plf" + preplf_ldflags = f"$ldflags -sdata 0 -sdata2 0 -r" + plf_ldflags = f"$ldflags -sdata 0 -sdata2 0 -m {self.entry} -r1 -strip_partial -lcf {self.ldscript}" + if config.generate_map: + preplf_map = map_path(preplf_path) + preplf_ldflags += f" -map {preplf_map}" + plf_map = map_path(plf_path) + plf_ldflags += f" -map {plf_map}" + else: + preplf_map = None + plf_map = None + n.build( + outputs=path(preplf_path), + rule="link", + inputs=path(self.inputs), + implicit=path(mwld_implicit), + implicit_outputs=path(preplf_map), + variables={"ldflags": preplf_ldflags}, + ) + n.build( + outputs=path(plf_path), + rule="link", + inputs=path(self.inputs), + implicit=path([self.ldscript, preplf_path, *mwld_implicit]), + implicit_outputs=path(plf_map), + variables={"ldflags": plf_ldflags}, + ) + n.newline() + + if build_config: + link_steps = [] + used_compiler_versions = set() + source_inputs = [] + host_source_inputs = [] + source_added = set() + + def add_unit(build_obj, link_step): + obj_path, obj_name = build_obj["object"], build_obj["name"] + result = config.find_object(obj_name) + if not result: + if config.warn_missing_config and not build_obj["autogenerated"]: + print(f"Missing configuration for {obj_name}") + link_step.add(obj_path) + return + + lib, obj = result + lib_name = lib["lib"] + + options = obj.options + completed = obj.completed + + unit_src_path = config.src_dir / options["source"] + if not unit_src_path.exists(): + if config.warn_missing_source: + print(f"Missing source file {unit_src_path}") + link_step.add(obj_path) + return + + mw_version = options["mw_version"] or lib["mw_version"] + cflags = options["cflags"] or lib["cflags"] + if type(cflags) is list: + cflags_str = " ".join(cflags) + else: + cflags_str = str(cflags) + used_compiler_versions.add(mw_version) + + base_object = Path(obj.name).with_suffix("") + src_obj_path = build_src_path / f"{base_object}.o" + src_base_path = build_src_path / base_object + + if src_obj_path not in source_added: + source_added.add(src_obj_path) + + n.comment(f"{obj_name}: {lib_name} (linked {completed})") + n.build( + outputs=path(src_obj_path), + rule="mwcc_sjis" if options["shiftjis"] else "mwcc", + inputs=path(unit_src_path), + variables={ + "mw_version": path(Path(mw_version)), + "cflags": cflags_str, + "basedir": os.path.dirname(src_base_path), + "basefile": path(src_base_path), + }, + implicit=path( + mwcc_sjis_implicit if options["shiftjis"] else mwcc_implicit + ), + ) + + if lib["host"]: + host_obj_path = build_host_path / f"{base_object}.o" + host_base_path = build_host_path / base_object + n.build( + outputs=path(host_obj_path), + rule="host_cc" if unit_src_path.suffix == ".c" else "host_cpp", + inputs=path(unit_src_path), + variables={ + "basedir": os.path.dirname(host_base_path), + "basefile": path(host_base_path), + }, + ) + if options["add_to_all"]: + host_source_inputs.append(host_obj_path) + n.newline() + + if options["add_to_all"]: + source_inputs.append(src_obj_path) + + if completed: + obj_path = src_obj_path + link_step.add(obj_path) + + # Add DOL link step + link_step = LinkStep(build_config) + for unit in build_config["units"]: + add_unit(unit, link_step) + link_steps.append(link_step) + + if config.build_rels: + # Add REL link steps + for module in build_config["modules"]: + module_link_step = LinkStep(module) + for unit in module["units"]: + add_unit(unit, module_link_step) + link_steps.append(module_link_step) + n.newline() + + # Check if all compiler versions exist + for mw_version in used_compiler_versions: + mw_path = compilers / mw_version / "mwcceppc.exe" + if config.compilers_path and not os.path.exists(mw_path): + sys.exit(f"Compiler {mw_path} does not exist") + + # Check if linker exists + mw_path = compilers / config.linker_version / "mwldeppc.exe" + if config.compilers_path and not os.path.exists(mw_path): + sys.exit(f"Linker {mw_path} does not exist") + + ### + # Link + ### + for step in link_steps: + step.write(n) + n.newline() + + ### + # Generate RELs + ### + rel_outputs = list( + map( + lambda step: step.output(), + filter(lambda step: step.module_id != 0, link_steps), + ) + ) + if len(rel_outputs) > 0: + n.comment("Generate RELs") + n.build( + outputs=path(rel_outputs), + rule="makerel", + inputs=path(list(map(lambda step: step.partial_output(), link_steps))), + implicit=path([dtk, config.config_path]), + variables={"config": path(config.config_path)}, + ) + n.newline() + + ### + # Helper rule for building all source files + ### + n.comment("Build all source files") + n.build( + outputs="all_source", + rule="phony", + inputs=path(source_inputs), + ) + n.newline() + + ### + # Helper rule for building all source files, with a host compiler + ### + n.comment("Build all source files with a host compiler") + n.build( + outputs="all_source_host", + rule="phony", + inputs=path(host_source_inputs), + ) + n.newline() + + ### + # Check hash + ### + n.comment("Check hash") + ok_path = build_path / "ok" + quiet = "-q " if len(link_steps) > 3 else "" + n.rule( + name="check", + command=f"{dtk} shasum {quiet} -c $in -o $out", + description="CHECK $in", + ) + n.build( + outputs=path(ok_path), + rule="check", + inputs=path(config.check_sha_path), + implicit=path([dtk, *map(lambda step: step.output(), link_steps)]), + ) + n.newline() + + ### + # Calculate progress + ### + n.comment("Calculate progress") + progress_path = build_path / "progress.json" + n.rule( + name="progress", + command=f"$python {configure_script} $configure_args progress", + description="PROGRESS", + ) + n.build( + outputs=path(progress_path), + rule="progress", + implicit=path([ok_path, configure_script, python_lib, config.config_path]), + ) + + ### + # Helper tools + ### + # TODO: make these rules work for RELs too + dol_link_step = link_steps[0] + dol_elf_path = dol_link_step.partial_output() + n.comment("Check for mismatching symbols") + n.rule( + name="dol_diff", + command=f"{dtk} -L error dol diff $in", + description=f"DIFF {dol_elf_path}", + ) + n.build( + inputs=path([config.config_path, dol_elf_path]), + outputs="dol_diff", + rule="dol_diff", + ) + n.build( + outputs="diff", + rule="phony", + inputs="dol_diff", + ) + n.newline() + + n.comment("Apply symbols from linked ELF") + n.rule( + name="dol_apply", + command=f"{dtk} dol apply $in", + description=f"APPLY {dol_elf_path}", + ) + n.build( + inputs=path([config.config_path, dol_elf_path]), + outputs="dol_apply", + rule="dol_apply", + implicit=path([ok_path]), + ) + n.build( + outputs="apply", + rule="phony", + inputs="dol_apply", + ) + n.newline() + + ### + # Split DOL + ### + n.comment("Split DOL into relocatable objects") + n.rule( + name="split", + command=f"{dtk} dol split $in $out_dir", + description="SPLIT $in", + depfile="$out_dir/dep", + deps="gcc", + ) + n.build( + inputs=path(config.config_path), + outputs=path(build_config_path), + rule="split", + implicit=path(dtk), + variables={"out_dir": path(build_path)}, + ) + n.newline() + + ### + # Regenerate on change + ### + n.comment("Reconfigure on change") + n.rule( + name="configure", + command=f"$python {configure_script} $configure_args", + generator=True, + description=f"RUN {configure_script}", + ) + n.build( + outputs="build.ninja", + rule="configure", + implicit=path( + [ + build_config_path, + configure_script, + python_lib, + Path(python_lib_dir) / "ninja_syntax.py", + ] + ), + ) + n.newline() + + ### + # Default rule + ### + n.comment("Default rule") + if build_config: + n.default(path(progress_path)) + else: + n.default(path(build_config_path)) + + # Write build.ninja + with open("build.ninja", "w", encoding="utf-8") as f: + f.write(out.getvalue()) + out.close() + + +# Generate objdiff.json +def generate_objdiff_config(config, build_config): + if not build_config: + return + + objdiff_config = { + "min_version": "0.4.3", + "custom_make": "ninja", + "build_target": False, + "watch_patterns": [ + "*.c", + "*.cp", + "*.cpp", + "*.h", + "*.hpp", + "*.py", + "*.yml", + "*.txt", + "*.json", + ], + "units": [], + } + + build_path = config.out_path() + + def add_unit(build_obj, module_name): + if build_obj["autogenerated"]: + # Skip autogenerated objects + return + + obj_path, obj_name = build_obj["object"], build_obj["name"] + base_object = Path(obj_name).with_suffix("") + unit_config = { + "name": unix_str(Path(module_name) / base_object), + "target_path": unix_str(obj_path), + } + + result = config.find_object(obj_name) + if not result: + objdiff_config["units"].append(unit_config) + return + + lib, obj = result + unit_src_path = config.src_dir / obj.options["source"] + if not unit_src_path.exists(): + objdiff_config["units"].append(unit_config) + return + + cflags = obj.options["cflags"] or lib["cflags"] + src_obj_path = build_path / "src" / f"{base_object}.o" + + reverse_fn_order = False + if type(cflags) is list: + for flag in cflags: + if not flag.startswith("-inline "): + continue + for value in flag.split(" ")[1].split(","): + if value == "deferred": + reverse_fn_order = True + elif value == "nodeferred": + reverse_fn_order = False + + unit_config["base_path"] = unix_str(src_obj_path) + unit_config["reverse_fn_order"] = reverse_fn_order + unit_config["complete"] = obj.completed + objdiff_config["units"].append(unit_config) + + # Add DOL units + for unit in build_config["units"]: + add_unit(unit, build_config["name"]) + + # Add REL units + for module in build_config["modules"]: + for unit in module["units"]: + add_unit(unit, module["name"]) + + # Write objdiff.json + with open("objdiff.json", "w", encoding="utf-8") as w: + json.dump(objdiff_config, w, indent=4) + + +# Calculate, print and write progress to progress.json +def calculate_progress(config): + out_path = config.out_path() + build_config = load_build_config(config, out_path / "config.json") + if not build_config: + return + + class ProgressUnit: + def __init__(self, name): + self.name = name + self.code_total = 0 + self.code_progress = 0 + self.data_total = 0 + self.data_progress = 0 + self.objects_progress = 0 + self.objects_total = 0 + self.objects = set() + + def add(self, build_obj): + self.code_total += build_obj["code_size"] + self.data_total += build_obj["data_size"] + + # Avoid counting the same object in different modules twice + include_object = build_obj["name"] not in self.objects + if include_object: + self.objects.add(build_obj["name"]) + self.objects_total += 1 + + if build_obj["autogenerated"]: + # Skip autogenerated objects + return + + result = config.find_object(build_obj["name"]) + if not result: + return + + _, obj = result + if not obj.completed: + return + + self.code_progress += build_obj["code_size"] + self.data_progress += build_obj["data_size"] + if include_object: + self.objects_progress += 1 + + def code_frac(self): + return self.code_progress / self.code_total + + def data_frac(self): + return self.data_progress / self.data_total + + # Add DOL units + all_progress = ProgressUnit("All") if config.progress_all else None + dol_progress = ProgressUnit("DOL") + for unit in build_config["units"]: + if all_progress: + all_progress.add(unit) + dol_progress.add(unit) + + # Add REL units + rels_progress = ProgressUnit("Modules") if config.progress_modules else None + modules_progress = [] + for module in build_config["modules"]: + progress = ProgressUnit(module["name"]) + modules_progress.append(progress) + for unit in module["units"]: + if all_progress: + all_progress.add(unit) + if rels_progress: + rels_progress.add(unit) + progress.add(unit) + + # Print human-readable progress + print("Progress:") + + def print_category(unit): + code_frac = unit.code_frac() + data_frac = unit.data_frac() + print( + f" {unit.name}: {code_frac:.2%} code, {data_frac:.2%} data ({unit.objects_progress} / {unit.objects_total} files)" + ) + print(f" Code: {unit.code_progress} / {unit.code_total} bytes") + print(f" Data: {unit.data_progress} / {unit.data_total} bytes") + + if all_progress: + print_category(all_progress) + print_category(dol_progress) + module_count = len(build_config["modules"]) + if module_count > 0: + print_category(rels_progress) + if config.progress_each_module: + for progress in modules_progress: + print_category(progress) + + # Generate and write progress.json + progress_json = {} + + def add_category(category, unit): + progress_json[category] = { + "code": unit.code_progress, + "code/total": unit.code_total, + "data": unit.data_progress, + "data/total": unit.data_total, + } + + if all_progress: + add_category("all", all_progress) + add_category("dol", dol_progress) + if len(build_config["modules"]) > 0: + if rels_progress: + add_category("modules", rels_progress) + if config.progress_each_module: + for progress in modules_progress: + add_category(progress.name, progress) + with open(out_path / "progress.json", "w", encoding="utf-8") as w: + json.dump(progress_json, w, indent=4) \ No newline at end of file diff --git a/tools/transform_dep.py b/tools/transform_dep.py new file mode 100644 index 0000000..86bd2ec --- /dev/null +++ b/tools/transform_dep.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 + +### +# Transforms .d files, converting Windows paths to Unix paths. +# Allows usage of the mwcc -MMD flag on platforms other than Windows. +# +# Usage: +# python3 tools/transform_dep.py build/src/file.d build/src/file.d +# +# If changes are made, please submit a PR to +# https://github.com/encounter/dtk-template +### + +import argparse +import os +from platform import uname + +wineprefix = os.path.join(os.environ["HOME"], ".wine") +if "WINEPREFIX" in os.environ: + wineprefix = os.environ["WINEPREFIX"] +winedevices = os.path.join(wineprefix, "dosdevices") + + +def in_wsl() -> bool: + return "microsoft-standard" in uname().release + + +def import_d_file(in_file) -> str: + out_text = "" + + with open(in_file) as file: + for idx, line in enumerate(file): + if idx == 0: + if line.endswith(" \\\n"): + out_text += line[:-3].replace("\\", "/") + " \\\n" + else: + out_text += line.replace("\\", "/") + else: + suffix = "" + if line.endswith(" \\\n"): + suffix = " \\" + path = line.lstrip()[:-3] + else: + path = line.strip() + # lowercase drive letter + path = path[0].lower() + path[1:] + if path[0] == "z": + # shortcut for z: + path = path[2:].replace("\\", "/") + elif in_wsl(): + path = path[0:1] + path[2:] + path = os.path.join("/mnt", path.replace("\\", "/")) + else: + # use $WINEPREFIX/dosdevices to resolve path + path = os.path.realpath( + os.path.join(winedevices, path.replace("\\", "/")) + ) + out_text += "\t" + path + suffix + "\n" + + return out_text + + +def main(): + parser = argparse.ArgumentParser( + description="""Transform a .d file from Wine paths to normal paths""" + ) + parser.add_argument( + "d_file", + help="""Dependency file in""", + ) + parser.add_argument( + "d_file_out", + help="""Dependency file out""", + ) + args = parser.parse_args() + + output = import_d_file(args.d_file) + + with open(args.d_file_out, "w", encoding="UTF-8") as f: + f.write(output) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tools/upload_progress.py b/tools/upload_progress.py new file mode 100644 index 0000000..673bb3d --- /dev/null +++ b/tools/upload_progress.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 + +### +# Uploads progress information to https://github.com/decompals/frogress. +# +# Usage: +# python3 tools/upload_progress.py -b https://progress.decomp.club/ -p [project] -v [version] build/[version]/progress.json +# +# If changes are made, please submit a PR to +# https://github.com/encounter/dtk-template +### + +import argparse +import json +import os +import requests +import subprocess +import sys + + +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(args: argparse.Namespace) -> str: + url_components = [args.base_url.rstrip("/"), "data"] + + for arg in [args.project, args.version]: + if arg != "": + url_components.append(arg) + + return str.join("/", url_components) + "/" + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Upload progress information.") + parser.add_argument("-b", "--base_url", help="API base URL", required=True) + parser.add_argument("-a", "--api_key", help="API key (env var PROGRESS_API_KEY)") + parser.add_argument("-p", "--project", help="Project slug", required=True) + parser.add_argument("-v", "--version", help="Version slug", required=True) + parser.add_argument("input", help="Progress JSON input") + + args = parser.parse_args() + api_key = args.api_key or os.environ.get("PROGRESS_API_KEY") + if not api_key: + raise "API key required" + url = generate_url(args) + + entries = [] + with open(args.input, "r") as f: + data = json.load(f) + entries.append( + { + "timestamp": get_git_commit_timestamp(), + "git_hash": get_git_commit_sha(), + "categories": data, + } + ) + + print("Publishing entry to", url) + json.dump(entries[0], sys.stdout, indent=4) + print() + r = requests.post(url, json={ + "api_key": api_key, + "entries": entries, + }) + r.raise_for_status() + print("Done!") \ No newline at end of file