Currently I'm working on a big project where we have a lot of legacy code. The big problem was build configurations. There were around 8 or 10 different configurations, of cause DEBUG and RELEASE, but besides we had QA, Production, Integration and Staging. Problem is that we had also some garbage, some old configurations like Demo1, Demo2, Test and something else. Another problem is that some projects were configured as Any CPU other as x86. We had real mess in csproj and sln files.
To normalize everything I created small tool. Basically we need to do two things:
1. Remove all build configurations from csproj files and add there only those that we need.
2. Remove all build mappings from sln files and created them from scratch.
Let's talk about the first step. csproj-file is simple xml file. So what we need is just open xml file, remove some nodes and add another nodes. Here is the part of the code.
Let's discuss what we do here. Each csproj file contains several PropertyGroup nodes. One of them is main one or a default one. It has no conditions. Here's an example:
So we have 2 nodes here. First one is what I call main one, witout any conditions. It has improtant info like project type or target framework. The second node has a condition. And thta is very important. We actualy take main one and then, if codition is satisfied, we take additional node. Then we add or overwrite some properties. It mean tha if configuration is Debug and target platform is AnyCPU then we take additonal properties from tha section as DebugType or OutputPath. So we want to keep a main node and remove the rest. That is what does our code in lines 14-20. Then we need to if thta is a web project. We will need it later. So don't care about it right now. Thaen we have a loop through the collection of build configurations that we want to add. Inside fe have a condition.
It's a bit haky...what I actually want here is to spleet all build configurations into 2 categories: based on debug configuration and based on release configuration. The difference is not so big, we just set some properties a bit differently. We set differently DebugSymbols and Optimization. We will take a look mode deeply. Let's just take a look how we create debug based nodes and release based nodes:
The difference is not so big as you can see. As I wrote above we set a bit differently DebugType and Optimize properties. Now we have to discuss why do we need that isWeb parameter. Difference is that noraly build path cotains buildconfiguration name, like bin\Debug or bin\Release. But it's not relevant for web applications. Important thing is the xml namespace that we retriev from csproj file and then use to create the nodes. Wthout that namespace output file will look differetly, but we want to keep everything clean and keep everything as like it was generated by visual studio.
Now we should take a look what should we do with sln files. Here is the code that does all magic.
It's definitely not the best code you've ever seen, it's more like a working prototype. Later I'm going to ckean it up and publish as a console tool. But it works pretty fine and does what I need.
We should star from short intro to what sln file is. It's not an xml file. It's more or less plain text devided in couple sections. At the begining of that file there is a list of all projects linked in that solution. Bellow there are some global sections. We need only two of them. One fefines a list of all available build configurations, another one defines how to map those configurations to configurations that are defined in csproj files.
Here is an example how looks definition of available configurations in solution file:
It's really straight forward approach. Nothing interesting. It means tha we have 3 configurations available in our solution.
Second scectoins maps solution build configurations to project build configurations. We can do it as one to one mappings or as many to many. Here is an example how we can map one to one.
It means that we have 3 build configurations defined on solution level and 3 configurations defined on project level. It menas that project file should contains 3 conditional PropertyGroup nodes.
Bellow is an example how we can map many to many.
It means that we have only 2 configurations on project level: debug and release. It means that we have 2 conditional PropertyGroup nodes in csproj file. But we still have 3 build configurations on solution level. We just define that in case of QA it should treat it as it were Release configuation.
You might ask why do I need all that stuff. So in my case there are about 20 sln files and 296 csproj files.And everytime it's diferrent...some solution have more build configurations some less, sometimes it's mpped one to one, sometimes many to many, we just want to keep to same strategy everywhere. Another reason that latter we want to have only Debug and Release build configuraions instead of 8 that we have now. With code that we have seen above it's realy easy. I forget to show what mappings actualy are. So bellow is usage of CureSolution method.
In this particular case we map Debug and Integration as debug on project level, and the rest as release configurations.
Last things. I forgot to mention how I parse solution file and get projecs guid's. To do that I use nuget package Onion.SolutionParser. Anothe things is variable vsFolderGuid. Thing is that if solution contains some folder, those folder are treated also like projects with some special project type, we can filter them out by type guid. We have othing to do with solution folders when we do something with build configurations.
To normalize everything I created small tool. Basically we need to do two things:
1. Remove all build configurations from csproj files and add there only those that we need.
2. Remove all build mappings from sln files and created them from scratch.
Let's talk about the first step. csproj-file is simple xml file. So what we need is just open xml file, remove some nodes and add another nodes. Here is the part of the code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | private void CureProject(string projectPath, IEnumerable<string> buildConfigurations) { var doc = XDocument.Load(projectPath); var ns = doc.Root.GetDefaultNamespace(); var mainPropertyGroup = doc.Root.Elements() .Single( x => x.Name.LocalName == "PropertyGroup" && x.Elements().Any(sx => sx.Name.LocalName == "OutputType")); doc.Root.Elements() .Where( x => x.Name.LocalName == "PropertyGroup" && x.HasAttributes && x.Attributes().Any(atr => atr.Name.LocalName == "Condition")) .ToList() .ForEach(x => x.Remove()); var isWeb = doc.Root.ToString().Contains("WebProjectProperties"); foreach (var buildConfiguration in buildConfigurations) { if (buildConfiguration == "Debug" || buildConfiguration == "Integration") { mainPropertyGroup.AddAfterSelf(CreateDebugBasedBuildConfiguration(ns, buildConfiguration, isWeb)); } else { mainPropertyGroup.AddAfterSelf(CreateReleaseBasedBuildConfiguration(ns, buildConfiguration, isWeb)); } } doc.Save(projectPath); } |
Let's discuss what we do here. Each csproj file contains several PropertyGroup nodes. One of them is main one or a default one. It has no conditions. Here's an example:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <PropertyGroup> <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration> <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform> <ProjectGuid>{449C9CCD-35FC-4D38-932E-6243C4517090}</ProjectGuid> <OutputType>Library</OutputType> <AppDesignerFolder>Properties</AppDesignerFolder> <RootNamespace>SolutionNormalizer</RootNamespace> <AssemblyName>SolutionNormalizer</AssemblyName> <TargetFrameworkVersion>v4.5</TargetFrameworkVersion> <FileAlignment>512</FileAlignment> </PropertyGroup> <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' "> <DebugSymbols>true</DebugSymbols> <DebugType>full</DebugType> <Optimize>false</Optimize> <OutputPath>bin\Debug\</OutputPath> <DefineConstants>DEBUG;TRACE</DefineConstants> <ErrorReport>prompt</ErrorReport> <WarningLevel>4</WarningLevel> </PropertyGroup> |
So we have 2 nodes here. First one is what I call main one, witout any conditions. It has improtant info like project type or target framework. The second node has a condition. And thta is very important. We actualy take main one and then, if codition is satisfied, we take additional node. Then we add or overwrite some properties. It mean tha if configuration is Debug and target platform is AnyCPU then we take additonal properties from tha section as DebugType or OutputPath. So we want to keep a main node and remove the rest. That is what does our code in lines 14-20. Then we need to if thta is a web project. We will need it later. So don't care about it right now. Thaen we have a loop through the collection of build configurations that we want to add. Inside fe have a condition.
if (buildConfiguration == "Debug" || buildConfiguration == "Integration")
It's a bit haky...what I actually want here is to spleet all build configurations into 2 categories: based on debug configuration and based on release configuration. The difference is not so big, we just set some properties a bit differently. We set differently DebugSymbols and Optimization. We will take a look mode deeply. Let's just take a look how we create debug based nodes and release based nodes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | private XNode CreateDebugBasedBuildConfiguration(XNamespace ns, string buildConfiguration, bool isWeb) { var result = new XElement(ns + "PropertyGroup", new XAttribute("Condition", string.Format(" '$(Configuration)|$(Platform)' == '{0}|AnyCPU' ", buildConfiguration)), new XElement(ns + "DebugSymbols", "true"), new XElement(ns + "DebugType", "full"), new XElement(ns + "Optimize", "false"), new XElement(ns + "OutputPath", isWeb ? @"bin\" : string.Format("bin\\{0}\\", buildConfiguration)), new XElement(ns + "DefineConstants", "DEBUG;TRACE"), new XElement(ns + "ErrorReport", "prompt"), new XElement(ns + "WarningLevel", "4") ); return result; } private XNode CreateReleaseBasedBuildConfiguration(XNamespace ns, string buildConfiguration, bool isWeb) { var result = new XElement(ns + "PropertyGroup", new XAttribute("Condition", string.Format(" '$(Configuration)|$(Platform)' == '{0}|AnyCPU' ", buildConfiguration)), new XElement(ns + "DebugType", "pdbonly"), new XElement(ns + "Optimize", "true"), new XElement(ns + "OutputPath", isWeb ? @"bin\" : string.Format("bin\\{0}\\", buildConfiguration)), new XElement(ns + "DefineConstants", "TRACE"), new XElement(ns + "ErrorReport", "prompt"), new XElement(ns + "WarningLevel", "4") ); return result; |
The difference is not so big as you can see. As I wrote above we set a bit differently DebugType and Optimize properties. Now we have to discuss why do we need that isWeb parameter. Difference is that noraly build path cotains buildconfiguration name, like bin\Debug or bin\Release. But it's not relevant for web applications. Important thing is the xml namespace that we retriev from csproj file and then use to create the nodes. Wthout that namespace output file will look differetly, but we want to keep everything clean and keep everything as like it was generated by visual studio.
Now we should take a look what should we do with sln files. Here is the code that does all magic.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 | private void CureSolution(string solutionPath, IEnumerable<Tuple<string, string>> mappings) { var solution = SolutionParser.Parse(solutionPath); var vsFolderGuid = new Guid("2150e333-8fdc-42a3-9474-1a3956d46de8"); var projects = solution.Projects.Where(p => p.TypeGuid != vsFolderGuid).ToList(); var projectGuids = projects.Select(p => p.Guid).ToList(); var slnContent = File.ReadAllText(solutionPath); var projectConfigurationRegex = new Regex( @"GlobalSection\(ProjectConfigurationPlatforms\) = postSolution.*?EndGlobalSection", RegexOptions.Singleline); var newContent = CreateProjectConfigurationPlatforms(projectGuids, mappings); var result = projectConfigurationRegex.Replace(slnContent, newContent); newContent = CreateSolutionConfigurationPlatforms(mappings); var solutionConfigurationRegex = new Regex( @"GlobalSection\(SolutionConfigurationPlatforms\) = preSolution.*?EndGlobalSection", RegexOptions.Singleline); result = solutionConfigurationRegex.Replace(result, newContent); File.WriteAllText(solutionPath, result); } private string CreateSolutionConfigurationPlatforms(IEnumerable<Tuple<string, string>> mappings) { var sb = new StringBuilder(); sb.Append("GlobalSection(SolutionConfigurationPlatforms) = preSolution\r\n"); foreach (var mapping in mappings) { sb.AppendFormat("\t\t{0}|Any CPU = {1}|Any CPU\r\n", mapping.Item1, mapping.Item1); } sb.Append("\tEndGlobalSection"); return sb.ToString(); } private string CreateProjectConfigurationPlatforms(IEnumerable<Guid> projectGuids, IEnumerable<Tuple<string, string>> mappings) { var sb = new StringBuilder(); sb.Append("GlobalSection(ProjectConfigurationPlatforms) = postSolution\r\n"); foreach (var projectGuid in projectGuids) { foreach (var mapping in mappings) { var projectFormatedGuid = projectGuid.ToString("B"); sb.AppendFormat("\t\t{0}.{1}|Any CPU.ActiveCfg = {2}|Any CPU\r\n", projectFormatedGuid.ToUpper(), mapping.Item1, mapping.Item2); sb.AppendFormat("\t\t{0}.{1}|Any CPU.Build.0 = {2}|Any CPU\r\n", projectFormatedGuid.ToUpper(), mapping.Item1, mapping.Item2); } } sb.Append("\tEndGlobalSection"); return sb.ToString(); } |
It's definitely not the best code you've ever seen, it's more like a working prototype. Later I'm going to ckean it up and publish as a console tool. But it works pretty fine and does what I need.
We should star from short intro to what sln file is. It's not an xml file. It's more or less plain text devided in couple sections. At the begining of that file there is a list of all projects linked in that solution. Bellow there are some global sections. We need only two of them. One fefines a list of all available build configurations, another one defines how to map those configurations to configurations that are defined in csproj files.
Here is an example how looks definition of available configurations in solution file:
1 2 3 4 5 | GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU QA|Any CPU = QA|Any CPU EndGlobalSection |
It's really straight forward approach. Nothing interesting. It means tha we have 3 configurations available in our solution.
Second scectoins maps solution build configurations to project build configurations. We can do it as one to one mappings or as many to many. Here is an example how we can map one to one.
1 2 3 4 5 6 7 8 | GlobalSection(ProjectConfigurationPlatforms) = postSolution {449C9CCD-35FC-4D38-932E-6243C4517090}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {449C9CCD-35FC-4D38-932E-6243C4517090}.Debug|Any CPU.Build.0 = Debug|Any CPU {449C9CCD-35FC-4D38-932E-6243C4517090}.Release|Any CPU.ActiveCfg = Release|Any CPU {449C9CCD-35FC-4D38-932E-6243C4517090}.Release|Any CPU.Build.0 = Release|Any CPU {449C9CCD-35FC-4D38-932E-6243C4517090}.QA|Any CPU.ActiveCfg = QA|Any CPU {449C9CCD-35FC-4D38-932E-6243C4517090}.QA|Any CPU.Build.0 = QA|Any CPU EndGlobalSection |
It means that we have 3 build configurations defined on solution level and 3 configurations defined on project level. It menas that project file should contains 3 conditional PropertyGroup nodes.
Bellow is an example how we can map many to many.
1 2 3 4 5 6 7 8 | GlobalSection(ProjectConfigurationPlatforms) = postSolution {449C9CCD-35FC-4D38-932E-6243C4517090}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {449C9CCD-35FC-4D38-932E-6243C4517090}.Debug|Any CPU.Build.0 = Debug|Any CPU {449C9CCD-35FC-4D38-932E-6243C4517090}.Release|Any CPU.ActiveCfg = Release|Any CPU {449C9CCD-35FC-4D38-932E-6243C4517090}.Release|Any CPU.Build.0 = Release|Any CPU {449C9CCD-35FC-4D38-932E-6243C4517090}.QA|Any CPU.ActiveCfg = Release|Any CPU {449C9CCD-35FC-4D38-932E-6243C4517090}.QA|Any CPU.Build.0 = Release|Any CPU EndGlobalSection |
It means that we have only 2 configurations on project level: debug and release. It means that we have 2 conditional PropertyGroup nodes in csproj file. But we still have 3 build configurations on solution level. We just define that in case of QA it should treat it as it were Release configuation.
You might ask why do I need all that stuff. So in my case there are about 20 sln files and 296 csproj files.And everytime it's diferrent...some solution have more build configurations some less, sometimes it's mpped one to one, sometimes many to many, we just want to keep to same strategy everywhere. Another reason that latter we want to have only Debug and Release build configuraions instead of 8 that we have now. With code that we have seen above it's realy easy. I forget to show what mappings actualy are. So bellow is usage of CureSolution method.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | var mappings = new[] { Tuple.Create("Debug", "Debug"), Tuple.Create("Integration", "Debug"), Tuple.Create("Release", "Release"), Tuple.Create("QA", "Release"), Tuple.Create("Production", "Release"), Tuple.Create("Staging", "Release") }; foreach (var slnPath in solutions) { CureSolution(slnPath, mappings); } |
In this particular case we map Debug and Integration as debug on project level, and the rest as release configurations.
Last things. I forgot to mention how I parse solution file and get projecs guid's. To do that I use nuget package Onion.SolutionParser. Anothe things is variable vsFolderGuid. Thing is that if solution contains some folder, those folder are treated also like projects with some special project type, we can filter them out by type guid. We have othing to do with solution folders when we do something with build configurations.
No comments:
Post a Comment