// Copyright Epic Games, Inc. All Rights Reserved. using EpicGames.Core; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Xml; using AutomationTool; using UnrealBuildBase; using System.Threading.Tasks; using System.Text.RegularExpressions; using Microsoft.Extensions.Logging; namespace AutomationTool.Tasks { /// /// Parameters for a Docker-Build task /// public class DockerBuildTaskParameters { /// /// Base directory for the build /// [TaskParameter] public string BaseDir; /// /// Files to be staged before building the image /// [TaskParameter] public string Files; /// /// Path to the Dockerfile. Uses the root of basedir if not specified. /// [TaskParameter(Optional = true)] public string DockerFile; /// /// Path to a .dockerignore. Will be copied to basedir if specified. /// [TaskParameter(Optional = true)] public string DockerIgnoreFile; /// /// Use BuildKit in Docker /// [TaskParameter(Optional = true)] public bool UseBuildKit; /// /// Type of progress output (--progress) /// [TaskParameter(Optional = true)] public string ProgressOutput; /// /// Tag for the image /// [TaskParameter(Optional = true)] public string Tag; /// /// Optional arguments /// [TaskParameter(Optional = true)] public string Arguments; /// /// List of additional directories to overlay into the staged input files. Allows credentials to be staged, etc... /// [TaskParameter(Optional = true)] public string OverlayDirs; /// /// Environment variables to set /// [TaskParameter(Optional = true)] public string Environment; /// /// File to read environment variables from /// [TaskParameter(Optional = true)] public string EnvironmentFile; } /// /// Spawns Docker and waits for it to complete. /// [TaskElement("Docker-Build", typeof(DockerBuildTaskParameters))] public class DockerBuildTask : SpawnTaskBase { /// /// Parameters for this task /// DockerBuildTaskParameters Parameters; /// /// Construct a Docker task /// /// Parameters for the task public DockerBuildTask(DockerBuildTaskParameters InParameters) { Parameters = InParameters; } /// /// Execute the task. /// /// Information about the current job /// Set of build products produced by this node. /// Mapping from tag names to the set of files they include public override async Task ExecuteAsync(JobContext Job, HashSet BuildProducts, Dictionary> TagNameToFileSet) { Logger.LogInformation("Building Docker image"); using (LogIndentScope Scope = new LogIndentScope(" ")) { DirectoryReference BaseDir = ResolveDirectory(Parameters.BaseDir); List SourceFiles = ResolveFilespec(BaseDir, Parameters.Files, TagNameToFileSet).ToList(); bool isStagingEnabled = SourceFiles.Count > 0; DirectoryReference StagingDir = DirectoryReference.Combine(Unreal.EngineDirectory, "Intermediate", "Docker"); FileUtils.ForceDeleteDirectoryContents(StagingDir); List TargetFiles = SourceFiles.ConvertAll(x => FileReference.Combine(StagingDir, x.MakeRelativeTo(BaseDir))); CommandUtils.ThreadedCopyFiles(SourceFiles, BaseDir, StagingDir); FileReference DockerIgnoreFileInBaseDir = FileReference.Combine(BaseDir, ".dockerignore"); FileReference.Delete(DockerIgnoreFileInBaseDir); if (!String.IsNullOrEmpty(Parameters.OverlayDirs)) { foreach (string OverlayDir in Parameters.OverlayDirs.Split(';')) { CommandUtils.ThreadedCopyFiles(ResolveDirectory(OverlayDir), StagingDir); } } StringBuilder Arguments = new StringBuilder("build ."); if (Parameters.Tag != null) { Arguments.Append($" -t {Parameters.Tag}"); } if (Parameters.DockerFile != null) { FileReference DockerFile = ResolveFile(Parameters.DockerFile); if (!DockerFile.IsUnderDirectory(BaseDir)) { throw new AutomationException($"Dockerfile '{DockerFile}' is not under base directory ({BaseDir})"); } Arguments.Append($" -f {DockerFile.MakeRelativeTo(BaseDir).QuoteArgument()}"); } if (Parameters.DockerIgnoreFile != null) { FileReference DockerIgnoreFile = ResolveFile(Parameters.DockerIgnoreFile); FileReference.Copy(DockerIgnoreFile, DockerIgnoreFileInBaseDir); } if (Parameters.ProgressOutput != null) { Arguments.Append($" --progress={Parameters.ProgressOutput}"); } if (Parameters.Arguments != null) { Arguments.Append($" {Parameters.Arguments}"); } Dictionary EnvVars = ParseEnvVars(Parameters.Environment, Parameters.EnvironmentFile); if (Parameters.UseBuildKit) { EnvVars["DOCKER_BUILDKIT"] = "1"; } string WorkingDir = isStagingEnabled ? StagingDir.FullName : BaseDir.FullName; string Exe = DockerTask.GetDockerExecutablePath(); await SpawnTaskBase.ExecuteAsync(Exe, Arguments.ToString(), EnvVars: EnvVars, WorkingDir: WorkingDir, SpewFilterCallback: FilterOutput); } } static Regex FilterOutputPattern = new Regex(@"^#\d+ (?:\d+\.\d+ )?"); static string FilterOutput(string Line) => FilterOutputPattern.Replace(Line, ""); /// /// Output this task out to an XML writer. /// public override void Write(XmlWriter Writer) { Write(Writer, Parameters); } /// /// Find all the tags which are used as inputs to this task /// /// The tag names which are read by this task public override IEnumerable FindConsumedTagNames() { List TagNames = new List(); TagNames.AddRange(FindTagNamesFromFilespec(Parameters.DockerFile)); TagNames.AddRange(FindTagNamesFromFilespec(Parameters.Files)); return TagNames; } /// /// Find all the tags which are modified by this task /// /// The tag names which are modified by this task public override IEnumerable FindProducedTagNames() { yield break; } } }