/* **************************************************************************** * * Copyright (c) Microsoft Corporation. * * This source code is subject to terms and conditions of the Apache License, Version 2.0. A * copy of the license can be found in the License.html file at the root of this distribution. If * you cannot locate the Apache License, Version 2.0, please send an email to * vspython@microsoft.com. By using this source code in any fashion, you are agreeing to be bound * by the terms of the Apache License, Version 2.0. * * You must not remove this notice, or any other, from this software. * * ***************************************************************************/ using System; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Runtime.InteropServices; using Microsoft.VisualStudio; using Microsoft.VisualStudio.OLE.Interop; using Microsoft.VisualStudio.Shell; //#define ConfigTrace using Microsoft.VisualStudio.Shell.Interop; using MSBuildConstruction = Microsoft.Build.Construction; using MSBuildExecution = Microsoft.Build.Execution; namespace Microsoft.VisualStudio.Project { [ComVisible(true)] public abstract class Config : IVsCfg, IVsProjectCfg, IVsProjectCfg2, IVsProjectFlavorCfg, IVsDebuggableProjectCfg, ISpecifyPropertyPages, IVsSpecifyProjectDesignerPages, IVsCfgBrowseObject { public const string Debug = "Debug"; public const string AnyCPU = "AnyCPU"; internal string configName; private string platformName; private ProjectNode project; private MSBuildExecution.ProjectInstance currentConfig; private IVsProjectFlavorCfg flavoredCfg; private List outputGroups; private BuildableProjectConfig buildableCfg; #region properties public ProjectNode ProjectMgr { get { return this.project; } } public virtual string ConfigName { get { return configName; } } public virtual string PlatformName { get { return platformName; } } protected IList OutputGroups { get { if (null == this.outputGroups) { // Initialize output groups this.outputGroups = new List(); // Get the list of group names from the project. // The main reason we get it from the project is to make it easier for someone to modify // it by simply overriding that method and providing the correct MSBuild target(s). IList> groupNames = project.GetOutputGroupNames(); if (groupNames != null) { // Populate the output array foreach (KeyValuePair group in groupNames) { OutputGroup outputGroup = CreateOutputGroup(project, group); this.outputGroups.Add(outputGroup); } } } return this.outputGroups; } } #endregion #region ctors public Config(ProjectNode project, string configuration, string platform) { this.project = project; this.configName = configuration; this.platformName = platform; var flavoredCfgProvider = ProjectMgr.GetOuterInterface(); VsUtilities.ArgumentNotNull("flavoredCfgProvider", flavoredCfgProvider); ErrorHandler.ThrowOnFailure(flavoredCfgProvider.CreateProjectFlavorCfg(this, out flavoredCfg)); VsUtilities.ArgumentNotNull("flavoredCfg", flavoredCfg); // if the flavored object support XML fragment, initialize it IPersistXMLFragment persistXML = flavoredCfg as IPersistXMLFragment; if (null != persistXML) { this.project.LoadXmlFragment(persistXML, ConfigName, PlatformName); } } #endregion #region methods public virtual OutputGroup CreateOutputGroup(ProjectNode project, KeyValuePair group) { OutputGroup outputGroup = new OutputGroup(group.Key, group.Value, project, this); return outputGroup; } public void PrepareBuild(uint vsopts, bool clean) { project.PrepareBuild(vsopts, this.ConfigName, this.PlatformName, clean); } public virtual string GetConfigurationProperty(string propertyName, bool resetCache) { MSBuildExecution.ProjectPropertyInstance property = GetMsBuildProperty(propertyName, resetCache); if (property == null) return null; return property.EvaluatedValue; } public virtual void SetConfigurationProperty(string propertyName, string propertyValue) { if (!this.project.QueryEditProjectFile(false)) { throw Marshal.GetExceptionForHR(VSConstants.OLE_E_PROMPTSAVECANCELLED); } string condition = String.Format(CultureInfo.InvariantCulture, ConfigProvider.configString, this.ConfigName); SetPropertyUnderCondition(propertyName, propertyValue, condition); // property cache will need to be updated this.currentConfig = null; this.project.SetProjectFileDirty(true); return; } /// /// Emulates the behavior of SetProperty(name, value, condition) on the old MSBuild object model. /// This finds a property group with the specified condition (or creates one if necessary) then sets the property in there. /// private void SetPropertyUnderCondition(string propertyName, string propertyValue, string condition) { string conditionTrimmed = (condition == null) ? String.Empty : condition.Trim(); if (conditionTrimmed.Length == 0) { this.project.BuildProject.SetProperty(propertyName, propertyValue); return; } // New OM doesn't have a convenient equivalent for setting a property with a particular property group condition. // So do it ourselves. MSBuildConstruction.ProjectPropertyGroupElement newGroup = null; foreach (MSBuildConstruction.ProjectPropertyGroupElement group in this.project.BuildProject.Xml.PropertyGroups) { if (String.Equals(group.Condition.Trim(), conditionTrimmed, StringComparison.OrdinalIgnoreCase)) { newGroup = group; break; } } if (newGroup == null) { newGroup = this.project.BuildProject.Xml.AddPropertyGroup(); // Adds after last existing PG, else at start of project newGroup.Condition = condition; } foreach (MSBuildConstruction.ProjectPropertyElement property in newGroup.PropertiesReversed) // If there's dupes, pick the last one so we win { if (String.Equals(property.Name, propertyName, StringComparison.OrdinalIgnoreCase) && property.Condition.Length == 0) { property.Value = propertyValue; return; } } newGroup.AddProperty(propertyName, propertyValue); } /// /// If flavored, and if the flavor config can be dirty, ask it if it is dirty /// /// Project file or user file /// 0 = not dirty public int IsFlavorDirty(_PersistStorageType storageType) { int isDirty = 0; if (this.flavoredCfg != null && this.flavoredCfg is IPersistXMLFragment) { ErrorHandler.ThrowOnFailure(((IPersistXMLFragment)this.flavoredCfg).IsFragmentDirty((uint)storageType, out isDirty)); } return isDirty; } /// /// If flavored, ask the flavor if it wants to provide an XML fragment /// /// Guid of the flavor /// Project file or user file /// Fragment that the flavor wants to save /// HRESULT public int GetXmlFragment(Guid flavor, _PersistStorageType storageType, out string fragment) { fragment = null; int hr = VSConstants.S_OK; if (this.flavoredCfg != null && this.flavoredCfg is IPersistXMLFragment) { Guid flavorGuid = flavor; hr = ((IPersistXMLFragment)this.flavoredCfg).Save(ref flavorGuid, (uint)storageType, out fragment, 1); } return hr; } #endregion #region IVsSpecifyPropertyPages /// /// Retrieves the configuration dependent property pages. /// /// The pages to return. public virtual void GetPages(CAUUID[] pages) { // We do not check whether the supportsProjectDesigner is set to true on the ProjectNode. // We rely that the caller knows what to call on us. VsUtilities.ArgumentNotNull("pages", pages); if (pages.Length == 0) { throw new ArgumentException(SR.GetString(SR.InvalidParameter, CultureInfo.CurrentUICulture), "pages"); } // Retrive the list of guids from hierarchy properties. // Because a flavor could modify that list we must make sure we are calling the outer most implementation of IVsHierarchy string guidsList = String.Empty; IVsHierarchy hierarchy = project.GetOuterInterface(); object variant = null; ErrorHandler.ThrowOnFailure(hierarchy.GetProperty(VSConstants.VSITEMID_ROOT, (int)__VSHPROPID2.VSHPROPID_PropertyPagesCLSIDList, out variant), new int[] { VSConstants.DISP_E_MEMBERNOTFOUND, VSConstants.E_NOTIMPL }); guidsList = (string)variant; Guid[] guids = VsUtilities.GuidsArrayFromSemicolonDelimitedStringOfGuids(guidsList); if (guids == null || guids.Length == 0) { pages[0] = new CAUUID(); pages[0].cElems = 0; } else { pages[0] = PackageUtilities.CreateCAUUIDFromGuidArray(guids); } } #endregion #region IVsSpecifyProjectDesignerPages /// /// Implementation of the IVsSpecifyProjectDesignerPages. It will retun the pages that are configuration dependent. /// /// The pages to return. /// VSConstants.S_OK public virtual int GetProjectDesignerPages(CAUUID[] pages) { // We do not check whether the supportsProjectDesigner is set to true on the ProjectNode. // We rely that the caller knows what to call on us. VsUtilities.ArgumentNotNull("pages", pages); if (pages.Length == 0) { throw new ArgumentException(SR.GetString(SR.InvalidParameter, CultureInfo.CurrentUICulture), "pages"); } // Retrive the list of guids from hierarchy properties. // Because a flavor could modify that list we must make sure we are calling the outer most implementation of IVsHierarchy string guidsList = String.Empty; IVsHierarchy hierarchy = project.GetOuterInterface(); object variant = null; ErrorHandler.ThrowOnFailure(hierarchy.GetProperty(VSConstants.VSITEMID_ROOT, (int)__VSHPROPID2.VSHPROPID_CfgPropertyPagesCLSIDList, out variant), new int[] { VSConstants.DISP_E_MEMBERNOTFOUND, VSConstants.E_NOTIMPL }); guidsList = (string)variant; Guid[] guids = VsUtilities.GuidsArrayFromSemicolonDelimitedStringOfGuids(guidsList); if (guids == null || guids.Length == 0) { pages[0] = new CAUUID(); pages[0].cElems = 0; } else { pages[0] = PackageUtilities.CreateCAUUIDFromGuidArray(guids); } return VSConstants.S_OK; } #endregion #region IVsCfg methods /// /// The display name is a two part item /// first part is the config name, 2nd part is the platform name /// public virtual int get_DisplayName(out string name) { name = DisplayName; return VSConstants.S_OK; } private string DisplayName { get { return this.ConfigName + "|" + this.PlatformName; } } public virtual int get_IsDebugOnly(out int fDebug) { fDebug = 0; if (this.ConfigName == "Debug") { fDebug = 1; } return VSConstants.S_OK; } public virtual int get_IsReleaseOnly(out int fRelease) { fRelease = 0; if (this.ConfigName == "Release") { fRelease = 1; } return VSConstants.S_OK; } #endregion #region IVsProjectCfg methods public virtual int EnumOutputs(out IVsEnumOutputs eo) { eo = null; return VSConstants.E_NOTIMPL; } public virtual int get_BuildableProjectCfg(out IVsBuildableProjectCfg pb) { if (buildableCfg == null) { buildableCfg = new BuildableProjectConfig(this); } pb = buildableCfg; return VSConstants.E_NOTIMPL; } public virtual int get_CanonicalName(out string name) { name = ConfigName; return VSConstants.S_OK; } public virtual int get_IsPackaged(out int pkgd) { pkgd = 0; return VSConstants.S_OK; } public virtual int get_IsSpecifyingOutputSupported(out int f) { f = 1; return VSConstants.S_OK; } public virtual int get_Platform(out Guid platform) { platform = Guid.Empty; return VSConstants.E_NOTIMPL; } public virtual int get_ProjectCfgProvider(out IVsProjectCfgProvider p) { p = null; IVsCfgProvider cfgProvider = null; this.project.GetCfgProvider(out cfgProvider); if (cfgProvider != null) { p = cfgProvider as IVsProjectCfgProvider; } return (null == p) ? VSConstants.E_NOTIMPL : VSConstants.S_OK; } public virtual int get_RootURL(out string root) { root = null; return VSConstants.S_OK; } public virtual int get_TargetCodePage(out uint target) { target = (uint)System.Text.Encoding.Default.CodePage; return VSConstants.S_OK; } public virtual int get_UpdateSequenceNumber(ULARGE_INTEGER[] li) { VsUtilities.ArgumentNotNull("li", li); li[0] = new ULARGE_INTEGER(); li[0].QuadPart = 0; return VSConstants.S_OK; } public virtual int OpenOutput(string name, out IVsOutput output) { output = null; return VSConstants.E_NOTIMPL; } #endregion #region IVsProjectCfg2 Members public virtual int OpenOutputGroup(string szCanonicalName, out IVsOutputGroup ppIVsOutputGroup) { ppIVsOutputGroup = null; // Search through our list of groups to find the one they are looking forgroupName foreach (OutputGroup group in OutputGroups) { string groupName; group.get_CanonicalName(out groupName); if (String.Compare(groupName, szCanonicalName, StringComparison.OrdinalIgnoreCase) == 0) { ppIVsOutputGroup = group; break; } } return (ppIVsOutputGroup != null) ? VSConstants.S_OK : VSConstants.E_FAIL; } public virtual int OutputsRequireAppRoot(out int pfRequiresAppRoot) { pfRequiresAppRoot = 0; return VSConstants.E_NOTIMPL; } public virtual int get_CfgType(ref Guid iidCfg, out IntPtr ppCfg) { // Delegate to the flavored configuration (to enable a flavor to take control) // Since we can be asked for Configuration we don't support, avoid throwing and return the HRESULT directly int hr = flavoredCfg.get_CfgType(ref iidCfg, out ppCfg); return hr; } public virtual int get_IsPrivate(out int pfPrivate) { pfPrivate = 0; return VSConstants.S_OK; } public virtual int get_OutputGroups(uint celt, IVsOutputGroup[] rgpcfg, uint[] pcActual) { // Are they only asking for the number of groups? if (celt == 0) { if ((null == pcActual) || (0 == pcActual.Length)) { throw new ArgumentNullException("pcActual"); } pcActual[0] = (uint)OutputGroups.Count; return VSConstants.S_OK; } // Check that the array of output groups is not null if ((null == rgpcfg) || (rgpcfg.Length == 0)) { throw new ArgumentNullException("rgpcfg"); } // Fill the array with our output groups uint count = 0; foreach (OutputGroup group in OutputGroups) { if (rgpcfg.Length > count && celt > count && group != null) { rgpcfg[count] = group; ++count; } } if (pcActual != null && pcActual.Length > 0) pcActual[0] = count; // If the number asked for does not match the number returned, return S_FALSE return (count == celt) ? VSConstants.S_OK : VSConstants.S_FALSE; } public virtual int get_VirtualRoot(out string pbstrVRoot) { pbstrVRoot = null; return VSConstants.E_NOTIMPL; } #endregion #region IVsDebuggableProjectCfg methods /// /// Called by the vs shell to start debugging (managed or unmanaged). /// Override this method to support other debug engines. /// /// A flag that determines the conditions under which to start the debugger. For valid grfLaunch values, see __VSDBGLAUNCHFLAGS /// If the method succeeds, it returns S_OK. If it fails, it returns an error code public abstract int DebugLaunch(uint grfLaunch); /// /// Determines whether the debugger can be launched, given the state of the launch flags. /// /// Flags that determine the conditions under which to launch the debugger. /// For valid grfLaunch values, see __VSDBGLAUNCHFLAGS or __VSDBGLAUNCHFLAGS2. /// true if the debugger can be launched, otherwise false /// S_OK if the method succeeds, otherwise an error code public virtual int QueryDebugLaunch(uint flags, out int fCanLaunch) { string assembly = this.project.GetAssemblyName(this.ConfigName, this.PlatformName); fCanLaunch = (assembly != null && assembly.ToUpperInvariant().EndsWith(".exe", StringComparison.OrdinalIgnoreCase)) ? 1 : 0; if (fCanLaunch == 0) { string property = GetConfigurationProperty("StartProgram", true); fCanLaunch = (property != null && property.Length > 0) ? 1 : 0; } return VSConstants.S_OK; } #endregion #region IVsCfgBrowseObject /// /// Maps back to the configuration corresponding to the browse object. /// /// The IVsCfg object represented by the browse object /// If the method succeeds, it returns S_OK. If it fails, it returns an error code. public virtual int GetCfg(out IVsCfg cfg) { cfg = this; return VSConstants.S_OK; } /// /// Maps back to the hierarchy or project item object corresponding to the browse object. /// /// Reference to the hierarchy object. /// Reference to the project item. /// If the method succeeds, it returns S_OK. If it fails, it returns an error code. public virtual int GetProjectItem(out IVsHierarchy hier, out uint itemid) { VsUtilities.CheckNotNull(this.project); VsUtilities.CheckNotNull(this.project.NodeProperties); return this.project.NodeProperties.GetProjectItem(out hier, out itemid); } #endregion #region helper methods private MSBuildExecution.ProjectPropertyInstance GetMsBuildProperty(string propertyName, bool resetCache) { if (resetCache || this.currentConfig == null) { // Get properties for current configuration from project file and cache it this.project.SetConfiguration(this.ConfigName, this.PlatformName); this.project.BuildProject.ReevaluateIfNecessary(); // Create a snapshot of the evaluated project in its current state this.currentConfig = this.project.BuildProject.CreateProjectInstance(); // Restore configuration project.SetCurrentConfiguration(); } if (this.currentConfig == null) throw new Exception("Failed to retrieve properties"); // return property asked for return this.currentConfig.GetProperty(propertyName); } #endregion #region IVsProjectFlavorCfg Members /// /// This is called to let the flavored config let go /// of any reference it may still be holding to the base config /// /// int IVsProjectFlavorCfg.Close() { // This is used to release the reference the flavored config is holding // on the base config, but in our scenario these 2 are the same object // so we have nothing to do here. return VSConstants.S_OK; } /// /// Actual implementation of get_CfgType. /// When not flavored or when the flavor delegate to use /// we end up creating the requested config if we support it. /// /// IID representing the type of config object we should create /// Config object that the method created /// HRESULT int IVsProjectFlavorCfg.get_CfgType(ref Guid iidCfg, out IntPtr ppCfg) { ppCfg = IntPtr.Zero; // See if this is an interface we support if (iidCfg == typeof(IVsDebuggableProjectCfg).GUID) { ppCfg = Marshal.GetComInterfaceForObject(this, typeof(IVsDebuggableProjectCfg)); } else if (iidCfg == typeof(IVsBuildableProjectCfg).GUID) { IVsBuildableProjectCfg buildableConfig; this.get_BuildableProjectCfg(out buildableConfig); ppCfg = Marshal.GetComInterfaceForObject(buildableConfig, typeof(IVsBuildableProjectCfg)); } // If not supported if (ppCfg == IntPtr.Zero) return VSConstants.E_NOINTERFACE; return VSConstants.S_OK; } #endregion } [ComVisible(true)] [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly", MessageId = "Buildable")] public class BuildableProjectConfig : IVsBuildableProjectCfg { #region fields Config config = null; EventSinkCollection callbacks = new EventSinkCollection(); uint vsopts = 0; #endregion #region ctors public BuildableProjectConfig(Config config) { this.config = config; } #endregion #region IVsBuildableProjectCfg methods public virtual int AdviseBuildStatusCallback(IVsBuildStatusCallback callback, out uint cookie) { cookie = callbacks.Add(callback); return VSConstants.S_OK; } public virtual int get_ProjectCfg(out IVsProjectCfg p) { p = config; return VSConstants.S_OK; } public virtual int QueryStartBuild(uint options, int[] supported, int[] ready) { if (supported != null && supported.Length > 0) supported[0] = 1; if (ready != null && ready.Length > 0) ready[0] = (this.config.ProjectMgr.BuildInProgress) ? 0 : 1; return VSConstants.S_OK; } public virtual int QueryStartClean(uint options, int[] supported, int[] ready) { //config.PrepareBuild(false); if (supported != null && supported.Length > 0) supported[0] = 1; if (ready != null && ready.Length > 0) ready[0] = (this.config.ProjectMgr.BuildInProgress) ? 0 : 1; return VSConstants.S_OK; } public virtual int QueryStartUpToDateCheck(uint options, int[] supported, int[] ready) { //config.PrepareBuild(false); if (supported != null && supported.Length > 0) supported[0] = 0; // TODO: if (ready != null && ready.Length > 0) ready[0] = (this.config.ProjectMgr.BuildInProgress) ? 0 : 1; return VSConstants.S_OK; } public virtual int QueryStatus(out int done) { done = (this.config.ProjectMgr.BuildInProgress) ? 0 : 1; return VSConstants.S_OK; } public virtual int StartBuild(IVsOutputWindowPane pane, uint options) { this.vsopts = options; config.PrepareBuild(options, false); // Current version of MSBuild wish to be called in an STA uint flags = VSConstants.VS_BUILDABLEPROJECTCFGOPTS_REBUILD; // If we are not asked for a rebuild, then we build the default target (by passing null) this.Build(options, pane, ((options & flags) != 0) ? MsBuildTarget.Rebuild : null, null); return VSConstants.S_OK; } public virtual int StartClean(IVsOutputWindowPane pane, uint options) { this.vsopts = options; config.PrepareBuild(options, true); // Current version of MSBuild wish to be called in an STA this.Build(options, pane, MsBuildTarget.Clean, null); return VSConstants.S_OK; } public virtual int StartUpToDateCheck(IVsOutputWindowPane pane, uint options) { return VSConstants.E_NOTIMPL; } public virtual int Stop(int fsync) { return this.config.ProjectMgr.StopBuild(this.vsopts, fsync != 0); } public virtual int UnadviseBuildStatusCallback(uint cookie) { callbacks.RemoveAt(cookie); return VSConstants.S_OK; } public virtual int Wait(uint ms, int fTickWhenMessageQNotEmpty) { return VSConstants.E_NOTIMPL; } #endregion #region helpers [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] private bool NotifyBuildBegin() { int shouldContinue = 1; foreach (IVsBuildStatusCallback cb in callbacks) { try { ErrorHandler.ThrowOnFailure(cb.BuildBegin(ref shouldContinue)); if (shouldContinue == 0) { return false; } } catch (Exception e) { // If those who ask for status have bugs in their code it should not prevent the build/notification from happening Debug.Fail(String.Format(CultureInfo.CurrentCulture, SR.GetString(SR.BuildEventError, CultureInfo.CurrentUICulture), e.Message)); } } return true; } [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")] private void NotifyBuildEnd(MSBuildResult result, string buildTarget) { int success = ((result == MSBuildResult.Successful) ? 1 : 0); foreach (IVsBuildStatusCallback cb in callbacks) { OnNotifyBuildEnd(result, buildTarget, cb, success); } } protected virtual void OnNotifyBuildEnd(MSBuildResult result, string buildTarget, IVsBuildStatusCallback cb, int success) { try { ErrorHandler.ThrowOnFailure(cb.BuildEnd(success)); } catch (Exception e) { // If those who ask for status have bugs in their code it should not prevent the build/notification from happening Debug.Fail(String.Format(CultureInfo.CurrentCulture, SR.GetString(SR.BuildEventError, CultureInfo.CurrentUICulture), e.Message)); } finally { // We want to refresh the references if we are building with the Build or Rebuild target or if the project was opened for browsing only. bool shouldRepaintReferences = (buildTarget == null || buildTarget == MsBuildTarget.Build || buildTarget == MsBuildTarget.Rebuild); // Now repaint references if that is needed. // We hardly rely here on the fact the ResolveAssemblyReferences target has been run as part of the build. // One scenario to think at is when an assembly reference is renamed on disk thus becomming unresolvable, // but msbuild can actually resolve it. // Another one if the project was opened only for browsing and now the user chooses to build or rebuild. if (shouldRepaintReferences && (result == MSBuildResult.Successful)) { this.RefreshReferences(); } } } public void Build(uint options, IVsOutputWindowPane output, string target, IEnumerable files) { if (!this.NotifyBuildBegin()) { return; } try { config.ProjectMgr.BuildAsync(options, this.config.ConfigName, this.config.PlatformName, output, target, files, (result, buildTarget) => this.NotifyBuildEnd(result, buildTarget)); } catch (Exception e) { Trace.WriteLine("Exception : " + e.Message); ErrorHandler.ThrowOnFailure(output.OutputStringThreadSafe("Unhandled Exception:" + e.Message + "\n")); this.NotifyBuildEnd(MSBuildResult.Failed, target); throw; } finally { ErrorHandler.ThrowOnFailure(output.FlushToTaskList()); } } /// /// Refreshes references and redraws them correctly. /// protected virtual void RefreshReferences() { // Refresh the reference container node for assemblies that could be resolved. IReferenceContainer referenceContainer = this.config.ProjectMgr.GetReferenceContainer(); if (referenceContainer != null) { foreach (ReferenceNode referenceNode in referenceContainer.EnumReferences()) { referenceNode.RefreshReference(); } } } #endregion } }