// Copyright Epic Games, Inc. All Rights Reserved. using EnvDTE; 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; namespace UnrealVS { class CompileSingleFile : IDisposable { const int CompileSingleFileButtonID = 0x1075; static readonly List ValidExtensions = new List { ".c", ".cc", ".cpp", ".cxx" }; System.Diagnostics.Process ChildProcess; public CompileSingleFile() { CommandID CommandID = new CommandID(GuidList.UnrealVSCmdSet, CompileSingleFileButtonID); MenuCommand CompileSingleFileButtonCommand = new MenuCommand(new EventHandler(CompileSingleFileButtonHandler), CommandID); UnrealVSPackage.Instance.MenuCommandService.AddCommand(CompileSingleFileButtonCommand); } public void Dispose() { KillChildProcess(); } void CompileSingleFileButtonHandler(object Sender, EventArgs Args) { if (!TryCompileSingleFile()) { DTE DTE = UnrealVSPackage.Instance.DTE; DTE.ExecuteCommand("Build.Compile"); } } void KillChildProcess() { if (ChildProcess != null) { if (!ChildProcess.HasExited) { ChildProcess.Kill(); ChildProcess.WaitForExit(); } ChildProcess.Dispose(); ChildProcess = null; } } bool TryCompileSingleFile() { 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.OutputString($"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.Item(ActiveConfigurationName); 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.Item("VCNMakeTool"); if (ActiveNMakeTool == null) { MessageBox.Show($"No NMakeTool set for Project {VCStartupProject.Name} set for single-file compile.", "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); if (!ValidExtensions.Contains(FileToCompileExt.ToLowerInvariant())) { MessageBox.Show($"Invalid file extension {FileToCompileExt} for single-file compile.", "Invalid Extension", MessageBoxButtons.OK); return true; } // 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(); BuildOutputPane.OutputString($"1>------ Build started: Project: {StartupProject.Name}, Configuration: {ActiveConfiguration.ConfigurationName} {ActiveConfiguration.PlatformName} ------{Environment.NewLine}"); BuildOutputPane.OutputString($"1> Compiling {FileToCompile}{Environment.NewLine}"); // Set up event handlers DTE.Events.BuildEvents.OnBuildBegin += BuildEvents_OnBuildBegin; // Create a delegate for handling output messages void OutputHandler(object Sender, DataReceivedEventArgs Args) { if (Args.Data != null) BuildOutputPane.OutputString($"1> {Args.Data}{Environment.NewLine}"); } // Get the build command line and escape any environment variables that we use string BuildCommandLine = ActiveNMakeTool.BuildCommandLine; BuildCommandLine = BuildCommandLine.Replace("$(SolutionDir)", Path.GetDirectoryName(UnrealVSPackage.Instance.SolutionFilepath).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar); BuildCommandLine = BuildCommandLine.Replace("$(ProjectName)", VCStartupProject.Name); // Spawn the new process ChildProcess = new System.Diagnostics.Process(); ChildProcess.StartInfo.FileName = Path.Combine(Environment.SystemDirectory, "cmd.exe"); ChildProcess.StartInfo.Arguments = $"/C \"{BuildCommandLine} -singlefile=\"{FileToCompile}\"\""; ChildProcess.StartInfo.WorkingDirectory = Path.GetDirectoryName(StartupProject.FullName); ChildProcess.StartInfo.UseShellExecute = false; ChildProcess.StartInfo.RedirectStandardOutput = true; ChildProcess.StartInfo.RedirectStandardError = true; ChildProcess.StartInfo.CreateNoWindow = true; ChildProcess.OutputDataReceived += OutputHandler; ChildProcess.ErrorDataReceived += OutputHandler; ChildProcess.Start(); ChildProcess.BeginOutputReadLine(); ChildProcess.BeginErrorReadLine(); return true; } private void BuildEvents_OnBuildBegin(vsBuildScope Scope, vsBuildAction Action) { KillChildProcess(); } } }