Skip to content

Commit

Permalink
Add a task&target to update project configurations
Browse files Browse the repository at this point in the history
Previously there was just a target to flag errors for projects.

This wasn't all that useful so I added a target to fix them.
  • Loading branch information
ericstj committed Jan 30, 2017
1 parent fb4862b commit a783a4c
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 41 deletions.
14 changes: 7 additions & 7 deletions build.proj
Original file line number Diff line number Diff line change
Expand Up @@ -57,16 +57,16 @@
<MSBuild Projects="@(ExternalProject)"
ContinueOnError="ErrorAndContinue" />
</Target>
<UsingTask TaskName="ValidateVSConfigurations" AssemblyFile="$(CoreFxToolsTaskDir)CoreFx.Tools.dll"/>
<Target Name="ValidateVSConfigurations" DependsOnTargets="BuildCoreFxTools" Condition="'$(CheckProjectConfiguration)'=='true'">
<Message Importance="High" Text="Validating configurations for projects ..." />

<UsingTask TaskName="ValidateVSConfigurations" AssemblyFile="$(CoreFxToolsTaskDir)CoreFx.Tools.dll"/>
<Target Name="UpdateVSConfigurations" DependsOnTargets="BuildCoreFxTools">
<Message Importance="High" Text="Updating configurations for projects ..." />
<ItemGroup>
<ProjectsToCheckHeaders Include="$(MSBuildThisFileDirectory)src/**/*.*proj" Exclude="@(ProjectExclusions)" />
</ItemGroup>
<ValidateVSConfigurations ProjectsToValidate="@(ProjectsToCheckHeaders)" />
<Message Importance="High" Text="Validating configurations for projects ... Done." />
</Target>
<ValidateVSConfigurations ProjectsToValidate="@(ProjectsToCheckHeaders)" UpdateProjects="true" />
<Message Importance="High" Text="Updating configurations for projects ... Done." />
</Target>

<!-- Override CleanAllProjects from dir.traversal.targets and just remove the full BinDir -->
<Target Name="CleanAllProjects">
Expand Down
1 change: 1 addition & 0 deletions src/Tools/CoreFx.Tools/CoreFx.Tools.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
<Compile Include="AssemblyResolver.cs" />
<TargetingPackReference Include="System" />
<TargetingPackReference Include="System.Core" />
<TargetingPackReference Include="System.Xml" />
<TargetingPackReference Include="Microsoft.Build" />
<TargetingPackReference Include="Microsoft.Build.Framework" />
<TargetingPackReference Include="Microsoft.Build.Utilities.v4.0" />
Expand Down
173 changes: 139 additions & 34 deletions src/Tools/CoreFx.Tools/ValidateVSConfigurations.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
using System.IO;
using System.Text.RegularExpressions;
using Microsoft.Build.Framework;
using Microsoft.Build.Evaluation;
using System.Linq;
using Microsoft.Build.Construction;

namespace Microsoft.DotNet.Build.Tasks
{
Expand All @@ -11,57 +14,159 @@ public class ValidateVSConfigurations : BuildTask
[Required]
public ITaskItem[] ProjectsToValidate { get; set; }

private const string CONFIGURATION_PROPS = "Configurations.props";
private const string VS_CONFIG_REGEX = @"'\$\(Configuration\)\|\$\(Platform\)' == '(.*?)'";
public bool UpdateProjects { get; set; }

private const string ConfigurationPropsFilename = "Configurations.props";
private static Regex s_configurationConditionRegex = new Regex(@"'\$\(Configuration\)\|\$\(Platform\)' ?== ?'(?<config>.*)'");
private static string[] s_configurationSuffixes = new [] { "-Debug|AnyCPU", "-Release|AnyCPU" };

public override bool Execute()
{
bool matchesConfigs = true;
//Parallellise this task to make faster
foreach (var item in ProjectsToValidate)
{
string projLocation = item.ItemSpec;
string projConfigurationLocation = Path.Combine(Path.GetDirectoryName(projLocation), CONFIGURATION_PROPS);
if (File.Exists(projConfigurationLocation))
string projectFile = item.ItemSpec;
string projectConfigurationPropsFile = Path.Combine(Path.GetDirectoryName(projectFile), ConfigurationPropsFilename);

if (File.Exists(projectConfigurationPropsFile))
{
string projContents = File.ReadAllText(projLocation);
string projConfigurationContents = File.ReadAllText(projConfigurationLocation);
var expectedConfigurations = GetConfigurationStrings(projectConfigurationPropsFile);

var matchCollection = Regex.Matches(projContents, VS_CONFIG_REGEX);
var configCollection = Regex.Matches(projConfigurationContents,
"<BuildConfigurations>(.*?)</BuildConfigurations>", RegexOptions.Singleline);
var project = ProjectRootElement.Open(projectFile);

string[] configs = null;
HashSet<string> vsConfigs = new HashSet<string>();
foreach (Match configGroup in configCollection)
{
configs = configGroup.Groups[1].Value.Replace("\r\n", "").Trim().Split(new char[]{ ';' }, StringSplitOptions.RemoveEmptyEntries);
}
foreach (var config in configs)
{
string str = config + "_Debug|AnyCPU";
string str1 = config + "_Release|AnyCPU";
vsConfigs.Add(str);
vsConfigs.Add(str1);
}
ICollection<ProjectPropertyGroupElement> propertyGroups;
var actualConfigurations = GetConfigurationFromPropertyGroups(project, out propertyGroups);

foreach (Match match in matchCollection)
if (!actualConfigurations.SequenceEqual(expectedConfigurations))
{
string configFromProj = match.Groups[1].Value;
if (!vsConfigs.Contains(configFromProj))
if (!UpdateProjects)
{
Log.LogError($"{item} configurations does not match it's Configurations.props.");
}
else
{
matchesConfigs = false;
break;
ReplaceConfigurationPropertyGroups(project, propertyGroups, expectedConfigurations);
project.Save();
}
}
}
if (!matchesConfigs)
}

return !Log.HasLoggedErrors;
}

/// <summary>
/// Gets a sorted list of configuration strings from a Configurations.props file
/// </summary>
/// <param name="configurationProjectFile">Path to Configuration.props file</param>
/// <returns>Sorted list of configuration strings</returns>
private static string[] GetConfigurationStrings(string configurationProjectFile)
{
var configurationProject = new Project(configurationProjectFile);

var buildConfigurations = configurationProject.GetPropertyValue("BuildConfigurations");

ProjectCollection.GlobalProjectCollection.UnloadProject(configurationProject);

return buildConfigurations.Trim()
.Split(';')
.Select(c => c.Trim())
.Where(c => !String.IsNullOrEmpty(c))
.SelectMany(c => s_configurationSuffixes.Select(s => c + s))
.OrderBy(c => c, StringComparer.OrdinalIgnoreCase)
.ToArray();
}

/// <summary>
/// Gets a sorted list of configuration strings from a project file's PropertyGroups
/// </summary>
/// <param name="project">Project</param>
/// <param name="propertyGroups">collection that accepts the list of property groups representing configuration strings</param>
/// <returns>Sorted list of configuration strings</returns>
private static string[] GetConfigurationFromPropertyGroups(ProjectRootElement project, out ICollection<ProjectPropertyGroupElement> propertyGroups)
{
propertyGroups = new List<ProjectPropertyGroupElement>();
var configurations = new SortedSet<string>(StringComparer.OrdinalIgnoreCase);

foreach (var propertyGroup in project.PropertyGroups)
{
var match = s_configurationConditionRegex.Match(propertyGroup.Condition);

if (match.Success)
{
configurations.Add(match.Groups["config"].Value);
propertyGroups.Add(propertyGroup);
}
}

return configurations.ToArray();
}

/// <summary>
/// Replaces all configuration propertygroups with empty property groups corresponding to the expected configurations.
/// Doesn't attempt to preserve any content since it can all be regenerated.
/// Does attempt to preserve the ordering in the project file.
/// </summary>
/// <param name="project">Project</param>
/// <param name="oldPropertyGroups">PropertyGroups to remove</param>
/// <param name="newConfigurations"></param>
private static void ReplaceConfigurationPropertyGroups(ProjectRootElement project, IEnumerable<ProjectPropertyGroupElement> oldPropertyGroups, IEnumerable<string> newConfigurations)
{
ProjectElement insertAfter = null, insertBefore = null;

foreach (var oldPropertyGroup in oldPropertyGroups)
{
insertBefore = oldPropertyGroup.NextSibling;
project.RemoveChild(oldPropertyGroup);
}

if (insertBefore == null)
{
// find first itemgroup after imports
var insertAt = project.Imports.FirstOrDefault()?.NextSibling;

while (insertAt != null)
{
if (insertAt is ProjectItemGroupElement)
{
insertBefore = insertAt;
break;
}

insertAt = insertAt.NextSibling;
}
}

if (insertBefore == null)
{
// find last propertygroup after imports, defaulting to after imports
insertAfter = project.Imports.FirstOrDefault();

while (insertAfter?.NextSibling != null && insertAfter.NextSibling is ProjectPropertyGroupElement)
{
insertAfter = insertAfter.NextSibling;
}
}


foreach (var newConfiguration in newConfigurations)
{
var newPropertyGroup = project.CreatePropertyGroupElement();
newPropertyGroup.Condition = $"'$(Configuration)|$(Platform)' == '{newConfiguration}'";
if (insertBefore != null)
{
project.InsertBeforeChild(newPropertyGroup, insertBefore);
}
else if (insertAfter != null)
{
project.InsertAfterChild(newPropertyGroup, insertAfter);
}
else
{
Log.LogError($"{item} configurations does not match it's Configurations.props.");
break;
project.AppendChild(newPropertyGroup);
}
insertBefore = null;
insertAfter = newPropertyGroup;
}
return matchesConfigs;
}
}
}

0 comments on commit a783a4c

Please sign in to comment.