389 lines
15 KiB
Python
Executable File
389 lines
15 KiB
Python
Executable File
#!/usr/bin/python3
|
|
#
|
|
# The one-and-only build script for luprex.
|
|
# Do not follow any of the build instructions on the Unreal Engine
|
|
# websites! Instead, use these instructions.
|
|
#
|
|
#
|
|
# HOW TO BUILD THE FIRST TIME
|
|
#
|
|
# First, install the following software:
|
|
#
|
|
# apt-get install git-lfs
|
|
# apt-get install code
|
|
# apt-get install dotnet6
|
|
# apt-get install clangd-15 or better.
|
|
#
|
|
# Then, git clone the UnrealEngine and integration repositories:
|
|
#
|
|
# cd $HOME
|
|
# git clone https://github.com/EpicGames/UnrealEngine.git
|
|
# git clone https://www.gnaut.com/team/integration.git
|
|
#
|
|
# It is important that these two repositories be located
|
|
# at $HOME/UnrealEngine and $HOME/integration.
|
|
#
|
|
# Of course, you will have to jump through a bunch of hoops to
|
|
# get access to these repositories. See the instructions on the
|
|
# unreal engine website.
|
|
#
|
|
# After cloning the two repositories, change directory into
|
|
# the "integration" repository and run "build.py all".
|
|
# This will build both unreal engine and integration. It will
|
|
# also build project files for vscode, an intellisense database,
|
|
# and several other things. Once it is done, everything is built.
|
|
# You can start up the debugger.
|
|
#
|
|
#
|
|
# HOW TO REBUILD WHEN YOU'VE EDITED SOMETHING
|
|
#
|
|
# You can also use "build.py all" to rebuild. This is the preferred
|
|
# way to rebuild when you aren't sure what's been edited. However,
|
|
# this takes almost 30 seconds even when there's nothing new to build,
|
|
# so it can be a little slow.
|
|
#
|
|
# If you're sure that the only thing you've edited recently are
|
|
# C++ files in the integration repository, then you can get away with
|
|
# doing a lightweight build: "build.py c++". This only works if you've
|
|
# already completed a successful full build, and the only thing you've
|
|
# done since then is edit C++ files in integration. If you've edited
|
|
# anything else, you need to use "build.py all" to rebuild.
|
|
#
|
|
# Editing Lua or Blueprint code doesn't require any kind of rebuild.
|
|
#
|
|
#
|
|
# USING VISUAL STUDIO CODE
|
|
#
|
|
# To start up vscode, change directory to the integration repository,
|
|
# and run "code Integration.code-workspace". From inside vscode, you can
|
|
# use Terminal/Run_Build_Task to run "build.py all" or "build.py c++".
|
|
# You can also select Run/Start_Debugging to launch the game.
|
|
#
|
|
# The first time you launch vscode, you will see a popup about
|
|
# recommended extensions. These are actually recommended by this
|
|
# script, build.py, so they're actually good recommendations. Install
|
|
# them: you will only have to do this once.
|
|
#
|
|
|
|
|
|
import sys, os, json, shutil, subprocess, re, time, tarfile, itertools, hashlib
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
|
|
#
|
|
# Utility subroutines
|
|
#
|
|
|
|
def shell(dir, cmd):
|
|
"Run a shell command in a directory"
|
|
subprocess.run(cmd, shell=True, check=True, cwd=dir)
|
|
|
|
def create_tarfile(directory, glob_pattern, outputfile):
|
|
"Create a tarfile from a source directory and a glob pattern"
|
|
directory = Path(directory)
|
|
with tarfile.open(outputfile, mode='w:gz') as tar:
|
|
for path in directory.rglob(glob_pattern):
|
|
if path.is_file():
|
|
tar.add(path, arcname=path.relative_to(directory))
|
|
|
|
def find_cpp(dir):
|
|
"Find all the C++ and C files in a given directory"
|
|
list1 = list(Path(dir).rglob("*.[ch]pp"))
|
|
list2 = list(Path(dir).rglob("*.[ch]"))
|
|
return sorted([str(x) for x in list1 + list2])
|
|
|
|
def hash_json(data):
|
|
"Calculate a sha256 hexdigest of any valid json data structure"
|
|
serialized = json.dumps(data, sort_keys=True).encode("utf-8")
|
|
return hashlib.sha256(serialized).hexdigest()
|
|
|
|
def read_if_exists(fn):
|
|
"Read a file if it exists, otherwise, return null string"
|
|
try: return Path(fn).read_text()
|
|
except: return ""
|
|
|
|
#
|
|
# A JSON preprocessor.
|
|
#
|
|
|
|
JSON_VAR_REGEX = re.compile(r'\[([A-Z0-9_]+)\]')
|
|
|
|
def expand_json(data, vars):
|
|
"""
|
|
A JSON preprocessor that can expand for-each loops and substitute variables.
|
|
The for-loop thing is mainly used by the code-workspace template, to avoid
|
|
extreme code duplication in the launch configurations and build configurations.
|
|
Variable substitution is used to plug hardwired paths into the file, and also
|
|
to implement the for-loop thing.
|
|
"""
|
|
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):
|
|
full = JSON_VAR_REGEX.fullmatch(data)
|
|
if full: return vars.get(full.group(1), data)
|
|
else: return JSON_VAR_REGEX.sub(lambda m: str(vars.get(m.group(1), m.group(0))), data)
|
|
else:
|
|
return data
|
|
|
|
|
|
def expand_json_file(sourcefile, outputfile, config):
|
|
"""
|
|
Apply the json preprocessor to a file on disk
|
|
"""
|
|
Path(outputfile).unlink(missing_ok=True)
|
|
data = json.loads(Path(sourcefile).read_text())
|
|
expanded = expand_json(data, vars(config))
|
|
Path(outputfile).write_text(json.dumps(expanded, indent=4))
|
|
|
|
#
|
|
# Determining the build configuration.
|
|
#
|
|
|
|
def get_build_mode_from_command_line():
|
|
"""
|
|
Build.py accepts a single argument, which should
|
|
be one of the following modes: all, c++, clean.
|
|
If nothing is specified, the mode is all.
|
|
We understand cpp and cxx as synonyms for c++.
|
|
"""
|
|
mode = sys.argv[1].lower() if len(sys.argv) > 1 else 'all'
|
|
if mode in ["cpp", "cxx"]: mode = "c++"
|
|
if not mode in ["all", "c++", "clean", "experiment"]:
|
|
sys.exit(f"Invalid build mode: {mode}")
|
|
return mode
|
|
|
|
|
|
def autodetect_system_config():
|
|
"""
|
|
This autodetects where integration and unrealengine are installed,
|
|
and it also autodetects your operating system. Based on these,
|
|
it returns a config object containing a variety of useful
|
|
configuration settings. We haven't tested the Windows version.
|
|
"""
|
|
config = SimpleNamespace()
|
|
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")
|
|
test1 = Path(f"{config.INTEGRATION}/Source/Integration")
|
|
test2 = Path(f"{config.UNREALENGINE}/Engine/Source/Editor")
|
|
if not test1.is_dir(): sys.exit(f"Integration repository is not valid: {config.INTEGRATION}")
|
|
if not test2.is_dir(): sys.exit(f"UnrealEngine repository is not valid: {config.UNREALENGINE}")
|
|
return config
|
|
|
|
|
|
def store_system_config_in_globals(config):
|
|
"""
|
|
Copy all the config data from the config object into global variables.
|
|
"""
|
|
global OS,DLL,BAT,DOT_EXE,USER,BUILD_BAT,INTEGRATION,UNREALENGINE
|
|
OS = config.OS
|
|
DLL = config.DLL
|
|
BAT = config.BAT
|
|
DOT_EXE = config.DOT_EXE
|
|
USER = config.USER
|
|
BUILD_BAT = config.BUILD_BAT
|
|
INTEGRATION = config.INTEGRATION
|
|
UNREALENGINE = config.UNREALENGINE
|
|
|
|
#
|
|
# The actual build steps.
|
|
#
|
|
|
|
def checkout_correct_unreal_engine_branch_and_apply_patch():
|
|
"""
|
|
Check out the correct branch, then apply a patch to the UnrealEngine
|
|
source code. Patch application consists of two steps: first, checkout clean,
|
|
unpatched versions of the files from git. Then, apply the patch to the clean code.
|
|
Note: don't git-commit the patch in UnrealEngine. Instead,
|
|
just let this script reapply this patch as necessary.
|
|
"""
|
|
patch = Path(f"{INTEGRATION}/EnginePatches/EnginePatch")
|
|
patch_lines = patch.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 checkout 5.3.1-release")
|
|
shell(UNREALENGINE, f"git apply {INTEGRATION}/EnginePatches/EnginePatch")
|
|
|
|
|
|
def generate_buildconfiguration_xml():
|
|
"""
|
|
Generates BuildConfiguration.xml. We actually have two versions of this
|
|
file in git, one for windows, one for linux. This just copies the appropriate
|
|
version to the relevant target directories.
|
|
"""
|
|
dir1 = Path(f"{INTEGRATION}/Saved/UnrealBuildTool")
|
|
dir2 = Path(f"{UNREALENGINE}/Engine/Saved/UnrealBuildTool")
|
|
target1 = Path(f"{INTEGRATION}/Saved/UnrealBuildTool/BuildConfiguration.xml")
|
|
target2 = Path(f"{UNREALENGINE}/Engine/Saved/UnrealBuildTool/BuildConfiguration.xml")
|
|
source = Path(f"{INTEGRATION}/EnginePatches/BuildConfiguration{OS}.xml")
|
|
template = source.read_text();
|
|
dir1.mkdir(parents=True, exist_ok=True)
|
|
dir2.mkdir(parents=True, exist_ok=True)
|
|
target1.unlink(missing_ok=True)
|
|
target2.unlink(missing_ok=True)
|
|
target1.write_text(template)
|
|
target2.write_text(template)
|
|
|
|
|
|
def generate_lpx_paths():
|
|
"""
|
|
Unreal needs to be able to find the Luprex DLL, and it also needs to find the
|
|
Lua source code. For now, we just compile some hardwired paths into the
|
|
binary. Someday we'll do something more sophisticated.
|
|
"""
|
|
target = Path(f"{INTEGRATION}/Source/Integration/lpx-paths.hpp")
|
|
line1 = f'#define LUPREX_DLL_PATH "{INTEGRATION}/luprex/build/{OS}/luprexlib.{DLL}"'
|
|
line2 = f'#define LUPREX_ROOT_PATH "{INTEGRATION}/luprex"'
|
|
code = line1 + "\n" + line2 + "\n"
|
|
target.unlink(missing_ok=True)
|
|
target.write_text(code)
|
|
|
|
|
|
def generate_integration_uproject():
|
|
"""
|
|
Generate integration.uproject
|
|
The uproject file is used by UnrealBuildTool to guide the build process.
|
|
"""
|
|
template = f"{INTEGRATION}/Integration.uproject.tpl.json"
|
|
target = f"{INTEGRATION}/Integration.uproject"
|
|
expand_json_file(template, target, CONFIG)
|
|
|
|
|
|
def run_unrealengine_setup_bat():
|
|
"""
|
|
Run Setup.bat in UnrealEngine.
|
|
This script downloads assets that aren't stored in git.
|
|
"""
|
|
shell(UNREALENGINE, f"{UNREALENGINE}/Setup.{BAT}")
|
|
|
|
|
|
def build_unrealbuildtool():
|
|
"""
|
|
Build the unreal build tool itself, also including shader compile worker.
|
|
"""
|
|
shell(UNREALENGINE, f"{UNREALENGINE}/Engine/Build/BatchFiles/{BUILD_BAT} -buildubt ShaderCompileWorker {OS} Development")
|
|
|
|
|
|
def build_luprex_and_integration():
|
|
"""
|
|
This builds our C++ code, and also UnrealEngine's C++ code.
|
|
"""
|
|
shell(f"{INTEGRATION}/luprex", "make all")
|
|
shell(INTEGRATION, f"{UNREALENGINE}/Engine/Build/BatchFiles/{BUILD_BAT} -waitmutex IntegrationEditor {OS} DebugGame {INTEGRATION}/Integration.uproject")
|
|
|
|
|
|
def build_intellisense_database_for_clangd(force):
|
|
"""
|
|
This builds compile_commands.json, which tells the intellisense system
|
|
based on clangd how to compile each source file. We automatically rebuild
|
|
if a C++ file has been added or removed. That probably isn't sufficient:
|
|
we should also rebuild if a Build.cs file has been edited. Coming Soon.
|
|
Until then, if your intellisense starts acting badly, do a 'build.py all',
|
|
which sets the 'force' argument above to true.
|
|
|
|
Rebuilding the intellisense database touches rsp files, which
|
|
unfortunately, causes the entire C++ build to be restarted the next
|
|
time you run 'build'. This is terrible behavior from Unreal's build
|
|
system. We have a hacky workaround: we save the RSP files before
|
|
running the intellisense build, then we restore them afterward.
|
|
"""
|
|
Path(f"{INTEGRATION}/.vscode").mkdir(exist_ok = True)
|
|
Path(f"{UNREALENGINE}/.vscode").mkdir(exist_ok = True)
|
|
hash_file = Path(f"{INTEGRATION}/.vscode/cpp_hash")
|
|
new_hash = hash_json(find_cpp(f"{INTEGRATION}/Source"))
|
|
old_hash = read_if_exists(hash_file).strip()
|
|
if (new_hash != old_hash) or force:
|
|
hash_file.unlink(missing_ok=True)
|
|
create_tarfile(f"{INTEGRATION}/Intermediate", "*.rsp", f"{INTEGRATION}/rsp_files.tgz")
|
|
create_tarfile(f"{UNREALENGINE}/Engine", "*.rsp", f"{UNREALENGINE}/rsp_files.tgz")
|
|
try:
|
|
shell(INTEGRATION, f"{UNREALENGINE}/Engine/Build/BatchFiles/{BUILD_BAT} -waitmutex IntegrationEditor {OS} DebugGame {INTEGRATION}/Integration.uproject -mode=GenerateClangDatabase -OutputDir={UNREALENGINE}/.vscode")
|
|
shell(UNREALENGINE, f"{UNREALENGINE}/Engine/Build/BatchFiles/{BUILD_BAT} -waitmutex UnrealEditor {OS} DebugGame -mode=GenerateClangDatabase -OutputDir={UNREALENGINE}/.vscode")
|
|
shell(INTEGRATION, f"cat {UNREALENGINE}/.vscode/compile_commands.json >> {INTEGRATION}/.vscode/compile_commands.json")
|
|
except Exception as e:
|
|
error = e
|
|
else:
|
|
error = None
|
|
finally:
|
|
tarfile.open(f"{INTEGRATION}/rsp_files.tgz").extractall(path=f"{INTEGRATION}/Intermediate")
|
|
tarfile.open(f"{UNREALENGINE}/rsp_files.tgz").extractall(path=f"{UNREALENGINE}/Engine")
|
|
Path(f"{INTEGRATION}/rsp_files.tgz").unlink()
|
|
Path(f"{UNREALENGINE}/rsp_files.tgz").unlink()
|
|
if error: raise error
|
|
hash_file.write_text(new_hash)
|
|
|
|
|
|
def generate_integration_code_workspace():
|
|
"""
|
|
We build Integration.code-workspace from a template that we
|
|
wrote ourselves, Integration.code-workspace.tpl.json.
|
|
We use UnrealBuildTool to generate Integration.code-workspace.ubt,
|
|
but we don't use it: we just keep it as a reference that you can
|
|
refer to when editing the template.
|
|
"""
|
|
workspace = f"{INTEGRATION}/Integration.code-workspace"
|
|
workspace_ubt = f"{INTEGRATION}/Integration.code-workspace.ubt"
|
|
template = f"{INTEGRATION}/Integration.code-workspace.tpl.json"
|
|
Path(workspace).unlink(missing_ok=True)
|
|
Path(workspace_ubt).unlink(missing_ok=True)
|
|
shell(INTEGRATION, f'{UNREALENGINE}/GenerateProjectFiles.{BAT} -projectfiles -project="{INTEGRATION}/Integration.uproject" -game')
|
|
Path(workspace).rename(workspace_ubt)
|
|
expand_json_file(template, workspace, CONFIG)
|
|
|
|
|
|
def build_clean():
|
|
"""
|
|
This code is underdeveloped.
|
|
"""
|
|
shell(f"{INTEGRATION}/luprex", "make clean")
|
|
shell(INTEGRATION, f"{UNREALENGINE}/Engine/Build/BatchFiles/{BUILD_BAT} -waitmutex IntegrationEditor {OS} DebugGame {INTEGRATION}/Integration.uproject -clean")
|
|
Path(f"{INTEGRATION}/.vscode/compile_commands.json").unlink(missing_ok = True)
|
|
|
|
#
|
|
# MAIN PROGRAM
|
|
#
|
|
|
|
MODE = get_build_mode_from_command_line()
|
|
CONFIG = autodetect_system_config()
|
|
store_system_config_in_globals(CONFIG)
|
|
os.chdir(f"{INTEGRATION}/EnginePatches")
|
|
|
|
if MODE == "all":
|
|
checkout_correct_unreal_engine_branch_and_apply_patch()
|
|
generate_buildconfiguration_xml()
|
|
generate_lpx_paths()
|
|
generate_integration_uproject()
|
|
run_unrealengine_setup_bat()
|
|
build_unrealbuildtool()
|
|
generate_integration_code_workspace()
|
|
|
|
if MODE in ["all", "c++"]:
|
|
build_luprex_and_integration()
|
|
build_intellisense_database_for_clangd(MODE == "all")
|
|
|
|
if MODE == "clean":
|
|
build_clean()
|
|
|