From 0b5323b85463b19aa697ba3e1b0b30d5ac1d6fca Mon Sep 17 00:00:00 2001 From: "Denis Kuzmin [ GitHub/3F ]" Date: Mon, 29 Jul 2019 21:09:23 +0300 Subject: [PATCH] IXProjectEnv improvements, fixes, and other relevant changes due to vsSolutionBuildEvent: https://github.com/3F/vsSolutionBuildEvent/pull/53 https://github.com/3F/MvsSln/pull/17 ## IConfPlatform changes: +`bool IsEqualByRule(string config, string platform, bool icase = false);` Checking an config/platform by using {Rule} instance. ## IXProjectEnv changes: +`IXProject XProjectByFile(...);` Find project by full path to file. +`IEnumerable Assign(IEnumerable projects = null);` Assign an existing `Microsoft.Build.Evaluation.Project` instances for local collection. +`IXProject AddOrGet(Project project);` Adds `Microsoft.Build.Evaluation.Project` instance into IXProject collection if it does not exist. +`ProjectItemCfg ExtractItemCfg(Project project);` Prepares data from `Microsoft.Build.Evaluation.Project` instance. +`void UnloadAll(bool throwIfErr = true);` Unloads all evaluated projects at current time. +`bool Unload(IXProject xp);` Unloads specified project. +`IEnumerable ValidProjects` List of valid projects such as something except `.user` but contains FirstChild / LastChild XML node. ## ISlnResult changes: +`string SolutionFile` Full path to an solution file. ## New extension methods: ``` +TVal GetOrDefault(this IDictionary data, TKey key, TVal def) +bool IsEqual(this Project a, Project b) +string GetSolutionDir(this Project eProject) +string GetProjectRelativePath(this Project eProject) +string GetConfig(this Project eProject) +string GetPlatform(this Project eProject) +string GetSolutionExt(this Project eProject) ``` ## Other * Removed pGuid checking from `IXProjectEnv.GetOrLoadProject()` * Added `FileExt` type for work with `ProjectType` via its file only. --- MvsSln/Core/ConfigItem.cs | 29 ++- MvsSln/Core/Guids.cs | 68 ++++-- MvsSln/Core/IConfPlatform.cs | 20 +- MvsSln/Core/ISlnResult.cs | 6 + MvsSln/Core/IXProjectEnv.cs | 92 ++++++- MvsSln/Core/IsolatedEnv.cs | 40 +--- MvsSln/Core/ProjectType.cs | 1 + MvsSln/Core/SlnResult.cs | 10 + MvsSln/Core/XProjectEnv.cs | 293 ++++++++++++++++++++--- MvsSln/Exceptions/UnloadException.cs | 45 ++++ MvsSln/Extensions/CollectionExtension.cs | 19 +- MvsSln/Extensions/ProjectExtension.cs | 42 ++++ MvsSln/MvsSln.csproj | 2 + MvsSln/Types/FileExt.cs | 83 +++++++ MvsSlnTest/Core/ConfigItemTest.cs | 17 ++ 15 files changed, 665 insertions(+), 102 deletions(-) create mode 100644 MvsSln/Exceptions/UnloadException.cs create mode 100644 MvsSln/Types/FileExt.cs diff --git a/MvsSln/Core/ConfigItem.cs b/MvsSln/Core/ConfigItem.cs index f9ace82..b80ca35 100644 --- a/MvsSln/Core/ConfigItem.cs +++ b/MvsSln/Core/ConfigItem.cs @@ -45,7 +45,8 @@ public IRuleOfConfig Rule } = new RuleOfConfig(); /// - /// To use virtual `Sensitivity` method in comparing objects. + /// To use an `Sensitivity` logic when comparing {IConfPlatform} + /// together with `==` , `!=`. /// public bool SensitivityComparing { @@ -64,6 +65,10 @@ public string ConfigurationByRule get => Rule?.Configuration(Configuration); } + /// + /// {ConfigurationByRule} with optional case insensitive logic. + /// Uses {SensitivityComparing} flag. + /// public string ConfigurationByRuleICase { get => Sensitivity(ConfigurationByRule); @@ -80,11 +85,31 @@ public string PlatformByRule get => Rule?.Platform(Platform); } + /// + /// {PlatformByRule} with optional case insensitive logic. + /// Uses {SensitivityComparing} flag. + /// public string PlatformByRuleICase { get => Sensitivity(PlatformByRule); } + /// + /// Checking an config/platform by using {Rule} instance. + /// + /// Configuration name. + /// Platform name. + /// Case insensitive flag. + /// + public bool IsEqualByRule(string config, string platform, bool icase = false) + { + var cmp = icase ? StringComparison.InvariantCultureIgnoreCase + : StringComparison.InvariantCulture; + + return string.Equals(ConfigurationByRule, Rule?.Configuration(config), cmp) + && string.Equals(PlatformByRule, Rule?.Platform(platform), cmp); + } + public static bool operator ==(ConfigItem a, ConfigItem b) { return Object.ReferenceEquals(a, null) ? @@ -104,6 +129,8 @@ public override bool Equals(object obj) var b = (ConfigItem)obj; + // NOTE: {SensitivityComparing} will control an `Sensitivity` logic, + // thus we need only `...ByRuleICase` properties: return ConfigurationByRuleICase == b.ConfigurationByRuleICase && PlatformByRuleICase == b.PlatformByRuleICase; } diff --git a/MvsSln/Core/Guids.cs b/MvsSln/Core/Guids.cs index cea847e..d7137be 100644 --- a/MvsSln/Core/Guids.cs +++ b/MvsSln/Core/Guids.cs @@ -25,8 +25,9 @@ using System.Collections.Generic; using System.Linq; +using net.r_eg.MvsSln.Extensions; -// TODO: move to '.Types' namespace +// TODO: move to '.Types' namespace and split ProjectType processing into new additional type. namespace net.r_eg.MvsSln.Core { public static class Guids @@ -34,20 +35,59 @@ public static class Guids /// /// Solution Folder. /// - public const string SLN_FOLDER = "{2150E333-8FDC-42A3-9474-1A3956D46DE8}"; + public const string SLN_FOLDER = "{2150E333-8FDC-42A3-9474-1A3956D46DE8}"; - public const string PROJECT_CS = "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"; - public const string PROJECT_DB = "{C8D11400-126E-41CD-887F-60BD40844F9E}"; - public const string PROJECT_FS = "{F2A71F9B-5D33-465A-A702-920D77279786}"; - public const string PROJECT_VB = "{F184B08F-C81C-45F6-A57F-5ABD9991F28F}"; - public const string PROJECT_VC = "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}"; - public const string PROJECT_VJ = "{E6FDF86B-F3D1-11D4-8576-0002A516ECE8}"; - public const string PROJECT_WD = "{2CFEAB61-6A3B-4EB8-B523-560B4BEEF521}"; - public const string PROJECT_WEB = "{E24C65DC-7377-472B-9ABA-BC803B73C61A}"; + /// + /// .csproj + /// + public const string PROJECT_CS = "{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}"; + + /// + /// .dbproj + /// + public const string PROJECT_DB = "{C8D11400-126E-41CD-887F-60BD40844F9E}"; + + /// + /// .fsproj + /// + public const string PROJECT_FS = "{F2A71F9B-5D33-465A-A702-920D77279786}"; + + /// + /// .vbproj + /// + public const string PROJECT_VB = "{F184B08F-C81C-45F6-A57F-5ABD9991F28F}"; + + /// + /// .vcxproj + /// + public const string PROJECT_VC = "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}"; + + /// + /// .vjsproj + /// + public const string PROJECT_VJ = "{E6FDF86B-F3D1-11D4-8576-0002A516ECE8}"; + + /// + /// .wdproj + /// + public const string PROJECT_WD = "{2CFEAB61-6A3B-4EB8-B523-560B4BEEF521}"; + + /// + /// + /// + public const string PROJECT_WEB = "{E24C65DC-7377-472B-9ABA-BC803B73C61A}"; + + /// + /// .deployproj + /// public const string PROJECT_DEPLOY = "{151D2E53-A2C4-4D7D-83FE-D05416EBD58E}"; - public const string PROJECT_SF = "{A07B5EB6-E848-4116-A8D0-A826331D98C6}"; - private static Dictionary ProjectTypeGuids = new Dictionary() + /// + /// .sfproj + /// + public const string PROJECT_SF = "{A07B5EB6-E848-4116-A8D0-A826331D98C6}"; + + private static Dictionary projectTypeGuids = new Dictionary() { { ProjectType.Cs, PROJECT_CS }, { ProjectType.Db, PROJECT_DB }, @@ -70,7 +110,7 @@ public static class Guids /// public static ProjectType ProjectTypeBy(string guid) { - return ProjectTypeGuids.FirstOrDefault(p => p.Value == guid).Key; + return projectTypeGuids.FirstOrDefault(p => p.Value == guid).Key; } /// @@ -80,7 +120,7 @@ public static ProjectType ProjectTypeBy(string guid) /// public static string GuidBy(ProjectType type) { - return ProjectTypeGuids[type]; + return projectTypeGuids.GetOrDefault(type); } } } \ No newline at end of file diff --git a/MvsSln/Core/IConfPlatform.cs b/MvsSln/Core/IConfPlatform.cs index a4edfc7..21ed70d 100644 --- a/MvsSln/Core/IConfPlatform.cs +++ b/MvsSln/Core/IConfPlatform.cs @@ -33,7 +33,8 @@ public interface IConfPlatform IRuleOfConfig Rule { get; } /// - /// To use virtual `Sensitivity` method to compare objects. + /// To use an `Sensitivity` logic when comparing {IConfPlatform} + /// together with `==` , `!=`. /// bool SensitivityComparing { get; set; } @@ -41,12 +42,29 @@ public interface IConfPlatform string ConfigurationByRule { get; } + /// + /// {ConfigurationByRule} with optional case insensitive logic. + /// Uses {SensitivityComparing} flag. + /// string ConfigurationByRuleICase { get; } string Platform { get; } string PlatformByRule { get; } + /// + /// {PlatformByRule} with optional case insensitive logic. + /// Uses {SensitivityComparing} flag. + /// string PlatformByRuleICase { get; } + + /// + /// Checking an config/platform by using {Rule} instance. + /// + /// Configuration name. + /// Platform name. + /// Case insensitive flag. + /// + bool IsEqualByRule(string config, string platform, bool icase = false); } } diff --git a/MvsSln/Core/ISlnResult.cs b/MvsSln/Core/ISlnResult.cs index 25589e0..b318d49 100644 --- a/MvsSln/Core/ISlnResult.cs +++ b/MvsSln/Core/ISlnResult.cs @@ -34,6 +34,11 @@ public interface ISlnResult /// string SolutionDir { get; } + /// + /// Full path to an solution file. + /// + string SolutionFile { get; } + /// /// Processed type for result. /// @@ -81,6 +86,7 @@ public interface ISlnResult /// /// All available global properties for solution. + /// Use optional {PropertyNames} to access to popular properties. /// RoProperties Properties { get; } diff --git a/MvsSln/Core/IXProjectEnv.cs b/MvsSln/Core/IXProjectEnv.cs index 05c0f59..a5b819a 100644 --- a/MvsSln/Core/IXProjectEnv.cs +++ b/MvsSln/Core/IXProjectEnv.cs @@ -30,6 +30,7 @@ namespace net.r_eg.MvsSln.Core { + // TODO: typo, add Get+ prefix for all XProjectBy...(), ie. GetXProjectBy...() [Guid("1BED7620-25A0-4FC3-BD44-A284782CA68A")] public interface IXProjectEnv { @@ -45,15 +46,23 @@ public interface IXProjectEnv IEnumerable Projects { get; } /// - /// List of evaluated projects that filtered by Guid. + /// List of evaluated projects that was filtered by Guid. /// IEnumerable UniqueByGuidProjects { get; } /// - /// Access to GlobalProjectCollection + /// Access to global Microsoft.Build.Evaluation.ProjectCollection. + /// Only if you know what you're doing. /// ProjectCollection PrjCollection { get; } + /// + /// List of valid projects from {PrjCollection}. + /// Such as something except `.user` but contains FirstChild / LastChild XML node. + /// Only if you know what you're doing. + /// + IEnumerable ValidProjects { get; } + /// /// Find project by Guid. /// @@ -69,6 +78,31 @@ public interface IXProjectEnv /// IXProject[] XProjectsByGuid(string guid); + /// + /// Find project by full path to file. + /// + /// Full path to file. + /// Specified configuration. + /// Try to load if not found in current project collection. + /// + IXProject XProjectByFile(string file, IConfPlatform cfg, bool tryLoad = false); + + /// + /// Find or load project by full path to file. + /// + /// Full path to file. + /// Specified configuration. + /// Optional properties when loading or null. + /// + IXProject XProjectByFile(string file, IConfPlatform cfg, IDictionary props); + + /// + /// Find project by full path to file. + /// + /// Full path to file. + /// + IEnumerable XProjectsByFile(string file); + /// /// Find projects by name. /// @@ -85,25 +119,25 @@ public interface IXProjectEnv IXProject[] XProjectsByName(string name); /// - /// Get or firstly load into collection the project. - /// Use default configuration. + /// Get or load project using global collection. + /// Uses default configuration. /// /// Specific project. /// Project GetOrLoadProject(ProjectItem pItem); /// - /// Get or firstly load into collection the project. + /// Get or load project using global collection. /// - /// Specific project. - /// Configuration of project to load. + /// Specified project. + /// Configuration of project to load. /// - Project GetOrLoadProject(ProjectItem pItem, IConfPlatform conf); + Project GetOrLoadProject(ProjectItem pItem, IConfPlatform cfg); /// - /// Get or firstly load into collection the project. + /// Get or load project using global collection. /// - /// Specific project. + /// Specified project. /// /// Project GetOrLoadProject(ProjectItem pItem, IDictionary properties); @@ -120,7 +154,7 @@ public interface IXProjectEnv /// Load available projects via configurations. /// It will be added without unloading of previous. /// - /// Specific list or null value to load all available. + /// Specified list or null value to load all available. /// Loaded projects. IEnumerable LoadProjects(IEnumerable pItems = null); @@ -129,5 +163,41 @@ public interface IXProjectEnv /// /// Loaded projects. IEnumerable LoadMinimalProjects(); + + /// + /// Assign an existing `Microsoft.Build.Evaluation.Project` instances for local collection. + /// + /// Will use {ValidProjects} if null. + /// + IEnumerable Assign(IEnumerable projects = null); + + /// + /// Adds `Microsoft.Build.Evaluation.Project` instance into IXProject collection if it does not exist. + /// + /// + /// + IXProject AddOrGet(Project project); + + /// + /// Prepares data from `Microsoft.Build.Evaluation.Project` instance. + /// + /// + /// + ProjectItemCfg ExtractItemCfg(Project project); + + /// + /// Unloads all evaluated projects at current time. + /// Decreases `IXProjectEnv.Projects` collection. + /// + /// When true, may throw exception if some project cannot be unloaded by some reason. + void UnloadAll(bool throwIfErr = true); + + /// + /// Unloads specified project. + /// Decreases `IXProjectEnv.Projects` collection. + /// + /// + /// False if project was not unloaded by some reason. Otherwise true. + bool Unload(IXProject xp); } } \ No newline at end of file diff --git a/MvsSln/Core/IsolatedEnv.cs b/MvsSln/Core/IsolatedEnv.cs index 7fb41fc..abb7eec 100644 --- a/MvsSln/Core/IsolatedEnv.cs +++ b/MvsSln/Core/IsolatedEnv.cs @@ -25,7 +25,6 @@ using System; using System.Collections.Generic; -using net.r_eg.MvsSln.Log; namespace net.r_eg.MvsSln.Core { @@ -51,44 +50,10 @@ public IsolatedEnv(ISlnResult data, IDictionary raw = null) } - //TODO: user option along with `LoadProjects` func - protected bool Free() - { - if(Projects == null) { - return true; - } - - LSender.Send(this, $"Release loaded projects for current environment (total: {PrjCollection.LoadedProjects.Count})", Message.Level.Debug); - foreach(var xp in Projects) - { - if(xp.Project == null) { - continue; - } - - try - { - if(xp.Project.FullPath != null) { - PrjCollection.UnloadProject(xp.Project); - } - else if(xp.Project.Xml != null) { - PrjCollection.TryUnloadProject(xp.Project.Xml); - } - } - catch(Exception ex) { - LSender.Send(this, $"Project '{xp.ProjectGuid}:{xp.ProjectItem.projectConfig}' was not unloaded: '{ex.Message}'", Message.Level.Trace); - } - } - - LSender.Send(this, $"Collection now contains '{PrjCollection.LoadedProjects.Count}' loaded projects.", Message.Level.Debug); - return true; - } - #region IDisposable - // To detect redundant calls private bool disposed = false; - // To correctly implement the disposable pattern. public void Dispose() { Dispose(true); @@ -101,8 +66,9 @@ protected virtual void Dispose(bool disposing) } disposed = true; - //... - Free(); + if(Projects != null) { + UnloadAll(false); + } } #endregion diff --git a/MvsSln/Core/ProjectType.cs b/MvsSln/Core/ProjectType.cs index b8a89ea..0cab383 100644 --- a/MvsSln/Core/ProjectType.cs +++ b/MvsSln/Core/ProjectType.cs @@ -25,6 +25,7 @@ namespace net.r_eg.MvsSln.Core { + // TODO: move to '.Types' namespace public enum ProjectType { Unknown, diff --git a/MvsSln/Core/SlnResult.cs b/MvsSln/Core/SlnResult.cs index cd85720..a331651 100644 --- a/MvsSln/Core/SlnResult.cs +++ b/MvsSln/Core/SlnResult.cs @@ -24,6 +24,7 @@ */ using System.Collections.Generic; +using net.r_eg.MvsSln.Extensions; namespace net.r_eg.MvsSln.Core { @@ -38,6 +39,14 @@ public string SolutionDir set; } + /// + /// Full path to an solution file. + /// + public string SolutionFile + { + get => Properties?.GetOrDefault(PropertyNames.SLN_PATH); + } + /// /// Processed type for result. /// @@ -117,6 +126,7 @@ public IConfPlatform DefaultConfig /// /// All available global properties for solution. + /// Use optional {PropertyNames} to access to popular properties. /// public RoProperties Properties { diff --git a/MvsSln/Core/XProjectEnv.cs b/MvsSln/Core/XProjectEnv.cs index a9934ff..b8ff71d 100644 --- a/MvsSln/Core/XProjectEnv.cs +++ b/MvsSln/Core/XProjectEnv.cs @@ -32,6 +32,7 @@ using net.r_eg.MvsSln.Exceptions; using net.r_eg.MvsSln.Extensions; using net.r_eg.MvsSln.Log; +using net.r_eg.MvsSln.Types; namespace net.r_eg.MvsSln.Core { @@ -56,6 +57,8 @@ public class XProjectEnv: IXProjectEnv /// protected IDictionary rawXmlProjects; + protected List xpProjects = new List(); + /// /// Access to Solution data. /// @@ -71,12 +74,11 @@ public ISlnResult Sln /// public IEnumerable Projects { - get; - set; + get => xpProjects; } /// - /// List of evaluated projects that filtered by Guid. + /// List of evaluated projects that was filtered by Guid. /// public IEnumerable UniqueByGuidProjects { @@ -84,13 +86,26 @@ public IEnumerable UniqueByGuidProjects } /// - /// Access to GlobalProjectCollection + /// Access to global Microsoft.Build.Evaluation.ProjectCollection. + /// Only if you know what you're doing. /// public ProjectCollection PrjCollection { get => ProjectCollection.GlobalProjectCollection; } + /// + /// List of valid projects from {PrjCollection}. + /// Such as something except `.user` but contains FirstChild / LastChild XML node. + /// Only if you know what you're doing. + /// + public IEnumerable ValidProjects + { + get => PrjCollection.LoadedProjects + .Where(p => p.Xml.FirstChild != null && p.Xml.LastChild != null) + .Where(p => !p.FullPath.ToLower().EndsWith(".user", StringComparison.InvariantCultureIgnoreCase)); + } + /// /// Find project by Guid. /// @@ -99,10 +114,9 @@ public ProjectCollection PrjCollection /// public IXProject XProjectByGuid(string guid, IConfPlatform cfg) { - return Projects?.Where(p => + return Projects?.FirstOrDefault(p => Eq(p.ProjectItem.projectConfig, cfg) && (p.ProjectGuid == guid) - ) - .FirstOrDefault(); + ); } /// @@ -115,6 +129,40 @@ public IXProject[] XProjectsByGuid(string guid) return Projects?.Where(p => p.ProjectGuid == guid).ToArray(); } + /// + /// Find project by full path to file. + /// + /// Full path to file. + /// Specified configuration. + /// Try to load if not found in current project collection. + /// + public IXProject XProjectByFile(string file, IConfPlatform cfg, bool tryLoad = false) + { + return XProjectByFile(file, cfg, tryLoad, null); + } + + /// + /// Find or load project by full path to file. + /// + /// Full path to file. + /// Specified configuration. + /// Optional properties when loading or null. + /// + public IXProject XProjectByFile(string file, IConfPlatform cfg, IDictionary props) + { + return XProjectByFile(file, cfg, true, props); + } + + /// + /// Find project by full path to file. + /// + /// Full path to file. + /// + public IEnumerable XProjectsByFile(string file) + { + return Projects?.Where(p => p.ProjectFullPath == file); + } + /// /// Find projects by name. /// @@ -140,8 +188,8 @@ public IXProject[] XProjectsByName(string name) } /// - /// Get or firstly load into collection the project. - /// Use default configuration. + /// Get or load project using global collection. + /// Uses default configuration. /// /// Specific project. /// @@ -151,9 +199,9 @@ public Project GetOrLoadProject(ProjectItem pItem) } /// - /// Get or firstly load into collection the project. + /// Get or load project using global collection. /// - /// Specific project. + /// Specified project. /// Configuration of project to load. /// public Project GetOrLoadProject(ProjectItem pItem, IConfPlatform cfg) @@ -167,9 +215,9 @@ public Project GetOrLoadProject(ProjectItem pItem, IConfPlatform cfg) } /// - /// Get or firstly load into collection the project. + /// Get or load project using global collection. /// - /// Specific project. + /// Specified project. /// /// public Project GetOrLoadProject(ProjectItem pItem, IDictionary properties) @@ -182,16 +230,9 @@ public Project GetOrLoadProject(ProjectItem pItem, IDictionary p throw new ArgumentException($"Properties does not contain {PropertyNames.CONFIG} or {PropertyNames.PLATFORM} key."); } - foreach(var eProject in PrjCollection.LoadedProjects) + foreach(var eProject in ValidProjects) { - if(eProject.GetProjectGuid() != pItem.pGuid) { - continue; - } - - var eCfg = new ConfigItem(eProject.GetPropertyValue(PropertyNames.CONFIG), eProject.GetPropertyValue(PropertyNames.PLATFORM)); - var propCfg = new ConfigItem(properties[PropertyNames.CONFIG], properties[PropertyNames.PLATFORM]); - - if(eCfg == propCfg) { + if(IsEqual(eProject, pItem, properties[PropertyNames.CONFIG], properties[PropertyNames.PLATFORM])) { //LSender.Send(this, $"Found project from collection {pItem.pGuid}: {propCfg} == {eCfg}", Message.Level.Trace); return eProject; } @@ -217,22 +258,23 @@ public IDictionary GetProjectProperties(ProjectItem pItem, IDict throw new ArgumentException("Solution Configuration or Platform is not defined in used properties."); } - var cfg = Sln - .ProjectConfigs - .FirstOrDefault(c => - c.PGuid == pItem.pGuid - && c.Sln.Configuration == slnProps[PropertyNames.CONFIG] - && c.Sln.Platform == slnProps[PropertyNames.PLATFORM] - ); - - if(cfg.Configuration == null || cfg.Platform == null) + var cfg = Sln.ProjectConfigs + .FirstOrDefault(c => + c.PGuid == pItem.pGuid + && c.Sln.IsEqualByRule + ( + slnProps[PropertyNames.CONFIG], + slnProps[PropertyNames.PLATFORM], + true + ) + ); + + if(cfg?.Configuration == null || cfg.Platform == null) { LSender.Send( this, String.Format( - "Something went wrong with project configuration `{0}|{1}` <- sln [{2}|{3}]", - cfg.Configuration, - cfg.Platform, + "Project configuration is not found <- sln [{0}|{1}]", slnProps[PropertyNames.CONFIG], slnProps[PropertyNames.PLATFORM] ), @@ -255,11 +297,17 @@ public IDictionary GetProjectProperties(ProjectItem pItem, IDict /// Load available projects via configurations. /// It will be added without unloading of previous. /// - /// Specific list or null value to load all available. + /// Specified list or null value to load all available. /// Loaded projects. public virtual IEnumerable LoadProjects(IEnumerable pItems = null) { - Projects = Load(pItems ?? Sln.ProjectItemsConfigs); + var loaded = Load(pItems ?? Sln.ProjectItemsConfigs); + + if(loaded == null) { + return Projects; + } + + xpProjects?.AddRange(loaded); return Projects; } @@ -277,12 +325,141 @@ public IEnumerable LoadMinimalProjects() return LoadProjects(Sln.ProjectItemsConfigs?.Where(p => p.solutionConfig == slnCfg)); } + /// + /// Assign an existing `Microsoft.Build.Evaluation.Project` instances for local collection. + /// + /// Will use {ValidProjects} if null. + /// + public IEnumerable Assign(IEnumerable projects = null) + { + return (projects ?? ValidProjects).Select(p => AddOrGet(p)).ToArray(); + } + + /// + /// Adds `Microsoft.Build.Evaluation.Project` instance into IXProject collection if it does not exist. + /// + /// + /// + public IXProject AddOrGet(Project project) + { + if(project == null) { + return null; + } + + foreach(var prj in Projects) + { + if(prj.Project.IsEqual(project)) { + return prj; + } + } + + var xp = new XProject + ( + Sln, + ExtractItemCfg(project), + project + ); + + xpProjects.Add(xp); + return xp; + } + + /// + /// Prepares data from `Microsoft.Build.Evaluation.Project` instance. + /// + /// + /// + public ProjectItemCfg ExtractItemCfg(Project project) + { + if(project == null) { + throw new ArgumentNullException(nameof(project)); + } + + foreach(ProjectItemCfg cfg in Sln.ProjectItemsConfigs) + { + if(IsEqual(project, cfg.project, project.GetConfig(), project.GetPlatform())) { + return cfg; + } + } + + var pItem = new ProjectItem + ( + project.GetProjectGuid(), + project.GetProjectName(), + FileExt.GetProjectTypeByFile(project.FullPath), + project.GetProjectRelativePath() + ); + + ConfigSln slnCfg = null; // Sln.SolutionConfigs?.FirstOrDefault() ?? Sln.DefaultConfig; + + var prjCfg = new ConfigPrj + ( + project.GetConfig(), + project.GetPlatform(), + pItem.pGuid, + false, + slnCfg + ); + + return new ProjectItemCfg(pItem, slnCfg, prjCfg); + } + + /// + /// Unloads all evaluated projects at current time. + /// Decreases `IXProjectEnv.Projects` collection. + /// + /// When true, may throw exception if some project cannot be unloaded by some reason. + public void UnloadAll(bool throwIfErr = true) + { + LSender.Send(this, $"Release all loaded projects for current environment (total: {PrjCollection.LoadedProjects.Count})", Message.Level.Debug); + + foreach(var xp in Projects) + { + if(!Unload(xp) && throwIfErr) { + throw new UnloadException($"Failed unload: '{xp.ProjectGuid}:{xp.ProjectItem.projectConfig}'", xp); + } + } + + LSender.Send(this, $"Collection now contains '{PrjCollection.LoadedProjects.Count}' loaded projects.", Message.Level.Debug); + } + + /// + /// Unloads specified project. + /// Decreases `IXProjectEnv.Projects` collection. + /// + /// + /// False if project was not unloaded by some reason. Otherwise true. + public bool Unload(IXProject xp) + { + if(xp.Project == null) { + return false; + } + + LSender.Send(this, $"Release loaded project {xp.ProjectGuid}:{xp.ProjectItem.projectConfig}", Message.Level.Debug); + try + { + if(xp.Project.FullPath != null) { + PrjCollection.UnloadProject(xp.Project); + } + else if(xp.Project.Xml != null) { + PrjCollection.TryUnloadProject(xp.Project.Xml); + } + + return true; + } + catch(Exception ex) + { + LSender.Send(this, $"Project '{xp.ProjectGuid}:{xp.ProjectItem.projectConfig}':'{xp.ProjectFullPath}' was not unloaded: '{ex.Message}'", Message.Level.Warn); + return false; + } + } + /// Prepared data from solution parser. /// Specified sln properties. /// Optional dictionary of raw xml projects by Guid. public XProjectEnv(ISlnResult data, IDictionary properties, IDictionary raw = null) { - Sln = data; + Sln = data ?? throw new ArgumentNullException(nameof(data)); rawXmlProjects = raw; slnProperties = DefProperties( @@ -347,6 +524,48 @@ protected IEnumerable GetUniqPrjCfgs(IEnumerable return pItems.GroupBy(p => new{ p.project.pGuid, p.projectConfig }).Select(g => g.First()); } + protected bool IsEqual(Project prj, ProjectItem pItem, string config, string platform) + { + // TODO: https://github.com/3F/vsSolutionBuildEvent/issues/40 + // https://github.com/3F/DllExport/issues/36#issuecomment-320794498 + + //if(eProject.GetProjectGuid() != pItem.pGuid) { + // continue; + //} + + if(prj.FullPath != pItem.fullPath) { + return false; + } + + return prj.GetConfig() == config + && prj.GetPlatform() == platform; + } + + protected IXProject XProjectByFile(string file, IConfPlatform cfg, bool tryLoad, IDictionary props) + { + var found = Projects?.FirstOrDefault(p => + Eq(p.ProjectItem.projectConfig, cfg) && (p.ProjectFullPath == file) + ); + + if(found != null || !tryLoad || string.IsNullOrWhiteSpace(file)) { + return found; + } + + var prj = GetOrLoadProject + ( + new ProjectItem() + { + fullPath = file, + path = Sln?.SolutionDir.MakeRelativePath(file), + pGuid = props.GetOrDefault(PropertyNames.PRJ_GUID), + EpType = FileExt.GetProjectTypeByFile(file), + }, + DefProperties(cfg, slnProperties.AddOrUpdate(props)) + ); + + return AddOrGet(prj); + } + /// /// Defines required properties for project via IConfPlatform. /// diff --git a/MvsSln/Exceptions/UnloadException.cs b/MvsSln/Exceptions/UnloadException.cs new file mode 100644 index 0000000..efa34c7 --- /dev/null +++ b/MvsSln/Exceptions/UnloadException.cs @@ -0,0 +1,45 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2019 Denis Kuzmin < entry.reg@gmail.com > GitHub/3F + * Copyright (c) MvsSln contributors: https://github.com/3F/MvsSln/graphs/contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. +*/ + +using System; + +namespace net.r_eg.MvsSln.Exceptions +{ + [Serializable] + public class UnloadException: CommonException + { + public T UnloadedInstance + { + get; + protected set; + } + + public UnloadException(string message, T instance) + : base(message) + { + UnloadedInstance = instance; + } + } +} \ No newline at end of file diff --git a/MvsSln/Extensions/CollectionExtension.cs b/MvsSln/Extensions/CollectionExtension.cs index a9c6127..34c7e3e 100644 --- a/MvsSln/Extensions/CollectionExtension.cs +++ b/MvsSln/Extensions/CollectionExtension.cs @@ -58,7 +58,7 @@ public static void ForEach(this IEnumerable items, Action act) /// Updated source. public static IDictionary AddOrUpdate(this IDictionary source, IDictionary items) { - if(items == null) { + if(source == null || items == null) { return source; } @@ -67,5 +67,22 @@ public static IDictionary AddOrUpdate(this IDictionary + /// Returns either value from dictionary or configured default value. + /// + /// + /// + /// + /// + /// Use this if key is not found. + /// + public static TVal GetOrDefault(this IDictionary data, TKey key, TVal def = default(TVal)) + { + if(data == null) { + return def; + } + return data.ContainsKey(key) ? data[key] : def; + } } } \ No newline at end of file diff --git a/MvsSln/Extensions/ProjectExtension.cs b/MvsSln/Extensions/ProjectExtension.cs index 7342337..026b942 100644 --- a/MvsSln/Extensions/ProjectExtension.cs +++ b/MvsSln/Extensions/ProjectExtension.cs @@ -29,6 +29,47 @@ namespace net.r_eg.MvsSln.Extensions { public static class ProjectExtension { + public static bool IsEqual(this Project a, Project b) + { + if(a == null || b == null) { + return a == b; + } + + return a.FullPath == b.FullPath + && a.GetConfig() == b.GetConfig() + && a.GetPlatform() == b.GetPlatform() + && a.GetProjectGuid() == b.GetProjectGuid() + && a.GetProjectName() == b.GetProjectName(); + } + + public static string GetSolutionDir(this Project eProject) + { + return eProject?.GetPropertyValue(PropertyNames.SLN_DIR); + } + + public static string GetProjectRelativePath(this Project eProject) + { + if(eProject == null) { + return null; + } + return eProject.GetSolutionDir().MakeRelativePath(eProject.FullPath); + } + + public static string GetConfig(this Project eProject) + { + return eProject?.GetPropertyValue(PropertyNames.CONFIG); + } + + public static string GetPlatform(this Project eProject) + { + return eProject?.GetPropertyValue(PropertyNames.PLATFORM); + } + + public static string GetSolutionExt(this Project eProject) + { + return eProject?.GetPropertyValue(PropertyNames.SLN_EXT); + } + public static string GetProjectGuid(this Project eProject) { return eProject?.GetPropertyValue(PropertyNames.PRJ_GUID); @@ -36,6 +77,7 @@ public static string GetProjectGuid(this Project eProject) public static string GetProjectName(this Project eProject) { + //NOTE: this property can also define an unified project name between various .sln files (_2010.sln, _2017.sln) return eProject?.GetPropertyValue(PropertyNames.PRJ_NAME); } } diff --git a/MvsSln/MvsSln.csproj b/MvsSln/MvsSln.csproj index 2adeadb..4494d25 100644 --- a/MvsSln/MvsSln.csproj +++ b/MvsSln/MvsSln.csproj @@ -94,6 +94,7 @@ + @@ -138,6 +139,7 @@ + diff --git a/MvsSln/Types/FileExt.cs b/MvsSln/Types/FileExt.cs new file mode 100644 index 0000000..b4e2b2b --- /dev/null +++ b/MvsSln/Types/FileExt.cs @@ -0,0 +1,83 @@ +/* + * The MIT License (MIT) + * + * Copyright (c) 2013-2019 Denis Kuzmin < entry.reg@gmail.com > GitHub/3F + * Copyright (c) MvsSln contributors: https://github.com/3F/MvsSln/graphs/contributors + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. +*/ + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using net.r_eg.MvsSln.Core; +using net.r_eg.MvsSln.Extensions; + +namespace net.r_eg.MvsSln.Types +{ + public static class FileExt + { + private static Dictionary projectTypeFileExt = new Dictionary() + { + { ProjectType.Cs, ".csproj" }, + { ProjectType.Db, ".dbproj" }, + { ProjectType.Fs, ".fsproj" }, + { ProjectType.Vb, ".vbproj" }, + { ProjectType.Vc, ".vcxproj" }, + { ProjectType.Vj, ".vjsproj" }, + { ProjectType.Wd, ".wdproj" }, + { ProjectType.Deploy, ".deployproj" }, + { ProjectType.Sf, ".sfproj" }, + { ProjectType.Unknown, null } + }; + + /// + /// Evaluate project type via its file. + /// + /// File name with extension. + /// + public static ProjectType GetProjectTypeByFile(string file) + { + if(string.IsNullOrWhiteSpace(file)) { + return ProjectType.Unknown; + } + return GetProjectTypeByExt(Path.GetExtension(file)); + } + + /// + /// Evaluate project type via its file extension. + /// + /// File extension. + /// + public static ProjectType GetProjectTypeByExt(string ext) + { + return projectTypeFileExt.FirstOrDefault(p => p.Value == ext).Key; + } + + /// + /// Evaluate file extension via ProjectType enum. + /// + /// + /// + public static string GetFileExtensionBy(ProjectType type) + { + return projectTypeFileExt.GetOrDefault(type); + } + } +} \ No newline at end of file diff --git a/MvsSlnTest/Core/ConfigItemTest.cs b/MvsSlnTest/Core/ConfigItemTest.cs index bdeef2c..977adf6 100644 --- a/MvsSlnTest/Core/ConfigItemTest.cs +++ b/MvsSlnTest/Core/ConfigItemTest.cs @@ -65,5 +65,22 @@ public void EqRuleTest2() Assert.AreNotEqual(new ConfigItem("Debug", "Platform1") { SensitivityComparing = false }, new ConfigItem("deBUG", "platFORM1") { SensitivityComparing = false }); Assert.AreNotEqual(new ConfigItem("Debug", "Platform1") { SensitivityComparing = false }, new ConfigItem("deBUG", "platFORM1") { SensitivityComparing = true }); } + + [TestMethod] + public void IsEqualByRuleTest1() + { + var target = new ConfigItem("Debug", "Any CPU"); + + Assert.AreEqual(true, target.IsEqualByRule("Debug", "Any CPU", false)); + Assert.AreEqual(false, target.IsEqualByRule("debug", "Any CPU", false)); + + Assert.AreEqual(true, target.IsEqualByRule("Debug", "AnyCPU", false)); + Assert.AreEqual(false, target.IsEqualByRule("Debug", "Anycpu", false)); + + Assert.AreEqual(true, target.IsEqualByRule("Debug", "Anycpu", true)); + + Assert.AreEqual(false, target.IsEqualByRule(null, null, false)); + Assert.AreEqual(false, target.IsEqualByRule(null, null, true)); + } } }