// Copyright 1998-2015 Epic Games, Inc. All Rights Reserved. using AutomationTool; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Xml; using UnrealBuildTool; namespace AutomationScripts.Automation { [Help("List labels valid for this branch.")] [Help("type", "Type of labels to list. Could be one of: all, promotable and promoted. Default: all.")] [Help("ticks", "Print ticks along with the label name.")] [Help("game=GameName", "Name of the game to filter labels. If not set then assuming shared promotable.")] [RequireP4] [DoesNotNeedP4CL] class UnrealSyncList : BuildCommand { enum QueryType { All, Promotable, Promoted } public override void ExecuteBuild() { var GameName = ParseParamValue("game"); var QueryType = GetTypeParam(); var BranchPath = CommandUtils.GetDirectoryName(P4Env.BuildRootP4); var Ticks = ParseParam("ticks"); var BranchAndGameName = string.IsNullOrWhiteSpace(GameName) ? string.Format("branch {0} shared-promotable", BranchPath) : string.Format("branch {0} and game {1}", BranchPath, GameName); if (string.IsNullOrWhiteSpace(BranchPath)) { throw new AutomationException("The branch path is not set. Something went wrong."); } if (QueryType == QueryType.Promoted) { Log("Promoted labels for {0}.", BranchAndGameName); Print(GetPromotedLabels(BranchPath, GameName), Ticks); } else if(QueryType == QueryType.Promotable) { Log("Promotable labels for {0}.", BranchAndGameName); Print(GetPromotableLabels(BranchPath, GameName), Ticks); } else { Log("All labels for {0}.", BranchPath); Print(GetBranchLabels(BranchPath), Ticks); } } /// /// Gets promoted label list for given branch. /// /// A branch path. /// The game name for which provide the label. If null or empty then provides shared-promotable label. /// List of promoted labels for given branch path. public static P4Label[] GetPromotedLabels(string BranchPath, string GameName) { return P4.GetLabels(BranchPath + "/Promoted" + (GameName != null ? ("-" + GameName) : "") + "-CL-*"); } /// /// Gets promoted label list for given branch. /// /// A branch path. /// The game name for which provide the label. If null or empty then provides shared-promotable label. /// List of promoted labels for given branch path. public static P4Label[] GetPromotableLabels(string BranchPath, string GameName) { var Combined = P4.GetLabels(BranchPath + "/Promot*" + (GameName != null ? ("-" + GameName) : "") + "-CL-*"); Combined.OrderByDescending((Label) => Label.Date); var Output = new List(); foreach(var PossiblePromotable in Combined) { if(PossiblePromotable.Name.StartsWith(BranchPath + "/Promoted")) { break; } if (PossiblePromotable.Name.StartsWith(BranchPath + "/Promotable")) { Output.Add(PossiblePromotable); } // else skip } return Output.ToArray(); } /// /// Prints the labels. /// /// Labels to print. /// Print ticks along with label name? public void Print(P4Label[] Labels, bool Ticks) { foreach(var Label in Labels.OrderByDescending((Label) => Label.Date)) { Log(Label.Name + (Ticks ? (" " + Label.Date.Ticks.ToString()) : "")); } } /// /// Gets labels list for given branch. /// /// A branch path. /// List of labels for given branch path. public P4Label[] GetBranchLabels(string BranchPath) { return P4.GetLabels(BranchPath + "/*"); } /// /// Parses type param. /// /// Enum value of the query type param. private QueryType GetTypeParam() { var TypeString = ParseParamValue("type"); if(string.IsNullOrWhiteSpace(TypeString)) { return QueryType.All; } switch (TypeString.ToLower()) { case "all": return QueryType.All; case "promotable": return QueryType.Promotable; case "promoted": return QueryType.Promoted; default: throw new AutomationException("Unsupported query type. Allowed are: all, promotable and promoted."); } } } [Help("Syncs promotable build. Use either -game or -label parameter.")] [Help("artist", "Artist sync i.e. sync content to head and all the rest to promoted label.")] [Help("preview", "This option makes that syncs are done in the preview mode (i.e. p4 sync -n).")] [Help("game=GameName", "Name of the game to sync. If not set then shared promotable will be synced.")] [Help("label=LabelName", "Promotable label name to sync to.")] [RequireP4] [DoesNotNeedP4CL] class UnrealSync : BuildCommand { public override void ExecuteBuild() { var Preview = ParseParam("preview"); var ArtistSync = ParseParam("artist"); var BranchPath = CommandUtils.GetDirectoryName(P4Env.BuildRootP4); var LabelParam = ParseParamValue("label"); var GameName = ParseParamValue("game"); if(GameName == null) { GameName = ""; } string ProgramSyncLabelName = null; if (!string.IsNullOrWhiteSpace(LabelParam)) { if (!LabelParam.StartsWith(BranchPath) || !P4.ValidateLabelContent(LabelParam)) { throw new AutomationException("Label {0} either doesn't exist or is not valid for the current branch path {1}.", LabelParam, BranchPath); } ProgramSyncLabelName = LabelParam; } else { ProgramSyncLabelName = GetLatestPromotedLabel(BranchPath, GameName, true); } if (ProgramSyncLabelName == null) { throw new AutomationException("Label for {0} was not found.", string.IsNullOrWhiteSpace(GameName) ? string.Format("branch {0} shared-promotable", BranchPath) : string.Format("branch {0} and game {1}", BranchPath, GameName)); } SyncToLabel(BranchPath, ProgramSyncLabelName, ArtistSync, Preview); } /// /// Picks latest label from the provided group and returns its name. /// /// Labels to chose from. /// If the method should skip labels that has no tagged files in it. /// The name of the label. If none was valid returns null. public static string PickLatest(P4Label[] Labels, bool bVerifyContent) { if (Labels.Length == 0) { return null; } var OrderedLabels = Labels.OrderByDescending((Label) => Label.Date); if (bVerifyContent) { foreach (var Label in OrderedLabels) { if (P4.ValidateLabelContent(Label.Name)) { return Label.Name; } } // Haven't found valid label. return null; } return OrderedLabels.First().Name; } /// /// Get latest promoted label given branch and game name. /// /// The branch path of the label. /// The game name for which provide the label. If null or empty then provides shared-promotable label. /// Verify if label tags at least one file. /// Label name if it exists, null otherwise. public static string GetLatestPromotedLabel(string BranchPath, string GameName, bool bVerifyContent) { if (string.IsNullOrWhiteSpace(GameName)) { GameName = null; } return PickLatest(UnrealSyncList.GetPromotedLabels(BranchPath, GameName), bVerifyContent); } /// /// Syncs to given label. /// /// Current branch path. /// Label name to sync. /// Perform artist sync? (binaries to label, content to latest) /// Perform preview sync? (p4 sync -n) private void SyncToLabel(string BranchPath, string LabelName, bool bArtistSync = true, bool bPreview = false) { var ProgramRevisionSpec = "@" + LabelName; List SyncSteps; if (bArtistSync) { // Get latest CL number to sync cause @head can change during // different syncs and it could create integrity problems in // workspace. var ContentRevisionSpec = "@" + P4.GetLatestCLNumber().ToString(); var GameName = ParseGameNameFromLabel(LabelName); var ArtistSyncRulesPath = string.Format("{0}/{1}/Build/ArtistSyncRules.xml", BranchPath, string.IsNullOrWhiteSpace(GameName) ? "Samples" : GameName); var SyncRules = string.Join("\n", P4.P4Print(ArtistSyncRulesPath + "#head")); if (string.IsNullOrWhiteSpace(SyncRules)) { throw new AutomationException("The path {0} is not valid or file is empty.", ArtistSyncRulesPath); } SyncSteps = GenerateSyncSteps(SyncRules, ContentRevisionSpec, ProgramRevisionSpec); } else { SyncSteps = new List(); SyncSteps.Add("/..." + ProgramRevisionSpec); // all files to label } foreach (var SyncStep in SyncSteps) { P4.Sync((bPreview ? "-n " : "") + BranchPath + SyncStep); } } /// /// Generates sync steps based on the sync rules xml content. /// /// ArtistSyncRules.xml content. /// Revision spec to which sync the content. Different for artist sync. /// Revision spec to which sync everything except content. /// An array of sync steps to perform. private List GenerateSyncSteps(string SyncRules, string ContentRevisionSpec, string ProgramRevisionSpec) { var SyncRulesDocument = new XmlDocument(); SyncRulesDocument.LoadXml(SyncRules); var RuleNodes = SyncRulesDocument.DocumentElement.SelectNodes("/ArtistSyncRules/Rules/string"); var OutputList = new List(); foreach (XmlNode Node in RuleNodes) { var SyncStep = Node.InnerText.Replace("%LABEL_TO_SYNC_TO%", ProgramRevisionSpec); // If there was no label in sync step. // TODO: This is hack for messy ArtistSyncRules.xml format. // Needs to be changed soon. if (!SyncStep.Contains("@")) { SyncStep += ContentRevisionSpec; } OutputList.Add(SyncStep); } return OutputList; } /* Pattern used for parsing game name from label name. */ private static readonly Regex GameNameFromLabelParsingPattern = new Regex(@"^.+/Promot(ed|able)-((?\w+)-)?CL.+$", RegexOptions.Compiled); /// /// Tries to parse game name from label name. /// /// The label name to parse from. /// Game name if found. Null otherwise. private string ParseGameNameFromLabel(string LabelName) { return GameNameFromLabelParsingPattern.Match(LabelName).Groups["game"].Value; } } }