summaryrefslogtreecommitdiff
path: root/tools/setup.py
diff options
context:
space:
mode:
authorLéo Lam <leo@leolam.fr>2021-07-23 00:35:03 +0200
committerLéo Lam <leo@leolam.fr>2021-07-23 12:40:49 +0200
commit3fc83fd0512226e4e28e4977d55d9371088e71a9 (patch)
treed7d791f2cb056c7d62f3db745785a66abfa5ac11 /tools/setup.py
parentfc04f184c8d583e915cc4131e974e7f85df0377f (diff)
Streamline project setup by automating NSO conversion (if needed)
Diffstat (limited to 'tools/setup.py')
-rwxr-xr-xtools/setup.py117
1 files changed, 112 insertions, 5 deletions
diff --git a/tools/setup.py b/tools/setup.py
index 773f591a..227fd84b 100755
--- a/tools/setup.py
+++ b/tools/setup.py
@@ -1,5 +1,7 @@
#!/usr/bin/env python3
+import argparse
+import hashlib
import platform
from pathlib import Path
import subprocess
@@ -12,14 +14,112 @@ ROOT = Path(__file__).parent.parent
def fail(error: str):
- print(error)
+ print(">>> " + error)
sys.exit(1)
+def _get_tool_binary_path():
+ base = ROOT / "tools" / "nx-decomp-tools-binaries"
+ system = platform.system()
+ if system == "Linux":
+ return str(base / "linux") + "/"
+ if system == "Darwin":
+ return str(base / "macos") + "/"
+ return ""
+
+
+def _convert_nso_to_elf(nso_path: Path):
+ print(">>>> converting NSO to ELF...")
+ binpath = _get_tool_binary_path()
+ subprocess.check_call([binpath + "nx2elf", str(nso_path)])
+
+
+def _decompress_nso(nso_path: Path, dest_path: Path):
+ print(">>>> decompressing NSO...")
+ binpath = _get_tool_binary_path()
+ subprocess.check_call([binpath + "hactool", "-tnso",
+ "--uncompressed=" + str(dest_path), str(nso_path)])
+
+
+def _download_v160_to_v150_patch(dest: Path):
+ print(">>>> downloading patch...")
+ urllib.request.urlretrieve("https://s.botw.link/v150_downgrade/v160_to_v150.patch", dest)
+
+
+def _apply_xdelta3_patch(input: Path, patch: Path, dest: Path):
+ print(">>>> applying patch...")
+ try:
+ subprocess.check_call(["xdelta3", "-d", "-s", str(input), str(patch), str(dest)])
+ except FileNotFoundError:
+ fail("error: install xdelta3 and try again")
+
+
+def prepare_executable(original_nso: Path):
+ COMPRESSED_V150_HASH = "898dc199301f7c419be5144bb5cb27e2fc346e22b27345ba3fb40c0060c2baf8"
+ UNCOMPRESSED_V150_HASH = "d9fa308d0ee7c0ab081c66d987523385e1afe06f66731bbfa32628438521c106"
+ COMPRESSED_V160_HASH = "15cfca7b89348956f85d945fade2e215a6af5991ed1071e181f97ca72f7ae20b"
+ UNCOMPRESSED_V160_HASH = "8a2fc8b1111a35a76fd2d53a8670599da4a7a9706a3d91215d30fd62149f00c1"
+
+ # The uncompressed v1.5.0 main NSO.
+ TARGET_HASH = UNCOMPRESSED_V150_HASH
+ TARGET_PATH = ROOT / "data" / "main.nso"
+ TARGET_ELF_PATH = ROOT / "data" / "main.elf"
+
+ if TARGET_PATH.is_file() and hashlib.sha256(TARGET_PATH.read_bytes()).hexdigest() == TARGET_HASH and TARGET_ELF_PATH.is_file():
+ print(">>> NSO is already set up")
+ return
+
+ if not original_nso.is_file():
+ fail(f"{original_nso} is not a file")
+
+ nso_data = original_nso.read_bytes()
+ nso_hash = hashlib.sha256(nso_data).hexdigest()
+
+ if nso_hash == UNCOMPRESSED_V150_HASH:
+ print(">>> found uncompressed 1.5.0 NSO")
+ TARGET_PATH.write_bytes(nso_data)
+
+ elif nso_hash == COMPRESSED_V150_HASH:
+ print(">>> found compressed 1.5.0 NSO")
+ _decompress_nso(original_nso, TARGET_PATH)
+
+ elif nso_hash == UNCOMPRESSED_V160_HASH:
+ print(">>> found uncompressed 1.6.0 NSO")
+
+ with tempfile.TemporaryDirectory() as tmpdir:
+ patch_path = Path(tmpdir) / "patch"
+ _download_v160_to_v150_patch(patch_path)
+ _apply_xdelta3_patch(original_nso, patch_path, TARGET_PATH)
+
+ elif nso_hash == COMPRESSED_V160_HASH:
+ print(">>> found compressed 1.6.0 NSO")
+
+ with tempfile.TemporaryDirectory() as tmpdir:
+ patch_path = Path(tmpdir) / "patch"
+ decompressed_nso_path = Path(tmpdir) / "v160.nso"
+
+ _decompress_nso(original_nso, decompressed_nso_path)
+ _download_v160_to_v150_patch(patch_path)
+ _apply_xdelta3_patch(decompressed_nso_path, patch_path, TARGET_PATH)
+
+ else:
+ fail(f"unknown executable: {nso_hash}")
+
+ if not TARGET_PATH.is_file():
+ fail("internal error while preparing executable (missing NSO); please report")
+ if hashlib.sha256(TARGET_PATH.read_bytes()).hexdigest() != TARGET_HASH:
+ fail("internal error while preparing executable (wrong NSO hash); please report")
+
+ _convert_nso_to_elf(TARGET_PATH)
+
+ if not TARGET_ELF_PATH.is_file():
+ fail("internal error while preparing executable (missing ELF); please report")
+
+
def set_up_compiler():
compiler_dir = ROOT / "toolchain" / "clang"
if compiler_dir.is_dir():
- print("clang is already set up: nothing to do")
+ print(">>> clang is already set up: nothing to do")
return
system = platform.system()
@@ -55,12 +155,12 @@ def set_up_compiler():
url: str = build_info["url"]
dir_name: str = build_info["dir_name"]
- print(f"downloading Clang from {url}...")
+ print(f">>> downloading Clang from {url}...")
with tempfile.TemporaryDirectory() as tmpdir:
path = tmpdir + "/" + url.split("/")[-1]
urllib.request.urlretrieve(url, path)
- print(f"extracting Clang...")
+ print(f">>> extracting Clang...")
with tarfile.open(path) as f:
f.extractall(compiler_dir.parent)
(compiler_dir.parent / dir_name).rename(compiler_dir)
@@ -71,7 +171,7 @@ def set_up_compiler():
def create_build_dir():
build_dir = ROOT / "build"
if build_dir.is_dir():
- print("build directory already exists: nothing to do")
+ print(">>> build directory already exists: nothing to do")
return
subprocess.check_call(
@@ -80,6 +180,13 @@ def create_build_dir():
def main():
+ parser = argparse.ArgumentParser(
+ "setup.py", description="Set up the Breath of the Wild decompilation project")
+ parser.add_argument("original_nso", type=Path,
+ help="Path to the original NSO (1.5.0 or 1.6.0, compressed or not)")
+ args = parser.parse_args()
+
+ prepare_executable(args.original_nso)
set_up_compiler()
create_build_dir()