#!/usr/bin/python3 # # This python script builds integration from scratch. That includes # everything we need: # # - Generates BuildConfiguration.xml in integration repository # - Generates BuildConfiguration.xml in UnrealEngine repository # - Hardwires paths into Source/Integration/lpx-paths.hpp # - Generates Integration.uproject # - Generates Integration.code-workspace # - Applies patch to Unreal Engine source. # - Runs Setup.sh in the UnrealEngine repository # - Builds luprex # - Builds ShaderCompileWorker # - Builds Unreal Engine and Unreal Editor # - Builds integration # # Once this is all done, everything is ready to go. It is now possible # to start up the IDE and run luprex in the debugger. # # If you edit the code, you can run this python script again to # rebuild. It is always safe to use this script to rebuild after # editing anything. # # However, if you only edited C++ code and Unreal Build.cs files, then # it may be quicker and more convenient to rebuild from inside the # VSCODE IDE. Bear in mind that doing so only works if you only edited # the C++ code and the blueprint code. If you edited anything else, you # should rerun this python script. # import sys, os, json, shutil, subprocess, re, time from pathlib import Path # # Build the config table: a set of global constants that affect # just about everything. These values become global variables # in this script, they are also used as variables when expanding # template files. # CONFIG = {} if sys.platform == "windows": CONFIG["OS"] = "Windows" CONFIG["DLL"] = "dll" CONFIG["BAT"] = "bat" CONFIG["DOT_EXE"] = ".exe" CONFIG["USER"] = "Unknown" CONFIG["BUILD_BAT"] = "Build.bat" else: CONFIG["OS"] = "Linux" CONFIG["DLL"] = "so" CONFIG["BAT"] = "sh" CONFIG["DOT_EXE"] = "" CONFIG["USER"] = os.environ["USER"] CONFIG["BUILD_BAT"] = "Linux/Build.sh" CONFIG["INTEGRATION"] = os.path.dirname(os.path.abspath(sys.argv[0])) CONFIG["UNREALENGINE"] = os.path.join(os.path.dirname(CONFIG["INTEGRATION"]), "UnrealEngine") CONFIG["UE_BUILD_BAT"] = CONFIG["UNREALENGINE"] + "/Engine/Build/BatchFiles/" + CONFIG["BUILD_BAT"] + " -waitmutex" globals().update(CONFIG) # # Sanity check the INTEGRATION and UNREALENGINE paths. # if not os.path.isdir(f"{INTEGRATION}/Source/Integration"): sys.exit(f"Integration repository is not valid: {INTEGRATION}") if not os.path.isdir(f"{UNREALENGINE}/Engine/Source/Editor"): sys.exit(f"Integration repository is not valid: {UNREALENGINE}") # # This is the code for a simple json preprocessor that can # expand "for-each" loops and substitute variables. # JSON_VAR_REGEX = re.compile(r'\[([A-Z0-9_]+)\]') def expand_json(data, vars): if isinstance(data, dict): if "for-each" in data and "body" in data: body = data["body"] foreach = data["for-each"] return [ expand_json(body, vars | lvars) for lvars in foreach ] else: return { key: expand_json(value, vars) for key, value in data.items() } elif isinstance(data, list): return [ expand_json(item, vars) for item in data ] elif isinstance(data, str): return JSON_VAR_REGEX.sub(lambda m: str(vars.get(m.group(1), m.group(0))), data) else: return data # # Apply the json expander to a file on disk: read the source file, # apply the expander, write the result back out to disk. # def expand_json_file(sourcefile, outputfile): Path(outputfile).unlink(missing_ok=True) data = json.loads(Path(sourcefile).read_text()) expanded = expand_json(data, CONFIG) Path(outputfile).write_text(json.dumps(expanded, indent=4)) # # Because a single string is valid json, we can also use the json # expander to substitute variables in plain old text files. # def expand_text_file(sourcefile, outputfile): Path(outputfile).unlink(missing_ok=True) data = Path(sourcefile).read_text() expanded = expand_json(data, CONFIG) Path(outputfile).write_text(expanded) # # A simplified interface to subprocess.run # def shell(dir, cmd): start = time.time() subprocess.run(cmd, shell=True, check=True, cwd=dir) elapsed = time.time() - start if elapsed > 0.2: print("") print("TIMING: ", elapsed, " CMD:", cmd) print("") # # Change directory to an arbitrary subdirectory. Doing this enforces # the rule that we specify absolute paths for everything. # os.chdir(f"{INTEGRATION}/EnginePatches") # # Write BuildConfiguration.xml # BUILDCONFIG=Path(f"{INTEGRATION}/EnginePatches/BuildConfiguration{OS}.xml").read_text() Path(f"{INTEGRATION}/Saved/UnrealBuildTool").mkdir(parents=True, exist_ok=True) Path(f"{UNREALENGINE}/Engine/Saved/UnrealBuildTool").mkdir(parents=True, exist_ok=True) Path(f"{INTEGRATION}/Saved/UnrealBuildTool/BuildConfiguration.xml").unlink(missing_ok=True) Path(f"{UNREALENGINE}/Engine/Saved/UnrealBuildTool/BuildConfiguration.xml").unlink(missing_ok=True) Path(f"{INTEGRATION}/Saved/UnrealBuildTool/BuildConfiguration.xml").write_text(BUILDCONFIG) Path(f"{UNREALENGINE}/Engine/Saved/UnrealBuildTool/BuildConfiguration.xml").write_text(BUILDCONFIG) # # Write lpx-paths.hpp. # Path(f"{INTEGRATION}/Source/Integration/lpx-paths.hpp").unlink(missing_ok=True) Path(f"{INTEGRATION}/Source/Integration/lpx-paths.hpp").write_text(f""" #define LUPREX_DLL_PATH "{INTEGRATION}/luprex/build/{OS}/luprexlib.{DLL}" #define LUPREX_ROOT_PATH "{INTEGRATION}/luprex" """) # # Apply patch to the unreal engine source. Check out HEAD version of # affected sourcefiles before applying patch. # print("Applying patch to Unreal Engine...") PATCH_LINES = Path(f"{INTEGRATION}/EnginePatches/EnginePatch").read_text().splitlines() PATCHED_FILES = [line[6:] for line in PATCH_LINES if line.startswith("--- a/")] for file in PATCHED_FILES: shell(UNREALENGINE, f"git show HEAD:{file} > {file}") shell(UNREALENGINE, f"git apply {INTEGRATION}/EnginePatches/EnginePatch") # # Write Integration.uproject. # expand_json_file(f"{INTEGRATION}/Integration.uproject.tpl.json", f"{INTEGRATION}/Integration.uproject") # # Run Setup.sh in UNREALENGINE # shell(UNREALENGINE, f"{UNREALENGINE}/Setup.{BAT}") # # Use UnrealBuildTool to generate Integration.code-workspace.ubt # # We're not going to use it, but we keep it as a reference that you can # use when editing Integration.code-workspace.tpl.json. # Path(f"{INTEGRATION}/Integration.code-workspace").unlink(missing_ok=True) Path(f"{INTEGRATION}/Integration.code-workspace.ubt").unlink(missing_ok=True) shell(INTEGRATION, f'{UNREALENGINE}/GenerateProjectFiles.{BAT} -projectfiles -project="{INTEGRATION}/Integration.uproject" -game') Path(f"{INTEGRATION}/Integration.code-workspace").rename(f"{INTEGRATION}/Integration.code-workspace.ubt") # # Build Integration.code-workspace from Integration.code-workspace.tpl.json. # expand_json_file(f"{INTEGRATION}/Integration.code-workspace.tpl.json", f"{INTEGRATION}/Integration.code-workspace") # # Create Makefile from Makefile.tpl.txt # expand_text_file(f"{INTEGRATION}/Makefile.tpl.txt", f"{INTEGRATION}/Makefile") # # Build ShaderCompileWorker # print("Building ShaderCompileWorker...") shell(UNREALENGINE, f"{UNREALENGINE}/Engine/Build/BatchFiles/{BUILD_BAT} -waitmutex ShaderCompileWorker {OS} Shipping") Path(f"Engine/Binaries/{OS}/ShaderCompileWorker{DOT_EXE}").unlink(missing_ok=True) shutil.copyfile(f"{UNREALENGINE}/Engine/Binaries/{OS}/ShaderCompileWorker-{OS}-Shipping{DOT_EXE}", f"{UNREALENGINE}/Engine/Binaries/{OS}/ShaderCompileWorker{DOT_EXE}") # # Run Make # shell(INTEGRATION, 'make intellisense')