330 lines
11 KiB
C#
330 lines
11 KiB
C#
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using System.IO;
|
|
using AutomationTool;
|
|
using Ionic.Zip;
|
|
using UnrealBuildTool;
|
|
using EpicGames.Core;
|
|
|
|
namespace Turnkey
|
|
{
|
|
[Flags]
|
|
public enum CopyExecuteSpecialMode
|
|
{
|
|
None = 0,
|
|
UsePermanentStorage = 1,
|
|
DownloadOnly = 2,
|
|
}
|
|
|
|
abstract class CopyProvider
|
|
{
|
|
/// <summary>
|
|
/// Unique provider token to direct a copy operation ("perforce://UE5/Main/...)
|
|
/// </summary>
|
|
public abstract string ProviderToken { get; }
|
|
|
|
/// <summary>
|
|
/// Perform the copy operation
|
|
/// </summary>
|
|
/// <param name="Operation">Description for the operation</param>
|
|
/// <param name="SpecialMode"></param>
|
|
/// <param name="SpecialModeHint"></param>
|
|
/// <returns>The output path of the copied file, or a directory that contains all of the wildcards in the operation ("perforce://depot/CarefullyRedist/.../Windows/*" would return something like "d:\\sdks")</returns>
|
|
public abstract string Execute(string Operation, CopyExecuteSpecialMode SpecialMode, string SpecialModeHint);
|
|
|
|
/// <summary>
|
|
/// Uses the CopyProvider to return a list of files and directories (a directory MUST end with a / or \ character to denote directory since
|
|
/// we can't test locally what type it is
|
|
/// </summary>
|
|
/// <param name="Operation">Description for the operation, including a ProviderToken</param>
|
|
/// <param name="Expansions">A list of what *'s expanded to, one entry for each result</param>
|
|
/// <returns></returns>
|
|
public abstract string[] Enumerate(string Operation, List<List<string>> Expansions);
|
|
|
|
|
|
|
|
|
|
private static Dictionary<string, CopyProvider> CachedProviders = new Dictionary<string, CopyProvider>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
static CopyProvider()
|
|
{
|
|
// look for all subclasses, and cache by their ProviderToken
|
|
foreach (Type AssemType in Assembly.GetExecutingAssembly().GetTypes())
|
|
{
|
|
if (typeof(CopyProvider).IsAssignableFrom(AssemType) && AssemType != typeof(CopyProvider))
|
|
{
|
|
CopyProvider Provider = (CopyProvider)Activator.CreateInstance(AssemType);
|
|
CachedProviders[Provider.ProviderToken] = Provider;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static bool ParseOperation(string Operation, out CopyProvider Provider, out string ProviderParam, bool bCanFail)
|
|
{
|
|
Operation = TurnkeyUtils.ExpandVariables(Operation);
|
|
|
|
Provider = null;
|
|
ProviderParam = null;
|
|
|
|
int ColonLocation = Operation.IndexOf(':');
|
|
if (ColonLocation < 0)
|
|
{
|
|
if (bCanFail)
|
|
{
|
|
return false;
|
|
}
|
|
throw new AutomationException("Malformed copy operation: {0}", Operation);
|
|
}
|
|
// get the token before the :
|
|
string Token = Operation.Substring(0, ColonLocation);
|
|
|
|
if (!CachedProviders.TryGetValue(Token, out Provider))
|
|
{
|
|
if (bCanFail)
|
|
{
|
|
return false;
|
|
}
|
|
throw new AutomationException("Unable to find a CopyProvider for copy type {0}", Token);
|
|
}
|
|
|
|
ProviderParam = Operation.Substring(ColonLocation + 1);
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Runs a Copy command, and returns the local path (either a directory or a file, depending on the operation
|
|
/// </summary>
|
|
/// <param name="CopyOperation"></param>
|
|
/// <param name="SpecialMode"></param>
|
|
/// <param name="SpecialModeHint"></param>
|
|
/// <returns>Output path, which could then be used as $(OutputPath) in later operations</returns>
|
|
public static string ExecuteCopy(string CopyOperation, CopyExecuteSpecialMode SpecialMode = CopyExecuteSpecialMode.None, string SpecialModeHint = null)
|
|
{
|
|
TurnkeyUtils.ClearVariable("CopyOutputPath");
|
|
|
|
CopyProvider Provider;
|
|
string ProviderParam;
|
|
|
|
ParseOperation(CopyOperation, out Provider, out ProviderParam, false);
|
|
|
|
string OperationExt = Path.GetExtension(ProviderParam).ToLower();
|
|
bool bWillDecompressToLocation = (OperationExt == ".zip" || OperationExt == ".7z") && SpecialMode == CopyExecuteSpecialMode.UsePermanentStorage;
|
|
|
|
// execute what comes after the colon (if we guess we are going to decompress, don't pass in a specified location, we will decompress to it after)
|
|
string OutputPath = bWillDecompressToLocation ? Provider.Execute(ProviderParam, CopyExecuteSpecialMode.None, null) :
|
|
Provider.Execute(ProviderParam, SpecialMode, SpecialModeHint);
|
|
|
|
// always unzip the file into a temp directory (or downloadpath if it's a permanent download), and make that directory be the outputpath
|
|
if (OutputPath != null)
|
|
{
|
|
OutputPath = OutputPath.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar);
|
|
string Ext = Path.GetExtension(OutputPath).ToLower();
|
|
if (Ext == ".zip" || Ext == ".7z")
|
|
{
|
|
// check if this file already has been unzipped - if the provider output to a new temp location but the same file,
|
|
// we can't trust it, so we re-decompress, or if the source file's date changed
|
|
string FileVersion = File.GetLastWriteTimeUtc(OutputPath).ToString();
|
|
string Tag = OutputPath + "_Decompressed";
|
|
|
|
string CachedLocation = LocalCache.GetCachedPathByTag(Tag, FileVersion);
|
|
|
|
// if it was there, use it directly
|
|
if (CachedLocation != null)
|
|
{
|
|
OutputPath = CachedLocation;
|
|
}
|
|
else
|
|
{
|
|
// make a random temp directory, or use the download directory for permanent downloads
|
|
string DecompressLocation;
|
|
if (SpecialMode == CopyExecuteSpecialMode.UsePermanentStorage)
|
|
{
|
|
DecompressLocation = SpecialModeHint; // Path.Combine(Path.GetDirectoryName(OutputPath), Path.GetFileNameWithoutExtension(OutputPath) + "_Uncompressed");
|
|
}
|
|
else
|
|
{
|
|
DecompressLocation = LocalCache.CreateTempDirectory();
|
|
}
|
|
|
|
bool bFailed = false;
|
|
if (Ext == ".zip")
|
|
{
|
|
try
|
|
{
|
|
using (ZipFile ZipFile = ZipFile.Read(OutputPath))
|
|
{
|
|
TurnkeyUtils.Log("Unzipping {0} to {1}...", OutputPath, DecompressLocation);
|
|
|
|
// extract the zip to it
|
|
ZipFile.ExtractAll(DecompressLocation);
|
|
}
|
|
}
|
|
catch (Exception Ex)
|
|
{
|
|
TurnkeyUtils.Log("Unzip failed: {0}", Ex);
|
|
bFailed = true;
|
|
}
|
|
}
|
|
else if (Ext == ".7z")
|
|
{
|
|
TurnkeyUtils.Log("7zip decompressing {0} to {1}...", OutputPath, DecompressLocation);
|
|
|
|
int ExitCode = UnrealBuildTool.Utils.RunLocalProcessAndLogOutput(TurnkeyUtils.ExpandVariables("$(EngineDir)/Restricted/NotForLicensees/Extras/ThirdPartyNotUE/7-Zip/7z.exe"),
|
|
string.Format("x -o{0} {1}", DecompressLocation, OutputPath), Log.Logger);
|
|
if (ExitCode != 0)
|
|
{
|
|
TurnkeyUtils.Log("Failed to uncompress a .7z file {0}", OutputPath);
|
|
bFailed = true;
|
|
}
|
|
}
|
|
|
|
// the temp dir is now the outputpatb to return to later installation steps
|
|
if (!bFailed)
|
|
{
|
|
OutputPath = DecompressLocation;
|
|
LocalCache.CacheLocationByTag(Tag, OutputPath, FileVersion);
|
|
}
|
|
else
|
|
{
|
|
// return null on a failure
|
|
OutputPath = null;
|
|
}
|
|
|
|
// @todo turnkey: let the CopyProvider delete the file (p4 needs to perform deletes of synced files)
|
|
}
|
|
}
|
|
}
|
|
|
|
TurnkeyUtils.SetVariable("CopyOutputPath", OutputPath);
|
|
|
|
return OutputPath;
|
|
}
|
|
|
|
public static string[] ExecuteEnumerate(string CopyOperation, List<List<string>> Expansions=null)
|
|
{
|
|
CopyProvider Provider;
|
|
string ProviderParam;
|
|
|
|
// we allow this to fail (like an unknown variable, etc)
|
|
if (!ParseOperation(CopyOperation, out Provider, out ProviderParam, true))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
return Provider.Enumerate(ProviderParam, Expansions);
|
|
}
|
|
}
|
|
|
|
class TurnkeyContextImpl : ITurnkeyContext
|
|
{
|
|
public List<string> ErrorMessages = new List<string>();
|
|
|
|
public string RetrieveFileSource(object HintObject)
|
|
{
|
|
FileSource Sdk = (FileSource)HintObject;
|
|
|
|
// run the first copy for the host platform`
|
|
foreach (CopySource Copy in Sdk.Sources)
|
|
{
|
|
if (Copy.ShouldExecute())
|
|
{
|
|
return CopyProvider.ExecuteCopy(Copy.GetOperation());
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public int RunExternalCommand(string Command, string Params, bool bRequiresPrivilegeElevation, bool bUnattended, bool bCreateWindow)
|
|
{
|
|
TurnkeyUtils.Log("----------------------------------------------");
|
|
TurnkeyUtils.Log("Running '{0} {1}'", TurnkeyUtils.ExpandVariables(Command), TurnkeyUtils.ExpandVariables(Params));
|
|
|
|
TurnkeyUtils.Log("----------------------------------------------", Command);
|
|
|
|
int ExitCode = CopyAndRun.RunExternalCommand(Command, Params, this, bUnattended, bRequiresPrivilegeElevation, bCreateWindow);
|
|
|
|
TurnkeyUtils.Log("----------------------------------------------");
|
|
TurnkeyUtils.Log($"Finished with {ExitCode}");
|
|
|
|
TurnkeyUtils.Log("----------------------------------------------", Command);
|
|
|
|
return ExitCode;
|
|
}
|
|
|
|
public string RetrieveFileSource(string Name, string InType, string InPlatform, string SubType)
|
|
{
|
|
UnrealTargetPlatform? Platform = null;
|
|
FileSource.SourceType? Type = null;
|
|
|
|
if (InPlatform != null)
|
|
{
|
|
// let this throw exception on failure, as it's a setup error
|
|
Platform = UnrealTargetPlatform.Parse(InPlatform);
|
|
}
|
|
|
|
if (InType != null)
|
|
{
|
|
// let this throw exception on failure, as it's a setup error
|
|
Type = (FileSource.SourceType)Enum.Parse(typeof(FileSource.SourceType), InType);
|
|
}
|
|
|
|
List<FileSource> Sources = TurnkeyManifest.FilterDiscoveredFileSources(Platform, Type);
|
|
|
|
Sources = Sources.FindAll(x =>
|
|
{
|
|
if (Name.StartsWith("regex:"))
|
|
{
|
|
return TurnkeyUtils.IsValueValid(x.Name, Name, null);
|
|
}
|
|
// this will handle the case of x.Name starting with regex: or just doing a case insensitive string comparison of tag and CustomSdkId
|
|
// range: is not supported, at least yet - we would have to check Tag with range: above, and also support range without a Platform (or pass in a platform somehow?)
|
|
return TurnkeyUtils.IsValueValid(Name, x.Name, null);
|
|
});
|
|
|
|
if (Sources.Count == 0)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
// @todo turnkey: If > 1 in Sources, warn user
|
|
|
|
// execute the first found one
|
|
return CopyProvider.ExecuteCopy(Sources[0].GetCopySourceOperation());
|
|
}
|
|
|
|
public string GetVariable(string VariableName)
|
|
{
|
|
return TurnkeyUtils.GetVariableValue(VariableName);
|
|
}
|
|
|
|
public void Log(string Message)
|
|
{
|
|
TurnkeyUtils.Log(Message);
|
|
}
|
|
|
|
public void ReportError(string Message)
|
|
{
|
|
Log("ERROR: " + Message);
|
|
ErrorMessages.Add(Message);
|
|
}
|
|
|
|
public void PauseForUser(string Message)
|
|
{
|
|
TurnkeyUtils.PauseForUser(Message);
|
|
}
|
|
|
|
public int ReadInputInt(string Prompt, List<string> Options, bool bIsCancellable, int DefaultValue)
|
|
{
|
|
return TurnkeyUtils.ReadInputInt(Prompt, Options, bIsCancellable, DefaultValue);
|
|
}
|
|
|
|
}
|
|
}
|