Skip to content
This repository was archived by the owner on Apr 20, 2023. It is now read-only.

Commit f700910

Browse files
author
Peter Huene
committed
Map solution configurations to existing project configurations on add.
This commit implements solution configuration to project configuration mapping. Previously, when a project was added to the solution with the `sln add` command, solution configurations would be mapped to a project configuration and platform of the same name, regardless of whether or not the project had a configuration or platform of that name. This caused the solution to appear dirty when opened in Visual Studio if the configuration or platform did not exist at the project level because Visual Studio would attempt to correct the mapping. The fix is to check what configurations and platforms are supported by the project and only map to what is present. If a solution configuration can't be mapped, the first configuration/platform supported by the project is chosen; this is consistent with how Visual Studio does the fallback mapping. Fixes #6221.
1 parent b456668 commit f700910

File tree

10 files changed

+435
-60
lines changed

10 files changed

+435
-60
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
2+
Microsoft Visual Studio Solution File, Format Version 12.00
3+
# Visual Studio 15
4+
VisualStudioVersion = 15.0.26006.2
5+
MinimumVisualStudioVersion = 10.0.40219.1
6+
Global
7+
GlobalSection(SolutionConfigurationPlatforms) = preSolution
8+
Debug|Any CPU = Debug|Any CPU
9+
Debug|x64 = Debug|x64
10+
Debug|x86 = Debug|x86
11+
Release|Any CPU = Release|Any CPU
12+
Release|x64 = Release|x64
13+
Release|x86 = Release|x86
14+
Foo Bar|Any CPU = Foo Bar|Any CPU
15+
Foo Bar|x64 = Foo Bar|x64
16+
Foo Bar|x86 = Foo Bar|x86
17+
EndGlobalSection
18+
GlobalSection(SolutionProperties) = preSolution
19+
HideSolutionNode = FALSE
20+
EndGlobalSection
21+
EndGlobal
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System;
5+
6+
namespace ProjectWithAdditionalConfigs
7+
{
8+
public static class Library
9+
{
10+
public static string GetMessage()
11+
{
12+
return "Hello World!";
13+
}
14+
}
15+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<ProjectGuid>{A302325B-D680-4C0E-8680-7AE283981624}</ProjectGuid>
6+
<Platforms>AnyCPU;x64;x86;AdditionalPlatform</Platforms>
7+
<Configurations>Debug;Release;FooBar;AdditionalConfiguration</Configurations>
8+
</PropertyGroup>
9+
10+
</Project>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System;
5+
6+
namespace ProjectWithMatchingConfigs
7+
{
8+
public static class Library
9+
{
10+
public static string GetMessage()
11+
{
12+
return "Hello World!";
13+
}
14+
}
15+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<ProjectGuid>{C9601CA2-DB64-4FB6-B463-368C7764BF0D}</ProjectGuid>
6+
<Platforms>AnyCPU;x64;x86</Platforms>
7+
<Configurations>Debug;Release;FooBar</Configurations>
8+
</PropertyGroup>
9+
10+
</Project>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) .NET Foundation and contributors. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using System;
5+
6+
namespace ProjectWithoutMatchingConfigs
7+
{
8+
public static class Library
9+
{
10+
public static string GetMessage()
11+
{
12+
return "Hello World!";
13+
}
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
2+
3+
<PropertyGroup>
4+
<TargetFramework>netstandard2.0</TargetFramework>
5+
<ProjectGuid>{C49B64DE-4401-4825-8A88-10DCB5950E57}</ProjectGuid>
6+
</PropertyGroup>
7+
8+
</Project>

src/dotnet/ProjectInstanceExtensions.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using Microsoft.Build.Execution;
55
using Microsoft.DotNet.Cli.Sln.Internal;
66
using System;
7+
using System.Collections.Generic;
78
using System.Linq;
89

910
namespace Microsoft.DotNet.Tools.Common
@@ -45,5 +46,25 @@ public static string GetProjectTypeGuid(this ProjectInstance projectInstance)
4546

4647
return projectTypeGuid;
4748
}
49+
50+
public static IEnumerable<string> GetPlatforms(this ProjectInstance projectInstance)
51+
{
52+
return (projectInstance.GetPropertyValue("Platforms") ?? "")
53+
.Split(
54+
new char[] { ';' },
55+
StringSplitOptions.RemoveEmptyEntries)
56+
.Where(p => !string.IsNullOrWhiteSpace(p))
57+
.DefaultIfEmpty("AnyCPU");
58+
}
59+
60+
public static IEnumerable<string> GetConfigurations(this ProjectInstance projectInstance)
61+
{
62+
return (projectInstance.GetPropertyValue("Configurations") ?? "Debug;Release")
63+
.Split(
64+
new char[] { ';' },
65+
StringSplitOptions.RemoveEmptyEntries)
66+
.Where(c => !string.IsNullOrWhiteSpace(c))
67+
.DefaultIfEmpty("Debug");
68+
}
4869
}
4970
}

src/dotnet/SlnFileExtensions.cs

Lines changed: 98 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,16 @@ public static void AddProject(this SlnFile slnFile, string fullProjectPath)
5959
FilePath = relativeProjectPath
6060
};
6161

62-
slnFile.AddDefaultBuildConfigurations(slnProject);
62+
// NOTE: The order you create the sections determines the order they are written to the sln
63+
// file. In the case of an empty sln file, in order to make sure the solution configurations
64+
// section comes first we need to add it first. This doesn't affect correctness but does
65+
// stop VS from re-ordering things later on. Since we are keeping the SlnFile class low-level
66+
// it shouldn't care about the VS implementation details. That's why we handle this here.
67+
slnFile.AddDefaultBuildConfigurations();
68+
69+
slnFile.MapSolutionConfigurationsToProject(
70+
projectInstance,
71+
slnFile.ProjectConfigurationsSection.GetOrCreatePropertySet(slnProject.Id));
6372

6473
slnFile.AddSolutionFolders(slnProject);
6574

@@ -70,11 +79,13 @@ public static void AddProject(this SlnFile slnFile, string fullProjectPath)
7079
}
7180
}
7281

73-
public static void AddDefaultBuildConfigurations(this SlnFile slnFile, SlnProject slnProject)
82+
private static void AddDefaultBuildConfigurations(this SlnFile slnFile)
7483
{
75-
if (slnProject == null)
84+
var configurationsSection = slnFile.SolutionConfigurationsSection;
85+
86+
if (!configurationsSection.IsEmpty)
7687
{
77-
throw new ArgumentException();
88+
return;
7889
}
7990

8091
var defaultConfigurations = new List<string>()
@@ -87,57 +98,108 @@ public static void AddDefaultBuildConfigurations(this SlnFile slnFile, SlnProjec
8798
"Release|x86",
8899
};
89100

90-
// NOTE: The order you create the sections determines the order they are written to the sln
91-
// file. In the case of an empty sln file, in order to make sure the solution configurations
92-
// section comes first we need to add it first. This doesn't affect correctness but does
93-
// stop VS from re-ordering things later on. Since we are keeping the SlnFile class low-level
94-
// it shouldn't care about the VS implementation details. That's why we handle this here.
95-
AddDefaultSolutionConfigurations(defaultConfigurations, slnFile.SolutionConfigurationsSection);
96-
AddDefaultProjectConfigurations(
97-
defaultConfigurations,
98-
slnFile.ProjectConfigurationsSection.GetOrCreatePropertySet(slnProject.Id));
99-
}
100-
101-
private static void AddDefaultSolutionConfigurations(
102-
List<string> defaultConfigurations,
103-
SlnPropertySet solutionConfigs)
104-
{
105101
foreach (var config in defaultConfigurations)
106102
{
107-
if (!solutionConfigs.ContainsKey(config))
108-
{
109-
solutionConfigs[config] = config;
110-
}
103+
configurationsSection[config] = config;
111104
}
112105
}
113106

114-
private static void AddDefaultProjectConfigurations(
115-
List<string> defaultConfigurations,
116-
SlnPropertySet projectConfigs)
107+
private static void MapSolutionConfigurationsToProject(
108+
this SlnFile slnFile,
109+
ProjectInstance projectInstance,
110+
SlnPropertySet solutionProjectConfigs)
117111
{
118-
foreach (var config in defaultConfigurations)
112+
var (projectConfigurations, defaultProjectConfiguration) = GetKeysDictionary(projectInstance.GetConfigurations());
113+
var (projectPlatforms, defaultProjectPlatform) = GetKeysDictionary(projectInstance.GetPlatforms());
114+
115+
foreach (var solutionConfigKey in slnFile.SolutionConfigurationsSection.Keys)
119116
{
120-
var activeCfgKey = $"{config}.ActiveCfg";
121-
if (!projectConfigs.ContainsKey(activeCfgKey))
117+
var projectConfigKey = MapSolutionConfigKeyToProjectConfigKey(
118+
solutionConfigKey,
119+
projectConfigurations,
120+
defaultProjectConfiguration,
121+
projectPlatforms,
122+
defaultProjectPlatform);
123+
if (projectConfigKey == null)
122124
{
123-
projectConfigs[activeCfgKey] = config;
125+
continue;
124126
}
125127

126-
var build0Key = $"{config}.Build.0";
127-
if (!projectConfigs.ContainsKey(build0Key))
128+
var activeConfigKey = $"{solutionConfigKey}.ActiveCfg";
129+
if (!solutionProjectConfigs.ContainsKey(activeConfigKey))
128130
{
129-
projectConfigs[build0Key] = config;
131+
solutionProjectConfigs[activeConfigKey] = projectConfigKey;
132+
}
133+
134+
var buildKey = $"{solutionConfigKey}.Build.0";
135+
if (!solutionProjectConfigs.ContainsKey(buildKey))
136+
{
137+
solutionProjectConfigs[buildKey] = projectConfigKey;
130138
}
131139
}
132140
}
133141

134-
public static void AddSolutionFolders(this SlnFile slnFile, SlnProject slnProject)
142+
private static (Dictionary<string, string> Keys, string DefaultKey) GetKeysDictionary(IEnumerable<string> keys)
135143
{
136-
if (slnProject == null)
144+
// A dictionary mapping key -> key is used instead of a HashSet so the original case of the key can be retrieved from the set
145+
var dictionary = new Dictionary<string, string>(StringComparer.CurrentCultureIgnoreCase);
146+
147+
foreach (var key in keys)
137148
{
138-
throw new ArgumentException();
149+
dictionary[key] = key;
150+
}
151+
152+
return (dictionary, keys.FirstOrDefault());
153+
}
154+
155+
private static string GetMatchingProjectKey(IDictionary<string, string> projectKeys, string solutionKey)
156+
{
157+
string projectKey;
158+
if (projectKeys.TryGetValue(solutionKey, out projectKey))
159+
{
160+
return projectKey;
161+
}
162+
163+
var keyWithoutWhitespace = String.Concat(solutionKey.Where(c => !Char.IsWhiteSpace(c)));
164+
if (projectKeys.TryGetValue(keyWithoutWhitespace, out projectKey))
165+
{
166+
return projectKey;
167+
}
168+
169+
return null;
170+
}
171+
172+
private static string MapSolutionConfigKeyToProjectConfigKey(
173+
string solutionConfigKey,
174+
Dictionary<string, string> projectConfigurations,
175+
string defaultProjectConfiguration,
176+
Dictionary<string, string> projectPlatforms,
177+
string defaultProjectPlatform)
178+
{
179+
var pair = solutionConfigKey.Split(new char[] {'|'}, 2);
180+
if (pair.Length != 2)
181+
{
182+
return null;
183+
}
184+
185+
var projectConfiguration = GetMatchingProjectKey(projectConfigurations, pair[0]) ?? defaultProjectConfiguration;
186+
if (projectConfiguration == null)
187+
{
188+
return null;
189+
}
190+
191+
var projectPlatform = GetMatchingProjectKey(projectPlatforms, pair[1]) ?? defaultProjectPlatform;
192+
if (projectPlatform == null)
193+
{
194+
return null;
139195
}
140196

197+
// VS stores "Any CPU" platform in the solution regardless of how it is named at the project level
198+
return $"{projectConfiguration}|{(projectPlatform == "AnyCPU" ? "Any CPU" : projectPlatform)}";
199+
}
200+
201+
private static void AddSolutionFolders(this SlnFile slnFile, SlnProject slnProject)
202+
{
141203
var solutionFolders = slnProject.GetSolutionFoldersFromProject();
142204

143205
if (solutionFolders.Any())

0 commit comments

Comments
 (0)