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()