// Copyright Epic Games, Inc. All Rights Reserved. using EnvDTE; using Microsoft.VisualStudio.Shell; using Microsoft.VisualStudio.Shell.Interop; using System; using System.Collections.Generic; using System.ComponentModel.Design; using System.Diagnostics; using System.IO; using System.Windows.Forms; using System.Text; using System.Linq; using System.Threading.Tasks; namespace UnrealVS { class CompileSingleFile : IDisposable { private const int CompileSingleFileButtonID = 0x1075; private const int PreprocessSingleFileButtonID = 0x1076; private const int CompileSingleModuleButtonID = 0x1077; private const int CompileAndProfileSingleFileButtonID = 0x1078; private const int GenerateAssemblyFileButtonID = 0x1079; private const int UBTSubMenuID = 0x3103; private string FileToCompileOriginalExt = ""; static readonly HashSet ValidExtensions = new HashSet(StringComparer.OrdinalIgnoreCase) { ".c", ".cc", ".cpp", ".h", ".cxx", ".ispc" }; System.Diagnostics.Process ChildProcess; private OleMenuCommand SubMenuCommand; public CompileSingleFile() { CommandID CommandID = new CommandID(GuidList.UnrealVSCmdSet, CompileSingleFileButtonID); var CompileSingleFileButtonCommand = new OleMenuCommand(new EventHandler(CompileSingleFileButtonHandler), CommandID); CompileSingleFileButtonCommand.BeforeQueryStatus += CompileSingleFileButtonCommand_BeforeQueryStatus; UnrealVSPackage.Instance.MenuCommandService.AddCommand(CompileSingleFileButtonCommand); CommandID CommandID2 = new CommandID(GuidList.UnrealVSCmdSet, PreprocessSingleFileButtonID); var PreprocessSingleFileButtonCommand = new OleMenuCommand(new EventHandler(CompileSingleFileButtonHandler), CommandID2); PreprocessSingleFileButtonCommand.BeforeQueryStatus += CompileSingleFileButtonCommand_BeforeQueryStatus; UnrealVSPackage.Instance.MenuCommandService.AddCommand(PreprocessSingleFileButtonCommand); CommandID CommandID3 = new CommandID(GuidList.UnrealVSCmdSet, CompileSingleModuleButtonID); MenuCommand CompileSingleModuleButtonCommand = new MenuCommand(new EventHandler(CompileSingleFileButtonHandler), CommandID3); UnrealVSPackage.Instance.MenuCommandService.AddCommand(CompileSingleModuleButtonCommand); CommandID CommandID4 = new CommandID(GuidList.UnrealVSCmdSet, CompileAndProfileSingleFileButtonID); var CompileAndProfileSingleFileButtonCommand = new OleMenuCommand(new EventHandler(CompileSingleFileButtonHandler), CommandID4); CompileAndProfileSingleFileButtonCommand.BeforeQueryStatus += CompileSingleFileButtonCommand_BeforeQueryStatus; UnrealVSPackage.Instance.MenuCommandService.AddCommand(CompileAndProfileSingleFileButtonCommand); CommandID CommandID5 = new CommandID(GuidList.UnrealVSCmdSet, GenerateAssemblyFileButtonID); var GenerateAssemblyFileButtonCommand = new OleMenuCommand(new EventHandler(CompileSingleFileButtonHandler), CommandID5); GenerateAssemblyFileButtonCommand.BeforeQueryStatus += CompileSingleFileButtonCommand_BeforeQueryStatus; UnrealVSPackage.Instance.MenuCommandService.AddCommand(GenerateAssemblyFileButtonCommand); // add sub menu for UBT commands SubMenuCommand = new OleMenuCommand(null, new CommandID(GuidList.UnrealVSCmdSet, UBTSubMenuID)); UnrealVSPackage.Instance.MenuCommandService.AddCommand(SubMenuCommand); } private void CompileSingleFileButtonCommand_BeforeQueryStatus(object sender, EventArgs e) { ThreadHelper.ThrowIfNotOnUIThread(); DTE DTE = UnrealVSPackage.Instance.DTE; MenuCommand MenuCommand = sender as MenuCommand; if (MenuCommand == null) { return; } if (DTE?.ActiveDocument == null) { MenuCommand.Enabled = false; return; } // Check if the requested file is valid string FileToCompileExt = Path.GetExtension(DTE.ActiveDocument.FullName); MenuCommand.Enabled = ValidExtensions.Contains(FileToCompileExt); } public void Dispose() { KillChildProcess(); } void CompileSingleFileButtonHandler(object Sender, EventArgs Args) { ThreadHelper.ThrowIfNotOnUIThread(); MenuCommand SenderSubMenuCommand = (MenuCommand)Sender; bool bIsFile = SenderSubMenuCommand.CommandID.ID != CompileSingleModuleButtonID; bool bPreprocessOnly = SenderSubMenuCommand.CommandID.ID == PreprocessSingleFileButtonID; bool bProfile = SenderSubMenuCommand.CommandID.ID == CompileAndProfileSingleFileButtonID; bool bGenerateAssembly = SenderSubMenuCommand.CommandID.ID == GenerateAssemblyFileButtonID; if (!TryCompileSingleFileOrModule(bIsFile, bPreprocessOnly, bProfile, bGenerateAssembly)) { DTE DTE = UnrealVSPackage.Instance.DTE; if (bProfile) if (!StartTrace()) return; DTE.ExecuteCommand("Build.Compile"); if (bProfile) { var WaitThread = new System.Threading.Thread(() => { bool Done = false; while (!Done) { ThreadHelper.JoinableTaskFactory.Run(async () => { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); if (DTE.Solution.SolutionBuild.BuildState != vsBuildState.vsBuildStateDone) return; StopTrace(); Done = true; }); } }) { Priority = System.Threading.ThreadPriority.Lowest }; WaitThread.Start(); } } } void KillChildProcess() { if (ChildProcess != null) { if (!ChildProcess.HasExited) { ChildProcess.Kill(); ChildProcess.WaitForExit(); } ChildProcess.Dispose(); ChildProcess = null; } } string FindModuleForFile(string fileName, string rootDirectory) { string directory = Path.GetDirectoryName(fileName); IEnumerable buildFiles = Directory.EnumerateFiles(directory, "*.build.cs", SearchOption.TopDirectoryOnly); string buildFile = buildFiles.FirstOrDefault(); if (buildFile != null) { return Path.GetFileName(buildFile.Substring(0, buildFile.LastIndexOf(".build.cs", StringComparison.OrdinalIgnoreCase))); } if (string.Equals(directory, rootDirectory, StringComparison.OrdinalIgnoreCase)) { return null; } return FindModuleForFile(directory, rootDirectory); } bool StartTrace() { ThreadHelper.ThrowIfNotOnUIThread(); DTE DTE = UnrealVSPackage.Instance.DTE; var Commands = DTE.Commands.Cast(); #pragma warning disable VSTHRD010 Command Command = Commands.FirstOrDefault((C) => C.Name == "CompileScore.StartTrace"); #pragma warning restore VSTHRD010 if (Command == null || !Command.IsAvailable) { MessageBox.Show($"CompileSingleFileAndProfile requires a CompileScore visual studio extension that has CompileScore.StartTrace/CompileScore.StopTrace installed", "UnrealVS - Missing CompileScore extension ", MessageBoxButtons.OK); return false; } Object CustomIn = null; Object CustomOut = null; DTE.Commands.Raise(Command.Guid, Command.ID, ref CustomIn, ref CustomOut); //DTE.ExecuteCommand("CompileScore.StartTrace"); IVsOutputWindowPane BuildOutputPane = UnrealVSPackage.Instance.GetOutputPane(); if (BuildOutputPane == null) { BuildOutputPane.OutputStringThreadSafe($"1>------ Started trace for CompileScore ------{Environment.NewLine}"); } return true; } void StopTrace() { ThreadHelper.ThrowIfNotOnUIThread(); UnrealVSPackage.Instance.DTE.ExecuteCommand("CompileScore.StopTrace"); IVsOutputWindowPane BuildOutputPane = UnrealVSPackage.Instance.GetOutputPane(); if (BuildOutputPane != null) { BuildOutputPane.OutputStringThreadSafe($"1>------ Stopped trace for CompileScore ------{Environment.NewLine}"); } } bool TryCompileSingleFileOrModule(bool bIsFile, bool bPreProcessOnly, bool bProfile, bool bGenerateAssembly) { ThreadHelper.ThrowIfNotOnUIThread(); DTE DTE = UnrealVSPackage.Instance.DTE; // Activate the output window Window Window = DTE.Windows.Item(EnvDTE.Constants.vsWindowKindOutput); Window.Activate(); // Find or create the 'Build' window IVsOutputWindowPane BuildOutputPane = UnrealVSPackage.Instance.GetOutputPane(); if (BuildOutputPane == null) { Logging.WriteLine("CompileSingleFile: Build Output Pane not found"); return false; } // If there's already a build in progress, offer to cancel it if (ChildProcess != null && !ChildProcess.HasExited) { if (MessageBox.Show("Cancel current compile?", "Compile in progress", MessageBoxButtons.YesNo) == DialogResult.Yes) { KillChildProcess(); BuildOutputPane.OutputStringThreadSafe($"1> Build cancelled.{Environment.NewLine}"); } return true; } // Check we've got a file open if (DTE.ActiveDocument == null) { Logging.WriteLine("CompileSingleFile: ActiveDocument not found"); return false; } // Grab the current startup project UnrealVSPackage.Instance.SolutionBuildManager.get_StartupProject(out IVsHierarchy ProjectHierarchy); if (ProjectHierarchy == null) { Logging.WriteLine("CompileSingleFile: ProjectHierarchy not found"); return false; } Project StartupProject = Utils.HierarchyObjectToProject(ProjectHierarchy); if (StartupProject == null) { Logging.WriteLine("CompileSingleFile: StartupProject not found"); return false; } if (!(StartupProject.Object is Microsoft.VisualStudio.VCProjectEngine.VCProject VCStartupProject)) { Logging.WriteLine("CompileSingleFile: VCStartupProject not found"); return false; } // Get the active configuration for the startup project Configuration ActiveConfiguration = StartupProject.ConfigurationManager.ActiveConfiguration; string ActiveConfigurationName = $"{ActiveConfiguration.ConfigurationName}|{ActiveConfiguration.PlatformName}"; Microsoft.VisualStudio.VCProjectEngine.VCConfiguration ActiveVCConfiguration = (VCStartupProject.Configurations as Microsoft.VisualStudio.VCProjectEngine.IVCCollection).Item(ActiveConfigurationName) as Microsoft.VisualStudio.VCProjectEngine.VCConfiguration; if (ActiveVCConfiguration == null) { Logging.WriteLine("CompileSingleFile: VCStartupProject ActiveConfiguration not found"); return false; } // Get the NMake settings for this configuration Microsoft.VisualStudio.VCProjectEngine.VCNMakeTool ActiveNMakeTool = (ActiveVCConfiguration.Tools as Microsoft.VisualStudio.VCProjectEngine.IVCCollection).Item("VCNMakeTool") as Microsoft.VisualStudio.VCProjectEngine.VCNMakeTool; if (ActiveNMakeTool == null) { MessageBox.Show($"No NMakeTool set for Project {VCStartupProject.Name} set for single-file compile.", "UnrealVS - NMakeTool not set", MessageBoxButtons.OK); return false; } // Save all the open documents DTE.ExecuteCommand("File.SaveAll"); // Check if the requested file is valid string FileToCompile = DTE.ActiveDocument.FullName; string FileToCompileExt = Path.GetExtension(FileToCompile); string CompilingText; List UBTArguments = new List { "-WorkingDir=\"$(MSBuildProjectDirectory)\"", }; if (bPreProcessOnly) { UBTArguments.Add("-NoXGE -NoSNDBS -NoFASTBuild"); UBTArguments.Add("-Preprocess"); } else if (bGenerateAssembly) { UBTArguments.Add("-NoXGE -NoSNDBS -NoFASTBuild"); UBTArguments.Add("-WithAssembly"); } if (bIsFile) { if (!ValidExtensions.Contains(FileToCompileExt.ToLowerInvariant())) { MessageBox.Show($"Invalid file extension {FileToCompileExt} for single-file compile.", "Invalid Extension", MessageBoxButtons.OK); return true; } CompilingText = FileToCompile; UBTArguments.Add($"-SingleFile=\"{FileToCompile}\""); } else { string ModuleName = FindModuleForFile(FileToCompile, Path.GetDirectoryName(DTE.Solution.FileName)); if (ModuleName == null) { MessageBox.Show($"Can't find module for for {FileToCompile} to compile.", "Invalid Module", MessageBoxButtons.OK); return true; } CompilingText = ModuleName; UBTArguments.Add($"-Module=\"{ModuleName}\""); } // If there's already a build in progress, don't let another one start if (DTE.Solution.SolutionBuild.BuildState == vsBuildState.vsBuildStateInProgress) { if (MessageBox.Show("Cancel current compile?", "Compile in progress", MessageBoxButtons.YesNo) == DialogResult.Yes) { DTE.ExecuteCommand("Build.Cancel"); } return true; } // Make sure any existing build is stopped KillChildProcess(); // Set up the output pane BuildOutputPane.Activate(); BuildOutputPane.Clear(); // Set up event handlers DTE.Events.BuildEvents.OnBuildBegin += BuildEvents_OnBuildBegin; // Create a delegate for handling output messages List PreprocessedFiles = new List(); List AssemblyFiles = new List(); void OutputHandler(object Sender, DataReceivedEventArgs Args) { if (Args.Data != null) { if (Args.Data.Contains("PreProcessPath:")) { PreprocessedFiles.Add(Args.Data.Replace("PreProcessPath:", "").Trim()); } else if (Args.Data.Contains("AssemblyPath:")) { AssemblyFiles.Add(Args.Data.Replace("AssemblyPath:", "").Trim()); } else { BuildOutputPane.OutputStringThreadSafe($"1> {Args.Data}{Environment.NewLine}"); } } } if (bProfile) if (!StartTrace()) return true; BuildOutputPane.OutputStringThreadSafe($"1>------ Build started: Project: {StartupProject.Name}, Configuration: {ActiveConfiguration.ConfigurationName} {ActiveConfiguration.PlatformName} ------{Environment.NewLine}"); BuildOutputPane.OutputStringThreadSafe($"1> Compiling {CompilingText}{Environment.NewLine}"); string SolutionDir = Path.GetDirectoryName(UnrealVSPackage.Instance.SolutionFilepath).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar; // Get the build command line and escape any environment variables that we use string BuildCommandLine = ActiveVCConfiguration.Evaluate(ActiveNMakeTool.BuildCommandLine); string UBTArgument = ActiveVCConfiguration.Evaluate(string.Join(" ", UBTArguments)); string WorkingDirectory = ActiveVCConfiguration.Evaluate("$(MSBuildProjectDirectory)"); FileToCompileOriginalExt = FileToCompileExt; // Spawn the new process ChildProcess = new System.Diagnostics.Process(); ChildProcess.StartInfo.FileName = Path.Combine(Environment.SystemDirectory, "cmd.exe"); ChildProcess.StartInfo.Arguments = $"/C \"{BuildCommandLine} {UBTArgument}\""; ChildProcess.StartInfo.WorkingDirectory = WorkingDirectory; ChildProcess.StartInfo.UseShellExecute = false; ChildProcess.StartInfo.RedirectStandardOutput = true; ChildProcess.StartInfo.RedirectStandardError = true; ChildProcess.StartInfo.CreateNoWindow = true; ChildProcess.OutputDataReceived += OutputHandler; ChildProcess.ErrorDataReceived += OutputHandler; if (bPreProcessOnly || bGenerateAssembly || bProfile) { // add an event handler to respond to the exit of the preprocess request // and open the generated file if it exists. ChildProcess.EnableRaisingEvents = true; ChildProcess.Exited += new EventHandler((s, e) => UbtProcessExitHandler(PreprocessedFiles, AssemblyFiles, bProfile)); } ChildProcess.Start(); ChildProcess.BeginOutputReadLine(); ChildProcess.BeginErrorReadLine(); return true; } private void UbtProcessExitHandler(IEnumerable PreprocessedFiles, IEnumerable AssemblyFiles, bool bIsProfiling) { if (bIsProfiling) { ThreadHelper.JoinableTaskFactory.Run(async () => { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); StopTrace(); }); } // not all compile actions support pre-process - check it exists foreach (string PreprocessedFile in PreprocessedFiles) { ThreadHelper.JoinableTaskFactory.Run(async () => { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); OpenPreprocessedFile(PreprocessedFile); }); } foreach (string AssemblyFile in AssemblyFiles) { ThreadHelper.JoinableTaskFactory.Run(async () => { await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); OpenAssemblyFile(AssemblyFile); }); } } private void OpenPreprocessedFile(string PPFullPath) { ThreadHelper.ThrowIfNotOnUIThread(); if (File.Exists(PPFullPath)) { // if the file exists, rename it to isolate the file and have its extension be the original to maintain syntax highlighting string Dir = Path.GetDirectoryName(PPFullPath); string FileName = Path.GetFileNameWithoutExtension(Path.GetFileNameWithoutExtension(PPFullPath)) + "_preprocessed"; string RenamedFile = Path.Combine(Dir, FileName) + FileToCompileOriginalExt; File.Copy(PPFullPath, RenamedFile, true /*overwrite*/); UnrealVSPackage.Instance.DTE.ExecuteCommand("File.OpenFile", $"\"{RenamedFile}\""); } } private void OpenAssemblyFile(string AsmFullPath) { ThreadHelper.ThrowIfNotOnUIThread(); if (File.Exists(AsmFullPath)) { UnrealVSPackage.Instance.DTE.ExecuteCommand("File.OpenFile", $"\"{AsmFullPath}\""); } } private void BuildEvents_OnBuildBegin(vsBuildScope Scope, vsBuildAction Action) { KillChildProcess(); } } }