diff options
author | Lluis Sanchez <lluis@xamarin.com> | 2014-01-20 19:32:53 +0400 |
---|---|---|
committer | Lluis Sanchez Gual <lluis@xamarin.com> | 2014-10-21 18:39:14 +0400 |
commit | f73b2a9990495587d514aa1856919c21170a3940 (patch) | |
tree | 9842411ee8644ead5915a715ee7e53984a601df4 /main/src/core/MonoDevelop.Core/MonoDevelop.Projects/Project.cs | |
parent | a2046b6331a271e8203df7d3d0f14500d33d60fa (diff) |
Initial drop of the new project model
Diffstat (limited to 'main/src/core/MonoDevelop.Core/MonoDevelop.Projects/Project.cs')
-rw-r--r-- | main/src/core/MonoDevelop.Core/MonoDevelop.Projects/Project.cs | 1548 |
1 files changed, 1484 insertions, 64 deletions
diff --git a/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/Project.cs b/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/Project.cs index b3bf2233e3..ce145e9c78 100644 --- a/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/Project.cs +++ b/main/src/core/MonoDevelop.Core/MonoDevelop.Projects/Project.cs @@ -37,6 +37,14 @@ using MonoDevelop; using MonoDevelop.Core; using MonoDevelop.Core.Serialization; using MonoDevelop.Projects; +using System.Threading.Tasks; +using MonoDevelop.Projects.Formats.MSBuild; +using System.Xml; +using MonoDevelop.Core.Instrumentation; +using MonoDevelop.Core.Assemblies; +using MonoDevelop.Projects.Extensions; +using System.Threading; +using Mono.Addins; namespace MonoDevelop.Projects @@ -48,27 +56,200 @@ namespace MonoDevelop.Projects /// This is the base class for MonoDevelop projects. A project is a solution item which has a list of /// source code files and which can be built to generate an output. /// </remarks> - [DataInclude(typeof(ProjectFile))] - [ProjectModelDataItem(FallbackType = typeof(UnknownProject))] - public abstract class Project : SolutionEntityItem + public class Project : SolutionItem { + string[] flavorGuids = new string[0]; string[] buildActions; + MSBuildProject sourceProject; + + string productVersion; + string schemaVersion; + bool modifiedInMemory; + + List<string> defaultImports = new List<string> (); protected Project () { FileService.FileChanged += OnFileChanged; + Runtime.SystemAssemblyService.DefaultRuntimeChanged += OnDefaultRuntimeChanged; files = new ProjectFileCollection (); Items.Bind (files); DependencyResolutionEnabled = true; } - + + protected Project (params string[] flavorGuids): this() + { + this.flavorGuids = flavorGuids; + } + + protected Project (string[] flavorIds, MSBuildProject sourceProject): this(flavorIds) + { + } + + protected Project (ProjectCreateInformation projectCreateInfo, XmlElement projectOptions): this() + { + var ids = projectOptions != null ? projectOptions.GetAttribute ("flavorIds") : null; + if (!string.IsNullOrEmpty (ids)) { + this.flavorGuids = ids.Split (new [] {';'}, StringSplitOptions.RemoveEmptyEntries); + } + } + + internal class CreationContext + { + static object theLock = new object (); + public MSBuildProject Project { get; set; } + public string TypeGuid { get; set; } + + internal static void LockContext (MSBuildProject p, string typeGuid) + { + Monitor.Enter (theLock); + Current = new CreationContext (); + Current.Project = p; + Current.TypeGuid = typeGuid; + } + + internal static void UnlockContext () + { + Current = null; + Monitor.Exit (theLock); + } + + public static CreationContext Current { get; private set; } + } + + protected override void OnInitialize () + { + base.OnInitialize (); + + if (CreationContext.Current != null) { + + if (IsExtensionChainCreated) + throw new InvalidOperationException ("Extension chain already created for this object"); + + TypeGuid = CreationContext.Current.TypeGuid; + this.sourceProject = CreationContext.Current.Project; + + IMSBuildPropertySet globalGroup = sourceProject.GetGlobalPropertyGroup (); + string projectTypeGuids = globalGroup.GetValue ("ProjectTypeGuids"); + + if (projectTypeGuids != null) { + var subtypeGuids = new List<string> (); + foreach (string guid in projectTypeGuids.Split (';')) { + string sguid = guid.Trim (); + if (sguid.Length > 0 && string.Compare (sguid, CreationContext.Current.TypeGuid, StringComparison.OrdinalIgnoreCase) != 0) + subtypeGuids.Add (guid); + } + flavorGuids = subtypeGuids.ToArray (); + } + } + } + + protected override void OnExtensionChainInitialized () + { + base.OnExtensionChainInitialized (); + if (CreationContext.Current != null) + FileName = CreationContext.Current.Project.FileName; + } + + void OnDefaultRuntimeChanged (object o, EventArgs args) + { + // If the default runtime changes, the project builder for this project may change + // so it has to be created again. + CleanupProjectBuilder (); + } + + public IEnumerable<string> FlavorGuids { + get { return flavorGuids; } + } + + public List<string> DefaultImports { + get { return defaultImports; } + } + + public string ToolsVersion { get; private set; } + + internal bool CheckAllFlavorsSupported () + { + return FlavorGuids.All (g => ProjectExtension.SupportsFlavor (g)); + } + + ProjectExtension projectExtension; + ProjectExtension ProjectExtension { + get { + if (projectExtension == null) + projectExtension = ExtensionChain.GetExtension<ProjectExtension> (); + return projectExtension; + } + } + + /// <summary>Whether to use the MSBuild engine by default.</summary> + internal bool UseMSBuildEngineByDefault { get; set; } + + /// <summary>Forces the MSBuild engine to be used.</summary> + internal bool RequireMSBuildEngine { get; set; } + + protected override void OnModified (SolutionItemModifiedEventArgs args) + { + if (!Loading) + modifiedInMemory = true; + base.OnModified (args); + } + + protected override Task OnLoad (ProgressMonitor monitor) + { + MSBuildProject p = sourceProject; + sourceProject = null; + + return Task.Factory.StartNew (delegate { + if (p == null) + p = MSBuildProject.LoadAsync (FileName).Result; + IMSBuildPropertySet globalGroup = p.GetGlobalPropertyGroup (); + // Avoid crash if there is not global group + if (globalGroup == null) + p.AddNewPropertyGroup (false); + + try { + ProjectExtensionUtil.BeginLoadOperation (); + ReadProject (monitor, p); + } finally { + ProjectExtensionUtil.EndLoadOperation (); + } + }); + } + + internal protected override Task OnSave (ProgressMonitor monitor) + { + modifiedInMemory = false; + + return Task.Factory.StartNew (delegate { + var msproject = WriteProject (monitor); + if (msproject == null) + return; + + // Don't save the file to disk if the content did not change + msproject.Save (FileName); + + if (projectBuilder != null) + projectBuilder.Refresh (); + }); + } + + protected override IEnumerable<WorkspaceObjectExtension> CreateDefaultExtensions () + { + return base.CreateDefaultExtensions ().Concat (Enumerable.Repeat (new DefaultMSBuildProjectExtension (), 1)); + } + + internal protected override IEnumerable<string> GetItemTypeGuids () + { + return base.GetItemTypeGuids ().Concat (flavorGuids); + } + /// <summary> /// Description of the project. /// </summary> - [ItemProperty("Description", DefaultValue = "")] private string description = ""; public string Description { - get { return description; } + get { return description ?? ""; } set { description = value; NotifyModified ("Description"); @@ -86,7 +267,7 @@ namespace MonoDevelop.Projects /// </param> public virtual bool IsCompileable (string fileName) { - return false; + return ProjectExtension.OnGetIsCompileable (fileName); } /// <summary> @@ -97,7 +278,6 @@ namespace MonoDevelop.Projects } private ProjectFileCollection files; - [ProjectPathItemProperty ("BaseIntermediateOutputPath")] FilePath baseIntermediateOutputPath; public virtual FilePath BaseIntermediateOutputPath { @@ -129,7 +309,31 @@ namespace MonoDevelop.Projects /// <summary> /// Gets the project type and its base types. /// </summary> - public abstract IEnumerable<string> GetProjectTypes (); + public IEnumerable<string> GetProjectTypes () + { + var types = new HashSet<string> (); + ProjectExtension.OnGetProjectTypes (types); + return types; + } + + protected virtual void OnGetProjectTypes (HashSet<string> types) + { + } + + public bool HasFlavor<T> () + { + return GetService (typeof(T)) != null; + } + + public T GetFlavor<T> () where T:ProjectExtension + { + return (T) GetService (typeof(T)); + } + + internal IEnumerable<ProjectExtension> GetFlavors () + { + return ExtensionChain.GetAllExtensions ().OfType<ProjectExtension> (); + } /// <summary> /// Gets or sets the icon of the project. @@ -138,10 +342,15 @@ namespace MonoDevelop.Projects /// The stock icon. /// </value> public virtual IconId StockIcon { - get { return stockIcon; } + get { + if (stockIcon != null) + return stockIcon.Value; + else + return ProjectExtension.StockIcon; + } set { this.stockIcon = value; NotifyModified ("StockIcon"); } } - IconId stockIcon = "md-project"; + IconId? stockIcon; /// <summary> /// List of languages that this project supports @@ -150,7 +359,7 @@ namespace MonoDevelop.Projects /// The identifiers of the supported languages. /// </value> public virtual string[] SupportedLanguages { - get { return new String[] { "" }; } + get { return ProjectExtension.SupportedLanguages; } } /// <summary> @@ -164,9 +373,60 @@ namespace MonoDevelop.Projects /// </param> public virtual string GetDefaultBuildAction (string fileName) { - return IsCompileable (fileName) ? BuildAction.Compile : BuildAction.None; + return ProjectExtension.OnGetDefaultBuildAction (fileName); } - + + public string GetDefaultResourceId (ProjectFile projectFile) + { + return ProjectExtension.OnGetDefaultResourceId (projectFile); + } + + protected virtual string OnGetDefaultResourceId (ProjectFile projectFile) + { + return MSBuildResourceHandler.Instance.GetDefaultResourceId (projectFile); + } + + internal ProjectItem CreateProjectItem (IMSBuildItemEvaluated item) + { + return ProjectExtension.OnCreateProjectItem (item); + } + + protected virtual ProjectItem OnCreateProjectItem (IMSBuildItemEvaluated item) + { + if (item.Name == "Folder") + return new ProjectFile (); + + // Unknown item. Must be a file. + if (!string.IsNullOrEmpty (item.Include) && !UnsupportedItems.Contains (item.Name) && IsValidFile (item.Include)) + return new ProjectFile (); + + return new UnknownProjectItem (item.Name, item.Include); + } + + bool IsValidFile (string path) + { + // If it is an absolute uri, it's not a valid file + try { + if (Uri.IsWellFormedUriString (path, UriKind.Absolute)) { + var f = new Uri (path); + return f.Scheme == "file"; + } + } catch { + // Old mono versions may crash in IsWellFormedUriString if the path + // is not an uri. + } + return true; + } + + // Items generated by VS but which MD is not using and should be ignored + + internal static readonly IList<string> UnsupportedItems = new string[] { + "BootstrapperFile", "AppDesigner", "WebReferences", "WebReferenceUrl", "Service", + "ProjectReference", "Reference", // Reference elements are included here because they are special-cased for DotNetProject, and they are unsupported in other types of projects + "InternalsVisibleTo", + "InternalsVisibleToTest" + }; + /// <summary> /// Gets a project file. /// </summary> @@ -255,7 +515,7 @@ namespace MonoDevelop.Projects /// </summary> protected virtual IEnumerable<string> GetStandardBuildActions () { - return BuildAction.StandardActions; + return ProjectExtension.OnGetStandardBuildActions (); } /// <summary> @@ -263,25 +523,244 @@ namespace MonoDevelop.Projects /// </summary> protected virtual IList<string> GetCommonBuildActions () { - return BuildAction.StandardActions; + return ProjectExtension.OnGetCommonBuildActions (); } - public static Project LoadProject (string filename, IProgressMonitor monitor) + public override void Dispose () { - Project prj = Services.ProjectService.ReadSolutionItem (monitor, filename) as Project; - if (prj == null) - throw new InvalidOperationException ("Invalid project file: " + filename); + FileService.FileChanged -= OnFileChanged; + Runtime.SystemAssemblyService.DefaultRuntimeChanged -= OnDefaultRuntimeChanged; + CleanupProjectBuilder (); + base.Dispose (); + } - return prj; + /// <summary> + /// Runs a build or execution target. + /// </summary> + /// <returns> + /// The result of the operation + /// </returns> + /// <param name='monitor'> + /// A progress monitor + /// </param> + /// <param name='target'> + /// Name of the target + /// </param> + /// <param name='configuration'> + /// Configuration to use to run the target + /// </param> + public Task<BuildResult> RunTarget (ProgressMonitor monitor, string target, ConfigurationSelector configuration) + { + return ProjectExtension.OnRunTarget (monitor, target, configuration); } + public bool SupportsTarget (string target) + { + return !IsUnsupportedProject && ProjectExtension.OnGetSupportsTarget (target); + } - public override void Dispose () + protected virtual bool OnGetSupportsTarget (string target) { - FileService.FileChanged -= OnFileChanged; - base.Dispose (); + return target == "Build" || target == "Clean"; } - + + /// <summary> + /// Runs a build or execution target. + /// </summary> + /// <returns> + /// The result of the operation + /// </returns> + /// <param name='monitor'> + /// A progress monitor + /// </param> + /// <param name='target'> + /// Name of the target + /// </param> + /// <param name='configuration'> + /// Configuration to use to run the target + /// </param> + /// <remarks> + /// Subclasses can override this method to provide a custom implementation of project operations such as + /// build or clean. The default implementation delegates the execution to the more specific OnBuild + /// and OnClean methods, or to the item handler for other targets. + /// </remarks> + internal async protected virtual Task<BuildResult> OnRunTarget (ProgressMonitor monitor, string target, ConfigurationSelector configuration) + { + if (target == ProjectService.BuildTarget) + return await RunBuildTarget (monitor, configuration); + else if (target == ProjectService.CleanTarget) + return await RunCleanTarget (monitor, configuration); + return await RunMSBuildTarget (monitor, target, configuration) ?? new BuildResult (); + } + + + async Task<BuildResult> DoRunTarget (ProgressMonitor monitor, string target, ConfigurationSelector configuration) + { + if (target == ProjectService.BuildTarget) { + SolutionItemConfiguration conf = GetConfiguration (configuration); + if (conf != null && conf.CustomCommands.HasCommands (CustomCommandType.Build)) { + if (!await conf.CustomCommands.ExecuteCommand (monitor, this, CustomCommandType.Build, configuration)) { + var r = new BuildResult (); + r.AddError (GettextCatalog.GetString ("Custom command execution failed")); + return r; + } + return BuildResult.Success; + } + } else if (target == ProjectService.CleanTarget) { + SolutionItemConfiguration config = GetConfiguration (configuration); + if (config != null && config.CustomCommands.HasCommands (CustomCommandType.Clean)) { + if (!await config.CustomCommands.ExecuteCommand (monitor, this, CustomCommandType.Clean, configuration)) { + var r = new BuildResult (); + r.AddError (GettextCatalog.GetString ("Custom command execution failed")); + return r; + } + return BuildResult.Success; + } + } + return await OnRunTarget (monitor, target, configuration); + } + + async Task<BuildResult> RunMSBuildTarget (ProgressMonitor monitor, string target, ConfigurationSelector configuration) + { + if (CheckUseMSBuildEngine (configuration)) { + LogWriter logWriter = new LogWriter (monitor.Log); + RemoteProjectBuilder builder = GetProjectBuilder (); + var configs = GetConfigurations (configuration); + + MSBuildResult result = null; + await Task.Factory.StartNew (delegate { + result = builder.Run (configs, logWriter, MSBuildProjectService.DefaultMSBuildVerbosity, new[] { target }, null, null); + System.Runtime.Remoting.RemotingServices.Disconnect (logWriter); + }); + + var br = new BuildResult (); + foreach (var err in result.Errors) { + FilePath file = null; + if (err.File != null) + file = Path.Combine (Path.GetDirectoryName (err.ProjectFile), err.File); + + if (err.IsWarning) + br.AddWarning (file, err.LineNumber, err.ColumnNumber, err.Code, err.Message); + else + br.AddError (file, err.LineNumber, err.ColumnNumber, err.Code, err.Message); + } + return br; + } + else { + CleanupProjectBuilder (); + if (this is DotNetProject) { + var handler = new MonoDevelop.Projects.Formats.MD1.MD1DotNetProjectHandler ((DotNetProject)this); + return await handler.RunTarget (monitor, target, configuration); + } + } + return null; + } + + internal ProjectConfigurationInfo[] GetConfigurations (ConfigurationSelector configuration) + { + // Returns a list of project/configuration information for the provided item and all its references + List<ProjectConfigurationInfo> configs = new List<ProjectConfigurationInfo> (); + var c = GetConfiguration (configuration); + configs.Add (new ProjectConfigurationInfo () { + ProjectFile = FileName, + Configuration = c.Name, + Platform = GetExplicitPlatform (c) + }); + foreach (var refProject in GetReferencedItems (configuration).OfType<Project> ()) { + var refConfig = refProject.GetConfiguration (configuration); + if (refConfig != null) { + configs.Add (new ProjectConfigurationInfo () { + ProjectFile = refProject.FileName, + Configuration = refConfig.Name, + Platform = GetExplicitPlatform (refConfig) + }); + } + } + return configs.ToArray (); + } + + //for some reason, MD internally handles "AnyCPU" as "", but we need to be explicit when + //passing it to the build engine + static string GetExplicitPlatform (SolutionItemConfiguration configObject) + { + if (string.IsNullOrEmpty (configObject.Platform)) { + return "AnyCPU"; + } + return configObject.Platform; + } + + #region Project builder management + + RemoteProjectBuilder projectBuilder; + string lastBuildToolsVersion; + string lastBuildRuntime; + string lastFileName; + string lastSlnFileName; + object builderLock = new object (); + + internal RemoteProjectBuilder GetProjectBuilder () + { + //FIXME: we can't really have per-project runtimes, has to be per-solution + TargetRuntime runtime = null; + var ap = this as IAssemblyProject; + runtime = ap != null ? ap.TargetRuntime : Runtime.SystemAssemblyService.CurrentRuntime; + + var sln = ParentSolution; + var slnFile = sln != null ? sln.FileName : null; + + lock (builderLock) { + if (projectBuilder == null || lastBuildToolsVersion != ToolsVersion || lastBuildRuntime != runtime.Id || lastFileName != FileName || lastSlnFileName != slnFile) { + if (projectBuilder != null) { + projectBuilder.Dispose (); + projectBuilder = null; + } + projectBuilder = MSBuildProjectService.GetProjectBuilder (runtime, ToolsVersion, FileName, slnFile); + projectBuilder.Disconnected += delegate { + CleanupProjectBuilder (); + }; + lastBuildToolsVersion = ToolsVersion; + lastBuildRuntime = runtime.Id; + lastFileName = FileName; + lastSlnFileName = slnFile; + } else if (modifiedInMemory) { + modifiedInMemory = false; + // TODO NPM +// var p = SaveProject (new NullProgressMonitor ()); +// projectBuilder.RefreshWithContent (p.SaveToString ()); + } + } + return projectBuilder; + } + + void CleanupProjectBuilder () + { + if (projectBuilder != null) { + projectBuilder.Dispose (); + projectBuilder = null; + } + } + + #endregion + + /// <summary>Whether to use the MSBuild engine for the specified item.</summary> + internal bool CheckUseMSBuildEngine (ConfigurationSelector sel, bool checkReferences = true) + { + // if the item mandates MSBuild, always use it + if (RequireMSBuildEngine) + return true; + // if the user has set the option, use the setting + if (UseMSBuildEngine.HasValue) + return UseMSBuildEngine.Value; + + // If the item type defaults to using MSBuild, only use MSBuild if its direct references also use MSBuild. + // This prevents a not-uncommon common error referencing non-MSBuild projects from MSBuild projects + // NOTE: This adds about 11ms to the load/build/etc times of the MonoDevelop solution. Doing it recursively + // adds well over a second. + return UseMSBuildEngineByDefault && ( + !checkReferences || GetReferencedItems (sel).OfType<Project>().All (i => i.CheckUseMSBuildEngine (sel, false)) + ); + } + /// <summary> /// Adds a file to the project /// </summary> @@ -394,11 +873,15 @@ namespace MonoDevelop.Projects //so in order to avoid doing them twice when using the msbuild engine, we special-case them bool UsingMSBuildEngine (ConfigurationSelector sel) { - var msbuildHandler = ItemHandler as MonoDevelop.Projects.Formats.MSBuild.MSBuildProjectHandler; - return msbuildHandler != null && msbuildHandler.UseMSBuildEngineForItem (this, sel); + return CheckUseMSBuildEngine (sel); } - protected override BuildResult OnBuild (IProgressMonitor monitor, ConfigurationSelector configuration) + protected override Task<BuildResult> OnBuild (ProgressMonitor monitor, ConfigurationSelector configuration) + { + return RunTarget (monitor, "Build", configuration); + } + + async Task<BuildResult> RunBuildTarget (ProgressMonitor monitor, ConfigurationSelector configuration) { // create output directory, if not exists ProjectConfiguration conf = GetConfiguration (configuration) as ProjectConfiguration; @@ -411,7 +894,7 @@ namespace MonoDevelop.Projects StringParserService.Properties["Project"] = Name; if (UsingMSBuildEngine (configuration)) { - return DoBuild (monitor, configuration); + return await DoBuild (monitor, configuration); } string outputDir = conf.OutputDirectory; @@ -429,7 +912,7 @@ namespace MonoDevelop.Projects monitor.Log.WriteLine ("Performing main compilation..."); - BuildResult res = DoBuild (monitor, configuration); + BuildResult res = await DoBuild (monitor, configuration); if (res != null) { string errorString = GettextCatalog.GetPluralString ("{0} error", "{0} errors", res.ErrorCount, res.ErrorCount); @@ -454,7 +937,7 @@ namespace MonoDevelop.Projects /// Copies all support files to the output directory of the given configuration. Support files /// include: assembly references with the Local Copy flag, data files with the Copy to Output option, etc. /// </remarks> - public void CopySupportFiles (IProgressMonitor monitor, ConfigurationSelector configuration) + public void CopySupportFiles (ProgressMonitor monitor, ConfigurationSelector configuration) { ProjectConfiguration config = (ProjectConfiguration) GetConfiguration (configuration); @@ -505,7 +988,7 @@ namespace MonoDevelop.Projects /// Deletes all support files from the output directory of the given configuration. Support files /// include: assembly references with the Local Copy flag, data files with the Copy to Output option, etc. /// </remarks> - public void DeleteSupportFiles (IProgressMonitor monitor, ConfigurationSelector configuration) + public async Task DeleteSupportFiles (ProgressMonitor monitor, ConfigurationSelector configuration) { ProjectConfiguration config = (ProjectConfiguration) GetConfiguration (configuration); @@ -517,7 +1000,7 @@ namespace MonoDevelop.Projects continue; try { - dest.Delete (); + await dest.DeleteAsync (); } catch (IOException ex) { monitor.ReportError (GettextCatalog.GetString ("Error deleting support file '{0}'.", dest), ex); } @@ -559,13 +1042,17 @@ namespace MonoDevelop.Projects /// </remarks> internal protected virtual void PopulateSupportFileList (FileCopySet list, ConfigurationSelector configuration) { + ProjectExtension.OnPopulateSupportFileList (list, configuration); + } + void DoPopulateSupportFileList (FileCopySet list, ConfigurationSelector configuration) + { foreach (ProjectFile pf in Files) { if (pf.CopyToOutputDirectory == FileCopyMode.None) continue; list.Add (pf.FilePath, pf.CopyToOutputDirectory == FileCopyMode.PreserveNewest, pf.ProjectVirtualPath); } } - + /// <summary> /// Gets a list of files generated when building this project /// </summary> @@ -601,6 +1088,10 @@ namespace MonoDevelop.Projects /// </remarks> internal protected virtual void PopulateOutputFileList (List<FilePath> list, ConfigurationSelector configuration) { + ProjectExtension.OnPopulateOutputFileList (list, configuration); + } + void DoPopulateOutputFileList (List<FilePath> list, ConfigurationSelector configuration) + { string file = GetOutputFileName (configuration); if (file != null) list.Add (file); @@ -622,55 +1113,64 @@ namespace MonoDevelop.Projects /// This method is invoked to build the project. Support files such as files with the Copy to Output flag will /// be copied before calling this method. /// </remarks> - protected virtual BuildResult DoBuild (IProgressMonitor monitor, ConfigurationSelector configuration) + protected async virtual Task<BuildResult> DoBuild (ProgressMonitor monitor, ConfigurationSelector configuration) { - BuildResult res = ItemHandler.RunTarget (monitor, "Build", configuration); + BuildResult res = await RunMSBuildTarget (monitor, "Build", configuration); return res ?? new BuildResult (); } - protected override void OnClean (IProgressMonitor monitor, ConfigurationSelector configuration) + protected override Task<BuildResult> OnClean (ProgressMonitor monitor, ConfigurationSelector configuration) + { + return RunTarget (monitor, "Clean", configuration); + } + + async Task<BuildResult> RunCleanTarget (ProgressMonitor monitor, ConfigurationSelector configuration) { ProjectConfiguration config = GetConfiguration (configuration) as ProjectConfiguration; if (config == null) { monitor.ReportError (GettextCatalog.GetString ("Configuration '{0}' not found in project '{1}'", configuration, Name), null); - return; + return BuildResult.Success; } if (UsingMSBuildEngine (configuration)) { - DoClean (monitor, config.Selector); - return; + return await DoClean (monitor, config.Selector); } monitor.Log.WriteLine ("Removing output files..."); - - // Delete generated files - foreach (FilePath file in GetOutputFiles (configuration)) { - if (File.Exists (file)) { - file.Delete (); - if (file.ParentDirectory.CanonicalPath != config.OutputDirectory.CanonicalPath && Directory.GetFiles (file.ParentDirectory).Length == 0) - file.ParentDirectory.Delete (); + + var filesToDelete = GetOutputFiles (configuration).ToArray (); + + await Task.Factory.StartNew (delegate { + // Delete generated files + foreach (FilePath file in filesToDelete) { + if (File.Exists (file)) { + file.Delete (); + if (file.ParentDirectory.CanonicalPath != config.OutputDirectory.CanonicalPath && Directory.GetFiles (file.ParentDirectory).Length == 0) + file.ParentDirectory.Delete (); + } } - } + }); - DeleteSupportFiles (monitor, configuration); + await DeleteSupportFiles (monitor, configuration); - DoClean (monitor, config.Selector); + var res = await DoClean (monitor, config.Selector); monitor.Log.WriteLine (GettextCatalog.GetString ("Clean complete")); + return res; } - protected virtual void DoClean (IProgressMonitor monitor, ConfigurationSelector configuration) + protected virtual Task<BuildResult> DoClean (ProgressMonitor monitor, ConfigurationSelector configuration) { - ItemHandler.RunTarget (monitor, "Clean", configuration); + return RunMSBuildTarget (monitor, "Clean", configuration); } - protected internal override void OnExecute (IProgressMonitor monitor, ExecutionContext context, ConfigurationSelector configuration) + protected async override Task OnExecute (ProgressMonitor monitor, ExecutionContext context, ConfigurationSelector configuration) { ProjectConfiguration config = GetConfiguration (configuration) as ProjectConfiguration; if (config == null) { monitor.ReportError (GettextCatalog.GetString ("Configuration '{0}' not found in project '{1}'", configuration, Name), null); return; } - DoExecute (monitor, context, configuration); + await DoExecute (monitor, context, configuration); } /// <summary> @@ -685,8 +1185,9 @@ namespace MonoDevelop.Projects /// <param name='configuration'> /// Configuration to execute. /// </param> - protected virtual void DoExecute (IProgressMonitor monitor, ExecutionContext context, ConfigurationSelector configuration) + protected virtual Task DoExecute (ProgressMonitor monitor, ExecutionContext context, ConfigurationSelector configuration) { + return new Task (delegate {}); } /// <summary> @@ -700,14 +1201,21 @@ namespace MonoDevelop.Projects /// </param> public virtual FilePath GetOutputFileName (ConfigurationSelector configuration) { - return FilePath.Null; + return ProjectExtension.OnGetOutputFileName (configuration); } - protected internal override bool OnGetNeedsBuilding (ConfigurationSelector configuration) + internal protected override bool OnGetNeedsBuilding (ConfigurationSelector configuration) { return CheckNeedsBuild (configuration); } + protected override void OnSetNeedsBuilding (ConfigurationSelector configuration) + { + var of = GetOutputFileName (configuration); + if (File.Exists (of)) + File.Delete (of); + } + /// <summary> /// Checks if the project needs to be built /// </summary> @@ -734,7 +1242,7 @@ namespace MonoDevelop.Projects } } - foreach (SolutionItem pref in GetReferencedItems (configuration)) { + foreach (SolutionFolderItem pref in GetReferencedItems (configuration)) { if (pref.GetLastBuildTime (configuration) > tim) return true; } @@ -778,16 +1286,19 @@ namespace MonoDevelop.Projects } } - protected internal override List<FilePath> OnGetItemFiles (bool includeReferencedFiles) + protected override IEnumerable<FilePath> OnGetItemFiles (bool includeReferencedFiles) { - List<FilePath> col = base.OnGetItemFiles (includeReferencedFiles); + var baseFiles = base.OnGetItemFiles (includeReferencedFiles); + if (includeReferencedFiles) { + List<FilePath> col = new List<FilePath> (); foreach (ProjectFile pf in Files) { if (pf.Subtype != Subtype.Directory) col.Add (pf.FilePath); } + baseFiles = baseFiles.Concat (col); } - return col; + return baseFiles; } protected internal override void OnItemsAdded (IEnumerable<ProjectItem> objs) @@ -908,6 +1419,775 @@ namespace MonoDevelop.Projects } } + internal void ReadProject (ProgressMonitor monitor, MSBuildProject msproject) + { + OnReadProjectHeader (monitor, msproject); + ProjectExtension.OnReadProject (monitor, msproject); + NeedsReload = false; + } + + bool newProject; + + internal MSBuildProject WriteProject (ProgressMonitor monitor) + { + MSBuildProject msproject = new MSBuildProject (); + newProject = FileName == null || !File.Exists (FileName); + if (newProject) { + if (SupportsBuild ()) + msproject.DefaultTargets = "Build"; + msproject.FileName = FileName; + } else { + msproject.Load (FileName); + } + + OnWriteProjectHeader (monitor, msproject); + ProjectExtension.OnWriteProject (monitor, msproject); + + return msproject; + } + + class ConfigData + { + public ConfigData (string conf, string plt, IMSBuildPropertySet grp) + { + Config = conf; + Platform = plt; + Group = grp; + } + + public bool FullySpecified { + get { return Config != Unspecified && Platform != Unspecified; } + } + + public string Config; + public string Platform; + public IMSBuildPropertySet Group; + public bool Exists; + public bool IsNew; // The group did not exist in the original file + } + + const string Unspecified = null; + ITimeTracker timer; + + protected virtual void OnReadProjectHeader (ProgressMonitor monitor, MSBuildProject msproject) + { + timer = Counters.ReadMSBuildProject.BeginTiming (); + + ToolsVersion = msproject.ToolsVersion; + if (string.IsNullOrEmpty (ToolsVersion)) + ToolsVersion = "2.0"; + + IMSBuildPropertySet globalGroup = msproject.GetGlobalPropertyGroup (); + // Avoid crash if there is not global group + if (globalGroup == null) + globalGroup = msproject.AddNewPropertyGroup (false); + + productVersion = globalGroup.GetValue ("ProductVersion"); + schemaVersion = globalGroup.GetValue ("SchemaVersion"); + + // Get the project ID + + string itemGuid = globalGroup.GetValue ("ProjectGuid"); + if (itemGuid == null) + throw new UserException ("Project file doesn't have a valid ProjectGuid"); + + // Workaround for a VS issue. VS doesn't include the curly braces in the ProjectGuid + // of shared projects. + if (!itemGuid.StartsWith ("{", StringComparison.Ordinal)) + itemGuid = "{" + itemGuid + "}"; + + ItemId = itemGuid.ToUpper (); + + // Get the project GUIDs + + string projectTypeGuids = globalGroup.GetValue ("ProjectTypeGuids"); + + var subtypeGuids = new List<string> (); + if (projectTypeGuids != null) { + foreach (string guid in projectTypeGuids.Split (';')) { + string sguid = guid.Trim (); + if (sguid.Length > 0 && string.Compare (sguid, TypeGuid, StringComparison.OrdinalIgnoreCase) != 0) + subtypeGuids.Add (guid); + } + } + flavorGuids = subtypeGuids.ToArray (); + + if (!CheckAllFlavorsSupported ()) { + var guids = new [] { TypeGuid }; + var projectInfo = MSBuildProjectService.GetUnknownProjectTypeInfo (guids.Concat (flavorGuids).ToArray (), FileName); + IsUnsupportedProject = true; + if (projectInfo != null) + UnsupportedProjectMessage = projectInfo.GetInstructions (); + } + + // Common properties + + Description = msproject.EvaluatedProperties.GetValue ("Description", ""); + baseIntermediateOutputPath = msproject.EvaluatedProperties.GetPathValue ("BaseIntermediateOutputPath", defaultValue:BaseDirectory.Combine ("obj"), relativeToProject:true); + + RemoveDuplicateItems (msproject); + } + + protected virtual void OnReadProject (ProgressMonitor monitor, MSBuildProject msproject) + { + timer.Trace ("Read project items"); + LoadProjectItems (msproject, ProjectItemFlags.None); + + timer.Trace ("Read configurations"); + + List<ConfigData> configData = GetConfigData (msproject, false); + List<ConfigData> partialConfigurations = new List<ConfigData> (); + HashSet<string> handledConfigurations = new HashSet<string> (); + var configurations = new HashSet<string> (); + var platforms = new HashSet<string> (); + + IMSBuildPropertySet globalGroup = msproject.GetGlobalPropertyGroup (); + configData.Insert (0, new ConfigData (Unspecified, Unspecified, globalGroup)); + + // Load configurations, skipping the dummy config at index 0. + for (int i = 1; i < configData.Count; i++) { + ConfigData cgrp = configData[i]; + string platform = cgrp.Platform; + string conf = cgrp.Config; + + if (platform != Unspecified) + platforms.Add (platform); + + if (conf != Unspecified) + configurations.Add (conf); + + if (conf == Unspecified || platform == Unspecified) { + // skip partial configurations for now... + partialConfigurations.Add (cgrp); + continue; + } + + string key = conf + "|" + platform; + if (handledConfigurations.Contains (key)) + continue; + + LoadConfiguration (monitor, configData, conf, platform); + + handledConfigurations.Add (key); + } + + // Now we can load any partial configurations by combining them with known configs or platforms. + if (partialConfigurations.Count > 0) { + if (platforms.Count == 0) + platforms.Add (string.Empty); // AnyCpu + + foreach (ConfigData cgrp in partialConfigurations) { + if (cgrp.Config != Unspecified && cgrp.Platform == Unspecified) { + string conf = cgrp.Config; + + foreach (var platform in platforms) { + string key = conf + "|" + platform; + + if (handledConfigurations.Contains (key)) + continue; + + LoadConfiguration (monitor, configData, conf, platform); + + handledConfigurations.Add (key); + } + } else if (cgrp.Config == Unspecified && cgrp.Platform != Unspecified) { + string platform = cgrp.Platform; + + foreach (var conf in configurations) { + string key = conf + "|" + platform; + + if (handledConfigurations.Contains (key)) + continue; + + LoadConfiguration (monitor, configData, conf, platform); + + handledConfigurations.Add (key); + } + } + } + } + + // Read extended properties + + timer.Trace ("Read extended properties"); + } + + List<ConfigData> GetConfigData (MSBuildProject msproject, bool includeGlobalGroups) + { + List<ConfigData> configData = new List<ConfigData> (); + foreach (MSBuildPropertyGroup cgrp in msproject.PropertyGroups) { + string conf, platform; + if (ParseConfigCondition (cgrp.Condition, out conf, out platform) || includeGlobalGroups) + configData.Add (new ConfigData (conf, platform, cgrp)); + } + return configData; + } + + bool ParseConfigCondition (string cond, out string config, out string platform) + { + config = platform = Unspecified; + int i = cond.IndexOf ("==", StringComparison.Ordinal); + if (i == -1) + return false; + if (cond.Substring (0, i).Trim () == "'$(Configuration)|$(Platform)'") { + if (!ExtractConfigName (cond.Substring (i + 2), out cond)) + return false; + i = cond.IndexOf ('|'); + if (i != -1) { + config = cond.Substring (0, i); + platform = cond.Substring (i+1); + } else { + // Invalid configuration + return false; + } + if (platform == "AnyCPU") + platform = string.Empty; + return true; + } + else if (cond.Substring (0, i).Trim () == "'$(Configuration)'") { + if (!ExtractConfigName (cond.Substring (i + 2), out config)) + return false; + platform = Unspecified; + return true; + } + else if (cond.Substring (0, i).Trim () == "'$(Platform)'") { + config = Unspecified; + if (!ExtractConfigName (cond.Substring (i + 2), out platform)) + return false; + if (platform == "AnyCPU") + platform = string.Empty; + return true; + } + return false; + } + + bool ExtractConfigName (string name, out string config) + { + config = name.Trim (' '); + if (config.Length <= 2) + return false; + if (config [0] != '\'' || config [config.Length - 1] != '\'') + return false; + config = config.Substring (1, config.Length - 2); + return config.IndexOf ('\'') == -1; + } + + void LoadConfiguration (ProgressMonitor monitor, List<ConfigData> configData, string conf, string platform) + { + IMSBuildPropertySet grp = GetMergedConfiguration (configData, conf, platform, null); + ProjectConfiguration config = (ProjectConfiguration) CreateConfiguration (conf); + + config.Platform = platform; + projectExtension.OnReadConfiguration (monitor, config, grp); + Configurations.Add (config); + } + + protected virtual void OnReadConfiguration (ProgressMonitor monitor, ProjectConfiguration config, IMSBuildPropertySet grp) + { + config.Read (grp, GetToolsFormat ()); + } + + void RemoveDuplicateItems (MSBuildProject msproject) + { + timer.Trace ("Checking for duplicate items"); + + var uniqueIncludes = new Dictionary<string,object> (); + var toRemove = new List<MSBuildItem> (); + foreach (MSBuildItem bi in msproject.GetAllItems ()) { + object existing; + string key = bi.Name + "<" + bi.Include; + if (!uniqueIncludes.TryGetValue (key, out existing)) { + uniqueIncludes[key] = bi; + continue; + } + var exBi = existing as MSBuildItem; + if (exBi != null) { + if (exBi.Condition != bi.Condition || exBi.Element.InnerXml != bi.Element.InnerXml) { + uniqueIncludes[key] = new List<MSBuildItem> { exBi, bi }; + } else { + toRemove.Add (bi); + } + continue; + } + + var exList = (List<MSBuildItem>)existing; + bool found = false; + foreach (var m in (exList)) { + if (m.Condition == bi.Condition && m.Element.InnerXml == bi.Element.InnerXml) { + found = true; + break; + } + } + if (!found) { + exList.Add (bi); + } else { + toRemove.Add (bi); + } + } + if (toRemove.Count == 0) + return; + + timer.Trace ("Removing duplicate items"); + + foreach (var t in toRemove) + msproject.RemoveItem (t); + } + + IMSBuildPropertySet GetMergedConfiguration (List<ConfigData> configData, string conf, string platform, IMSBuildPropertySet propGroupLimit) + { + IMSBuildPropertySet merged = null; + + foreach (ConfigData grp in configData) { + if (grp.Group == propGroupLimit) + break; + if ((grp.Config == conf || grp.Config == Unspecified || conf == Unspecified) && (grp.Platform == platform || grp.Platform == Unspecified || platform == Unspecified)) { + if (merged == null) + merged = grp.Group; + else if (merged is MSBuildPropertyGroupMerged) + ((MSBuildPropertyGroupMerged)merged).Add (grp.Group); + else { + MSBuildPropertyGroupMerged m = new MSBuildPropertyGroupMerged (merged.Project, GetToolsFormat()); + m.Add (merged); + m.Add (grp.Group); + merged = m; + } + } + } + return merged; + } + + //HACK: the solution's format is irrelevant to MSBuild projects, what matters is the ToolsVersion + // but other parts of the MD API expect a FileFormat + internal MSBuildFileFormat GetToolsFormat () + { + switch (ToolsVersion) { + case "2.0": + return new MSBuildFileFormatVS05 (); + case "3.5": + return new MSBuildFileFormatVS08 (); + case "4.0": + if (SolutionFormat != null && SolutionFormat.Id == "MSBuild10") + return SolutionFormat; + return new MSBuildFileFormatVS12 (); + case "12.0": + return new MSBuildFileFormatVS12 (); + default: + throw new Exception ("Unknown ToolsVersion '" + ToolsVersion + "'"); + } + } + + internal void LoadProjectItems (MSBuildProject msproject, ProjectItemFlags flags) + { + foreach (var buildItem in msproject.GetAllItems ()) { + ProjectItem it = ReadItem (buildItem); + if (it == null) + continue; + it.Flags = flags; + if (it is ProjectFile) { + var file = (ProjectFile)it; + if (file.Name.IndexOf ('*') > -1) { + // Thanks to IsOriginatedFromWildcard, these expanded items will not be saved back to disk. + foreach (var expandedItem in ResolveWildcardItems (file)) + Items.Add (expandedItem); + // Add to wildcard items (so it can be re-saved) instead of Items (where tools will + // try to compile and display these nonstandard items + WildcardItems.Add (it); + continue; + } + } + Items.Add (it); + } + } + + internal override void SetSolutionFormat (MSBuildFileFormat format, bool converting) + { + base.SetSolutionFormat (format, converting); + + // when converting formats, set ToolsVersion, ProductVersion, SchemaVersion to default values written by VS + // this happens on creation too + // else we leave them alone and just roundtrip them + if (converting) { + ToolsVersion = format.DefaultToolsVersion; + productVersion = format.DefaultProductVersion; + schemaVersion = format.DefaultSchemaVersion; + } + } + + internal ProjectItem ReadItem (IMSBuildItemEvaluated buildItem) + { + var item = CreateProjectItem (buildItem); + item.Read (this, buildItem); + return item; + } + + const string RecursiveDirectoryWildcard = "**"; + static readonly char[] directorySeparators = new [] { + Path.DirectorySeparatorChar, + Path.AltDirectorySeparatorChar + }; + + static string GetWildcardDirectoryName (string path) + { + int indexOfLast = path.LastIndexOfAny (directorySeparators); + if (indexOfLast < 0) + return String.Empty; + return path.Substring (0, indexOfLast); + } + + static string GetWildcardFileName (string path) + { + int indexOfLast = path.LastIndexOfAny (directorySeparators); + if (indexOfLast < 0) + return path; + if (indexOfLast == path.Length) + return String.Empty; + return path.Substring (indexOfLast + 1, path.Length - (indexOfLast + 1)); + } + + + static IEnumerable<string> ExpandWildcardFilePath (string filePath) + { + if (String.IsNullOrWhiteSpace (filePath)) + throw new ArgumentException ("Not a wildcard path"); + + string dir = GetWildcardDirectoryName (filePath); + string file = GetWildcardFileName (filePath); + + if (String.IsNullOrEmpty (dir) || String.IsNullOrEmpty (file)) + return null; + + SearchOption searchOption = SearchOption.TopDirectoryOnly; + if (dir.EndsWith (RecursiveDirectoryWildcard, StringComparison.Ordinal)) { + dir = dir.Substring (0, dir.Length - RecursiveDirectoryWildcard.Length); + searchOption = SearchOption.AllDirectories; + } + + if (!Directory.Exists (dir)) + return null; + + return Directory.GetFiles (dir, file, searchOption); + } + + static IEnumerable<ProjectFile> ResolveWildcardItems (ProjectFile wildcardFile) + { + var paths = ExpandWildcardFilePath (wildcardFile.Name); + if (paths == null) + yield break; + foreach (var resolvedFilePath in paths) { + var projectFile = (ProjectFile)wildcardFile.Clone (); + projectFile.Name = resolvedFilePath; + projectFile.Flags |= ProjectItemFlags.DontPersist; + yield return projectFile; + } + } + + struct MergedPropertyValue + { + public readonly string XmlValue; + public readonly bool PreserveExistingCase; + public readonly bool IsDefault; + + public MergedPropertyValue (string xmlValue, bool preserveExistingCase, bool isDefault) + { + this.XmlValue = xmlValue; + this.PreserveExistingCase = preserveExistingCase; + this.IsDefault = isDefault; + } + } + + protected virtual void OnWriteProjectHeader (ProgressMonitor monitor, MSBuildProject msproject) + { + IMSBuildPropertySet globalGroup = msproject.GetGlobalPropertyGroup (); + if (globalGroup == null) + globalGroup = msproject.AddNewPropertyGroup (false); + + if (Configurations.Count > 0) { + ItemConfiguration conf = Configurations.FirstOrDefault<ItemConfiguration> (c => c.Name == "Debug"); + if (conf == null) conf = Configurations [0]; + globalGroup.SetValue ("Configuration", conf.Name, condition:" '$(Configuration)' == '' "); + + string platform = conf.Platform.Length == 0 ? "AnyCPU" : conf.Platform; + globalGroup.SetValue ("Platform", platform, condition:" '$(Platform)' == '' "); + } + + if (TypeGuid == MSBuildProjectService.GenericItemGuid) { + DataType dt = MSBuildProjectService.DataContext.GetConfigurationDataType (GetType ()); + globalGroup.SetValue ("ItemType", dt.Name); + } + + globalGroup.SetValue ("ProductVersion", productVersion); + globalGroup.SetValue ("SchemaVersion", schemaVersion); + + globalGroup.SetValue ("ProjectGuid", ItemId); + + if (flavorGuids.Length > 0) { + string gg = string.Join (";", flavorGuids); + gg += ";" + TypeGuid; + globalGroup.SetValue ("ProjectTypeGuids", gg.ToUpper (), preserveExistingCase:true); + } else { + globalGroup.RemoveProperty ("ProjectTypeGuids"); + } + + // having no ToolsVersion is equivalent to 2.0, roundtrip that correctly + if (ToolsVersion != "2.0") + msproject.ToolsVersion = ToolsVersion; + else if (string.IsNullOrEmpty (msproject.ToolsVersion)) + msproject.ToolsVersion = null; + else + msproject.ToolsVersion = "2.0"; + + msproject.GetGlobalPropertyGroup ().SetValue ("Description", Description, ""); + msproject.GetGlobalPropertyGroup ().SetValue ("BaseIntermediateOutputPath", BaseIntermediateOutputPath, defaultValue:BaseDirectory.Combine ("obj"), relativeToProject:true); + } + + protected virtual void OnWriteProject (ProgressMonitor monitor, MSBuildProject msproject) + { + var toolsFormat = GetToolsFormat (); + + IMSBuildPropertySet globalGroup = msproject.GetGlobalPropertyGroup (); + + // Configurations + + if (Configurations.Count > 0) { + List<ConfigData> configData = GetConfigData (msproject, true); + + // Write configuration data, creating new property groups if necessary + + foreach (ProjectConfiguration conf in Configurations) { + ConfigData cdata = FindPropertyGroup (configData, conf); + if (cdata == null) { + MSBuildPropertyGroup pg = msproject.AddNewPropertyGroup (true); + pg.IgnoreDefaultValues = true; + pg.Condition = BuildConfigCondition (conf.Name, conf.Platform); + cdata = new ConfigData (conf.Name, conf.Platform, pg); + cdata.IsNew = true; + configData.Add (cdata); + } + ((MSBuildPropertyGroup)cdata.Group).IgnoreDefaultValues = true; + cdata.Exists = true; + ProjectExtension.OnWriteConfiguration (monitor, conf, cdata.Group); + } + + // Find the properties in all configurations that have the MergeToProject flag set + var mergeToProjectProperties = new HashSet<MergedProperty> (GetMergeToProjectProperties (configData)); + var mergeToProjectPropertyNames = new HashSet<string> (mergeToProjectProperties.Select (p => p.Name)); + var mergeToProjectPropertyValues = new Dictionary<string,MergedPropertyValue> (); + + foreach (ProjectConfiguration conf in Configurations) { + ConfigData cdata = FindPropertyGroup (configData, conf); + var propGroup = (MSBuildPropertyGroup) cdata.Group; + + IMSBuildPropertySet baseGroup = GetMergedConfiguration (configData, conf.Name, conf.Platform, propGroup); + + CollectMergetoprojectProperties (propGroup, mergeToProjectProperties, mergeToProjectPropertyValues); + + propGroup.UnMerge (baseGroup, mergeToProjectPropertyNames); + propGroup.IgnoreDefaultValues = false; + } + + // Move properties with common values from configurations to the main + // property group + foreach (KeyValuePair<string,MergedPropertyValue> prop in mergeToProjectPropertyValues) { + if (!prop.Value.IsDefault) + globalGroup.SetValue (prop.Key, prop.Value.XmlValue, preserveExistingCase: prop.Value.PreserveExistingCase); + else + globalGroup.RemoveProperty (prop.Key); + } + foreach (string prop in mergeToProjectPropertyNames) { + if (!mergeToProjectPropertyValues.ContainsKey (prop)) + globalGroup.RemoveProperty (prop); + } + foreach (SolutionItemConfiguration conf in Configurations) { + var propGroup = FindPropertyGroup (configData, conf).Group; + foreach (string mp in mergeToProjectPropertyValues.Keys) + propGroup.RemoveProperty (mp); + } + + // Remove groups corresponding to configurations that have been removed + // or groups which don't have any property and did not already exist + foreach (ConfigData cd in configData) { + if ((!cd.Exists && cd.FullySpecified) || (cd.IsNew && !cd.Group.Properties.Any ())) + msproject.RemoveGroup ((MSBuildPropertyGroup)cd.Group); + } + } + SaveProjectItems (monitor, toolsFormat, msproject); + + if (msproject.IsNewProject) { + foreach (var im in DefaultImports) + msproject.AddNewImport (im); + } + + foreach (var im in importsAdded) { + if (msproject.GetImport (im.Name, im.Condition) == null) + msproject.AddNewImport (im.Name, im.Condition); + } + foreach (var im in importsRemoved) { + var i = msproject.GetImport (im.Name, im.Condition); + if (i != null) + msproject.RemoveImport (i); + } + } + + void WriteConfiguration (ProgressMonitor monitor, List<ConfigData> configData, string conf, string platform) + { + IMSBuildPropertySet grp = GetMergedConfiguration (configData, conf, platform, null); + ProjectConfiguration config = (ProjectConfiguration) CreateConfiguration (conf); + + config.Platform = platform; + config.Read (grp, GetToolsFormat ()); + Configurations.Add (config); + projectExtension.OnReadConfiguration (monitor, config, grp); + } + + protected virtual void OnWriteConfiguration (ProgressMonitor monitor, ProjectConfiguration config, IMSBuildPropertySet pset) + { + config.Write (pset, SolutionFormat); + } + + IEnumerable<MergedProperty> GetMergeToProjectProperties (List<ConfigData> configData) + { + Dictionary<string,MergedProperty> mergeProps = new Dictionary<string, MergedProperty> (); + foreach (var cd in configData.Where (d => d.FullySpecified)) { + foreach (var prop in cd.Group.Properties) { + if (!prop.MergeToMainGroup) { + mergeProps [prop.Name] = null; + } else if (!mergeProps.ContainsKey (prop.Name)) + mergeProps [prop.Name] = prop.CreateMergedProperty (); + } + } + return mergeProps.Values.Where (p => p != null); + } + + void CollectMergetoprojectProperties (IMSBuildPropertySet pgroup, HashSet<MergedProperty> properties, Dictionary<string,MergedPropertyValue> mergeToProjectProperties) + { + // This method checks every property in pgroup which has the MergeToProject flag. + // If the value of this property is the same as the one stored in mergeToProjectProperties + // it means that the property can be merged to the main project property group (so far). + + foreach (var pinfo in new List<MergedProperty> (properties)) { + MSBuildProperty prop = pgroup.GetProperty (pinfo.Name); + + MergedPropertyValue mvalue; + if (!mergeToProjectProperties.TryGetValue (pinfo.Name, out mvalue)) { + if (prop != null) { + // This is the first time the value is checked. Just assign it. + mergeToProjectProperties.Add (pinfo.Name, new MergedPropertyValue (prop.Value, pinfo.PreserveExistingCase, pinfo.IsDefault)); + continue; + } + // If there is no value, it can't be merged + } + else if (prop != null && string.Equals (prop.Value, mvalue.XmlValue, mvalue.PreserveExistingCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)) + // Same value. It can be merged. + continue; + + // The property can't be merged because different configurations have different + // values for it. Remove it from the list. + properties.Remove (pinfo); + mergeToProjectProperties.Remove (pinfo.Name); + } + } + + struct ItemInfo { + public MSBuildItem Item; + public bool Added; + } + + internal void SaveProjectItems (ProgressMonitor monitor, MSBuildFileFormat toolsFormat, MSBuildProject msproject, string pathPrefix = null) + { + // Remove old items + Dictionary<string, ItemInfo> oldItems = new Dictionary<string, ItemInfo> (); + foreach (MSBuildItem item in msproject.GetAllItems ()) + oldItems [item.Name + "<" + item.UnevaluatedInclude + "<" + item.Condition] = new ItemInfo () { + Item = item + }; + // Add the new items + foreach (ProjectItem ob in Items.Concat (WildcardItems).Where (it => !it.Flags.HasFlag (ProjectItemFlags.DontPersist))) + SaveProjectItem (monitor, toolsFormat, msproject, ob, oldItems, pathPrefix); + foreach (ItemInfo itemInfo in oldItems.Values) { + if (!itemInfo.Added) + msproject.RemoveItem (itemInfo.Item); + } + } + + void SaveProjectItem (ProgressMonitor monitor, MSBuildFileFormat fmt, MSBuildProject msproject, ProjectItem item, Dictionary<string,ItemInfo> oldItems, string pathPrefix = null) + { + var include = item.UnevaluatedInclude ?? item.Include; + if (pathPrefix != null && !include.StartsWith (pathPrefix)) + include = pathPrefix + include; + + MSBuildItem buildItem = AddOrGetBuildItem (msproject, oldItems, item.ItemName, include, item.Condition); + item.Write (fmt, buildItem); + if (pathPrefix != null) + buildItem.Include = include; + } + + MSBuildItem AddOrGetBuildItem (MSBuildProject msproject, Dictionary<string,ItemInfo> oldItems, string name, string include, string condition) + { + ItemInfo itemInfo; + string key = name + "<" + include + "<" + condition; + if (oldItems.TryGetValue (key, out itemInfo)) { + if (!itemInfo.Added) { + itemInfo.Added = true; + oldItems [key] = itemInfo; + } + return itemInfo.Item; + } else { + return msproject.AddNewItem (name, include); + } + } + + ConfigData FindPropertyGroup (List<ConfigData> configData, SolutionItemConfiguration config) + { + foreach (ConfigData data in configData) { + if (data.Config == config.Name && data.Platform == config.Platform) + return data; + } + return null; + } + + string BuildConfigCondition (string config, string platform) + { + if (platform.Length == 0) + platform = "AnyCPU"; + return " '$(Configuration)|$(Platform)' == '" + config + "|" + platform + "' "; + } + + bool IsMergeToProjectProperty (ItemProperty prop) + { + foreach (object at in prop.CustomAttributes) { + if (at is MergeToProjectAttribute) + return true; + } + return false; + } + + public void AddImportIfMissing (string name, string condition) + { + importsAdded.Add (new DotNetProjectImport (name, condition)); + } + + public void RemoveImport (string name) + { + importsRemoved.Add (new DotNetProjectImport (name)); + } + + List <DotNetProjectImport> importsAdded = new List<DotNetProjectImport> (); + + internal IList<DotNetProjectImport> ImportsAdded { + get { return importsAdded; } + } + + List <DotNetProjectImport> importsRemoved = new List<DotNetProjectImport> (); + + internal IList<DotNetProjectImport> ImportsRemoved { + get { return importsRemoved; } + } + + void ImportsSaved () + { + importsAdded.Clear (); + importsRemoved.Clear (); + } internal void NotifyFileRenamedInProject (ProjectFileRenamedEventArgs args) { NotifyModified ("Files"); @@ -919,6 +2199,10 @@ namespace MonoDevelop.Projects /// </summary> protected virtual void OnFileRemovedFromProject (ProjectFileEventArgs e) { + ProjectExtension.OnFileRemovedFromProject (e); + } + void DoOnFileRemovedFromProject (ProjectFileEventArgs e) + { buildActions = null; if (FileRemovedFromProject != null) { FileRemovedFromProject (this, e); @@ -930,38 +2214,54 @@ namespace MonoDevelop.Projects /// </summary> protected virtual void OnFileAddedToProject (ProjectFileEventArgs e) { + ProjectExtension.OnFileAddedToProject (e); + } + void DoOnFileAddedToProject (ProjectFileEventArgs e) + { buildActions = null; if (FileAddedToProject != null) { FileAddedToProject (this, e); } } - + /// <summary> /// Raises the FileChangedInProject event. /// </summary> protected virtual void OnFileChangedInProject (ProjectFileEventArgs e) { + ProjectExtension.OnFileChangedInProject (e); + } + void DoOnFileChangedInProject (ProjectFileEventArgs e) + { if (FileChangedInProject != null) { FileChangedInProject (this, e); } } - + /// <summary> /// Raises the FilePropertyChangedInProject event. /// </summary> protected virtual void OnFilePropertyChangedInProject (ProjectFileEventArgs e) { + ProjectExtension.OnFilePropertyChangedInProject (e); + } + void DoOnFilePropertyChangedInProject (ProjectFileEventArgs e) + { buildActions = null; if (FilePropertyChangedInProject != null) { FilePropertyChangedInProject (this, e); } } - + /// <summary> /// Raises the FileRenamedInProject event. /// </summary> protected virtual void OnFileRenamedInProject (ProjectFileRenamedEventArgs e) { + ProjectExtension.OnFileRenamedInProject (e); + } + void DoOnFileRenamedInProject (ProjectFileRenamedEventArgs e) + { if (FileRenamedInProject != null) { FileRenamedInProject (this, e); } @@ -991,6 +2291,126 @@ namespace MonoDevelop.Projects /// Occurs when a file of this project has been renamed /// </summary> public event ProjectFileRenamedEventHandler FileRenamedInProject; + + + class DefaultMSBuildProjectExtension: ProjectExtension + { + internal protected override bool SupportsFlavor (string guid) + { + return false; + } + + internal protected override bool OnGetIsCompileable (string fileName) + { + return false; + } + + internal protected override void OnGetProjectTypes (HashSet<string> types) + { + Project.OnGetProjectTypes (types); + } + + internal protected override Task<BuildResult> OnRunTarget (ProgressMonitor monitor, string target, ConfigurationSelector configuration) + { + return Project.DoRunTarget (monitor, target, configuration); + } + + internal protected override bool OnGetSupportsTarget (string target) + { + return Project.OnGetSupportsTarget (target); + } + + internal protected override string OnGetDefaultBuildAction (string fileName) + { + return Project.IsCompileable (fileName) ? BuildAction.Compile : BuildAction.None; + } + + internal protected override string OnGetDefaultResourceId (ProjectFile projectFile) + { + return Project.OnGetDefaultResourceId (projectFile); + } + + internal protected override IEnumerable<string> OnGetStandardBuildActions () + { + return BuildAction.StandardActions; + } + + internal protected override IList<string> OnGetCommonBuildActions () + { + return BuildAction.StandardActions; + } + + internal protected override ProjectItem OnCreateProjectItem (IMSBuildItemEvaluated item) + { + return Project.OnCreateProjectItem (item); + } + + internal protected override void OnPopulateSupportFileList (FileCopySet list, ConfigurationSelector configuration) + { + Project.DoPopulateSupportFileList (list, configuration); + } + + internal protected override void OnPopulateOutputFileList (List<FilePath> list, ConfigurationSelector configuration) + { + Project.DoPopulateOutputFileList (list, configuration); + } + + internal protected override FilePath OnGetOutputFileName (ConfigurationSelector configuration) + { + return FilePath.Null; + } + + internal protected override string[] SupportedLanguages { + get { + return new String[] { "" }; + } + } + + internal protected override void OnFileRemovedFromProject (ProjectFileEventArgs e) + { + Project.DoOnFileRemovedFromProject (e); + } + + internal protected override void OnFileAddedToProject (ProjectFileEventArgs e) + { + Project.DoOnFileAddedToProject (e); + } + + internal protected override void OnFileChangedInProject (ProjectFileEventArgs e) + { + Project.DoOnFileChangedInProject (e); + } + + internal protected override void OnFilePropertyChangedInProject (ProjectFileEventArgs e) + { + Project.DoOnFilePropertyChangedInProject (e); + } + + internal protected override void OnFileRenamedInProject (ProjectFileRenamedEventArgs e) + { + Project.DoOnFileRenamedInProject (e); + } + + internal protected override void OnReadProject (ProgressMonitor monitor, MSBuildProject msproject) + { + Project.OnReadProject (monitor, msproject); + } + + internal protected override void OnWriteProject (ProgressMonitor monitor, MSBuildProject msproject) + { + Project.OnWriteProject (monitor, msproject); + } + + internal protected override void OnReadConfiguration (ProgressMonitor monitor, ProjectConfiguration config, IMSBuildPropertySet grp) + { + Project.OnReadConfiguration (monitor, config, grp); + } + + internal protected override void OnWriteConfiguration (ProgressMonitor monitor, ProjectConfiguration config, IMSBuildPropertySet grp) + { + Project.OnWriteConfiguration (monitor, config, grp); + } + } } public delegate void ProjectEventHandler (Object sender, ProjectEventArgs e); |