orca/scripts/build_runtime.py

303 lines
9.3 KiB
Python

import argparse
from datetime import datetime
import glob
import os
import platform
import urllib.request
import shutil
import subprocess
from zipfile import ZipFile
import checksum
from log import *
from utils import pushd, removeall
ANGLE_VERSION = "2023-07-05"
def attach_build_runtime(subparsers):
build = subparsers.add_parser("build-runtime", help="TODO")
build.add_argument("--release", action="store_true", help="compile Orca in release mode (default is debug)")
build.set_defaults(func=shellish(build_runtime))
def build_runtime(args):
ensure_programs()
ensure_angle()
build_milepost("lib", args.release)
def build_milepost(target, release):
print("Building milepost...")
with pushd("milepost"):
os.makedirs("bin", exist_ok=True)
os.makedirs("lib", exist_ok=True)
os.makedirs("resources", exist_ok=True)
if target == "lib":
if platform.system() == "Windows":
build_milepost_lib_win(release)
elif platform.system() == "Darwin":
build_milepost_lib_mac(release)
else:
log_error(f"can't build milepost for unknown platform '{platform.system()}'")
exit(1)
elif target == "test":
with pushd("examples/test_app"):
# TODO?
subprocess.run(["./build.sh"])
elif target == "clean":
removeall("bin")
else:
log_error(f"unrecognized milepost target '{target}'")
exit(1)
def build_milepost_lib_win(release):
# TODO(ben): delete embed_text.py
embed_text_glsl("src\\glsl_shaders.h", "glsl_", [
"src\\glsl_shaders\\common.glsl",
"src\\glsl_shaders\\blit_vertex.glsl",
"src\\glsl_shaders\\blit_fragment.glsl",
"src\\glsl_shaders\\path_setup.glsl",
"src\\glsl_shaders\\segment_setup.glsl",
"src\\glsl_shaders\\backprop.glsl",
"src\\glsl_shaders\\merge.glsl",
"src\\glsl_shaders\\raster.glsl",
])
includes = [
"/I", "src",
"/I", "src/util",
"/I", "src/platform",
"/I", "ext",
"/I", "ext/angle_headers",
]
libs = [
"user32.lib",
"opengl32.lib",
"gdi32.lib",
"shcore.lib",
"delayimp.lib",
"dwmapi.lib",
"comctl32.lib",
"ole32.lib",
"shell32.lib",
"shlwapi.lib",
"/LIBPATH:./bin",
"libEGL.dll.lib",
"libGLESv2.dll.lib",
"/DELAYLOAD:libEGL.dll",
"/DELAYLOAD:libGLESv2.dll",
]
# TODO(ben): check for cl
subprocess.run([
"cl",
"/we4013", "/Zi", "/Zc:preprocessor",
"/DMP_BUILD_DLL",
"/std:c11",
*includes,
"src/milepost.c", "/Fo:bin/milepost.o",
"/LD", "/link",
"/MANIFEST:EMBED", "/MANIFESTINPUT:src/win32_manifest.xml",
*libs,
"/OUT:bin/milepost.dll",
"/IMPLIB:bin/milepost.dll.lib",
], check=True)
def build_milepost_lib_mac(release):
sdk_dir = "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk"
flags = ["-mmacos-version-min=10.15.4", "-maes"]
cflags = ["-std=c11"]
debug_flags = ["-O3"] if release else ["-g", "-DDEBUG", "-DLOG_COMPILE_DEBUG"]
ldflags = [f"-L{sdk_dir}/usr/lib", f"-F{sdk_dir}/System/Library/Frameworks/"]
includes = ["-Isrc", "-Isrc/util", "-Isrc/platform", "-Iext", "-Iext/angle_headers"]
# compile metal shader
subprocess.run([
"xcrun", "-sdk", "macosx", "metal",
# TODO: shaderFlagParam
"-fno-fast-math", "-c",
"-o", "lib/mtl_renderer.air",
"src/mtl_renderer.metal",
], check=True)
subprocess.run([
"xcrun", "-sdk", "macosx", "metallib",
"-o", "lib/mtl_renderer.metallib",
"lib/mtl_renderer.air",
], check=True)
# compile milepost. We use one compilation unit for all C code, and one
# compilation unit for all Objective-C code
subprocess.run([
"clang",
*debug_flags, "-c",
"-o", "bin/milepost_c.o",
*cflags, *flags, *includes,
"src/milepost.c"
], check=True)
subprocess.run([
"clang",
*debug_flags, "-c",
"-o", "bin/milepost_objc.o",
*flags, *includes,
"src/milepost.m"
], check=True)
# build dynamic library
subprocess.run([
"ld",
*ldflags, "-dylib",
"-o", "bin/libmilepost.dylib",
"bin/milepost_c.o", "bin/milepost_objc.o",
"-Llib", "-lc",
"-framework", "Carbon", "-framework", "Cocoa", "-framework", "Metal", "-framework", "QuartzCore",
"-weak-lEGL", "-weak-lGLESv2",
], check=True)
# change dependent libs path to @rpath
subprocess.run([
"install_name_tool",
"-change", "./libEGL.dylib", "@rpath/libEGL.dylib",
"bin/libmilepost.dylib",
], check=True)
subprocess.run([
"install_name_tool",
"-change", "./libGLESv2.dylib", "@rpath/libGLESv2.dylib",
"bin/libmilepost.dylib",
], check=True)
# add executable path to rpath. Client executable can still add its own
# rpaths if needed, e.g. @executable_path/libs/ etc.
subprocess.run([
"install_name_tool",
"-id", "@rpath/libmilepost.dylib",
"bin/libmilepost.dylib",
], check=True)
def ensure_programs():
try:
subprocess.run(["clang", "-v"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except FileNotFoundError:
msg = log_error("clang was not found on your system.")
if platform.system() == "Windows":
msg.more("We recommend installing clang via the Visual Studio installer.")
# TODO(ben): Link to the Visual Studio download page (I have no internet right now)
elif platform.system() == "Darwin":
msg.more("Run the following to install it:")
msg.more()
msg.more(" brew install llvm")
msg.more()
exit(1)
# TODO(ben): Check for xcode command line tools
def ensure_angle():
checkfiles = None
if platform.system() == "Windows":
checkfiles = [
"milepost/lib/libEGL.dll",
"milepost/lib/libGLESv2.dll",
]
elif platform.system() == "Darwin":
if platform.machine() == "arm64":
log_warning(f"automated ANGLE builds are not yet available for Apple silicon")
return
checkfiles = [
"milepost/lib/libEGL.dylib",
"milepost/lib/libGLESv2.dylib",
]
if checkfiles is None:
log_warning("could not verify if the correct version of ANGLE is present; the build will probably fail.")
return
angle_exists = True
for file in checkfiles:
key = file[0]
filepath = file[1]
if not os.path.isfile(filepath):
angle_exists = False
break
if not checksum.checkfile(key, filepath):
angle_exists = False
log_warning("wrong version of ANGLE libraries installed")
break
if not angle_exists:
download_angle()
def download_angle():
print("Downloading ANGLE...")
if platform.system() == "Windows":
build = "win"
extension = "dll"
elif platform.system() == "Darwin":
# TODO(ben): make universal dylibs
build = "macos-12"
extension = "dylib"
else:
log_error(f"could not automatically download ANGLE for unknown platform {platform.system()}")
return
os.makedirs("scripts/files", exist_ok=True)
filename = f"angle-{build}-{ANGLE_VERSION}.zip"
filepath = f"scripts/files/{filename}"
url = f"https://github.com/HandmadeNetwork/build-angle/releases/download/{ANGLE_VERSION}/{filename}"
with urllib.request.urlopen(url) as response:
with open(filepath, "wb") as out:
shutil.copyfileobj(response, out)
if not checksum.checkfile(filepath):
log_error(f"ANGLE download did not match checksum")
exit(1)
with ZipFile(filepath, "r") as anglezip:
anglezip.extractall(path="scripts/files")
for filepath in glob.glob(f"scripts/files/angle/bin/*.{extension}"):
shutil.copy(filepath, "milepost/lib")
def embed_text_glsl(outputpath, prefix, shaders):
output = open(outputpath, "w")
output.write("/*********************************************************************\n")
output.write("*\n")
output.write("*\tfile: %s\n" % os.path.basename(outputpath))
output.write("*\tnote: string literals auto-generated by build_runtime.py\n")
output.write("*\tdate: %s\n" % datetime.now().strftime("%d/%m%Y"))
output.write("*\n")
output.write("**********************************************************************/\n")
outSymbol = (os.path.splitext(os.path.basename(outputpath))[0]).upper()
output.write("#ifndef __%s_H__\n" % outSymbol)
output.write("#define __%s_H__\n" % outSymbol)
output.write("\n\n")
for fileName in shaders:
f = open(fileName, "r")
lines = f.read().splitlines()
output.write("//NOTE: string imported from %s\n" % fileName)
stringName = os.path.splitext(os.path.basename(fileName))[0]
output.write(f"const char* {prefix}{stringName} = ")
for line in lines:
output.write("\n\"%s\\n\"" % line)
output.write(";\n\n")
f.close()
output.write("#endif // __%s_H__\n" % outSymbol)
output.close()