Full disclosure: I’m still learning Unreal Engine myself and am miles away from being proficient, but I want to share what I’m learning as I go!
Why Automate with Python?
Python excels at Unreal Engine automation because it’s simple to learn and integrates seamlessly with the engine’s Python API. Allowing you to quickly script and streamline complex tasks, making workflows much more efficient.
Step 1: Integrate Python into Your Build System (.Target.cs
File)
In a C/C++ Unreal Engine Project, you’ll have a build configuration file like PROJECTNAME.Target.cs (Used While Packaging)
. We’ll add a pre-build step here to run our Python script before compilation.
Add the following to your PROJECTNAME.Target.cs
file (e.g., within a SetupBuildEnvironment
method):
// Add the pre-build step to run the Python script
string ProjectDirectory = Directory.GetParent(Target.ProjectFile?.FullName)?.FullName ?? string.Empty;
string PythonExePath = "python.exe"; // Assumes Python is in your system's PATH
// Loop through all the .py files in the Automation directory
foreach (string pyFile in Directory.GetFiles(Path.Combine(ProjectDirectory, "Automation"), "*.py", SearchOption.AllDirectories))
{
//string PythonScriptPath = Path.Combine(ProjectDirectory, "Automation", "generate_build_info.py");
// On Windows, use cmd /c to execute. On Mac/Linux, just the python command
if (Target.Platform == UnrealTargetPlatform.Win64)
{
// The "$(ProjectDir)" macro expands to the root of your .uproject
// We pass it to the Python script so it can construct absolute paths.
PreBuildSteps.Add($"cmd /c \"\"{PythonExePath}\" \"{pyFile}\" \"$(ProjectDir)\"\"");
}
else if (Target.Platform == UnrealTargetPlatform.Mac || Target.Platform == UnrealTargetPlatform.Linux)
{
PreBuildSteps.Add($"\"{PythonExePath}\" \"{pyFile}\" \"$(ProjectDir)\"");
}
}
What this code does:
-
ProjectDirectory
: Gets your project’s root directory, making the script portable.
-
PythonExePath
: Points to your Python executable. Adjust if Python isn’t in your system’s PATH.
-
- Loops
Automation
Directory: Runs all.py
files in your project’sAutomation
folder (and subfolders) as pre-build steps. This lets you add more scripts easily.
- Loops
-
- Platform-Specific Commands: Uses
cmd /c
for Windows and a directpython
command for Mac/Linux, ensuring correct execution.
- Platform-Specific Commands: Uses
-
$(ProjectDir)
Macro: This crucial build system macro passes your project’s root path to the Python script, allowing it to find files correctly.
Step 2: The Python Sample Build Information Script
Next, create the Python script that generates and increments your build version. Save it as generate_build_info.py
inside an Automation
directory at your project’s root (e.g., YourProject/Automation/generate_build_info.py
).
import os
import datetime
import tempfile
import traceback
# Inform that the script is starting
print("[Automation Script] Starting build information generation...")
# --- Configuration ---
# Use pathlib for more robust path manipulations
from pathlib import Path
# Derive PROJECT_ROOT more cleanly
PROJECT_ROOT = Path(__file__).resolve().parent.parent
MODULE_NAME = "TheCulling"
OUTPUT_DIR = PROJECT_ROOT / 'Source' / MODULE_NAME / 'Public'
OUTPUT_FILENAME = 'BuildInformation.h'
VERSION_FILE = OUTPUT_DIR / 'last_version_info.txt'
BUILDS_PER_MINOR = 100 # Number of builds before minor increments
MINOR_MAX = 9 # Number of minors before major increments
# --- Helper Functions ---
def read_version_info(version_file_path: Path):
"""Reads the last version information from a file."""
try:
if version_file_path.exists():
with open(version_file_path, 'r') as f:
line = f.read().strip()
parts = line.split(',')
if len(parts) == 6:
return map(int, parts)
else:
print(f"[Automation Script] Warning: Malformed version file '{version_file_path}'. Resetting version.")
return None
except Exception as ex:
print(f"[Automation Script] Error reading version info from '{version_file_path}': {ex}")
traceback.print_exc()
return None
def write_version_info(version_file_path: Path, major, minor, patch, build, week, year):
"""Writes the current version information to a file using a temporary file for atomic write."""
try:
# Ensure the output directory exists
version_file_path.parent.mkdir(parents=True, exist_ok=True)
with tempfile.NamedTemporaryFile('w', dir=version_file_path.parent, delete=False) as tf:
tf.write(f"{major},{minor},{patch},{build},{week},{year}")
temp_filepath = Path(tf.name)
temp_filepath.replace(version_file_path)
except Exception as ex:
print(f"[Automation Script] Error writing version info to '{version_file_path}': {ex}")
traceback.print_exc()
exit(1) # Exit if we can't save version, as it will affect next build's numbering
# --- Main Logic ---
def main():
now = datetime.datetime.now()
build_date = now.strftime("%Y-%m-%d")
build_time = now.strftime("%H:%M:%S")
current_week = now.isocalendar()[1]
current_year = now.year
# Defaults
major, minor, patch, build = 0, 1, 0, 0
last_week = current_week
last_year = current_year
# Read last version info or initialize
version_data = read_version_info(VERSION_FILE)
if version_data:
major, minor, patch, build, last_week, last_year = version_data
else:
# If reading failed or file was malformed, reset and write immediately
write_version_info(VERSION_FILE, major, minor, patch, build, last_week, last_year)
# Versioning rules
if current_week == last_week and current_year == last_year:
patch += 1
else:
patch = 0 # Reset patch for new week or year
build += 1
if build > BUILDS_PER_MINOR:
build = 1
minor += 1
if minor > MINOR_MAX:
minor = 1
major += 1
game_version_major = major
game_version_minor = minor
game_version_patch = patch
current_build_number = build
game_version_string = f"{game_version_major}.{game_version_minor}.{game_version_patch}.{current_build_number}"
# --- Generate Header File Content ---
header_content = f"""// THIS FILE IS AUTOMATICALLY GENERATED BY THE BUILD SYSTEM. DO NOT EDIT MANUALLY.
#pragma once
#define GAME_BUILD_NUMBER {current_build_number}
#define GAME_VERSION_MAJOR {game_version_major}
#define GAME_VERSION_MINOR {game_version_minor}
#define GAME_VERSION_PATCH {game_version_patch}
#define GAME_VERSION_STRING TEXT("{game_version_string}")
#define GAME_BUILD_DATE TEXT("{build_date}")
#define GAME_BUILD_TIME TEXT("{build_time}")
// Add more build info as needed, e.g., git hash, changelist, etc.
"""
# --- Write the Header File ---
output_path = OUTPUT_DIR / OUTPUT_FILENAME
OUTPUT_DIR.mkdir(parents=True, exist_ok=True) # Ensure output directory exists
try:
with open(output_path, 'w') as f:
f.write(header_content)
print(f"[Automation Script] Successfully generated {output_path}")
# Save version info for next run
write_version_info(VERSION_FILE, game_version_major, game_version_minor, game_version_patch, current_build_number, current_week, current_year)
except Exception as e:
print(f"[Automation Script] ERROR: Failed to write {output_path}: {e}")
traceback.print_exc()
exit(1)
print("[Automation Script] Execution Ended.")
if __name__ == "__main__":
main()
Key script details:
-
- Dynamic
PROJECT_ROOT
: Reads the project path passed from the build system, ensuring portability.
- Dynamic
-
pathlib
: Used for cleaner, more robust path handling across operating systems.
-
- Versioning Logic: Reads the last version from
last_version_info.txt
, increments patch per build within the same week/year, and handles minor/major increments based on build counts.
- Versioning Logic: Reads the last version from
-
- Atomic File Writes (
tempfile
): Writes to a temporary file first, then atomically replaces the target file. This prevents corruption if the script is interrupted.
- Atomic File Writes (
-
- C++ Header Generation: Creates
BuildInformation.h
with#define
macros for version components, string, build date, and time.
- C++ Header Generation: Creates
Step 3: Use the Info in Your C++ Code
After a build, BuildInformation.h
will be in YourProject/Source/YourModuleName/Public/
. Include it in your C++ files:
// In any C++ file where you need build information
#include "YourModuleName/Public/BuildInformation.h" // Adjust path if different
// Example usage:
void PrintBuildInfo()
{
UE_LOG(LogTemp, Log, TEXT("Game Version: %s"), GAME_VERSION_STRING);
UE_LOG(LogTemp, Log, TEXT("Build Number: %d"), GAME_BUILD_NUMBER);
UE_LOG(LogTemp, Log, TEXT("Built On: %s at %s"), GAME_BUILD_DATE, GAME_BUILD_TIME);
}
// Or use in UI, debugging, analytics, etc.
The Workflow:
-
- You build.
-
- Your
PROJECTNAME.Target.cs
runs.
- Your
-
- It executes
generate_build_info.py
(and any other Python scripts in yourAutomation
folder).
- It executes
-
- The Python script updates
last_version_info.txt
andBuildInformation.h
.
- The Python script updates
-
- Your C++ compilation uses the new
BuildInformation.h
.
- Your C++ compilation uses the new
-
- Your compiled executable now contains the latest build version.