Skip to content

Commit 2bc26b4

Browse files
Merge pull request #3502 from petercrabtree/fix/ilspcmd-solution-references
Fix SDK-style (modern) inter-project references
2 parents 36d2135 + d4a27b9 commit 2bc26b4

File tree

4 files changed

+77
-55
lines changed

4 files changed

+77
-55
lines changed

ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetServices.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,11 +76,11 @@ public static TargetFramework DetectTargetFramework(MetadataFile module)
7676
targetFrameworkIdentifier = frameworkParts.FirstOrDefault(a => !a.StartsWith(VersionToken, StringComparison.OrdinalIgnoreCase) && !a.StartsWith(ProfileToken, StringComparison.OrdinalIgnoreCase));
7777
string frameworkVersion = frameworkParts.FirstOrDefault(a => a.StartsWith(VersionToken, StringComparison.OrdinalIgnoreCase));
7878

79-
if (frameworkVersion != null)
79+
if (frameworkVersion != null && Version.TryParse(frameworkVersion.Substring(VersionToken.Length).Replace("v", ""), out var version))
8080
{
81-
versionNumber = int.Parse(frameworkVersion.Substring(VersionToken.Length).Replace("v", "").Replace(".", ""));
82-
if (versionNumber < 100)
83-
versionNumber *= 10;
81+
versionNumber = version.Major * 100 + version.Minor * 10;
82+
if (version.Build > 0)
83+
versionNumber += version.Build;
8484
}
8585

8686
string frameworkProfile = frameworkParts.FirstOrDefault(a => a.StartsWith(ProfileToken, StringComparison.OrdinalIgnoreCase));

ICSharpCode.Decompiler/Solution/SolutionCreator.cs

Lines changed: 55 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,19 @@ namespace ICSharpCode.Decompiler.Solution
2929
/// </summary>
3030
public static class SolutionCreator
3131
{
32-
private static readonly XNamespace ProjectFileNamespace = XNamespace.Get("http://schemas.microsoft.com/developer/msbuild/2003");
32+
static readonly XNamespace NonSDKProjectFileNamespace = XNamespace.Get("http://schemas.microsoft.com/developer/msbuild/2003");
3333

3434
/// <summary>
3535
/// Writes a solution file to the specified <paramref name="targetFile"/>.
36+
/// Also fixes intra-solution project references in the project files.
3637
/// </summary>
3738
/// <param name="targetFile">The full path of the file to write.</param>
3839
/// <param name="projects">The projects contained in this solution.</param>
3940
///
4041
/// <exception cref="ArgumentException">Thrown when <paramref name="targetFile"/> is null or empty.</exception>
4142
/// <exception cref="ArgumentNullException">Thrown when <paramref name="projects"/> is null.</exception>
4243
/// <exception cref="InvalidOperationException">Thrown when <paramref name="projects"/> contains no items.</exception>
43-
public static void WriteSolutionFile(string targetFile, IEnumerable<ProjectItem> projects)
44+
public static void WriteSolutionFile(string targetFile, List<ProjectItem> projects)
4445
{
4546
if (string.IsNullOrWhiteSpace(targetFile))
4647
{
@@ -62,10 +63,10 @@ public static void WriteSolutionFile(string targetFile, IEnumerable<ProjectItem>
6263
WriteSolutionFile(writer, projects, targetFile);
6364
}
6465

65-
FixProjectReferences(projects);
66+
FixAllProjectReferences(projects);
6667
}
6768

68-
private static void WriteSolutionFile(TextWriter writer, IEnumerable<ProjectItem> projects, string solutionFilePath)
69+
static void WriteSolutionFile(TextWriter writer, List<ProjectItem> projects, string solutionFilePath)
6970
{
7071
WriteHeader(writer);
7172
WriteProjects(writer, projects, solutionFilePath);
@@ -90,7 +91,7 @@ private static void WriteHeader(TextWriter writer)
9091
writer.WriteLine("MinimumVisualStudioVersion = 10.0.40219.1");
9192
}
9293

93-
private static void WriteProjects(TextWriter writer, IEnumerable<ProjectItem> projects, string solutionFilePath)
94+
static void WriteProjects(TextWriter writer, List<ProjectItem> projects, string solutionFilePath)
9495
{
9596
foreach (var project in projects)
9697
{
@@ -103,7 +104,7 @@ private static void WriteProjects(TextWriter writer, IEnumerable<ProjectItem> pr
103104
}
104105
}
105106

106-
private static IEnumerable<string> WriteSolutionConfigurations(TextWriter writer, IEnumerable<ProjectItem> projects)
107+
static List<string> WriteSolutionConfigurations(TextWriter writer, List<ProjectItem> projects)
107108
{
108109
var platforms = projects.GroupBy(p => p.PlatformName).Select(g => g.Key).ToList();
109110

@@ -125,10 +126,10 @@ private static IEnumerable<string> WriteSolutionConfigurations(TextWriter writer
125126
return platforms;
126127
}
127128

128-
private static void WriteProjectConfigurations(
129+
static void WriteProjectConfigurations(
129130
TextWriter writer,
130-
IEnumerable<ProjectItem> projects,
131-
IEnumerable<string> solutionPlatforms)
131+
List<ProjectItem> projects,
132+
List<string> solutionPlatforms)
132133
{
133134
writer.WriteLine("\tGlobalSection(ProjectConfigurationPlatforms) = postSolution");
134135

@@ -152,47 +153,80 @@ private static void WriteProjectConfigurations(
152153
writer.WriteLine("\tEndGlobalSection");
153154
}
154155

155-
private static void FixProjectReferences(IEnumerable<ProjectItem> projects)
156+
static void FixAllProjectReferences(List<ProjectItem> projects)
156157
{
157-
var projectsMap = projects.ToDictionary(p => p.ProjectName, p => p);
158+
var projectsMap = projects.ToDictionary(
159+
p => p.ProjectName,
160+
p => p);
158161

159162
foreach (var project in projects)
160163
{
161164
XDocument projectDoc = XDocument.Load(project.FilePath);
162165

166+
if (projectDoc.Root?.Name.LocalName != "Project")
167+
{
168+
throw new InvalidOperationException(
169+
$"The file {project.FilePath} is not a valid project file, " +
170+
$"no <Project> at the root; could not fix project references.");
171+
}
172+
173+
// sdk style projects don't use a namespace for the elements,
174+
// but we still need to use the namespace for non-sdk style projects.
175+
var sdkStyle = projectDoc.Root.Attribute("Sdk") != null;
176+
var itemGroupTagName = sdkStyle ? "ItemGroup" : NonSDKProjectFileNamespace + "ItemGroup";
177+
var referenceTagName = sdkStyle ? "Reference" : NonSDKProjectFileNamespace + "Reference";
178+
163179
var referencesItemGroups = projectDoc.Root
164-
.Elements(ProjectFileNamespace + "ItemGroup")
165-
.Where(e => e.Elements(ProjectFileNamespace + "Reference").Any());
180+
.Elements(itemGroupTagName)
181+
.Where(e => e.Elements(referenceTagName).Any())
182+
.ToList();
166183

167184
foreach (var itemGroup in referencesItemGroups)
168185
{
169-
FixProjectReferences(project.FilePath, itemGroup, projectsMap);
186+
FixProjectReferences(project.FilePath, itemGroup, projectsMap, sdkStyle);
170187
}
171188

172189
projectDoc.Save(project.FilePath);
173190
}
174191
}
175192

176-
private static void FixProjectReferences(string projectFilePath, XElement itemGroup, IDictionary<string, ProjectItem> projects)
193+
static void FixProjectReferences(string projectFilePath, XElement itemGroup,
194+
Dictionary<string, ProjectItem> projects, bool sdkStyle)
177195
{
178-
foreach (var item in itemGroup.Elements(ProjectFileNamespace + "Reference").ToList())
196+
197+
XName GetElementName(string name) => sdkStyle ? name : NonSDKProjectFileNamespace + name;
198+
199+
var referenceTagName = GetElementName("Reference");
200+
var projectReferenceTagName = GetElementName("ProjectReference");
201+
202+
foreach (var item in itemGroup.Elements(referenceTagName).ToList())
179203
{
180204
var assemblyName = item.Attribute("Include")?.Value;
181205
if (assemblyName != null && projects.TryGetValue(assemblyName, out var referencedProject))
182206
{
183207
item.Remove();
184208

185-
var projectReference = new XElement(ProjectFileNamespace + "ProjectReference",
186-
new XElement(ProjectFileNamespace + "Project", referencedProject.Guid.ToString("B").ToUpperInvariant()),
187-
new XElement(ProjectFileNamespace + "Name", referencedProject.ProjectName));
188-
projectReference.SetAttributeValue("Include", GetRelativePath(projectFilePath, referencedProject.FilePath));
209+
var projectReference = new XElement(
210+
projectReferenceTagName,
211+
new XAttribute("Include", GetRelativePath(projectFilePath, referencedProject.FilePath)));
212+
213+
// SDK-style projects do not use the <Project> and <Name> elements for project references.
214+
// (Instead, those get read from the .csproj file in "Include".)
215+
if (!sdkStyle)
216+
{
217+
projectReference.Add(
218+
// no ToUpper() for uuids, most Microsoft tools seem to emit them in lowercase
219+
// (no .ToLower() as .ToString("B") already outputs lowercase)
220+
new XElement(NonSDKProjectFileNamespace + "Project", referencedProject.Guid.ToString("B")),
221+
new XElement(NonSDKProjectFileNamespace + "Name", referencedProject.ProjectName));
222+
}
189223

190224
itemGroup.Add(projectReference);
191225
}
192226
}
193227
}
194228

195-
private static string GetRelativePath(string fromFilePath, string toFilePath)
229+
static string GetRelativePath(string fromFilePath, string toFilePath)
196230
{
197231
Uri fromUri = new Uri(fromFilePath);
198232
Uri toUri = new Uri(toFilePath);

ILSpy/Commands/SaveCodeContextMenuEntry.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public static void Execute(IReadOnlyList<SharpTreeNode> selectedNodes, LanguageS
7878
{
7979
var assemblies = selectedNodes.OfType<AssemblyTreeNode>()
8080
.Select(n => n.LoadedAssembly)
81-
.Where(a => a.IsLoadedAsValidAssembly).ToArray();
81+
.Where(a => a.IsLoadedAsValidAssembly).ToList();
8282
SolutionWriter.CreateSolution(tabPage, textView, selectedPath, currentLanguage, assemblies);
8383
}
8484
return;

ILSpy/SolutionWriter.cs

Lines changed: 17 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ internal class SolutionWriter
5656
/// <exception cref="ArgumentNullException">Thrown when <paramref name="textView"/>> or
5757
/// <paramref name="assemblies"/> is null.</exception>
5858
public static void CreateSolution(TabPageModel tabPage, DecompilerTextView textView, string solutionFilePath,
59-
Language language, IEnumerable<LoadedAssembly> assemblies)
59+
Language language, List<LoadedAssembly> assemblies)
6060
{
6161
if (textView == null)
6262
{
@@ -77,7 +77,7 @@ public static void CreateSolution(TabPageModel tabPage, DecompilerTextView textV
7777

7878
textView
7979
.RunWithCancellation(ct => writer.CreateSolution(tabPage, assemblies, language, ct))
80-
.Then(output => textView.ShowText(output))
80+
.Then(textView.ShowText)
8181
.HandleExceptions();
8282
}
8383

@@ -94,41 +94,29 @@ public static void CreateSolution(TabPageModel tabPage, DecompilerTextView textV
9494
projects = new ConcurrentBag<ProjectItem>();
9595
}
9696

97-
async Task<AvalonEditTextOutput> CreateSolution(TabPageModel tabPage, IEnumerable<LoadedAssembly> assemblies, Language language, CancellationToken ct)
97+
async Task<AvalonEditTextOutput> CreateSolution(TabPageModel tabPage, List<LoadedAssembly> allAssemblies, Language language, CancellationToken ct)
9898
{
9999
var result = new AvalonEditTextOutput();
100100

101-
var assembliesByShortName = assemblies.ToLookup(_ => _.ShortName);
101+
var assembliesByShortName = allAssemblies.GroupBy(_ => _.ShortName).ToDictionary(_ => _.Key, _ => _.ToList());
102102
bool first = true;
103103
bool abort = false;
104104

105-
foreach (var item in assembliesByShortName)
105+
foreach (var (shortName, assemblies) in assembliesByShortName)
106106
{
107-
var enumerator = item.GetEnumerator();
108-
if (!enumerator.MoveNext())
109-
continue;
110-
var firstAssembly = enumerator.Current;
111-
if (!enumerator.MoveNext())
107+
if (assemblies.Count == 1)
108+
{
112109
continue;
110+
}
111+
113112
if (first)
114113
{
115114
result.WriteLine("Duplicate assembly names selected, cannot generate a solution:");
116115
abort = true;
116+
first = false;
117117
}
118118

119-
result.Write("- " + firstAssembly.Text + " conflicts with ");
120-
121-
first = true;
122-
do
123-
{
124-
var asm = enumerator.Current;
125-
if (!first)
126-
result.Write(", ");
127-
result.Write(asm.Text);
128-
first = false;
129-
} while (enumerator.MoveNext());
130-
result.WriteLine();
131-
first = false;
119+
result.WriteLine("- " + assemblies[0].Text + " conflicts with " + string.Join(", ", assemblies.Skip(1)));
132120
}
133121

134122
if (abort)
@@ -141,7 +129,7 @@ async Task<AvalonEditTextOutput> CreateSolution(TabPageModel tabPage, IEnumerabl
141129
// Explicitly create an enumerable partitioner here to avoid Parallel.ForEach's special cases for lists,
142130
// as those seem to use static partitioning which is inefficient if assemblies take differently
143131
// long to decompile.
144-
await Task.Run(() => Parallel.ForEach(Partitioner.Create(assemblies),
132+
await Task.Run(() => Parallel.ForEach(Partitioner.Create(allAssemblies),
145133
new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount, CancellationToken = ct },
146134
item => WriteProject(tabPage, item, language, solutionDirectory, ct)))
147135
.ConfigureAwait(false);
@@ -153,8 +141,8 @@ await Task.Run(() => Parallel.ForEach(Partitioner.Create(assemblies),
153141
}
154142
else
155143
{
156-
await Task.Run(() => SolutionCreator.WriteSolutionFile(solutionFilePath, projects))
157-
.ConfigureAwait(false);
144+
await Task.Run(() => SolutionCreator.WriteSolutionFile(solutionFilePath, projects.ToList()), ct)
145+
.ConfigureAwait(false);
158146
}
159147
}
160148
catch (AggregateException ae)
@@ -184,14 +172,14 @@ await Task.Run(() => SolutionCreator.WriteSolutionFile(solutionFilePath, project
184172
if (statusOutput.Count == 0)
185173
{
186174
result.WriteLine("Successfully decompiled the following assemblies into Visual Studio projects:");
187-
foreach (var item in assemblies.Select(n => n.Text.ToString()))
175+
foreach (var n in allAssemblies)
188176
{
189-
result.WriteLine(item);
177+
result.WriteLine(n.Text.ToString());
190178
}
191179

192180
result.WriteLine();
193181

194-
if (assemblies.Count() == projects.Count)
182+
if (allAssemblies.Count == projects.Count)
195183
{
196184
result.WriteLine("Created the Visual Studio Solution file.");
197185
}

0 commit comments

Comments
 (0)