Files
UnrealEngineUWP/Engine/Extras/P4VUtils/Program.cs

466 lines
17 KiB
C#
Raw Normal View History

// Copyright Epic Games, Inc. All Rights Reserved.
using EpicGames.Core;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;
using P4VUtils.Commands;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using System.Xml;
namespace P4VUtils
{
class CustomToolInfo
{
public string Name { get; set; }
public string Arguments { get; set; }
public bool AddToContextMenu { get; set; } = true;
public bool ShowConsole { get; set; }
public bool RefreshUI { get; set; } = true;
public string Shortcut { get; set; } = "";
public bool PromptForArgument { get; set; }
public string PromptText { get; set; } = "";
public CustomToolInfo(string Name, string Arguments)
{
this.Name = Name;
this.Arguments = Arguments;
}
}
abstract class Command
{
public abstract string Description { get; }
public abstract CustomToolInfo CustomTool { get; }
public abstract Task<int> Execute(string[] Args, IReadOnlyDictionary<string, string> ConfigValues, ILogger Logger);
}
class Program
{
// UEHelpersInRoot - commands that help with common but simple operations
public static IReadOnlyDictionary<string, Command> RootHelperCommands { get; } = new Dictionary<string, Command>(StringComparer.OrdinalIgnoreCase)
{
["describe"] = new DescribeCommand(),
#if !IS_LINUX
["copyclnum"] = new CopyCLCommand(),
#endif
};
Extend P4VUtils with a new command that allows the user to submit&virtualize package files (for projects with asset virtualization enabled) #rb Ben.Marsh #rnx #jira UE-143675 #preflight skip ### P4VUtils - Add a new submenu "UESubmit" for commands related to the submission of files and changelists -At the moment the submenu only has a single command SubmitAndVirtualizeCommand ### SubmitAndVirtualizeCommand - This command is used to run the asset virtualization process over all of the files in a changelist before submitting it using the following logic: -- Find all package files in a changelist -- Sort the files by project -- Find the engine installation associated with each project -- Build and run the stand alone virtualization tool (if needed) and pass it the packages for the project -- If the virtualization process succeeeded for all projects we can submit the original changelist, other wise print errors to the p4v terminal. - In most cases all of the packages in a changelist will be under a single project, but the command should be able to cope with many packages from many different projects using many different engine installations. - Note that unlike a normal submit, virtualizing a package can change it on disk (by removing the payload data) which means we will probably run into problems where a running instance of the editor has access locks on the file and they cannot be edited. At the moment this command and the stand alone tool will just error, there is no tool<->editor communication protocol set up to request that the editor release the package file. This will be addressed as a future work item. - This is the first pass of the command, there are a number of non-essential todo comments left in the code that will be addressed based on feedback from the end users. [CL 19752487 by paul chipchase in ue5-main branch]
2022-04-14 04:26:33 -04:00
// UESubmit - commands that help with submitting files/changelists
public static IReadOnlyDictionary<string, Command> SubmissionCommands { get; } = new Dictionary<string, Command>(StringComparer.OrdinalIgnoreCase)
{
["submitandvirtualize"] = new SubmitAndVirtualizeCommand(),
};
// UEHelpers - commands that help with common but simple operations
public static IReadOnlyDictionary<string, Command> HelperCommands { get; } = new Dictionary<string, Command>(StringComparer.OrdinalIgnoreCase)
{
["findlastedit"] = new FindLastEditCommand(),
["findlasteditbyline"] = new P4BlameCommand(),
["snapshot"] = new SnapshotCommand(),
["reconcilecode"] = new FastReconcileCodeEditsCommand(),
["reconcileall"] = new FastReconcileAllEditsCommand(),
["unshelvetocurrentrevision"] = new UnshelveToCurrentRevision(),
["unshelvemakedatawritable"] = new UnshelveMakeDataWritable(),
["convertcldatatolocalwritable"] = new ConvertCLDataToLocalWritable(),
["convertdatatolocalwritable"] = new ConvertDataToLocalWritable(),
};
// UEIntegrate Folder commands - complex commands to facilitate integrations/backout
public static IReadOnlyDictionary<string, Command> IntegrateCommands { get; } = new Dictionary<string, Command>(StringComparer.OrdinalIgnoreCase)
{
["cherrypick"] = new CherryPickCommand(),
["converttoedit"] = new ConvertToEditCommand(),
["edigrate"] = new EdigrateCommand(),
["backout"] = new BackoutCommand(),
};
// UEHorde Folder - local build and horde preflights
public static IReadOnlyDictionary<string, Command> HordeCommands { get; } = new Dictionary<string, Command>(StringComparer.OrdinalIgnoreCase)
{
["compile"] = new CompileCommand(),
["preflight"] = new PreflightCommand(),
["preflightandsubmit"] = new PreflightAndSubmitCommand(),
["movewriteablepreflightandsubmit"] = new MoveWriteableFilesthenPreflightAndSubmitCommand(),
};
Extend P4VUtils with a new command that allows the user to submit&virtualize package files (for projects with asset virtualization enabled) #rb Ben.Marsh #rnx #jira UE-143675 #preflight skip ### P4VUtils - Add a new submenu "UESubmit" for commands related to the submission of files and changelists -At the moment the submenu only has a single command SubmitAndVirtualizeCommand ### SubmitAndVirtualizeCommand - This command is used to run the asset virtualization process over all of the files in a changelist before submitting it using the following logic: -- Find all package files in a changelist -- Sort the files by project -- Find the engine installation associated with each project -- Build and run the stand alone virtualization tool (if needed) and pass it the packages for the project -- If the virtualization process succeeeded for all projects we can submit the original changelist, other wise print errors to the p4v terminal. - In most cases all of the packages in a changelist will be under a single project, but the command should be able to cope with many packages from many different projects using many different engine installations. - Note that unlike a normal submit, virtualizing a package can change it on disk (by removing the payload data) which means we will probably run into problems where a running instance of the editor has access locks on the file and they cannot be edited. At the moment this command and the stand alone tool will just error, there is no tool<->editor communication protocol set up to request that the editor release the package file. This will be addressed as a future work item. - This is the first pass of the command, there are a number of non-essential todo comments left in the code that will be addressed based on feedback from the end users. [CL 19752487 by paul chipchase in ue5-main branch]
2022-04-14 04:26:33 -04:00
public static IDictionary<string, Command> Commands = SubmissionCommands.Concat(RootHelperCommands).Concat(HelperCommands).Concat(IntegrateCommands).Concat(HordeCommands).ToDictionary(p => p.Key, p => p.Value, StringComparer.OrdinalIgnoreCase);
static void PrintHelp(ILogger Logger)
{
Logger.LogInformation("P4VUtils");
Logger.LogInformation("Provides useful shortcuts for working with P4V");
Logger.LogInformation("");
Logger.LogInformation("Usage:");
Logger.LogInformation(" P4VUtils [Command] [Arguments...]");
Logger.LogInformation("");
List<KeyValuePair<string, string>> Table = new List<KeyValuePair<string, string>>();
foreach (KeyValuePair<string, Command> Pair in Commands)
{
Table.Add(new KeyValuePair<string, string>(Pair.Key, Pair.Value.Description));
}
Logger.LogInformation("Commands:");
HelpUtils.PrintTable(Table, 2, 15, ConsoleUtils.WindowWidth - 1, Logger);
}
static async Task<int> Main(string[] Args)
{
using ILoggerFactory Factory = LoggerFactory.Create(Builder => Builder.AddEpicDefault());//.AddSimpleConsole(Options => { Options.SingleLine = true; Options.IncludeScopes = false; }));
ILogger Logger = Factory.CreateLogger<Program>();
Log.SetInnerLogger(Logger);
try
{
return await InnerMain(Args, Logger);
}
catch (Exception Ex)
{
Logger.LogError(Ex, "Unhandled exception: {Ex}", Ex.ToString());
return 1;
}
}
static async Task<int> InnerMain(string[] Args, ILogger Logger)
{
if (Args.Length == 0 || Args[0].Equals("-help", StringComparison.OrdinalIgnoreCase))
{
PrintHelp(Logger);
return 0;
}
else if (Args[0].StartsWith("-", StringComparison.Ordinal))
{
Logger.LogInformation("Missing command name");
PrintHelp(Logger);
return 1;
}
else if (Args[0].Equals("install", StringComparison.OrdinalIgnoreCase))
{
Logger.LogInformation("Adding custom tools...");
return await UpdateCustomToolRegistration(true, Logger);
}
else if (Args[0].Equals("uninstall", StringComparison.OrdinalIgnoreCase))
{
Logger.LogInformation("Removing custom tools...");
return await UpdateCustomToolRegistration(false, Logger);
}
else if (Commands.TryGetValue(Args[0], out Command? Command))
{
if (Args.Any(x => x.Equals("-help", StringComparison.OrdinalIgnoreCase)))
{
List<KeyValuePair<string, string>> Parameters = CommandLineArguments.GetParameters(Command.GetType());
Logger.LogInformation("{Title}", Args[0]);
Logger.LogInformation("{Description}", Command.GetType());
Logger.LogInformation("Parameters:");
HelpUtils.PrintTable(Parameters, 4, 24, HelpUtils.WindowWidth - 1, Logger);
return 0;
}
Dictionary<string, string> ConfigValues = ReadConfig();
return await Command.Execute(Args, ConfigValues, Logger);
}
else
{
Logger.LogError("Unknown command: {Command}", Args[0]);
PrintHelp(Logger);
return 1;
}
}
static Dictionary<string, string> ReadConfig()
{
Dictionary<string, string> ConfigValues = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
string BasePath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!;
AppendConfig(Path.Combine(BasePath, "P4VUtils.ini"), ConfigValues);
AppendConfig(Path.Combine(BasePath, "NotForLicensees", "P4VUtils.ini"), ConfigValues);
return ConfigValues;
}
static void AppendConfig(string SourcePath, Dictionary<string, string> ConfigValues)
{
if (File.Exists(SourcePath))
{
string[] Lines = File.ReadAllLines(SourcePath);
foreach (string Line in Lines)
{
int EqualsIdx = Line.IndexOf('=', StringComparison.Ordinal);
if (EqualsIdx != -1)
{
string Key = Line.Substring(0, EqualsIdx).Trim();
string Value = Line.Substring(EqualsIdx + 1).Trim();
ConfigValues[Key] = Value;
}
}
}
}
public static bool TryLoadXmlDocument(FileReference Location, XmlDocument Document)
{
if (FileReference.Exists(Location))
{
try
{
Document.Load(Location.FullName);
return true;
}
catch
{
}
}
return false;
}
static string GetToolName(XmlElement ToolNode)
{
return ToolNode.SelectSingleNode("Definition")?.SelectSingleNode("Name")?.InnerText ?? string.Empty;
}
// returns true if all tools were removed
static bool RemoveCustomToolsFromNode(XmlElement RootNode, FileReference DotNetLocation, FileReference AssemblyLocation, ILogger Logger)
{
int ToolsChecked = 0;
int ToolsRemoved = 0;
XmlNodeList? CustomToolDefList = RootNode.SelectNodes("CustomToolDef");
if (CustomToolDefList == null)
{
return false;
}
// Removes tools explicitly calling the assembly location identified above - i assume as a way to "filter" only those we explicitly added (@Ben.Marsh) - nochecking, remove this comment once verified.
foreach (XmlNode? ChildNode in CustomToolDefList)
{
XmlElement? ChildElement = ChildNode as XmlElement;
if (ChildElement != null)
{
ToolsChecked++;
XmlElement? CommandElement = ChildElement.SelectSingleNode("Definition/Command") as XmlElement;
if (CommandElement != null && new FileReference(CommandElement.InnerText) == DotNetLocation)
{
XmlElement? ArgumentsElement = ChildElement.SelectSingleNode("Definition/Arguments") as XmlElement;
if (ArgumentsElement != null)
{
string[] Arguments = CommandLineArguments.Split(ArgumentsElement.InnerText);
if (Arguments.Length > 0 && new FileReference(Arguments[0]) == AssemblyLocation)
{
Logger.LogInformation("Removing Tool {ToolName}", GetToolName(ChildElement));
RootNode.RemoveChild(ChildElement);
ToolsRemoved++;
}
}
}
}
}
return ToolsChecked == ToolsRemoved;
}
static void InstallCommandsListInFolder(string FolderName, bool AddFolderToContextMenu, IReadOnlyDictionary<string, Command> InputCommmands, XmlDocument Document, FileReference DotNetLocation, FileReference AssemblyLocation, ILogger Logger)
{
// <CustomToolDefList> // list of custom tools (top level)
// < CustomToolDef > // loose custom tool in top level
// < CustomToolFolder> // folder containing custom tools
// < Name > Test </ Name >
// < CustomToolDefList > // list of custom tools in folder
// < CustomToolDef > // definition of tool
// This is the top level node, there will also be a per folder node added of same name
XmlElement? Root = Document.SelectSingleNode("CustomToolDefList") as XmlElement;
if (Root != null)
{
XmlElement FolderDefinition = Document.CreateElement("CustomToolFolder");
XmlElement FolderDescription = Document.CreateElement("Name");
FolderDescription.InnerText = FolderName;
FolderDefinition.AppendChild(FolderDescription);
XmlElement FolderToContextMenu = Document.CreateElement("AddToContext");
FolderToContextMenu.InnerText = AddFolderToContextMenu ? "true" : "false";
FolderDefinition.AppendChild(FolderToContextMenu);
XmlElement FolderDefList = Document.CreateElement("CustomToolDefList");
foreach (KeyValuePair<string, Command> Pair in InputCommmands)
{
CustomToolInfo CustomTool = Pair.Value.CustomTool;
XmlElement ToolDef = Document.CreateElement("CustomToolDef");
{
XmlElement Definition = Document.CreateElement("Definition");
{
XmlElement Description = Document.CreateElement("Name");
Description.InnerText = CustomTool.Name;
Definition.AppendChild(Description);
XmlElement Command = Document.CreateElement("Command");
Command.InnerText = DotNetLocation.FullName.QuoteArgument();
Definition.AppendChild(Command);
XmlElement Arguments = Document.CreateElement("Arguments");
Arguments.InnerText = $"{AssemblyLocation.FullName.QuoteArgument()} {Pair.Key} {CustomTool.Arguments}";
Definition.AppendChild(Arguments);
if (CustomTool.Shortcut.Length > 1)
{
XmlElement Shortcut = Document.CreateElement("Shortcut");
Shortcut.InnerText = CustomTool.Shortcut;
Definition.AppendChild(Shortcut);
}
}
ToolDef.AppendChild(Definition);
if (CustomTool.ShowConsole)
{
XmlElement Console = Document.CreateElement("Console");
{
XmlElement CloseOnExit = Document.CreateElement("CloseOnExit");
CloseOnExit.InnerText = "false";
Console.AppendChild(CloseOnExit);
}
ToolDef.AppendChild(Console);
}
if (CustomTool.RefreshUI)
{
XmlElement Refresh = Document.CreateElement("Refresh");
Refresh.InnerText = CustomTool.RefreshUI ? "true" : "false";
ToolDef.AppendChild(Refresh);
}
if (CustomTool.PromptForArgument)
{
XmlElement Prompt = Document.CreateElement("Prompt");
{
XmlElement PromptText = Document.CreateElement("PromptText");
PromptText.InnerText = CustomTool.PromptText.Length > 0 ? CustomTool.PromptText : "Argument";
Prompt.AppendChild(PromptText);
}
ToolDef.AppendChild(Prompt);
}
XmlElement AddToContext = Document.CreateElement("AddToContext");
AddToContext.InnerText = CustomTool.AddToContextMenu ? "true" : "false";
ToolDef.AppendChild(AddToContext);
}
FolderDefList.AppendChild(ToolDef);
}
FolderDefinition.AppendChild(FolderDefList);
Root.AppendChild(FolderDefinition);
}
}
static void RemoveCustomToolsFromFolders(XmlElement RootNode, FileReference DotNetLocation, FileReference AssemblyLocation, ILogger Logger)
{
XmlNodeList? CustomToolFolderList = RootNode.SelectNodes("CustomToolFolder");
if(CustomToolFolderList == null)
{
return;
}
foreach (XmlNode? ChildNode in CustomToolFolderList)
{
if (ChildNode != null)
{
bool RemoveFolder = false;
XmlElement? FolderRoot = ChildNode.SelectSingleNode("CustomToolDefList") as XmlElement;
if (FolderRoot != null)
{
XmlElement? FolderNameNode = ChildNode.SelectSingleNode("Name") as XmlElement;
string FolderNameString = "";
if (FolderNameNode != null)
{
FolderNameString = FolderNameNode.InnerText;
}
Logger.LogInformation("Removing Tools from folder {Folder}", FolderNameString);
RemoveFolder = RemoveCustomToolsFromNode(FolderRoot, DotNetLocation, AssemblyLocation, Logger);
}
if (RemoveFolder)
{
// remove the folder itself.
RootNode.RemoveChild(ChildNode);
}
}
}
}
public static async Task<int> UpdateCustomToolRegistration(bool bInstall, ILogger Logger)
{
DirectoryReference? ConfigDir = DirectoryReference.GetSpecialFolder(Environment.SpecialFolder.UserProfile);
if (ConfigDir == null)
{
Logger.LogError("Unable to find config directory.");
return 1;
}
FileReference ConfigFile;
if (OperatingSystem.IsMacOS())
{
ConfigFile = FileReference.Combine(ConfigDir, "Library", "Preferences", "com.perforce.p4v", "customtools.xml");
}
else
{
ConfigFile = FileReference.Combine(ConfigDir, ".p4qt", "customtools.xml");
}
XmlDocument Document = new XmlDocument();
if (!TryLoadXmlDocument(ConfigFile, Document))
{
DirectoryReference.CreateDirectory(ConfigFile.Directory);
using (StreamWriter Writer = new StreamWriter(ConfigFile.FullName))
{
await Writer.WriteLineAsync(@"<?xml version=""1.0"" encoding=""UTF-8""?>");
await Writer.WriteLineAsync(@"<!--perforce-xml-version=1.0-->");
await Writer.WriteLineAsync(@"<CustomToolDefList varName=""customtooldeflist"">");
await Writer.WriteLineAsync(@"</CustomToolDefList>");
}
Document.Load(ConfigFile.FullName);
}
// so we don't need to run SetupDotnet.sh every time we run a tool, point to the executing dotnet, assuming it will be usable later
FileReference DotNetLocation = new FileReference(Environment.ProcessPath!);
FileReference AssemblyLocation = new FileReference(Assembly.GetExecutingAssembly().GetOriginalLocation());
XmlElement? Root = Document.SelectSingleNode("CustomToolDefList") as XmlElement;
if (Root == null)
{
Logger.LogError("Unknown schema for {ConfigFile}", ConfigFile);
return 1;
}
// Remove Custom tools at the root
RemoveCustomToolsFromNode(Root, DotNetLocation, AssemblyLocation, Logger);
// Remove Custom tools in folders, and the folders
RemoveCustomToolsFromFolders(Root, DotNetLocation, AssemblyLocation, Logger);
// Insert new entries
if (bInstall)
{
InstallCommandsListInFolder("UERootHelpers", false/*AddFolderToContextMenu*/, RootHelperCommands, Document, DotNetLocation, AssemblyLocation, Logger);
Extend P4VUtils with a new command that allows the user to submit&virtualize package files (for projects with asset virtualization enabled) #rb Ben.Marsh #rnx #jira UE-143675 #preflight skip ### P4VUtils - Add a new submenu "UESubmit" for commands related to the submission of files and changelists -At the moment the submenu only has a single command SubmitAndVirtualizeCommand ### SubmitAndVirtualizeCommand - This command is used to run the asset virtualization process over all of the files in a changelist before submitting it using the following logic: -- Find all package files in a changelist -- Sort the files by project -- Find the engine installation associated with each project -- Build and run the stand alone virtualization tool (if needed) and pass it the packages for the project -- If the virtualization process succeeeded for all projects we can submit the original changelist, other wise print errors to the p4v terminal. - In most cases all of the packages in a changelist will be under a single project, but the command should be able to cope with many packages from many different projects using many different engine installations. - Note that unlike a normal submit, virtualizing a package can change it on disk (by removing the payload data) which means we will probably run into problems where a running instance of the editor has access locks on the file and they cannot be edited. At the moment this command and the stand alone tool will just error, there is no tool<->editor communication protocol set up to request that the editor release the package file. This will be addressed as a future work item. - This is the first pass of the command, there are a number of non-essential todo comments left in the code that will be addressed based on feedback from the end users. [CL 19752487 by paul chipchase in ue5-main branch]
2022-04-14 04:26:33 -04:00
InstallCommandsListInFolder("UESubmit", true/*AddFolderToContextMenu*/, SubmissionCommands, Document, DotNetLocation, AssemblyLocation, Logger);
InstallCommandsListInFolder("UEHelpers", true/*AddFolderToContextMenu*/, HelperCommands, Document, DotNetLocation, AssemblyLocation, Logger);
InstallCommandsListInFolder("UEIntegrate", true/*AddFolderToContextMenu*/, IntegrateCommands, Document, DotNetLocation, AssemblyLocation, Logger);
InstallCommandsListInFolder("UEHorde", true/*AddFolderToContextMenu*/, HordeCommands, Document, DotNetLocation, AssemblyLocation, Logger);
}
// Save the new document
Document.Save(ConfigFile.FullName);
Logger.LogInformation("Written {ConfigFile}", ConfigFile.FullName);
return 0;
}
}
}