Last active
January 10, 2025 19:06
-
-
Save keith-miller/53dd58ea91177c6a87182abadda70e67 to your computer and use it in GitHub Desktop.
BuildGraph in C#
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"ProjectName" : "Mosaic", | |
"ProjectFile" : "Mosaic/Mosaic.uproject", | |
"BuildVersionPrefix" : "0.0", | |
"Targets": | |
{ | |
"Client": | |
{ | |
"Platforms": | |
{ | |
"Win64": | |
{ | |
"TargetConfiguration": "Development", | |
"BuildParams" : { | |
"Clean": false, | |
"Cook": true, | |
"Unattended": true, | |
"Pak": true, | |
"PrePak": false, | |
"Stage": true, | |
"Prereqs": true, | |
"Archive": true, | |
"Package": true, | |
"Deploy": true, | |
"CodeSign": false, | |
"IgnoreCookErrors": false | |
} | |
} | |
} | |
}, | |
"Server": | |
{ | |
"Platforms": | |
{ | |
"LinuxArm64": | |
{ | |
"TargetConfiguration": "Development", | |
"BuildParams" : { | |
"Clean": false, | |
"Cook": true, | |
"Unattended": true, | |
"Pak": false, | |
"PrePak": false, | |
"Stage": true, | |
"Prereqs": true, | |
"Archive": true, | |
"Package": true, | |
"Deploy": true, | |
"CodeSign": false, | |
"IgnoreCookErrors": false | |
} | |
} | |
} | |
} | |
}, | |
"AWS" : { | |
"Region" : "us-west-2", | |
"AccessKey" : "", | |
"SecretKey" : "", | |
"ECRRepository" : "" | |
}, | |
"EGS" : { | |
"ClientId" : "", | |
"ClientSecret" : "", | |
"OrganizationId" : "", | |
"ProductId" : "", | |
"ArtifactId" : "", | |
"AppArgs" : "", | |
"CloudDir" : "", | |
"SandboxId" : "", | |
"Platform" : "Windows", | |
"Label" : "Live" | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using AutomationTool; | |
using EpicGames.Core; | |
using UnrealBuildTool; | |
namespace Deployment.Automation.Config; | |
/** | |
* This class is a subset of AutomationTool.ProjectParams | |
*/ | |
public class BuildParams | |
{ | |
/// <summary> | |
/// Shared: true if we should build crash reporter | |
/// </summary> | |
[Help("CrashReporter", "true if we should build crash reporter")] | |
public bool CrashReporter { set; get; } = true; | |
/// <summary> | |
/// Shared: Determines if the build is going to use cooked data, commandline: -cook, -cookonthefly | |
/// </summary> | |
[Help("cook, -cookonthefly", "Determines if the build is going to use cooked data")] | |
public bool Cook { set; get; } | |
/// <summary> | |
/// Shared: Determines if the intermediate folders will be wiped before building, commandline: -clean | |
/// </summary> | |
[Help("clean", "whether or not to use an incremental workspace")] | |
public bool Clean { set; get; } = false; | |
/// <summary> | |
/// Shared: Assumes no user is sitting at the console, so for example kills clients automatically, commandline: -Unattended | |
/// </summary> | |
[Help("unattended", "assumes no operator is present, always terminates without waiting for something.")] | |
public bool Unattended { set; get; } | |
/// <summary> | |
/// Shared: True if pak file should be generated. | |
/// </summary> | |
[Help("pak", "generate a pak file")] | |
public bool Pak { set; get; } | |
/// <summary> | |
/// Shared: Encryption keys used for signing the pak file. | |
/// </summary> | |
[Help("signpak=keys", "sign the generated pak file with the specified key, i.e. -signpak=C:\\Encryption.keys. Also implies -signedpak.")] | |
public string SignPak { get; set; } = string.Empty; | |
/// <summary> | |
/// Shared: true if this build is staged, command line: -stage | |
/// </summary> | |
[Help("prepak", "attempt to avoid cooking and instead pull pak files from the network, implies pak and skipcook")] | |
public bool PrePak { set; get; } | |
/// <summary> | |
/// Shared: the game will use only signed content. | |
/// </summary> | |
[Help("signed", "the game should expect to use a signed pak file.")] | |
public bool SignedPak { set; get; } | |
/// <summary> | |
/// Shared: true if this build is staged, command line: -stage | |
/// </summary> | |
[Help("stage", "put this build in a stage directory")] | |
public bool Stage { set; get; } | |
/// <summary> | |
/// Cook: Do not include a version number in the cooked content | |
/// </summary> | |
public bool UnversionedCookedContent = false; | |
/// <summary> | |
/// Compress packages during cook. | |
/// </summary> | |
public bool Compressed = true; | |
[Help("prereqs", "stage prerequisites along with the project")] | |
public bool Prereqs { get; set; } | |
/// <summary> | |
/// Shared: true if this build is archived, command line: -archive | |
/// </summary> | |
[Help("archive", "put this build in an archive directory")] | |
public bool Archive { set; get; } | |
[Help("package", "package the project for the target platform")] | |
public bool Package { get; set; } | |
/// <summary> | |
/// Should the build be deployed or not | |
/// </summary> | |
public bool Deploy { get; set; } | |
/// <summary> | |
/// should the build sign the code | |
/// </summary> | |
public bool CodeSign { set; get; } = false; | |
/// <summary> | |
/// should the job ignore cook errors | |
/// </summary> | |
public bool IgnoreCookErrors { get; set; } | |
public ProjectParams Convert( | |
TargetType targetType, | |
UnrealTargetPlatform targetPlatform, | |
UnrealTargetConfiguration TargetConfiguration, | |
string projectPath) | |
{ | |
switch (targetType) | |
{ | |
case TargetType.Client: | |
return new ProjectParams | |
( | |
Command: new BuildCookRun(), | |
RawProjectPath: FileReference.FromString(projectPath), | |
Build: true, | |
Client: true, | |
DedicatedServer: false, | |
ClientConfigsToBuild: [TargetConfiguration], | |
ClientTargetPlatforms: [new TargetPlatformDescriptor(targetPlatform)], | |
CrashReporter: this.CrashReporter, | |
Clean: false, | |
Compressed: this.Compressed, | |
Cook: this.Cook, | |
UnversionedCookedContent: this.UnversionedCookedContent, | |
Prereqs: this.Prereqs, | |
Stage: this.Stage, | |
Pak: this.Pak, | |
SignPak: this.SignPak, | |
PrePak: this.PrePak, | |
Archive: this.Archive, | |
Package: this.Package, | |
CodeSign: this.CodeSign, | |
IgnoreCookErrors: this.IgnoreCookErrors | |
); | |
case TargetType.Server: | |
return new ProjectParams | |
( | |
Command: new BuildCookRun(), | |
RawProjectPath: FileReference.FromString(projectPath), | |
Build: true, | |
Client: false, | |
NoClient: true, | |
DedicatedServer: true, | |
ServerConfigsToBuild: [TargetConfiguration], | |
ServerTargetPlatforms: [new TargetPlatformDescriptor(targetPlatform)], | |
CrashReporter: this.CrashReporter, | |
Clean: false, | |
Compressed: this.Compressed, | |
Cook: this.Cook, | |
UnversionedCookedContent: this.UnversionedCookedContent, | |
Prereqs: this.Prereqs, | |
Stage: this.Stage, | |
Pak: this.Pak, | |
SignPak: this.SignPak, | |
PrePak: this.PrePak, | |
Archive: this.Archive, | |
Package: this.Package, | |
CodeSign: this.CodeSign, | |
IgnoreCookErrors: this.IgnoreCookErrors | |
); | |
case TargetType.Game: | |
case TargetType.Editor: | |
case TargetType.Program: | |
default: | |
throw new ArgumentOutOfRangeException(nameof(targetType), targetType, null); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.\RunUAT.bat BuildGraph -Target="RunAll" -Class=HordeBuildCookRun ` | |
-BuildConfigFile="Mosaic\Build\build_settings.json" ` | |
-buildmachine ` | |
-unattended ` | |
-utf8output ` | |
-P4 ` | |
-nosplash ` | |
-stdout |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Text; | |
using Amazon; | |
using Amazon.Runtime; | |
using Amazon.ECR; | |
using Amazon.ECR.Model; | |
using AutomationTool; | |
using Deployment.Automation.Config; | |
using EpicGames.BuildGraph; | |
using EpicGames.BuildGraph.Expressions; | |
using Microsoft.Extensions.Logging; | |
using UnrealBuildTool; | |
using AutomationTool.Tasks; | |
using Deployment.Automation.Tools; | |
using EpicGames.Core; | |
using YamlDotNet.Serialization; | |
using YamlDotNet.Serialization.NamingConventions; | |
namespace Deployment.Automation; | |
// Descriptions for command line options outside of this script may be found in AutomationTool/Program.cs | |
public class HordeBuildCookRun : BgGraphBuilder | |
{ | |
public override BgGraph CreateGraph(BgEnvironment env) | |
{ | |
if (!CommandUtils.P4Enabled) | |
{ | |
throw new AutomationException("Option '-P4' needs to be set to run this script"); | |
} | |
var buildConfigFile = CommandUtils.ParseParamValue(Environment.GetCommandLineArgs().ToArray<object>(), | |
"-BuildConfigFile", ""); | |
var buildConfig = ConfigUtils.ParseConfig($"{CommandUtils.CmdEnv.LocalRoot}/{buildConfigFile}"); | |
var buildVersion = $"{buildConfig.BuildVersionPrefix}.{CommandUtils.P4Env.Changelist}"; | |
var projectPath = $"{CommandUtils.CmdEnv.LocalRoot}/{buildConfig.ProjectFile}"; | |
var aggregates = new List<BgAggregate>(); | |
var allNodes = new List<BgNode>(); | |
foreach (var target in buildConfig.Targets.Keys) | |
{ | |
foreach (var platform in buildConfig.Targets[target].Platforms.Keys) | |
{ | |
Logger.LogInformation("Processing {projectName} for target {target} for platform {platform}", buildConfig.ProjectName, target, platform); | |
var nodes = new List<BgNode>(); | |
var targetSettings = buildConfig.Targets[target].Platforms[platform]; | |
var projectParams = targetSettings.BuildParams.Convert(target, | |
platform, | |
targetSettings.TargetConfiguration, | |
projectPath); | |
// set up the build agent | |
var agentType = targetSettings.BuildParams.Clean ? $"{platform.ToString()}" : $"Incremental{platform.ToString()}"; | |
var buildAgent = new BgAgent($"BuildCookRun {target} {platform}", agentType); | |
// set version | |
var setVersionNode = buildAgent.AddNode(x => UpdateVersionFilesAsync(x, platform, buildVersion)); | |
nodes.Add(setVersionNode); | |
// compile | |
var compileNode = buildAgent.AddNode(x => | |
CompileAsync(x, $"{buildConfig.ProjectName}{target}", platform, targetSettings.TargetConfiguration, buildVersion)) | |
.Requires(setVersionNode); | |
nodes.Add(compileNode); | |
// run BuildCookRun job | |
var buildCookRunNode = buildAgent.AddNode(x => BuildCookRunAsync(x, platform, projectParams)) | |
.Requires(compileNode); | |
nodes.Add(buildCookRunNode); | |
// deploy | |
if (targetSettings.BuildParams.Deploy) | |
{ | |
var deployNode = buildAgent.AddNode(x => DeployAsync(x, target, platform, buildConfig, buildVersion)) | |
.Requires(buildCookRunNode); | |
nodes.Add(deployNode); | |
} | |
aggregates.Add(new BgAggregate($"Run {platform}", nodes)); | |
allNodes.AddRange(nodes); | |
} | |
} | |
aggregates.Add(new BgAggregate("RunAll", allNodes)); | |
return new BgGraph(BgList<BgNode>.Empty, aggregates); | |
} | |
/// <summary> | |
/// Update the build version | |
/// </summary> | |
/// <param name="state">BgContent for the node</param> | |
/// <param name="platform">Used to name the node</param> | |
/// <param name="buildVersion">Build version</param> | |
[BgNodeName("UpdateVersionFiles {platform}")] | |
static async Task UpdateVersionFilesAsync(BgContext state, UnrealTargetPlatform platform, string buildVersion) | |
{ | |
if (state.IsBuildMachine) | |
{ | |
await StandardTasks.SetVersionAsync(state.Change, state.Stream.Replace('/', '+'), build: buildVersion); | |
} | |
} | |
/// <summary> | |
/// Run the BuildCookRun task | |
/// </summary> | |
/// <param name="state">BgContent for the node</param> | |
/// <param name="platform">Used to name the node</param> | |
/// <param name="projectParams">Project params for the BuildCookRunJob</param> | |
[BgNodeName("BuildCookRun {platform}")] | |
static async Task BuildCookRunAsync(BgContext state, UnrealTargetPlatform platform, ProjectParams projectParams) | |
{ | |
if (state.IsBuildMachine) | |
{ | |
await new HordeBuildCookRun().DoBuildCookRunAsync(projectParams); | |
} | |
} | |
/// <summary> | |
/// Compile the project | |
/// </summary> | |
/// <param name="state">BgContent for the node</param> | |
/// <param name="projectTarget">Project name</param> | |
/// <param name="platform">Target platform</param> | |
/// <param name="configuration">Target configuration</param> | |
/// <param name="buildVersion">Build version</param> | |
[BgNodeName("Compile {platform}")] | |
static async Task CompileAsync(BgContext state, | |
string projectTarget, | |
UnrealTargetPlatform platform, | |
UnrealTargetConfiguration configuration, | |
string buildVersion) | |
{ | |
if (state.IsBuildMachine) | |
{ | |
await StandardTasks.CompileAsync(projectTarget, platform, configuration, arguments: $"-buildVersion={buildVersion}"); | |
} | |
} | |
/// <summary> | |
/// Deploy the project | |
/// </summary> | |
/// <param name="state">BgContent for the node</param> | |
/// <param name="targetType">Unreal target type</param> | |
/// <param name="platform">Target platform</param> | |
/// <param name="buildConfig">Build config object</param> | |
/// <param name="buildVersion">Build version</param> | |
[BgNodeName("Deploy {platform}")] | |
static async Task DeployAsync(BgContext state, | |
TargetType targetType, | |
UnrealTargetPlatform platform, | |
BuildConfig buildConfig, | |
string buildVersion) | |
{ | |
var allowedServerPlatforms = new[] { UnrealTargetPlatform.Linux, UnrealTargetPlatform.LinuxArm64 }; | |
switch (targetType) | |
{ | |
case TargetType.Server when !allowedServerPlatforms.Contains(platform): | |
case TargetType.Client when allowedServerPlatforms.Contains(platform): | |
throw new AutomationException($"TargetType {targetType} cannot be deployed for platform {platform}"); | |
// TODO: might want to allow the Game and Editor target | |
case TargetType.Game: | |
case TargetType.Editor: | |
case TargetType.Program: | |
throw new ArgumentOutOfRangeException(nameof(targetType), targetType, null); | |
} | |
if (allowedServerPlatforms.Contains(platform)) | |
{ | |
await DeployToAws(buildConfig, buildVersion, state.IsBuildMachine); | |
} | |
else if (platform == UnrealTargetPlatform.Win64) | |
{ | |
await DeployToEGS(targetType, platform, buildConfig, buildVersion, state.IsBuildMachine); | |
} | |
else | |
{ | |
throw new AutomationException($"Platform {platform} cannot be deployed at this time"); | |
} | |
} | |
static async Task DeployToEGS(TargetType targetType, | |
UnrealTargetPlatform platform, | |
BuildConfig buildConfig, | |
string buildVersion, | |
bool IsBuildMachine) | |
{ | |
var platformFolder = platform == UnrealTargetPlatform.Win64 ? | |
$"Windows{targetType}" : $"{platform}{targetType}"; | |
var appLaunch = platform == UnrealTargetPlatform.Win64 ? | |
$"{buildConfig.ProjectName}/Binaries/{platform}/{buildConfig.ProjectName}{targetType}.exe" : | |
$"{buildConfig.ProjectName}/Binaries/{platform}/{buildConfig.ProjectName}{targetType}"; | |
var BPTExec = $"{CommandUtils.CmdEnv.LocalRoot}/Engine/Binaries/Win64/BuildPatchTool/Engine/Binaries/Win64/BuildPatchTool.exe"; | |
buildConfig.EGS.BuildVersion = buildVersion; | |
buildConfig.EGS.BuildRoot = $"{buildConfig.ProjectName}/ArchivedBuilds/{platformFolder}"; | |
buildConfig.EGS.AppLaunch = appLaunch; | |
Logger.LogInformation("Running BuildPatchTool Patch Generation..."); | |
if (IsBuildMachine) | |
{ | |
await StandardTasks.SpawnAsync(BPTExec, buildConfig.EGS.GenerateUploadCommandLineArgs()); | |
} | |
Logger.LogInformation("Applying BuildPatchTool Label..."); | |
if (IsBuildMachine) | |
{ | |
await StandardTasks.SpawnAsync(BPTExec, buildConfig.EGS.GenerateLabelCommandLineArgs()); | |
} | |
} | |
static async Task DeployToAws(BuildConfig buildConfig, string buildVersion, bool IsBuildMachine) | |
{ | |
var projectPath = $"{CommandUtils.CmdEnv.LocalRoot}/{buildConfig.ProjectName}"; | |
var parameters = new DockerTaskParameters | |
{ | |
Arguments = "buildx use arm-builder", | |
WorkingDir = projectPath | |
}; | |
if (IsBuildMachine) | |
{ | |
await new DockerTask(parameters) | |
.ExecuteAsync(new JobContext(null!, null!), [], new Dictionary<string, HashSet<FileReference>>()); | |
} | |
var awsRegion = RegionEndpoint.GetBySystemName(buildConfig.AWS.Region); | |
var awsCredentials = new BasicAWSCredentials(buildConfig.AWS.AccessKey, buildConfig.AWS.SecretKey); | |
var ecrClient = new AmazonECRClient(awsCredentials, awsRegion); | |
var tokenResponse = await ecrClient.GetAuthorizationTokenAsync(new GetAuthorizationTokenRequest()); | |
var tokenData = Convert.FromBase64String(tokenResponse.AuthorizationData.Single().AuthorizationToken); | |
var tokenContent = Encoding.UTF8.GetString(tokenData).Replace("AWS:", ""); | |
parameters = new DockerTaskParameters | |
{ | |
Arguments = $"login --username AWS --password {tokenContent} {buildConfig.AWS.ECRRepository}", | |
WorkingDir = projectPath | |
}; | |
if (IsBuildMachine) | |
{ | |
await new DockerTask(parameters) | |
.ExecuteAsync(new JobContext(null!, null!), [], new Dictionary<string, HashSet<FileReference>>()); | |
} | |
var tag = $"{buildConfig.AWS.ECRRepository}/{buildConfig.ProjectName.ToLower()}/{buildConfig.ProjectName.ToLower()}-dedi:{buildVersion}"; | |
parameters = new DockerTaskParameters | |
{ | |
Arguments = $"buildx build -t {tag} --platform linux/arm64 . --push", | |
WorkingDir = projectPath | |
}; | |
if (IsBuildMachine) | |
{ | |
await new DockerTask(parameters) | |
.ExecuteAsync(new JobContext(null!, null!), [], new Dictionary<string, HashSet<FileReference>>()); | |
} | |
var helmValuesFile = $"{CommandUtils.CmdEnv.LocalRoot}\\{buildConfig.ProjectName}\\Infrastructure\\Helm\\dedicated-server\\values.yaml"; | |
var deserializer = new DeserializerBuilder().WithCaseInsensitivePropertyMatching().Build(); | |
using var textReader = File.OpenText(helmValuesFile); | |
var helmValues = deserializer.Deserialize<HelmValues>(textReader); | |
textReader.Close(); | |
helmValues.Image.Tag = buildVersion; | |
if (IsBuildMachine && CommandUtils.P4Enabled && GlobalCommandLine.Submit && CommandUtils.AllowSubmit) | |
{ | |
Logger.LogInformation("Updating {helmFile} tag to build version {buildVersion}", helmValuesFile, buildVersion); | |
var workingCL = CommandUtils.P4.CreateChange(CommandUtils.P4Env.Client, | |
$"[skip-ci] Setting build version for dedicated server to {buildVersion}"); | |
CommandUtils.P4.Edit(workingCL, [helmValuesFile]); | |
await using var textWriter = File.CreateText(helmValuesFile); | |
var serializer = new SerializerBuilder().Build(); | |
serializer.Serialize(textWriter, helmValues); | |
textWriter.Close(); | |
CommandUtils.P4.Submit(workingCL, out _, true, true); | |
} | |
} | |
} |
Next steps are to move all the sensitive data into AWS and use Horde secrets to retrieve them
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Few comments:
BuildCookRun.cs
as I wanted to callDoBuildCookRunAsync
directly and it was a protected method.dotnet restore
on custom Automation Tool projects before trying to use them. You have to download and add the DLLs to the project manually.