Created
November 8, 2024 12:52
-
-
Save regner/8fac36b4b30b6af58db4c676fc8851a8 to your computer and use it in GitHub Desktop.
BuildHorde.cs
This file contains 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.Collections.Generic; | |
using System.Linq; | |
using System.Threading.Tasks; | |
using AutomationTool; | |
using AutomationTool.Tasks; | |
using EpicGames.BuildGraph; | |
using EpicGames.BuildGraph.Expressions; | |
using EpicGames.Core; | |
using UnrealBuildBase; | |
using Splash.BgArgParser; | |
using Splash.BgArgParser.OptionTypes; | |
namespace SDBuildGraph.Automation.Scripts; | |
public class BuildHorde : BgGraphBuilder | |
{ | |
private static readonly DirectoryReference DotNetDirectory = DirectoryReference.Combine(Unreal.RootDirectory, "Engine/Binaries/ThirdParty/DotNet"); | |
private static readonly FileReference DotNet6Path = FileReference.Combine(DotNetDirectory, "6.0.302/windows/dotnet.exe"); | |
private static readonly FileReference DotNet8Path = FileReference.Combine(DotNetDirectory, "8.0.100/windows/dotnet.exe"); | |
private static readonly DirectoryReference StagingDirectory = DirectoryReference.Combine(Unreal.RootDirectory, "HordeStaging"); | |
private static readonly DirectoryReference CmdDirectory = DirectoryReference.Combine(StagingDirectory, "cmd"); | |
private static readonly DirectoryReference AgentDirectory = DirectoryReference.Combine(StagingDirectory, "agent"); | |
public override BgGraph CreateGraph(BgEnvironment environment) | |
{ | |
// Script arguments | |
BooleanOption publishCmd = new BooleanOption("PublishCmd", false); | |
BooleanOption publishAgent = new BooleanOption("PublishAgent", false); | |
BooleanOption publishServer = new BooleanOption("PublishServer", false); | |
Parser parser = Parser.NewFromEnvironment(); | |
parser.Add(publishCmd); | |
parser.Add(publishAgent); | |
parser.Add(publishServer); | |
parser.Parse(); | |
// Agents | |
BgAgent testAgent = new BgAgent("Test Horde", "TestWin64"); | |
BgAgent compileAgent = new BgAgent("Build Horde Agent", "CompileWin64"); | |
BgAgent dockerAgent = new BgAgent("Build Horde Server", "DockerLinuxIncremental"); | |
// Labels | |
BgLabel testsLabel = new(name: "Tests", category: "Other"); | |
BgLabel cmdLabel = new(name: "CMD", category: "Other"); | |
BgLabel agentLabel = new(name: "Agent", category: "Other"); | |
BgLabel dashboardLabel = new(name: "Dashboard", category: "Other"); | |
BgLabel serverLabel = new(name: "Server", category: "Other"); | |
// Nodes | |
BgNode dotnetTestsNode = testAgent.AddNode(name: "DotNet Tests", func: context => RunDotnetTests(context)) | |
.AddLabel(testsLabel); | |
BgNode documentationTestNode = testAgent.AddNode(name: "Validate Documentation", func: context => RunMarkdownTests(context)) | |
.AddLabel(testsLabel); | |
BgNode staticAnalysisNode = testAgent.AddNode(name: "Static Analysis", func: context => RunBuildAnalyzer(context)) | |
.AddLabel(testsLabel); | |
BgNode cmdNode = compileAgent.AddNode(name: "Horde (Command-Line)", func: context => BuildCmd(context)) | |
.AddLabel(cmdLabel); | |
BgNode cmdPublishNode = compileAgent.AddNode(name: "Publish Horde (Command-Line)", func: context => PublishCmd(context)) | |
.Requires(cmdNode) | |
.AddLabel(cmdLabel); | |
BgNode agentNode = compileAgent.AddNode(name: "Horde Agent (Cross-Platform)", func: context => BuildAgent(context)) | |
.AddLabel(agentLabel); | |
BgNode agentPublishNode = compileAgent.AddNode(name: "Publish Horde Agent (Cross-Platform)", func: context => PublishAgent(context)) | |
.Requires(agentNode) | |
.AddLabel(agentLabel); | |
BgNode dashboardNode = dockerAgent.AddNode(name: "Horde Dashboard", func: context => BuildDashboard(context)) | |
.AddLabel(dashboardLabel); | |
BgNode serverNode = dockerAgent.AddNode(name: "Horde Server", func: context => BuildServer(context)) | |
.After(dashboardNode) | |
.AddLabel(serverLabel); | |
BgNode serverPublishNode = dockerAgent.AddNode(name: "Publish Horde Server", func: context => PublishServer(context)) | |
.After(serverNode) | |
.AddLabel(serverLabel); | |
// Finally put it all together | |
List<BgNode> nodes = new List<BgNode>() | |
{ | |
dotnetTestsNode, | |
documentationTestNode, | |
staticAnalysisNode, | |
cmdNode, | |
agentNode, | |
dashboardNode, | |
serverNode, | |
}; | |
if (publishCmd.Value) | |
{ | |
nodes.Add(cmdPublishNode); | |
} | |
if (publishAgent.Value) | |
{ | |
nodes.Add(agentPublishNode); | |
} | |
if (publishServer.Value) | |
{ | |
nodes.Add(serverPublishNode); | |
} | |
BgAggregate aggregate = new BgAggregate("Aggregate", nodes.ToList()); | |
return new BgGraph(nodes, aggregate); | |
} | |
/// <summary> | |
/// Runs the dotnet tests for Horde and related projects. | |
/// </summary> | |
private static async Task RunDotnetTests(BgContext context) | |
{ | |
// Test net6 projects | |
List<string> net6ProjectsToTest = new List<string>() | |
{ | |
"Engine/Source/Programs/Horde/Horde.Agent.Tests/Horde.Agent.Tests.csproj", | |
"Engine/Source/Programs/Shared/EpicGames.BuildGraph.Tests/EpicGames.BuildGraph.Tests.csproj", | |
"Engine/Source/Programs/Shared/EpicGames.Core.Tests/EpicGames.Core.Tests.csproj", | |
"Engine/Source/Programs/Shared/EpicGames.Horde.Tests/EpicGames.Horde.Tests.csproj", | |
"Engine/Source/Programs/Shared/EpicGames.IoHash.Tests/EpicGames.IoHash.Tests.csproj", | |
"Engine/Source/Programs/Shared/EpicGames.Perforce.Managed.Tests/EpicGames.Perforce.Managed.Tests.csproj", | |
"Engine/Source/Programs/Shared/EpicGames.Perforce.Tests/EpicGames.Perforce.Tests.csproj", | |
"Engine/Source/Programs/Shared/EpicGames.Redis.Tests/EpicGames.Redis.Tests.csproj", | |
"Engine/Source/Programs/Shared/EpicGames.Serialization.Tests/EpicGames.Serialization.Tests.csproj", | |
}; | |
foreach (string testProject in net6ProjectsToTest) | |
{ | |
await context.ExecuteAsync(new SpawnTask(new SpawnTaskParameters() | |
{ | |
Exe = DotNet6Path.FullName, | |
Arguments = $"test \"{testProject}\" --blame-hang-timeout 5m --blame-hang-dump-type mini --logger 'console;verbosity=normal'", | |
Environment = $"UE_DOTNET_VERSION=net6.0", | |
})); | |
} | |
// Test net8 projects | |
// Wasn't able to get these working, so commented out for now, tests are fine, the problem is more about using dotnet 6 and 8 | |
// List<string> net8ProjectsToTest = new List<string>() | |
// { | |
// "Engine/Source/Programs/Horde/Horde.Server.Tests/Horde.Server.Tests.csproj", | |
// }; | |
// | |
// foreach (string testProject in net8ProjectsToTest) | |
// { | |
// await context.ExecuteAsync(new SpawnTask(new SpawnTaskParameters() | |
// { | |
// Exe = DotNet8Path.FullName, | |
// Arguments = $"test \"{testProject}\" --blame-hang-timeout 5m --blame-hang-dump-type mini --logger 'console;verbosity=normal'", | |
// Environment = $"UE_DOTNET_VERSION=net8.0", | |
// })); | |
// } | |
} | |
/// <summary> | |
/// Validate the Horde documentation. | |
/// </summary> | |
private static async Task RunMarkdownTests(BgContext context) | |
{ | |
await context.ExecuteAsync(new CheckMarkdownTask(new CheckMarkdownTaskParameters() | |
{ | |
Files = $"{Unreal.RootDirectory}/Engine/Source/Programs/Horde/README.md;{Unreal.RootDirectory}/Engine/Source/Programs/Horde/Docs/..." | |
})); | |
} | |
/// <summary> | |
/// Build the Horde projects with the analyzer enabled. | |
/// </summary> | |
private static async Task RunBuildAnalyzer(BgContext context) | |
{ | |
// net6 projects to analyze | |
List<string> net6ProjectsToTest = new List<string>() | |
{ | |
"Engine/Source/Programs/Horde/Horde.Agent/Horde.Agent.csproj", | |
"Engine/Source/Programs/Horde/Horde.Agent.Tests/Horde.Agent.Tests.csproj", | |
}; | |
foreach (string testProject in net6ProjectsToTest) | |
{ | |
await context.ExecuteAsync(new SpawnTask(new SpawnTaskParameters() | |
{ | |
Exe = DotNet6Path.FullName, | |
Arguments = $"build \"{testProject}\" -p:Configuration=Analyze", | |
Environment = $"UE_DOTNET_VERSION=net6.0", | |
})); | |
} | |
// net8 projects to analyze | |
List<string> net8ProjectsToTest = new List<string>() | |
{ | |
"Engine/Source/Programs/Horde/Horde/Horde.csproj", | |
"Engine/Source/Programs/Horde/Horde.Server/Horde.Server.csproj", | |
"Engine/Source/Programs/Horde/Horde.Server.Tests/Horde.Server.Tests.csproj", | |
}; | |
foreach (string testProject in net8ProjectsToTest) | |
{ | |
await context.ExecuteAsync(new SpawnTask(new SpawnTaskParameters() | |
{ | |
Exe = DotNet8Path.FullName, | |
Arguments = $"build \"{testProject}\" -p:Configuration=Analyze", | |
Environment = $"UE_DOTNET_VERSION=net8.0", | |
})); | |
} | |
} | |
/// <summary> | |
/// Builds the Horde project which produces the Horde CLI tool. | |
/// </summary> | |
private static async Task<BgFileSet> BuildCmd(BgContext context) | |
{ | |
FileUtils.ForceDeleteDirectory(CmdDirectory); | |
string arguments = "publish"; | |
arguments += $" \"{Unreal.RootDirectory}/Engine/Source/Programs/Horde/Horde/Horde.csproj\""; | |
arguments += $" --output \"{CmdDirectory}\""; | |
arguments += $" --runtime win-x64 --self-contained"; | |
arguments += Utilities.DotnetBuilds.GetVersionArguments(context); | |
await context.ExecuteAsync(new SpawnTask(new SpawnTaskParameters() | |
{ | |
Exe = DotNet8Path.FullName, | |
Arguments = arguments, | |
Environment = $"UE_DOTNET_VERSION=net8.0", | |
})); | |
if (context.IsBuildMachine) | |
{ | |
await context.ExecuteAsync(new CreateArtifactTask(new CreateArtifactTaskParameters() | |
{ | |
Name = "horde-cmd", | |
BaseDir = CmdDirectory.FullName, | |
Type = "tools-artifact", | |
})); | |
} | |
return FileSet.FromDirectory(CmdDirectory); | |
} | |
/// <summary> | |
/// Publishes the Horde CLI tool to Horde tools. | |
/// </summary> | |
private static async Task PublishCmd(BgContext context) | |
{ | |
if (context.IsBuildMachine) | |
{ | |
await context.ExecuteAsync(new DeployToolTask(new DeployToolTaskParameters() | |
{ | |
Id = "horde-cmd", | |
Version = $"{Utilities.DotnetBuilds.GetEngineVersion()}-{context.Change}", | |
Directory = CmdDirectory.FullName, | |
})); | |
} | |
} | |
/// <summary> | |
/// Builds the Horde agent itself. | |
/// </summary> | |
private static async Task<BgFileSet> BuildAgent(BgContext context) | |
{ | |
FileUtils.ForceDeleteDirectory(AgentDirectory); | |
string arguments = "publish"; | |
arguments += $" \"{Unreal.RootDirectory.FullName}/Engine/Source/Programs/Horde/Horde.Agent/Horde.Agent.csproj\""; | |
arguments += $" --output \"{AgentDirectory}\""; | |
arguments += Utilities.DotnetBuilds.GetVersionArguments(context); | |
await context.ExecuteAsync(new SpawnTask(new SpawnTaskParameters() | |
{ | |
Exe = DotNet6Path.FullName, | |
Arguments = arguments, | |
Environment = $"UE_DOTNET_VERSION=net6.0", | |
})); | |
if (context.IsBuildMachine) | |
{ | |
await context.ExecuteAsync(new CreateArtifactTask(new CreateArtifactTaskParameters() | |
{ | |
Name = $"horde-agent", | |
BaseDir = AgentDirectory.FullName, | |
Type = "tools-artifact", | |
})); | |
} | |
return FileSet.FromDirectory(AgentDirectory); | |
} | |
/// <summary> | |
/// Publishes the Horde agent tool to Horde tools. | |
/// </summary> | |
private static async Task PublishAgent(BgContext context) | |
{ | |
if (context.IsBuildMachine) | |
{ | |
await context.ExecuteAsync(new DeployToolTask(new DeployToolTaskParameters() | |
{ | |
Id = $"horde-agent", | |
Version = $"{Utilities.DotnetBuilds.GetEngineVersion()}-{context.Change}", | |
Directory = AgentDirectory.FullName, | |
})); | |
} | |
} | |
/// <summary> | |
/// Builds the Horde dashboard Docker image. | |
/// | |
/// We only intend to distribute the dashboard as part of the Horde server itself. We therefor | |
/// do not support pushing the Horde dashboard Docker image to a remote registry. | |
/// </summary> | |
private static async Task BuildDashboard(BgContext context) | |
{ | |
await context.ExecuteAsync(new CopyTask(new CopyTaskParameters() | |
{ | |
From = "Engine/Source/Programs/Horde/Docs/...", | |
To = "Engine/Source/Programs/Horde/HordeDashboard/documentation/Docs/...", | |
})); | |
await context.ExecuteAsync(new CopyTask(new CopyTaskParameters() | |
{ | |
From = "Engine/Source/Programs/Horde/README.md", | |
To = "Engine/Source/Programs/Horde/HordeDashboard/documentation/README.md", | |
})); | |
await context.ExecuteAsync(new TagTask(new TagTaskParameters() | |
{ | |
Files = "Engine/Source/Programs/Horde/HordeDashboard/...", | |
Except = "Engine/Source/Programs/Horde/HordeDashboard/node_modules/...", | |
With = "#InputFiles", | |
})); | |
string buildArgs = $"--build-arg \"VersionInfo={Utilities.DotnetBuilds.GetEngineVersion()}-{context.Change}\""; | |
buildArgs += " --build-arg \"DashboardConfig=Production\""; | |
await context.ExecuteAsync(new DockerBuildTask(new DockerBuildTaskParameters() | |
{ | |
BaseDir = "Engine", | |
Files = "#InputFiles", | |
DockerFile = "Engine/Source/Programs/Horde/HordeDashboard/Dockerfile", | |
Arguments = buildArgs, | |
Tag = "hordedashboard-public", // This name is referenced in the Dockerfile.dashboard from Horde server | |
})); | |
} | |
/// <summary> | |
/// Builds the Horde server Docker image. | |
/// | |
/// This node must be built on the same agent as the dashboard as it relies on the dashboard image | |
/// existing on the local machine. | |
/// </summary> | |
private static async Task BuildServer(BgContext context) | |
{ | |
string[] files = { | |
"Engine/Binaries/DotNET/EpicGames.Perforce.Native/...", | |
"Engine/Source/Programs/Shared/...", | |
"Engine/Source/Programs/Horde/...", | |
"Engine/Source/Programs/AutomationTool/AutomationUtils/Matchers/...", | |
"Engine/Source/Programs/UnrealBuildTool/Matchers/...", | |
}; | |
await context.ExecuteAsync(new TagTask(new TagTaskParameters() | |
{ | |
Files = string.Join(";", files), | |
Except = ".../.vs/...;.../.git/...;.../bin/...;.../obj/...", | |
With = "#InputFiles", | |
})); | |
// Build a Docker image with just the Horde server | |
await context.ExecuteAsync(new DockerBuildTask(new DockerBuildTaskParameters() | |
{ | |
BaseDir = "Engine", | |
Files = "#InputFiles", | |
DockerFile = "Engine/Source/Programs/Horde/Horde.Server/Dockerfile", | |
Arguments = $"--build-arg msbuild_args=\"{Utilities.DotnetBuilds.GetVersionArguments(context)}\"", | |
Tag = "horde-server-bare", // This name is referenced in the Dockerfile.dashboard from Horde server | |
})); | |
// Build a Docker image that combines the bare Horde server with the Horde dashboard | |
await context.ExecuteAsync(new DockerBuildTask(new DockerBuildTaskParameters() | |
{ | |
BaseDir = "Engine/Source/Programs/Horde/Horde.Server", | |
Files = "Dockerfile*", | |
DockerFile = "Engine/Source/Programs/Horde/Horde.Server/Dockerfile.dashboard", | |
Tag = "horde-server", | |
})); | |
} | |
/// <summary> | |
/// Publishes the Horde server Docker image. | |
/// </summary> | |
private static async Task PublishServer(BgContext context) | |
{ | |
if (context.IsBuildMachine) | |
{ | |
await context.ExecuteAsync(new DockerPushTask(new DockerPushTaskParameters() | |
{ | |
Repository = "internal-docker-registry", | |
Image = "horde-server", | |
TargetImage = $"epic/horde/server:{context.Change}", | |
RepositoryAuthFile = "/some/path/nexus.json" | |
})); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment