Unreal Engine 5 Packaging Automation

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:

    1. ProjectDirectory: Gets your project’s root directory, making the script portable.

    1. PythonExePath: Points to your Python executable. Adjust if Python isn’t in your system’s PATH.

    1. Loops Automation Directory: Runs all .py files in your project’s Automation folder (and subfolders) as pre-build steps. This lets you add more scripts easily.

    1. Platform-Specific Commands: Uses cmd /c for Windows and a direct python command for Mac/Linux, ensuring correct execution.

    1. $(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:

    1. Dynamic PROJECT_ROOT: Reads the project path passed from the build system, ensuring portability.

    1. pathlib: Used for cleaner, more robust path handling across operating systems.

    1. 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.

    1. Atomic File Writes (tempfile): Writes to a temporary file first, then atomically replaces the target file. This prevents corruption if the script is interrupted.

    1. C++ Header Generation: Creates BuildInformation.h with #define macros for version components, string, build date, and time.

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:

    1. You build.

    1. Your PROJECTNAME.Target.cs runs.

    1. It executes generate_build_info.py (and any other Python scripts in your Automation folder).

    1. The Python script updates last_version_info.txt and BuildInformation.h.

    1. Your C++ compilation uses the new BuildInformation.h.

    1. Your compiled executable now contains the latest build version.

 

more updates

Move to Indiegogo

Hi Everyone, We’re writing to let you know about an important change: The Lask Ark crowdfunding campaign is moving to Indiegogo. Transparency is important to

Read more >