import argparse 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() == "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_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 = [ ["libEGL-win", "milepost/lib/libEGL.dll"], ["libGLESv2-win", "milepost/lib/libGLESv2.dll"], ] elif platform.system() == "Darwin": # TODO(ben): Do we need specific builds of ANGLE for each macOS version? if platform.machine() == "arm64": checkfiles = [ ["libEGL-mac-arm64", "milepost/lib/libEGL.dylib"], ["libGLESv2-mac-arm64", "milepost/lib/libGLESv2.dylib"], ] else: checkfiles = [ ["libEGL-mac-x64", "milepost/lib/libEGL.dylib"], ["libGLESv2-mac-x64", "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): log_error("wrong version of ANGLE libraries installed") exit(1) if not angle_exists: download_angle() def download_angle(): print("Downloading ANGLE...") if platform.system() == "Windows": build = "win" checksumkey = "angle.zip-win" extension = "dll" elif platform.system() == "Darwin": extension = "dylib" build = "macos-12" checksumkey = "angle.zip-mac" # TODO(ben): make universal dylibs if platform.machine() == "arm64": log_error(f"automated ANGLE builds are not yet available for Apple silicon") return else: log_error(f"could not automatically download ANGLE for unknown platform {platform.system()}") return url = f"https://github.com/HandmadeNetwork/build-angle/releases/download/{ANGLE_VERSION}/angle-{build}-{ANGLE_VERSION}.zip" with urllib.request.urlopen(url) as response: os.makedirs("scripts/files", exist_ok=True) with open("scripts/files/angle.zip", "wb") as out: shutil.copyfileobj(response, out) if not checksum.checkfile(checksumkey, "scripts/files/angle.zip"): log_error(f"ANGLE download did not match checksum") exit(1) with ZipFile("scripts/files/angle.zip", "r") as anglezip: anglezip.extractall(path="scripts/files") for filepath in glob.glob(f"scripts/files/angle/bin/*.{extension}"): shutil.copy(filepath, "milepost/lib")