Browse Source

Merge pull request #3502 from petercrabtree/fix/ilspcmd-solution-references

Fix SDK-style (modern) inter-project references
pull/3481/merge
Siegfried Pammer 2 weeks ago committed by GitHub
parent
commit
2bc26b4fd4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 8
      ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetServices.cs
  2. 76
      ICSharpCode.Decompiler/Solution/SolutionCreator.cs
  3. 2
      ILSpy/Commands/SaveCodeContextMenuEntry.cs
  4. 46
      ILSpy/SolutionWriter.cs

8
ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetServices.cs

@ -76,11 +76,11 @@ namespace ICSharpCode.Decompiler.CSharp.ProjectDecompiler
targetFrameworkIdentifier = frameworkParts.FirstOrDefault(a => !a.StartsWith(VersionToken, StringComparison.OrdinalIgnoreCase) && !a.StartsWith(ProfileToken, StringComparison.OrdinalIgnoreCase)); targetFrameworkIdentifier = frameworkParts.FirstOrDefault(a => !a.StartsWith(VersionToken, StringComparison.OrdinalIgnoreCase) && !a.StartsWith(ProfileToken, StringComparison.OrdinalIgnoreCase));
string frameworkVersion = frameworkParts.FirstOrDefault(a => a.StartsWith(VersionToken, StringComparison.OrdinalIgnoreCase)); string frameworkVersion = frameworkParts.FirstOrDefault(a => a.StartsWith(VersionToken, StringComparison.OrdinalIgnoreCase));
if (frameworkVersion != null) if (frameworkVersion != null && Version.TryParse(frameworkVersion.Substring(VersionToken.Length).Replace("v", ""), out var version))
{ {
versionNumber = int.Parse(frameworkVersion.Substring(VersionToken.Length).Replace("v", "").Replace(".", "")); versionNumber = version.Major * 100 + version.Minor * 10;
if (versionNumber < 100) if (version.Build > 0)
versionNumber *= 10; versionNumber += version.Build;
} }
string frameworkProfile = frameworkParts.FirstOrDefault(a => a.StartsWith(ProfileToken, StringComparison.OrdinalIgnoreCase)); string frameworkProfile = frameworkParts.FirstOrDefault(a => a.StartsWith(ProfileToken, StringComparison.OrdinalIgnoreCase));

76
ICSharpCode.Decompiler/Solution/SolutionCreator.cs

@ -29,10 +29,11 @@ namespace ICSharpCode.Decompiler.Solution
/// </summary> /// </summary>
public static class SolutionCreator public static class SolutionCreator
{ {
private static readonly XNamespace ProjectFileNamespace = XNamespace.Get("http://schemas.microsoft.com/developer/msbuild/2003"); static readonly XNamespace NonSDKProjectFileNamespace = XNamespace.Get("http://schemas.microsoft.com/developer/msbuild/2003");
/// <summary> /// <summary>
/// Writes a solution file to the specified <paramref name="targetFile"/>. /// Writes a solution file to the specified <paramref name="targetFile"/>.
/// Also fixes intra-solution project references in the project files.
/// </summary> /// </summary>
/// <param name="targetFile">The full path of the file to write.</param> /// <param name="targetFile">The full path of the file to write.</param>
/// <param name="projects">The projects contained in this solution.</param> /// <param name="projects">The projects contained in this solution.</param>
@ -40,7 +41,7 @@ namespace ICSharpCode.Decompiler.Solution
/// <exception cref="ArgumentException">Thrown when <paramref name="targetFile"/> is null or empty.</exception> /// <exception cref="ArgumentException">Thrown when <paramref name="targetFile"/> is null or empty.</exception>
/// <exception cref="ArgumentNullException">Thrown when <paramref name="projects"/> is null.</exception> /// <exception cref="ArgumentNullException">Thrown when <paramref name="projects"/> is null.</exception>
/// <exception cref="InvalidOperationException">Thrown when <paramref name="projects"/> contains no items.</exception> /// <exception cref="InvalidOperationException">Thrown when <paramref name="projects"/> contains no items.</exception>
public static void WriteSolutionFile(string targetFile, IEnumerable<ProjectItem> projects) public static void WriteSolutionFile(string targetFile, List<ProjectItem> projects)
{ {
if (string.IsNullOrWhiteSpace(targetFile)) if (string.IsNullOrWhiteSpace(targetFile))
{ {
@ -62,10 +63,10 @@ namespace ICSharpCode.Decompiler.Solution
WriteSolutionFile(writer, projects, targetFile); WriteSolutionFile(writer, projects, targetFile);
} }
FixProjectReferences(projects); FixAllProjectReferences(projects);
} }
private static void WriteSolutionFile(TextWriter writer, IEnumerable<ProjectItem> projects, string solutionFilePath) static void WriteSolutionFile(TextWriter writer, List<ProjectItem> projects, string solutionFilePath)
{ {
WriteHeader(writer); WriteHeader(writer);
WriteProjects(writer, projects, solutionFilePath); WriteProjects(writer, projects, solutionFilePath);
@ -90,7 +91,7 @@ namespace ICSharpCode.Decompiler.Solution
writer.WriteLine("MinimumVisualStudioVersion = 10.0.40219.1"); writer.WriteLine("MinimumVisualStudioVersion = 10.0.40219.1");
} }
private static void WriteProjects(TextWriter writer, IEnumerable<ProjectItem> projects, string solutionFilePath) static void WriteProjects(TextWriter writer, List<ProjectItem> projects, string solutionFilePath)
{ {
foreach (var project in projects) foreach (var project in projects)
{ {
@ -103,7 +104,7 @@ namespace ICSharpCode.Decompiler.Solution
} }
} }
private static IEnumerable<string> WriteSolutionConfigurations(TextWriter writer, IEnumerable<ProjectItem> projects) static List<string> WriteSolutionConfigurations(TextWriter writer, List<ProjectItem> projects)
{ {
var platforms = projects.GroupBy(p => p.PlatformName).Select(g => g.Key).ToList(); var platforms = projects.GroupBy(p => p.PlatformName).Select(g => g.Key).ToList();
@ -125,10 +126,10 @@ namespace ICSharpCode.Decompiler.Solution
return platforms; return platforms;
} }
private static void WriteProjectConfigurations( static void WriteProjectConfigurations(
TextWriter writer, TextWriter writer,
IEnumerable<ProjectItem> projects, List<ProjectItem> projects,
IEnumerable<string> solutionPlatforms) List<string> solutionPlatforms)
{ {
writer.WriteLine("\tGlobalSection(ProjectConfigurationPlatforms) = postSolution"); writer.WriteLine("\tGlobalSection(ProjectConfigurationPlatforms) = postSolution");
@ -152,47 +153,80 @@ namespace ICSharpCode.Decompiler.Solution
writer.WriteLine("\tEndGlobalSection"); writer.WriteLine("\tEndGlobalSection");
} }
private static void FixProjectReferences(IEnumerable<ProjectItem> projects) static void FixAllProjectReferences(List<ProjectItem> projects)
{ {
var projectsMap = projects.ToDictionary(p => p.ProjectName, p => p); var projectsMap = projects.ToDictionary(
p => p.ProjectName,
p => p);
foreach (var project in projects) foreach (var project in projects)
{ {
XDocument projectDoc = XDocument.Load(project.FilePath); XDocument projectDoc = XDocument.Load(project.FilePath);
if (projectDoc.Root?.Name.LocalName != "Project")
{
throw new InvalidOperationException(
$"The file {project.FilePath} is not a valid project file, " +
$"no <Project> at the root; could not fix project references.");
}
// sdk style projects don't use a namespace for the elements,
// but we still need to use the namespace for non-sdk style projects.
var sdkStyle = projectDoc.Root.Attribute("Sdk") != null;
var itemGroupTagName = sdkStyle ? "ItemGroup" : NonSDKProjectFileNamespace + "ItemGroup";
var referenceTagName = sdkStyle ? "Reference" : NonSDKProjectFileNamespace + "Reference";
var referencesItemGroups = projectDoc.Root var referencesItemGroups = projectDoc.Root
.Elements(ProjectFileNamespace + "ItemGroup") .Elements(itemGroupTagName)
.Where(e => e.Elements(ProjectFileNamespace + "Reference").Any()); .Where(e => e.Elements(referenceTagName).Any())
.ToList();
foreach (var itemGroup in referencesItemGroups) foreach (var itemGroup in referencesItemGroups)
{ {
FixProjectReferences(project.FilePath, itemGroup, projectsMap); FixProjectReferences(project.FilePath, itemGroup, projectsMap, sdkStyle);
} }
projectDoc.Save(project.FilePath); projectDoc.Save(project.FilePath);
} }
} }
private static void FixProjectReferences(string projectFilePath, XElement itemGroup, IDictionary<string, ProjectItem> projects) static void FixProjectReferences(string projectFilePath, XElement itemGroup,
Dictionary<string, ProjectItem> projects, bool sdkStyle)
{ {
foreach (var item in itemGroup.Elements(ProjectFileNamespace + "Reference").ToList())
XName GetElementName(string name) => sdkStyle ? name : NonSDKProjectFileNamespace + name;
var referenceTagName = GetElementName("Reference");
var projectReferenceTagName = GetElementName("ProjectReference");
foreach (var item in itemGroup.Elements(referenceTagName).ToList())
{ {
var assemblyName = item.Attribute("Include")?.Value; var assemblyName = item.Attribute("Include")?.Value;
if (assemblyName != null && projects.TryGetValue(assemblyName, out var referencedProject)) if (assemblyName != null && projects.TryGetValue(assemblyName, out var referencedProject))
{ {
item.Remove(); item.Remove();
var projectReference = new XElement(ProjectFileNamespace + "ProjectReference", var projectReference = new XElement(
new XElement(ProjectFileNamespace + "Project", referencedProject.Guid.ToString("B").ToUpperInvariant()), projectReferenceTagName,
new XElement(ProjectFileNamespace + "Name", referencedProject.ProjectName)); new XAttribute("Include", GetRelativePath(projectFilePath, referencedProject.FilePath)));
projectReference.SetAttributeValue("Include", GetRelativePath(projectFilePath, referencedProject.FilePath));
// SDK-style projects do not use the <Project> and <Name> elements for project references.
// (Instead, those get read from the .csproj file in "Include".)
if (!sdkStyle)
{
projectReference.Add(
// no ToUpper() for uuids, most Microsoft tools seem to emit them in lowercase
// (no .ToLower() as .ToString("B") already outputs lowercase)
new XElement(NonSDKProjectFileNamespace + "Project", referencedProject.Guid.ToString("B")),
new XElement(NonSDKProjectFileNamespace + "Name", referencedProject.ProjectName));
}
itemGroup.Add(projectReference); itemGroup.Add(projectReference);
} }
} }
} }
private static string GetRelativePath(string fromFilePath, string toFilePath) static string GetRelativePath(string fromFilePath, string toFilePath)
{ {
Uri fromUri = new Uri(fromFilePath); Uri fromUri = new Uri(fromFilePath);
Uri toUri = new Uri(toFilePath); Uri toUri = new Uri(toFilePath);

2
ILSpy/Commands/SaveCodeContextMenuEntry.cs

@ -78,7 +78,7 @@ namespace ICSharpCode.ILSpy.TextView
{ {
var assemblies = selectedNodes.OfType<AssemblyTreeNode>() var assemblies = selectedNodes.OfType<AssemblyTreeNode>()
.Select(n => n.LoadedAssembly) .Select(n => n.LoadedAssembly)
.Where(a => a.IsLoadedAsValidAssembly).ToArray(); .Where(a => a.IsLoadedAsValidAssembly).ToList();
SolutionWriter.CreateSolution(tabPage, textView, selectedPath, currentLanguage, assemblies); SolutionWriter.CreateSolution(tabPage, textView, selectedPath, currentLanguage, assemblies);
} }
return; return;

46
ILSpy/SolutionWriter.cs

@ -56,7 +56,7 @@ namespace ICSharpCode.ILSpy
/// <exception cref="ArgumentNullException">Thrown when <paramref name="textView"/>> or /// <exception cref="ArgumentNullException">Thrown when <paramref name="textView"/>> or
/// <paramref name="assemblies"/> is null.</exception> /// <paramref name="assemblies"/> is null.</exception>
public static void CreateSolution(TabPageModel tabPage, DecompilerTextView textView, string solutionFilePath, public static void CreateSolution(TabPageModel tabPage, DecompilerTextView textView, string solutionFilePath,
Language language, IEnumerable<LoadedAssembly> assemblies) Language language, List<LoadedAssembly> assemblies)
{ {
if (textView == null) if (textView == null)
{ {
@ -77,7 +77,7 @@ namespace ICSharpCode.ILSpy
textView textView
.RunWithCancellation(ct => writer.CreateSolution(tabPage, assemblies, language, ct)) .RunWithCancellation(ct => writer.CreateSolution(tabPage, assemblies, language, ct))
.Then(output => textView.ShowText(output)) .Then(textView.ShowText)
.HandleExceptions(); .HandleExceptions();
} }
@ -94,41 +94,29 @@ namespace ICSharpCode.ILSpy
projects = new ConcurrentBag<ProjectItem>(); projects = new ConcurrentBag<ProjectItem>();
} }
async Task<AvalonEditTextOutput> CreateSolution(TabPageModel tabPage, IEnumerable<LoadedAssembly> assemblies, Language language, CancellationToken ct) async Task<AvalonEditTextOutput> CreateSolution(TabPageModel tabPage, List<LoadedAssembly> allAssemblies, Language language, CancellationToken ct)
{ {
var result = new AvalonEditTextOutput(); var result = new AvalonEditTextOutput();
var assembliesByShortName = assemblies.ToLookup(_ => _.ShortName); var assembliesByShortName = allAssemblies.GroupBy(_ => _.ShortName).ToDictionary(_ => _.Key, _ => _.ToList());
bool first = true; bool first = true;
bool abort = false; bool abort = false;
foreach (var item in assembliesByShortName) foreach (var (shortName, assemblies) in assembliesByShortName)
{ {
var enumerator = item.GetEnumerator(); if (assemblies.Count == 1)
if (!enumerator.MoveNext()) {
continue;
var firstAssembly = enumerator.Current;
if (!enumerator.MoveNext())
continue; continue;
}
if (first) if (first)
{ {
result.WriteLine("Duplicate assembly names selected, cannot generate a solution:"); result.WriteLine("Duplicate assembly names selected, cannot generate a solution:");
abort = true; abort = true;
first = false;
} }
result.Write("- " + firstAssembly.Text + " conflicts with "); result.WriteLine("- " + assemblies[0].Text + " conflicts with " + string.Join(", ", assemblies.Skip(1)));
first = true;
do
{
var asm = enumerator.Current;
if (!first)
result.Write(", ");
result.Write(asm.Text);
first = false;
} while (enumerator.MoveNext());
result.WriteLine();
first = false;
} }
if (abort) if (abort)
@ -141,7 +129,7 @@ namespace ICSharpCode.ILSpy
// Explicitly create an enumerable partitioner here to avoid Parallel.ForEach's special cases for lists, // Explicitly create an enumerable partitioner here to avoid Parallel.ForEach's special cases for lists,
// as those seem to use static partitioning which is inefficient if assemblies take differently // as those seem to use static partitioning which is inefficient if assemblies take differently
// long to decompile. // long to decompile.
await Task.Run(() => Parallel.ForEach(Partitioner.Create(assemblies), await Task.Run(() => Parallel.ForEach(Partitioner.Create(allAssemblies),
new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount, CancellationToken = ct }, new ParallelOptions { MaxDegreeOfParallelism = Environment.ProcessorCount, CancellationToken = ct },
item => WriteProject(tabPage, item, language, solutionDirectory, ct))) item => WriteProject(tabPage, item, language, solutionDirectory, ct)))
.ConfigureAwait(false); .ConfigureAwait(false);
@ -153,8 +141,8 @@ namespace ICSharpCode.ILSpy
} }
else else
{ {
await Task.Run(() => SolutionCreator.WriteSolutionFile(solutionFilePath, projects)) await Task.Run(() => SolutionCreator.WriteSolutionFile(solutionFilePath, projects.ToList()), ct)
.ConfigureAwait(false); .ConfigureAwait(false);
} }
} }
catch (AggregateException ae) catch (AggregateException ae)
@ -184,14 +172,14 @@ namespace ICSharpCode.ILSpy
if (statusOutput.Count == 0) if (statusOutput.Count == 0)
{ {
result.WriteLine("Successfully decompiled the following assemblies into Visual Studio projects:"); result.WriteLine("Successfully decompiled the following assemblies into Visual Studio projects:");
foreach (var item in assemblies.Select(n => n.Text.ToString())) foreach (var n in allAssemblies)
{ {
result.WriteLine(item); result.WriteLine(n.Text.ToString());
} }
result.WriteLine(); result.WriteLine();
if (assemblies.Count() == projects.Count) if (allAssemblies.Count == projects.Count)
{ {
result.WriteLine("Created the Visual Studio Solution file."); result.WriteLine("Created the Visual Studio Solution file.");
} }

Loading…
Cancel
Save