1027 lines
35 KiB
C#
1027 lines
35 KiB
C#
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using AutomationTool;
|
|
using UnrealBuildTool;
|
|
using System.Text.RegularExpressions;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using EpicGames.Core;
|
|
using System.Text.Json.Serialization;
|
|
using System.Text.Json;
|
|
using UnrealGame;
|
|
|
|
namespace Gauntlet
|
|
{
|
|
public class UnrealBuildSource : IBuildSource
|
|
{
|
|
public DirectoryReference UnrealPath { get; protected set; }
|
|
|
|
public string ProjectName { get; protected set; }
|
|
|
|
public FileReference ProjectPath { get; protected set; }
|
|
|
|
public bool UsesSharedBuildType { get; protected set; }
|
|
|
|
public string BuildName { get; protected set; }
|
|
|
|
public IEnumerable<string> BuildPaths { get; protected set; }
|
|
|
|
public string Branch { get; protected set; }
|
|
|
|
public int Changelist { get; protected set; }
|
|
|
|
public int Preflight { get; protected set; }
|
|
|
|
public bool EditorValid { get; protected set; }
|
|
|
|
protected Dictionary<UnrealTargetPlatform, List<IBuild>> DiscoveredBuilds;
|
|
|
|
protected class ArtifactMetadata
|
|
{
|
|
[JsonPropertyName("change")]
|
|
public string Change { get; set; }
|
|
[JsonPropertyName("preflight")]
|
|
public string Preflight { get; set; }
|
|
[JsonPropertyName("branch")]
|
|
public string Branch { get; set; }
|
|
[JsonPropertyName("buildName")]
|
|
public string BuildName { get; set; }
|
|
[JsonPropertyName("configuration")]
|
|
public string Configuration { get; set; }
|
|
[JsonPropertyName("platform")]
|
|
public string Platform { get; set; }
|
|
[JsonPropertyName("type")]
|
|
public string Type { get; set; }
|
|
|
|
public static ArtifactMetadata LoadFromJson(string FilePath)
|
|
{
|
|
JsonSerializerOptions Options = new JsonSerializerOptions
|
|
{
|
|
PropertyNameCaseInsensitive = true
|
|
};
|
|
string JsonString = File.ReadAllText(FilePath);
|
|
ArtifactMetadata JsonTestPassResults = JsonSerializer.Deserialize<ArtifactMetadata>(JsonString, Options);
|
|
return JsonTestPassResults;
|
|
}
|
|
}
|
|
|
|
public UnrealBuildSource(string InProjectName, FileReference InProjectPath, DirectoryReference InUnrealPath, bool InUsesSharedBuildType, string BuildReference)
|
|
{
|
|
InitBuildSource(InProjectName, InProjectPath, InUnrealPath, InUsesSharedBuildType, BuildReference, null);
|
|
}
|
|
|
|
public UnrealBuildSource(string InProjectName, FileReference InProjectPath, DirectoryReference InUnrealPath, bool InUsesSharedBuildType, string BuildReference, Func<string, string> ResolutionDelegate)
|
|
{
|
|
InitBuildSource(InProjectName, InProjectPath, InUnrealPath, InUsesSharedBuildType, BuildReference, ResolutionDelegate);
|
|
}
|
|
|
|
public UnrealBuildSource(string InProjectName, FileReference InProjectPath, DirectoryReference InUnrealPath, bool InUsesSharedBuildType, string BuildReference, IEnumerable<string> InSearchPaths)
|
|
{
|
|
InitBuildSource(InProjectName, InProjectPath, InUnrealPath, InUsesSharedBuildType, BuildReference, (string BuildRef) =>
|
|
{
|
|
foreach (string SearchPath in InSearchPaths)
|
|
{
|
|
string AggregatedPath = Path.Combine(SearchPath, BuildRef);
|
|
if (AggregatedPath.Length > 0)
|
|
{
|
|
DirectoryInfo SearchDir = new DirectoryInfo(AggregatedPath);
|
|
|
|
if (SearchDir.Exists)
|
|
{
|
|
return SearchDir.FullName;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
});
|
|
}
|
|
|
|
public bool CanSupportPlatform(UnrealTargetPlatform Platform)
|
|
{
|
|
return UnrealTargetPlatform.GetValidPlatforms().Contains(Platform);
|
|
}
|
|
|
|
|
|
protected void InitBuildSource(string InProjectName, FileReference InProjectPath, DirectoryReference InUnrealPath, bool InUsesSharedBuildType, string InBuildArgument, Func<string, string> ResolutionDelegate)
|
|
{
|
|
UnrealPath = InUnrealPath;
|
|
UsesSharedBuildType = InUsesSharedBuildType;
|
|
|
|
ProjectPath = InProjectPath;
|
|
ProjectName = InProjectName;
|
|
|
|
// Resolve the build argument into something meaningful
|
|
string ResolvedBuildName;
|
|
IEnumerable<string> ResolvedPaths = null;
|
|
|
|
if (!ResolveBuildReference(InBuildArgument, ResolutionDelegate, out ResolvedPaths, out ResolvedBuildName))
|
|
{
|
|
throw new AutomationException("Unable to resolve {0} to a valid build", InBuildArgument);
|
|
}
|
|
|
|
BuildName = ResolvedBuildName;
|
|
BuildPaths = ResolvedPaths;
|
|
|
|
// any Branch/CL info?
|
|
Match M = Regex.Match(BuildName, @"(\+\+.+)-CL-(\d+)");
|
|
|
|
if (M.Success)
|
|
{
|
|
Branch = M.Groups[1].Value.Replace("+", "/");
|
|
Changelist = Convert.ToInt32(M.Groups[2].Value);
|
|
}
|
|
else
|
|
{
|
|
Branch = "";
|
|
Changelist = 0;
|
|
}
|
|
|
|
// Preflight?
|
|
M = Regex.Match(BuildName, @"-PF-(\d+)");
|
|
Preflight = M.Success? Convert.ToInt32(M.Groups[1].Value) : 0;
|
|
|
|
// Look if the build has an artifact metadata file
|
|
string ArtifactMetadataFilePath = Path.Combine(InBuildArgument, "artifactmetadata.json");
|
|
if (File.Exists(ArtifactMetadataFilePath))
|
|
{
|
|
ArtifactMetadata Metadata = null;
|
|
try
|
|
{
|
|
Metadata = ArtifactMetadata.LoadFromJson(ArtifactMetadataFilePath);
|
|
}
|
|
catch (Exception Ex)
|
|
{
|
|
Log.Warning("Could not parse ArtifactMetadata file '{File}': {Exception}", ArtifactMetadataFilePath, Ex.Message);
|
|
}
|
|
if (Metadata != null)
|
|
{
|
|
if (!string.IsNullOrEmpty(Metadata.Branch))
|
|
{
|
|
Branch = Metadata.Branch;
|
|
}
|
|
if (!string.IsNullOrEmpty(Metadata.Change))
|
|
{
|
|
int MetaChange;
|
|
if (int.TryParse(Metadata.Change, out MetaChange))
|
|
{
|
|
Changelist = MetaChange;
|
|
}
|
|
}
|
|
if (!string.IsNullOrEmpty(Metadata.Preflight))
|
|
{
|
|
int MetaPreflight;
|
|
if (int.TryParse(Metadata.Preflight, out MetaPreflight))
|
|
{
|
|
Preflight = MetaPreflight;
|
|
}
|
|
}
|
|
if (!string.IsNullOrEmpty(Metadata.BuildName))
|
|
{
|
|
BuildName = Metadata.BuildName;
|
|
}
|
|
}
|
|
}
|
|
|
|
// allow user overrides (TODO - centralize all this!)
|
|
Branch = Globals.Params.ParseValue("branch", Branch);
|
|
Changelist = Convert.ToInt32(Globals.Params.ParseValue("changelist", Changelist.ToString()));
|
|
Preflight = Convert.ToInt32(Globals.Params.ParseValue("preflight", Preflight.ToString()));
|
|
|
|
// We resolve these on demand
|
|
DiscoveredBuilds = new Dictionary<UnrealTargetPlatform, List<IBuild>>();
|
|
}
|
|
|
|
virtual protected bool ResolveBuildReference(string InBuildReference, Func<string, string> ResolutionDelegate, out IEnumerable<string> OutBuildPaths, out string OutBuildName)
|
|
{
|
|
OutBuildName = null;
|
|
// start as null. It's valid for some references to return empty paths so we use null to verify
|
|
// that a resolution did happen
|
|
OutBuildPaths = null;
|
|
|
|
if (string.IsNullOrEmpty(InBuildReference))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (InBuildReference.Equals("AutoP4", StringComparison.InvariantCultureIgnoreCase))
|
|
{
|
|
if (!CommandUtils.P4Enabled)
|
|
{
|
|
throw new AutomationException("-Build=AutoP4 requires -P4");
|
|
}
|
|
if (CommandUtils.P4Env.Changelist < 1000)
|
|
{
|
|
throw new AutomationException("-Build=AutoP4 requires a CL from P4 and we have {0}", CommandUtils.P4Env.Changelist);
|
|
}
|
|
|
|
string BuildRoot = CommandUtils.CombinePaths(CommandUtils.RootBuildStorageDirectory());
|
|
string CachePath = InternalUtils.GetEnvironmentVariable("UE-BuildCachePath", "");
|
|
|
|
string SrcBuildPath = CommandUtils.CombinePaths(BuildRoot, ProjectName);
|
|
string SrcBuildPath2 = CommandUtils.CombinePaths(BuildRoot, ProjectName.Replace("Game", "").Replace("game", ""));
|
|
|
|
string SrcBuildPath_Cache = CommandUtils.CombinePaths(CachePath, ProjectName);
|
|
string SrcBuildPath2_Cache = CommandUtils.CombinePaths(CachePath, ProjectName.Replace("Game", "").Replace("game", ""));
|
|
|
|
if (!InternalUtils.SafeDirectoryExists(SrcBuildPath))
|
|
{
|
|
if (!InternalUtils.SafeDirectoryExists(SrcBuildPath2))
|
|
{
|
|
throw new AutomationException("-Build=AutoP4: Neither {0} nor {1} exists.", SrcBuildPath, SrcBuildPath2);
|
|
}
|
|
SrcBuildPath = SrcBuildPath2;
|
|
SrcBuildPath_Cache = SrcBuildPath2_Cache;
|
|
}
|
|
string SrcCLPath = CommandUtils.CombinePaths(SrcBuildPath, CommandUtils.EscapePath(CommandUtils.P4Env.Branch) + "-CL-" + CommandUtils.P4Env.Changelist.ToString());
|
|
string SrcCLPath_Cache = CommandUtils.CombinePaths(SrcBuildPath_Cache, CommandUtils.EscapePath(CommandUtils.P4Env.Branch) + "-CL-" + CommandUtils.P4Env.Changelist.ToString());
|
|
if (!InternalUtils.SafeDirectoryExists(SrcCLPath))
|
|
{
|
|
throw new AutomationException("-Build=AutoP4: {0} does not exist.", SrcCLPath);
|
|
}
|
|
|
|
if (InternalUtils.SafeDirectoryExists(SrcCLPath_Cache))
|
|
{
|
|
InBuildReference = SrcCLPath_Cache;
|
|
}
|
|
else
|
|
{
|
|
InBuildReference = SrcCLPath;
|
|
}
|
|
Log.Verbose("Using AutoP4 path {0}", InBuildReference);
|
|
}
|
|
|
|
// BuildParam could be a path, a name that we should resolve to a path, Staged, or Editor
|
|
DirectoryInfo BuildDir = new DirectoryInfo(InBuildReference);
|
|
|
|
if (BuildDir.Exists)
|
|
{
|
|
// Easy option first - is this a full path?
|
|
OutBuildName = BuildDir.Name;
|
|
OutBuildPaths = new string[] { BuildDir.FullName };
|
|
}
|
|
else if (BuildDir.Name.Equals("editor", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// Second special case - "Editor" means run using the editor, no path needed
|
|
OutBuildName = "Editor";
|
|
OutBuildPaths = Enumerable.Empty<string>();
|
|
}
|
|
else if (BuildDir.Name.Equals("local", StringComparison.OrdinalIgnoreCase) || BuildDir.Name.Equals("staged", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// First special case - "Staged" means use whats locally staged
|
|
OutBuildName = "Local";
|
|
string StagedPath = Path.Combine(ProjectPath.Directory.FullName, "Saved", "StagedBuilds");
|
|
|
|
if (Directory.Exists(StagedPath) == false)
|
|
{
|
|
Log.Error("BuildReference was Staged but staged directory {0} not found", StagedPath);
|
|
return false;
|
|
}
|
|
|
|
// include binaries path for packaged builds if it exists
|
|
string BinariesPath = Path.Combine(ProjectPath.Directory.FullName, "Binaries");
|
|
OutBuildPaths = Directory.Exists(BinariesPath) ? new string[] { StagedPath, BinariesPath } : new string[] { StagedPath };
|
|
}
|
|
else if (BuildDir.Name.Equals("LatestGood", StringComparison.OrdinalIgnoreCase)
|
|
|| BuildDir.Name.Equals("LKG", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
string RequestedValidator = Globals.Params.ParseValue("BuildValidator", null);
|
|
IBuildValidator Validator = Utils.InterfaceHelpers.FindImplementations<IBuildValidator>(true)
|
|
.Where(Validator => Validator.CanSupportProject(ProjectName))
|
|
.Where(Validator => string.IsNullOrEmpty(RequestedValidator) || Validator.Name.Equals(RequestedValidator, StringComparison.OrdinalIgnoreCase))
|
|
.FirstOrDefault();
|
|
|
|
if(Validator == null)
|
|
{
|
|
Log.Error("No build validator that can support project {ProjectName} was found.", ProjectName);
|
|
return false;
|
|
}
|
|
|
|
string LatestGoodBuild = Validator.GetLatestGoodBuild();
|
|
if(string.IsNullOrEmpty(LatestGoodBuild))
|
|
{
|
|
Log.Error("No latest good build was able to be found!");
|
|
return false;
|
|
}
|
|
|
|
OutBuildPaths = new[] { LatestGoodBuild };
|
|
OutBuildName = Path.GetFileName(LatestGoodBuild);
|
|
|
|
Log.Info("{Validator} selected {Build} as the latest good build. Proceeding with this build", Validator.GetType().Name, OutBuildName);
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
// todo - make this more generic
|
|
if (BuildDir.Name.Equals("usesyncedbuild", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
BuildVersion Version;
|
|
if (BuildVersion.TryRead(BuildVersion.GetDefaultFileName(), out Version))
|
|
{
|
|
InBuildReference = Version.BranchName + "-CL-" + Version.Changelist.ToString();
|
|
}
|
|
}
|
|
|
|
// See if it's in the passed locations
|
|
if (ResolutionDelegate != null)
|
|
{
|
|
string FullPath = ResolutionDelegate(InBuildReference);
|
|
|
|
if (string.IsNullOrEmpty(FullPath) == false)
|
|
{
|
|
DirectoryInfo Di = new DirectoryInfo(FullPath);
|
|
|
|
if (Di.Exists == false)
|
|
{
|
|
throw new AutomationException("Resolution delegate returned non existent path");
|
|
}
|
|
|
|
OutBuildName = Di.Name;
|
|
OutBuildPaths = new string[] { Di.FullName };
|
|
}
|
|
}
|
|
}
|
|
|
|
if (string.IsNullOrEmpty(OutBuildName) || OutBuildPaths == null)
|
|
{
|
|
Log.Error("Unable to resolve build argument '{0}'", InBuildReference);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the provided build to our list (calls ShouldMakeBuildAvailable to verify).
|
|
/// </summary>
|
|
/// <param name="NewBuild"></param>
|
|
virtual protected void AddBuild(IBuild NewBuild)
|
|
{
|
|
NewBuild = ShouldMakeBuildAvailable(NewBuild);
|
|
|
|
if (NewBuild != null)
|
|
{
|
|
if (!DiscoveredBuilds.ContainsKey(NewBuild.Platform))
|
|
{
|
|
DiscoveredBuilds[NewBuild.Platform] = new List<IBuild>();
|
|
}
|
|
|
|
string Flavor = string.IsNullOrEmpty(NewBuild.Flavor) ? "None" : NewBuild.Flavor;
|
|
Log.Info("Adding {Platform} {BuildType} | Configuration: {Configuration} | Flavor: {Flavor} | Flags: {Flags} | PreferenceOrder: {PreferenceOrder}",
|
|
NewBuild.Platform, NewBuild.GetType().Name, NewBuild.Configuration, Flavor, NewBuild.Flags, NewBuild.PreferenceOrder);
|
|
DiscoveredBuilds[NewBuild.Platform].Add(NewBuild);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Allows derived classes to nix or modify builds as they are discovered
|
|
/// </summary>
|
|
/// <param name="InBuild"></param>
|
|
/// <returns></returns>
|
|
virtual protected IBuild ShouldMakeBuildAvailable(IBuild InBuild)
|
|
{
|
|
return InBuild;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds an Editor build to our list of available builds if one exists
|
|
/// </summary>
|
|
/// <param name="InUnrealPath"></param>
|
|
/// <param name="InConfiguration"></param>
|
|
virtual protected IBuild CreateEditorBuild(DirectoryReference InUnrealPath, UnrealTargetConfiguration InConfiguration = UnrealTargetConfiguration.Development)
|
|
{
|
|
if (InUnrealPath != null)
|
|
{
|
|
// check for the editor
|
|
string EditorExe = Path.Combine(InUnrealPath.FullName, GetRelativeExecutablePath(UnrealTargetRole.Editor, BuildHostPlatform.Current.Platform, InConfiguration));
|
|
string OverlayExe = Path.Combine(InUnrealPath.FullName, GetModuleOverlayExecutablePath(UnrealTargetRole.Editor, BuildHostPlatform.Current.Platform));
|
|
|
|
if (Utils.SystemHelpers.ApplicationExists(EditorExe))
|
|
{
|
|
EditorBuild NewBuild = new EditorBuild(EditorExe, InConfiguration);
|
|
|
|
return NewBuild;
|
|
}
|
|
|
|
if (Utils.SystemHelpers.ApplicationExists(OverlayExe))
|
|
{
|
|
return new EditorBuild(OverlayExe, InConfiguration);
|
|
}
|
|
|
|
Log.Info("No editor binaries found at {0}. Unable to create an editor build source.", EditorExe);
|
|
}
|
|
else
|
|
{
|
|
Log.Info("No path to Unreal found. Unable to create an editor build source.");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// True/false on whether we've tried to discover builds for the specified platform
|
|
/// </summary>
|
|
/// <param name="InPlatform"></param>
|
|
/// <returns></returns>
|
|
bool HaveDiscoveredBuilds(UnrealTargetPlatform InPlatform)
|
|
{
|
|
return DiscoveredBuilds.ContainsKey(InPlatform);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Discover all builds for the specified platform. Nop if this has already been run
|
|
/// for the provided platform
|
|
/// </summary>
|
|
/// <param name="InPlatform"></param>
|
|
/// <param name="InConfiguration"></param>
|
|
virtual protected void DiscoverBuilds(UnrealTargetPlatform InPlatform, UnrealTargetConfiguration InConfiguration = UnrealTargetConfiguration.Development)
|
|
{
|
|
if (!HaveDiscoveredBuilds(InPlatform))
|
|
{
|
|
Log.Info("Discovering builds for {0}", InPlatform);
|
|
DiscoveredBuilds[InPlatform] = new List<IBuild>();
|
|
|
|
// Add an editor build if this is our current platform.
|
|
if (InPlatform == BuildHostPlatform.Current.Platform)
|
|
{
|
|
IBuild EditorBuild = CreateEditorBuild(UnrealPath, InConfiguration);
|
|
|
|
if (EditorBuild == null)
|
|
{
|
|
Log.Info("Could not create editor build for project. Binaries are likely missing");
|
|
}
|
|
else
|
|
{
|
|
AddBuild(EditorBuild);
|
|
}
|
|
}
|
|
|
|
if (BuildPaths.Any())
|
|
{
|
|
foreach (string Path in BuildPaths)
|
|
{
|
|
IEnumerable<IFolderBuildSource> BuildSources = Gauntlet.Utils.InterfaceHelpers.FindImplementations<IFolderBuildSource>(true)
|
|
.Where(BS => BS.CanSupportPlatform(InPlatform));
|
|
|
|
foreach (var BS in BuildSources)
|
|
{
|
|
if (!BS.BuildName.Contains(InPlatform.ToString(), StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
IEnumerable<IBuild> Builds = BS.GetBuildsAtPath(ProjectName, Path);
|
|
|
|
foreach (IBuild Build in Builds)
|
|
{
|
|
AddBuild(Build);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns how many builds are available for the specified platform
|
|
/// </summary>
|
|
/// <param name="InPlatform"></param>
|
|
/// <param name="InFlags"></param>
|
|
/// <returns></returns>
|
|
public int GetBuildCount(UnrealTargetPlatform InPlatform, BuildFlags InFlags = BuildFlags.None)
|
|
{
|
|
if (!HaveDiscoveredBuilds(InPlatform))
|
|
{
|
|
DiscoverBuilds(InPlatform);
|
|
}
|
|
|
|
return DiscoveredBuilds[InPlatform].Where(B => (B.Flags & InFlags) == InFlags).Count();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns all builds that match the specified parameters. If no builds have been discovered then that is performed first
|
|
/// </summary>
|
|
/// <param name="InRole"></param>
|
|
/// <param name="InPlatform"></param>
|
|
/// <param name="InConfiguration"></param>
|
|
/// <param name="InFlags"></param>
|
|
/// <param name="InFlavor">Optional special flavor of the build, e.g. asan/ubsan/clang/etc..., which can be added on top of a standard configuration.</param>
|
|
/// <returns></returns>
|
|
IEnumerable<IBuild> GetMatchingBuilds(UnrealTargetRole InRole, UnrealTargetPlatform? InPlatform, UnrealTargetConfiguration InConfiguration, BuildFlags InFlags, string InFlavor="")
|
|
{
|
|
// can't get a build with no platform or if we have none
|
|
if (InPlatform == null)
|
|
{
|
|
return new IBuild[0];
|
|
}
|
|
|
|
if (!HaveDiscoveredBuilds(InPlatform.Value))
|
|
{
|
|
DiscoverBuilds(InPlatform.Value, InConfiguration);
|
|
}
|
|
|
|
IEnumerable<IBuild> PlatformBuilds = DiscoveredBuilds[InPlatform.Value];
|
|
|
|
IEnumerable<IBuild> MatchingBuilds = PlatformBuilds.Where((B) =>
|
|
{
|
|
if (B.CanSupportRole(InRole)
|
|
&& B.Configuration == InConfiguration
|
|
&& (B.Flags & InFlags) == InFlags
|
|
&& (B.Flavor == InFlavor))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
if (MatchingBuilds.Count() > 0)
|
|
{
|
|
return MatchingBuilds;
|
|
}
|
|
|
|
MatchingBuilds = PlatformBuilds.Where((B) =>
|
|
{
|
|
if ((InFlags & BuildFlags.CanReplaceExecutable) == BuildFlags.CanReplaceExecutable)
|
|
{
|
|
if (B.CanSupportRole(InRole)
|
|
&& (B.Flags & InFlags) == InFlags
|
|
&& (B.Flavor == InFlavor))
|
|
{
|
|
Log.Warning("Build did not have configuration {0} for {1}, but selecting due to presence of -dev flag",
|
|
InConfiguration, InPlatform);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
});
|
|
|
|
return MatchingBuilds;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if we are able to support the specified role. This will trigger build discovery if it has not yet
|
|
/// happened for the specified platform
|
|
/// </summary>
|
|
/// <param name="Role"></param>
|
|
/// <param name="Reasons"></param>
|
|
/// <returns></returns>
|
|
public bool CanSupportRole(UnrealSessionRole Role, ref List<string> Reasons)
|
|
{
|
|
if (Role.RoleType.UsesEditor() && UnrealPath == null)
|
|
{
|
|
Reasons.Add(string.Format("Role {0} wants editor but no path to Unreal exists", Role));
|
|
return false;
|
|
}
|
|
|
|
// null platform. Need a better way of specifying this
|
|
if (Role.IsNullRole())
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Query our build list
|
|
if (Role.Platform != null)
|
|
{
|
|
var MatchingBuilds = GetMatchingBuilds(Role.RoleType, Role.Platform.Value, Role.Configuration, Role.RequiredBuildFlags, Role.RequiredFlavor);
|
|
|
|
if (MatchingBuilds.Count() > 0)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
Reasons.Add(string.Format("No build at {0} that matches {1} (RequiredFlags={2})", string.Join(",", BuildPaths), Role.ToString(), Role.RequiredBuildFlags.ToString()));
|
|
|
|
return false;
|
|
}
|
|
|
|
virtual public UnrealAppConfig CreateConfiguration(UnrealSessionRole Role)
|
|
{
|
|
return CreateConfiguration(Role, new UnrealSessionRole[] { });
|
|
}
|
|
|
|
virtual public UnrealAppConfig CreateConfiguration(UnrealSessionRole Role, IEnumerable<UnrealSessionRole> OtherRoles)
|
|
{
|
|
List<string> Issues = new List<string>();
|
|
|
|
Log.Verbose("Creating configuration Role {0}", Role);
|
|
if (!CanSupportRole(Role, ref Issues))
|
|
{
|
|
Issues.ForEach(S => Log.Error(S));
|
|
return null;
|
|
}
|
|
|
|
UnrealTestConfiguration TestConfig = Role.Options as UnrealTestConfiguration;
|
|
|
|
UnrealAppConfig Config = new UnrealAppConfig();
|
|
|
|
Config.Name = this.BuildName;
|
|
Config.ProjectFile = this.ProjectPath;
|
|
Config.ProjectName = ProjectName;
|
|
Config.ProcessType = Role.RoleType;
|
|
Config.Platform = Role.Platform;
|
|
Config.Configuration = Role.Configuration;
|
|
Config.CommandLineParams = new GauntletCommandLine(Role.CommandLineParams);
|
|
Config.FilesToCopy = new List<UnrealFileToCopy>();
|
|
Config.MaxDuration = (int)(TestConfig?.MaxDuration ?? 0);
|
|
|
|
// new system of retrieving and encapsulating the info needed to install/launch. Android & Mac
|
|
Config.Build = GetMatchingBuilds(Role.RoleType, Role.Platform, Role.Configuration, Role.RequiredBuildFlags, Role.RequiredFlavor).OrderBy(B => B.PreferenceOrder).FirstOrDefault();
|
|
|
|
if (Config.Build == null)
|
|
{
|
|
if (Role.IsNullRole())
|
|
{
|
|
Log.Warning("No supported build found, however role is Null and not configure to run anything.");
|
|
}
|
|
else
|
|
{
|
|
var SupportedBuilds = String.Join("\n", DiscoveredBuilds.Select(B => B.ToString()));
|
|
|
|
Log.Info("Available builds:\n{0}", SupportedBuilds);
|
|
throw new AutomationException("No build found that can support a role of {0}.", Role);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Log.Info("Selected build {Build} for test run.", Config.Build.ToString());
|
|
}
|
|
|
|
if (Role.Options != null)
|
|
{
|
|
UnrealTestConfiguration ConfigOptions = Role.Options as UnrealTestConfiguration;
|
|
ConfigOptions.ApplyToConfig(Config, Role, OtherRoles);
|
|
}
|
|
|
|
// Cleanup the commandline
|
|
Log.Info("Processing CommandLine {0}", Config.CommandLine);
|
|
Config.CommandLine = GenerateProcessedCommandLine(Config.CommandLine);
|
|
|
|
// Now add the project (the above code doesn't handle arguments without a leading - so do this last
|
|
bool IsContentOnlyProject = (Config.Build != null) && ((Config.Build.Flags & BuildFlags.ContentOnlyProject) == BuildFlags.ContentOnlyProject);
|
|
|
|
// Add in editor - TODO, should this be in the editor build?
|
|
if (Role.RoleType.UsesEditor() || IsContentOnlyProject)
|
|
{
|
|
// add in -game or -server
|
|
if (Role.RoleType.IsClient())
|
|
{
|
|
Config.CommandLineParams.Add("game");
|
|
}
|
|
else if (Role.RoleType.IsServer())
|
|
{
|
|
Config.CommandLineParams.Add("server");
|
|
}
|
|
|
|
string ProjectParam = ProjectPath.FullName;
|
|
|
|
// if content only we need to provide a relative path to the uproject.
|
|
if (IsContentOnlyProject && !Role.RoleType.UsesEditor())
|
|
{
|
|
ProjectParam = string.Format("../../../{0}/{0}.uproject", ProjectName);
|
|
}
|
|
Config.CommandLineParams.Project = ProjectParam;
|
|
}
|
|
|
|
// Detect json log line output
|
|
if (Config.CommandLineParams.HasParam("JsonStdOut")
|
|
|| (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("UE_LOG_JSON_TO_STDOUT"))
|
|
&& (Role.Platform == BuildHostPlatform.Current.Platform || CanRunPlatformVirtualized(Role.Platform))))
|
|
{
|
|
Config.FilterLoggingDelegate = (string M, bool IsErr) => UnrealLogParser.SanitizeJsonOutputLine(M);
|
|
}
|
|
|
|
if (Role.FilesToCopy != null)
|
|
{
|
|
Config.FilesToCopy = Role.FilesToCopy;
|
|
}
|
|
|
|
if(Globals.IsRunningDev)
|
|
{
|
|
Config.OverlayExecutable = new OverlayExecutable(Role, Config.ProjectName);
|
|
}
|
|
|
|
return Config;
|
|
}
|
|
|
|
private bool CanRunPlatformVirtualized(UnrealTargetPlatform? Platform)
|
|
{
|
|
return Utils.InterfaceHelpers.FindImplementations<IVirtualLocalDevice>()
|
|
.Any(F => F.GetPlatform() == Platform && F.CanRunVirtualFromPlatform(BuildHostPlatform.Current.Platform));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Remove all duplicate flags and combine any execcmd strings that might be floating around in the commandline.
|
|
/// </summary>
|
|
/// <param name="InCommandLine"></param>
|
|
/// <returns></returns>
|
|
private string GenerateProcessedCommandLine(string InCommandLine)
|
|
{
|
|
|
|
// Break down Commandline into individual tokens
|
|
Dictionary<string, string> CommandlineTokens = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
|
// turn Name(p1,etc) into a collection of Name|(p1,etc) groups
|
|
MatchCollection Matches = Regex.Matches(InCommandLine, @"(?<option>-?[\w\d.:!\[\]\/\\?-]+)(=(?<value>(""([^""]*)"")|(\S+)))?");
|
|
|
|
foreach (Match M in Matches)
|
|
{
|
|
if (M.Groups["option"] == null || string.IsNullOrWhiteSpace(M.Groups["option"].ToString()))
|
|
{
|
|
Log.Warning("Unable to parse option in commandline. Please check syntax/regex. This should never be hit.");
|
|
continue;
|
|
}
|
|
|
|
string Name = M.Groups["option"].ToString().Trim();
|
|
|
|
string Params = M.Groups["value"] != null ? M.Groups["value"].ToString() : string.Empty;
|
|
|
|
if (CommandlineTokens.ContainsKey(Name))
|
|
{
|
|
|
|
if (string.IsNullOrWhiteSpace(Params))
|
|
{
|
|
Log.Info(string.Format("Duplicate flag {0} found and ignored. Please fix this as it will increase in severity in 01/2019. ", Name));
|
|
}
|
|
else if (Name.ToLower() == "-execcmds")
|
|
{
|
|
// Special cased as execcmds is something that is totally able to be appended to. Requote everything when we're done.
|
|
CommandlineTokens[Name] = string.Format("\"{0}, {1}\"", CommandlineTokens[Name].Replace("\"", ""), Params.Replace("\"", ""));
|
|
}
|
|
else
|
|
{
|
|
if (CommandlineTokens[Name] == Params)
|
|
{
|
|
Log.Info(string.Format("Duplicate flag {0}={1} found and ignored. Please fix this as this log line will increase in severity in 01/2019. ", Name, Params));
|
|
}
|
|
else
|
|
{
|
|
Log.Warning(string.Format("Multiple values for flag {0} found: {1} and {2}. The former value will be discarded. ", Name, CommandlineTokens[Name], Params));
|
|
CommandlineTokens[Name] = Params.Trim();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CommandlineTokens.Add(Name, (Params.Contains(' ') && !Params.Contains('\"')) ? string.Format("\"{0}\"", Params) : Params);
|
|
}
|
|
}
|
|
|
|
string CommandlineToReturn = "";
|
|
foreach (string DictKey in CommandlineTokens.Keys)
|
|
{
|
|
CommandlineToReturn += string.Format("{0}{1} ",
|
|
DictKey,
|
|
string.IsNullOrWhiteSpace(CommandlineTokens[DictKey]) ? "" : string.Format("={0}", CommandlineTokens[DictKey])
|
|
);
|
|
}
|
|
Gauntlet.Log.Verbose(string.Format("Pre-formatting Commandline: {0}", InCommandLine));
|
|
Gauntlet.Log.Verbose(string.Format("Post-formatting Commandline: {0}", CommandlineToReturn));
|
|
|
|
return CommandlineToReturn;
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// Get the module overlay binary path for a particular configuration
|
|
/// </summary>
|
|
private string GetModuleOverlayExecutablePath(UnrealTargetRole role, UnrealTargetPlatform platform)
|
|
{
|
|
return Path.Join(
|
|
"Engine", "Binaries", platform.ToString(),
|
|
UnrealHelpers.CustomModuleToRoles.FirstOrDefault(module => module.Value == role).Key
|
|
);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Given a role, platform, and config, returns the path to the binary for that config. E.g. Binaries\Win64\FooServer-Win64-Shipping.exe
|
|
/// </summary>
|
|
/// <param name="TargetRole"></param>
|
|
/// <param name="TargetPlatform"></param>
|
|
/// <param name="TargetConfiguration"></param>
|
|
/// <returns></returns>
|
|
virtual public string GetRelativeExecutablePath(UnrealTargetRole TargetRole, UnrealTargetPlatform TargetPlatform, UnrealTargetConfiguration TargetConfiguration)
|
|
{
|
|
string ExePath;
|
|
|
|
if (TargetRole.UsesEditor() || TargetRole.IsEditor())
|
|
{
|
|
bool HasCustomTarget = UnrealHelpers.CustomModuleToRoles.ContainsValue(UnrealTargetRole.Editor);
|
|
string EditorTarget = HasCustomTarget ? UnrealHelpers.CustomModuleToRoles.FirstOrDefault(M => M.Value == UnrealTargetRole.Editor).Key : string.Empty;
|
|
FileSystemReference EditorExe = null;
|
|
|
|
if (!HasCustomTarget)
|
|
{
|
|
try
|
|
{
|
|
EditorExe = ProjectUtils.GetProjectTarget(ProjectPath, UnrealBuildTool.TargetType.Editor, TargetPlatform, TargetConfiguration);
|
|
}
|
|
catch (Exception Ex)
|
|
{
|
|
string Message = string.Format("The project config is overriding build targets.\n"
|
|
+ "But no suitable editor build for {0} configuration found from target file.\n"
|
|
+ "{1}", TargetConfiguration, Ex.Message);
|
|
|
|
if (BuildName.Equals("Editor", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// An editor is being explicitly requested but no executable could be found from target file
|
|
// Hightlight the issue in the log and return an empty string to avoid taking an inappropriate editor
|
|
Log.Warning(Message);
|
|
return string.Empty;
|
|
}
|
|
Log.Info(Message);
|
|
}
|
|
}
|
|
|
|
if (EditorExe != null)
|
|
{
|
|
ExePath = EditorExe.FullName;
|
|
if (!string.IsNullOrEmpty(Globals.Params.ParseValue("EditorDir", null)))
|
|
{
|
|
// Trim the Editor absolute path from what the target file provided as the editor dir is being overriden
|
|
// https://regex101.com/r/7BttxH/1
|
|
ExePath = Regex.Replace(ExePath, @"(.+?)[/\\]((Engine[/\\])?Binaries[/\\].+)", "$2");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
string ExeFileName = HasCustomTarget ? EditorTarget : "UnrealEditor";
|
|
if (TargetConfiguration != UnrealTargetConfiguration.Development)
|
|
{
|
|
ExeFileName += string.Format("-{0}-{1}", TargetPlatform.ToString(), TargetConfiguration.ToString());
|
|
}
|
|
|
|
ExeFileName += Platform.GetExeExtension(TargetPlatform);
|
|
|
|
string BasePath = HasCustomTarget ? Globals.Params.ParseValue("EditorDir", ProjectPath.Directory.FullName) : "Engine";
|
|
ExePath = string.Format("{0}/Binaries/{1}/{2}", BasePath, BuildHostPlatform.Current.Platform, ExeFileName);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
string BuildType = "";
|
|
|
|
if (TargetRole == UnrealTargetRole.Client)
|
|
{
|
|
if (!UsesSharedBuildType)
|
|
{
|
|
BuildType = "Client";
|
|
}
|
|
}
|
|
else if (TargetRole == UnrealTargetRole.Server)
|
|
{
|
|
if (!UsesSharedBuildType)
|
|
{
|
|
BuildType = "Server";
|
|
}
|
|
}
|
|
|
|
// Turn FooGame into Foo
|
|
bool IsRunningDev = Globals.IsRunningDev;
|
|
string ExeBase = ProjectName.Replace("Game", "");
|
|
|
|
if (TargetPlatform == UnrealTargetPlatform.Android)
|
|
{
|
|
// use the archive results for android.
|
|
//var AndroidSource = new AndroidBuild(ProjectName, GetPlatformPath(TargetType, TargetPlatform), TargetConfiguration);
|
|
|
|
// We always (currently..) need to be able to replace the command line
|
|
BuildFlags Flags = BuildFlags.CanReplaceCommandLine;
|
|
if (IsRunningDev)
|
|
{
|
|
Flags |= BuildFlags.CanReplaceExecutable;
|
|
}
|
|
if (Globals.Params.ParseParam("bulk"))
|
|
{
|
|
Flags |= BuildFlags.Bulk;
|
|
}
|
|
else if(Globals.Params.ParseParam("notbulk"))
|
|
{
|
|
Flags |= BuildFlags.NotBulk;
|
|
}
|
|
|
|
var Build = GetMatchingBuilds(TargetRole, TargetPlatform, TargetConfiguration, Flags, "").FirstOrDefault();
|
|
|
|
if (Build != null)
|
|
{
|
|
AndroidBuild AndroidBuild = Build as AndroidBuild;
|
|
ExePath = AndroidBuild.SourceApkPath;
|
|
}
|
|
else
|
|
{
|
|
throw new AutomationException("No suitable build for {0} found at {1}", TargetPlatform, string.Join(",", BuildPaths));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
string ExeFileName = string.Format("{0}{1}", ExeBase, BuildType);
|
|
string ExeFileName2 = ProjectName;
|
|
|
|
if (TargetConfiguration != UnrealTargetConfiguration.Development)
|
|
{
|
|
ExeFileName += string.Format("-{0}-{1}", TargetPlatform.ToString(), TargetConfiguration.ToString());
|
|
ExeFileName2 += string.Format("-{0}-{1}", TargetPlatform.ToString(), TargetConfiguration.ToString());
|
|
}
|
|
|
|
if (TargetPlatform != UnrealTargetPlatform.IOS)
|
|
{
|
|
ExeFileName += Platform.GetExeExtension(TargetPlatform);
|
|
ExeFileName2 += Platform.GetExeExtension(TargetPlatform);
|
|
}
|
|
|
|
string BasePath = GetPlatformPath(TargetRole, TargetPlatform);
|
|
string ProjectBinary = string.Format("{0}\\Binaries\\{1}\\{2}", ProjectName, TargetPlatform.ToString(), ExeFileName);
|
|
string ProjectBinary2 = string.Format("{0}\\Binaries\\{1}\\{2}", ExeBase, TargetPlatform.ToString(), ExeFileName2);
|
|
string StubBinary = Path.Combine(BasePath, ExeFileName);
|
|
string StubBinary2 = Path.Combine(BasePath, ExeFileName2);
|
|
string DevBinary = Path.Combine(Environment.CurrentDirectory, ProjectBinary);
|
|
string DevBinary2 = Path.Combine(Environment.CurrentDirectory, ProjectBinary2);
|
|
|
|
string NonCodeProjectName = "UnrealGame" + Platform.GetExeExtension(TargetPlatform);
|
|
string NonCodeProjectBinary = Path.Combine(BasePath, "Engine", "Binaries", TargetPlatform.ToString());
|
|
NonCodeProjectBinary = Path.Combine(NonCodeProjectBinary, NonCodeProjectName);
|
|
|
|
// check the project binaries folder
|
|
if (File.Exists(Path.Combine(BasePath, ProjectBinary)))
|
|
{
|
|
ExePath = ProjectBinary;
|
|
}
|
|
else if (File.Exists(StubBinary))
|
|
{
|
|
ExePath = Path.Combine(BasePath, ExeFileName);
|
|
}
|
|
else if (IsRunningDev && File.Exists(DevBinary))
|
|
{
|
|
ExePath = DevBinary;
|
|
}
|
|
else if (File.Exists(NonCodeProjectBinary))
|
|
{
|
|
ExePath = NonCodeProjectBinary;
|
|
}
|
|
else if (File.Exists(Path.Combine(BasePath, ProjectBinary2)))
|
|
{
|
|
ExePath = Path.Combine(BasePath, ProjectBinary2);
|
|
}
|
|
else if (File.Exists(StubBinary2))
|
|
{
|
|
ExePath = Path.Combine(BasePath, ExeFileName2);
|
|
}
|
|
else if (IsRunningDev && File.Exists(DevBinary2))
|
|
{
|
|
ExePath = DevBinary2;
|
|
}
|
|
else
|
|
{
|
|
List<string> CheckedFiles = new List<String>() { Path.Combine(BasePath, ProjectBinary), StubBinary, NonCodeProjectBinary, Path.Combine(BasePath, ProjectBinary2), StubBinary2 };
|
|
if (IsRunningDev)
|
|
{
|
|
CheckedFiles.Add(DevBinary);
|
|
CheckedFiles.Add(DevBinary2);
|
|
}
|
|
|
|
throw new AutomationException("Executable not found, upstream compile job may have failed. Could not find executable {0} within {1}, binaries checked: {2}", ExeFileName, BasePath, String.Join(" - ", CheckedFiles));
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
return Utils.SystemHelpers.CorrectDirectorySeparators(ExePath);
|
|
}
|
|
|
|
public string GetPlatformPath(UnrealTargetRole Type, UnrealTargetPlatform Platform)
|
|
{
|
|
if (Type.UsesEditor())
|
|
{
|
|
return UnrealPath.FullName;
|
|
}
|
|
|
|
string BuildPath = BuildPaths.ElementAt(0);
|
|
|
|
if (string.IsNullOrEmpty(BuildPath))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
string PlatformPath = Path.Combine(BuildPath, UnrealHelpers.GetPlatformName(Platform, Type, UsesSharedBuildType));
|
|
|
|
// On some builds we stage the actual loose files into a "Staged" folder
|
|
if (Directory.Exists(PlatformPath) && Directory.Exists(Path.Combine(PlatformPath, "staged")))
|
|
{
|
|
PlatformPath = Path.Combine(PlatformPath, "Staged");
|
|
}
|
|
|
|
// Urgh - build share uses a different style...
|
|
if (Platform == UnrealTargetPlatform.Android && BuildName.Equals("Local", StringComparison.OrdinalIgnoreCase) == false)
|
|
{
|
|
PlatformPath = PlatformPath.Replace("Android_ETC2Client", "Android\\FullPackages");
|
|
}
|
|
|
|
return PlatformPath;
|
|
}
|
|
}
|
|
}
|