// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Xml; using EpicGames.Core; using Microsoft.Extensions.Logging; using UnrealBuildBase; namespace AutomationTool.Tasks { /// /// Parameters for a Docker-Build task /// public class DockerBuildTaskParameters { /// /// Base directory for the build /// [TaskParameter] public string BaseDir { get; set; } /// /// Files to be staged before building the image /// [TaskParameter] public string Files { get; set; } /// /// Path to the Dockerfile. Uses the root of basedir if not specified. /// [TaskParameter(Optional = true)] public string DockerFile { get; set; } /// /// Path to a .dockerignore. Will be copied to basedir if specified. /// [TaskParameter(Optional = true)] public string DockerIgnoreFile { get; set; } /// /// Use BuildKit in Docker /// [TaskParameter(Optional = true)] public bool UseBuildKit { get; set; } /// /// Set ulimit in build /// [TaskParameter(Optional = true)] public string Ulimit { get; set; } = "nofile=100000:100000"; /// /// Type of progress output (--progress) /// [TaskParameter(Optional = true)] public string ProgressOutput { get; set; } /// /// Tag for the image /// [TaskParameter(Optional = true)] public string Tag { get; set; } /// /// Set the target build stage to build (--target) /// [TaskParameter(Optional = true)] public string Target { get; set; } /// /// Custom output exporter. Requires BuildKit (--output) /// [TaskParameter(Optional = true)] public string Output { get; set; } /// /// Optional arguments /// [TaskParameter(Optional = true)] public string Arguments { get; set; } /// /// List of additional directories to overlay into the staged input files. Allows credentials to be staged, etc... /// [TaskParameter(Optional = true)] public string OverlayDirs { get; set; } /// /// Environment variables to set /// [TaskParameter(Optional = true)] public string Environment { get; set; } /// /// File to read environment variables from /// [TaskParameter(Optional = true)] public string EnvironmentFile { get; set; } } /// /// Spawns Docker and waits for it to complete. /// [TaskElement("Docker-Build", typeof(DockerBuildTaskParameters))] public class DockerBuildTask : SpawnTaskBase { readonly DockerBuildTaskParameters _parameters; /// /// Construct a Docker task /// /// Parameters for the task public DockerBuildTask(DockerBuildTaskParameters parameters) { _parameters = parameters; } /// /// ExecuteAsync 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.Target != null) { arguments.Append($" --target {_parameters.Target}"); } if (_parameters.Output != null) { if (!_parameters.UseBuildKit) { throw new AutomationException($"{nameof(_parameters.UseBuildKit)} must be enabled to use '{nameof(_parameters.Output)}' parameter"); } arguments.Append($" --output {_parameters.Output}"); } 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.Ulimit != null && _parameters.Ulimit.Length > 1) { arguments.Append($" --ulimit={_parameters.Ulimit}"); } 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 readonly Regex s_filterOutputPattern = new Regex(@"^#\d+ (?:\d+\.\d+ )?"); static string FilterOutput(string line) => s_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; } } }