2021-01-14 12:22:38 +01:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
2021-01-14 04:56:02 +01:00
|
|
|
import re
|
2021-01-14 11:25:55 +01:00
|
|
|
import os, sys
|
2021-01-14 04:56:02 +01:00
|
|
|
from glob import glob
|
|
|
|
import ninja_syntax
|
|
|
|
from argparse import ArgumentParser
|
|
|
|
import asyncio
|
|
|
|
from subprocess import PIPE
|
|
|
|
|
2021-01-14 12:22:38 +01:00
|
|
|
sys.path.append(os.path.dirname(__file__) + "/tools/n64splat")
|
|
|
|
import split
|
2021-01-14 04:56:02 +01:00
|
|
|
|
2021-01-14 05:16:42 +01:00
|
|
|
INCLUDE_ASM_RE = re.compile(r"_INCLUDE_ASM\([^,]+, ([^,]+), ([^,)]+)") # note _ prefix
|
2021-01-14 04:56:02 +01:00
|
|
|
CPPFLAGS = "-Iinclude -Isrc -D _LANGUAGE_C -D _FINALROM -ffreestanding -DF3DEX_GBI_2 -D_MIPS_SZLONG=32"
|
|
|
|
|
|
|
|
def obj(path: str):
|
2021-01-14 11:25:55 +01:00
|
|
|
if not path.startswith("$builddir/"):
|
|
|
|
path = "$builddir/" + path
|
|
|
|
return path + ".o"
|
|
|
|
|
2021-01-14 12:13:33 +01:00
|
|
|
def read_splat(splat_config: str):
|
2021-01-14 11:25:55 +01:00
|
|
|
import argparse
|
|
|
|
import yaml
|
|
|
|
|
|
|
|
# Load config
|
|
|
|
with open(splat_config) as f:
|
|
|
|
config = yaml.safe_load(f.read())
|
|
|
|
|
|
|
|
options = config.get("options")
|
|
|
|
assert options.get("ld_o_replace_extension", True) == False
|
|
|
|
|
|
|
|
# Initialize segments
|
2021-01-14 12:22:38 +01:00
|
|
|
all_segments = split.initialize_segments(options, splat_config, config["segments"])
|
2021-01-14 11:25:55 +01:00
|
|
|
|
|
|
|
objects = set()
|
2021-01-14 12:13:33 +01:00
|
|
|
segments = {}
|
2021-01-14 11:25:55 +01:00
|
|
|
|
|
|
|
for segment in all_segments:
|
|
|
|
for subdir, path, obj_type, start in segment.get_ld_files():
|
2021-01-14 12:13:33 +01:00
|
|
|
path = subdir + "/" + path
|
2021-01-14 11:25:55 +01:00
|
|
|
|
2021-01-14 12:13:33 +01:00
|
|
|
objects.add(path)
|
|
|
|
segments[path] = segment
|
2021-01-14 11:25:55 +01:00
|
|
|
|
2021-01-14 12:13:33 +01:00
|
|
|
# note: `objects` lacks .o extensions
|
|
|
|
return objects, segments
|
2021-01-14 04:56:02 +01:00
|
|
|
|
|
|
|
async def shell(cmd: str):
|
|
|
|
async with task_sem:
|
|
|
|
proc = await asyncio.create_subprocess_shell(cmd, stdout=PIPE, stderr=PIPE)
|
|
|
|
stdout, stderr = await proc.communicate()
|
|
|
|
|
|
|
|
assert proc.returncode == 0, f"{cmd} failed: {stderr}"
|
|
|
|
|
2021-01-14 05:16:42 +01:00
|
|
|
return stdout.decode("utf-8"), stderr.decode("utf-8")
|
2021-01-14 04:56:02 +01:00
|
|
|
|
2021-01-14 11:25:55 +01:00
|
|
|
async def task(coro):
|
|
|
|
global num_tasks, num_tasks_done
|
|
|
|
|
|
|
|
await coro
|
|
|
|
|
|
|
|
num_tasks_done += 1
|
|
|
|
print(f"\r{(num_tasks_done / num_tasks) * 100:.0f}%", end="")
|
|
|
|
|
|
|
|
async def build_c_file(c_file: str):
|
2021-01-14 05:16:42 +01:00
|
|
|
# preprocess c_file, but simply put an _ in front of INCLUDE_ASM and SCRIPT
|
|
|
|
stdout, stderr = await shell(f"{cpp} {CPPFLAGS} '-DINCLUDE_ASM(...)=_INCLUDE_ASM(__VA_ARGS__)' '-DSCRIPT(...)=_SCRIPT(__VA_ARGS__)' {c_file} -o -")
|
2021-01-14 04:56:02 +01:00
|
|
|
|
2021-01-14 05:16:42 +01:00
|
|
|
# search for macro usage (note _ prefix)
|
|
|
|
uses_dsl = "_SCRIPT(" in stdout
|
2021-01-14 04:56:02 +01:00
|
|
|
|
|
|
|
s_deps = []
|
|
|
|
for line in stdout.splitlines():
|
2021-01-14 05:16:42 +01:00
|
|
|
if line.startswith("_INCLUDE_ASM"):
|
2021-01-14 04:56:02 +01:00
|
|
|
match = INCLUDE_ASM_RE.match(line)
|
|
|
|
if match:
|
|
|
|
s_deps.append("asm/nonmatchings/" + eval(match[1]) + "/" + match[2] + ".s")
|
|
|
|
|
|
|
|
# add build task to ninja
|
2021-01-14 11:25:55 +01:00
|
|
|
n.build(obj(c_file), "cc_dsl" if uses_dsl else "cc", c_file, implicit=s_deps)
|
2021-01-14 04:56:02 +01:00
|
|
|
|
2021-01-14 11:25:55 +01:00
|
|
|
def build_yay0_file(bin_file: str):
|
|
|
|
yay0_file = f"$builddir/{os.path.splitext(bin_file)[0]}.Yay0"
|
|
|
|
n.build(yay0_file, "yay0compress", bin_file, implicit=["tools/Yay0compress"])
|
|
|
|
build_bin_object(yay0_file)
|
2021-01-14 04:56:02 +01:00
|
|
|
|
2021-01-14 11:25:55 +01:00
|
|
|
def build_bin_object(bin_file: str):
|
|
|
|
n.build(obj(bin_file), "bin", bin_file)
|
2021-01-14 04:56:02 +01:00
|
|
|
|
2021-01-14 12:13:33 +01:00
|
|
|
def build_image(f: str, segment):
|
|
|
|
path, img_type, png = f.rsplit(".", 2)
|
|
|
|
out = "$builddir/" + path + "." + img_type + ".png"
|
|
|
|
|
|
|
|
flags = ""
|
|
|
|
if img_type != "palette":
|
|
|
|
if segment.flip_horizontal:
|
|
|
|
flags += "--flip-x"
|
|
|
|
if segment.flip_vertical:
|
|
|
|
flags += "--flip-y"
|
|
|
|
|
|
|
|
n.build(out, "img", path + ".png", variables={
|
|
|
|
"img_type": img_type,
|
|
|
|
"img_flags": flags,
|
|
|
|
})
|
|
|
|
build_bin_object(out)
|
2021-01-14 11:49:14 +01:00
|
|
|
|
2021-01-14 04:56:02 +01:00
|
|
|
async def main():
|
|
|
|
global n, cpp, task_sem, num_tasks, num_tasks_done
|
|
|
|
|
|
|
|
parser = ArgumentParser(description="Generates build.ninja")
|
|
|
|
parser.add_argument("--cpp", help="C preprocessor command")
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
# on macOS, /usr/bin/cpp defaults to clang rather than gcc (but we need gcc's)
|
|
|
|
if args.cpp is None and sys.platform == "darwin" and "Free Software Foundation" not in await shell("cpp --version")[0]:
|
|
|
|
print("error: use 'brew' to obtain GNU cpp and run again with '--cpp cpp-10'")
|
|
|
|
exit(1)
|
|
|
|
|
|
|
|
cpp = args.cpp or "cpp"
|
2021-01-14 11:49:14 +01:00
|
|
|
task_sem = asyncio.Semaphore(8)
|
2021-01-14 04:56:02 +01:00
|
|
|
|
2021-01-14 12:22:38 +01:00
|
|
|
# split assets
|
|
|
|
split.main(
|
|
|
|
"baserom.z64",
|
|
|
|
"tools/splat.yaml",
|
|
|
|
".",
|
|
|
|
[ "ld", "bin", "Yay0", "PaperMarioMapFS", "PaperMarioMessages", "img", "PaperMarioNpcSprites" ],
|
|
|
|
False,
|
|
|
|
False,
|
|
|
|
)
|
|
|
|
|
|
|
|
# generate build.ninja
|
2021-01-14 04:56:02 +01:00
|
|
|
n = ninja_syntax.Writer(open("build.ninja", "w"), width=120)
|
|
|
|
|
|
|
|
n.variable("builddir", "build")
|
|
|
|
n.variable("cross", "mips-linux-gnu-")
|
|
|
|
n.variable("python", sys.executable)
|
|
|
|
n.variable("os", "mac" if sys.platform == "darwin" else "linux")
|
|
|
|
n.variable("iconv", "tools/iconv.py UTF-8 SHIFT-JIS" if sys.platform == "darwin" else "iconv --from UTF-8 --to SHIFT-JIS")
|
|
|
|
n.variable("cppflags", f"{CPPFLAGS} -Wcomment")
|
|
|
|
n.variable("cflags", "-O2 -quiet -G 0 -mcpu=vr4300 -mfix4300 -mips3 -mgp32 -mfp32 -Wuninitialized -Wshadow")
|
|
|
|
n.newline()
|
|
|
|
|
|
|
|
n.rule("cc",
|
|
|
|
command=f"{cpp} $cppflags $in -o - | $iconv | tools/$os/cc1 $cflags -o - | tools/$os/mips-nintendo-nu64-as -EB -G 0 - -o $out",
|
|
|
|
description="cc $in",
|
|
|
|
depfile="$out.d",
|
|
|
|
deps="gcc")
|
|
|
|
n.rule("cc_dsl",
|
|
|
|
command=f"{cpp} $cppflags $in -o - | $python tools/compile_dsl_macros.py | $iconv | tools/$os/cc1 $cflags -o - | tools/$os/mips-nintendo-nu64-as -EB -G 0 - -o $out",
|
|
|
|
description="cc (with dsl) $in",
|
|
|
|
depfile="$out.d",
|
|
|
|
deps="gcc")
|
|
|
|
n.newline()
|
|
|
|
|
2021-01-14 11:25:55 +01:00
|
|
|
n.rule("yay0compress",
|
|
|
|
command=f"tools/Yay0compress $in $out",
|
|
|
|
description="compress $in")
|
|
|
|
n.newline()
|
|
|
|
|
|
|
|
n.rule("bin",
|
|
|
|
command="${cross}ld -r -b binary $in -o $out",
|
|
|
|
description="bin $in")
|
|
|
|
n.newline()
|
|
|
|
|
2021-01-14 11:49:14 +01:00
|
|
|
n.rule("as",
|
|
|
|
command="${cross}as -EB -march=vr4300 -mtune=vr4300 -Iinclude $in -o $out",
|
|
|
|
description="assemble $in")
|
|
|
|
n.newline()
|
2021-01-14 11:25:55 +01:00
|
|
|
|
2021-01-14 11:59:55 +01:00
|
|
|
n.rule("img",
|
|
|
|
command="$python tools/convert_image.py $img_type $in $out $img_flags",
|
|
|
|
description="image $in")
|
|
|
|
n.newline()
|
|
|
|
|
2021-01-14 12:13:33 +01:00
|
|
|
objects, segments = read_splat("tools/splat.yaml") # no .o extension!
|
2021-01-14 11:25:55 +01:00
|
|
|
c_files = (f for f in objects if f.endswith(".c")) # glob("src/**/*.c", recursive=True)
|
|
|
|
|
|
|
|
# TODO: build elf
|
|
|
|
|
2021-01-14 11:49:14 +01:00
|
|
|
for f in objects:
|
2021-01-14 12:13:33 +01:00
|
|
|
segment = segments[f]
|
|
|
|
|
2021-01-14 11:49:14 +01:00
|
|
|
if f.endswith(".c"):
|
|
|
|
continue # these are handled later
|
|
|
|
elif f.endswith(".Yay0"):
|
|
|
|
build_yay0_file(os.path.splitext(f)[0] + ".bin")
|
|
|
|
elif f.endswith(".bin"):
|
|
|
|
build_bin_object(f)
|
|
|
|
elif f.endswith(".data"):
|
|
|
|
n.build(obj(f), "as", "asm/" + f + ".s")
|
|
|
|
elif f.endswith(".rodata"):
|
|
|
|
n.build(obj(f), "as", "asm/" + f[2:] + ".s")
|
|
|
|
elif f.endswith(".s"):
|
|
|
|
n.build(obj(f), "as", f)
|
2021-01-14 11:59:55 +01:00
|
|
|
elif f.endswith(".png"):
|
2021-01-14 12:13:33 +01:00
|
|
|
build_image(f, segment)
|
2021-01-14 11:49:14 +01:00
|
|
|
else:
|
|
|
|
print("warning: dont know what to do with object " + f)
|
2021-01-14 11:25:55 +01:00
|
|
|
n.newline()
|
|
|
|
|
|
|
|
# build slow tasks concurrently
|
|
|
|
n.comment("c")
|
2021-01-14 04:56:02 +01:00
|
|
|
tasks = [
|
2021-01-14 11:25:55 +01:00
|
|
|
*(task(build_c_file(f)) for f in c_files),
|
2021-01-14 04:56:02 +01:00
|
|
|
]
|
|
|
|
num_tasks = len(tasks)
|
|
|
|
num_tasks_done = 0
|
|
|
|
await asyncio.gather(*tasks)
|
2021-01-14 11:25:55 +01:00
|
|
|
print("")
|
2021-01-14 04:56:02 +01:00
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
asyncio.run(main())
|