diff --git a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetServices.cs b/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetServices.cs index 61b3ad39d..48675a2f1 100644 --- a/ICSharpCode.Decompiler/CSharp/ProjectDecompiler/TargetServices.cs +++ b/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)); 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(".", "")); - if (versionNumber < 100) - versionNumber *= 10; + versionNumber = version.Major * 100 + version.Minor * 10; + if (version.Build > 0) + versionNumber += version.Build; } string frameworkProfile = frameworkParts.FirstOrDefault(a => a.StartsWith(ProfileToken, StringComparison.OrdinalIgnoreCase)); diff --git a/ICSharpCode.Decompiler/Solution/SolutionCreator.cs b/ICSharpCode.Decompiler/Solution/SolutionCreator.cs index 1619ea8f1..2b11a712f 100644 --- a/ICSharpCode.Decompiler/Solution/SolutionCreator.cs +++ b/ICSharpCode.Decompiler/Solution/SolutionCreator.cs @@ -29,10 +29,11 @@ namespace ICSharpCode.Decompiler.Solution /// 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"); /// /// Writes a solution file to the specified . + /// Also fixes intra-solution project references in the project files. /// /// The full path of the file to write. /// The projects contained in this solution. @@ -40,7 +41,7 @@ namespace ICSharpCode.Decompiler.Solution /// Thrown when is null or empty. /// Thrown when is null. /// Thrown when contains no items. - public static void WriteSolutionFile(string targetFile, IEnumerable projects) + public static void WriteSolutionFile(string targetFile, List projects) { if (string.IsNullOrWhiteSpace(targetFile)) { @@ -62,10 +63,10 @@ namespace ICSharpCode.Decompiler.Solution WriteSolutionFile(writer, projects, targetFile); } - FixProjectReferences(projects); + FixAllProjectReferences(projects); } - private static void WriteSolutionFile(TextWriter writer, IEnumerable projects, string solutionFilePath) + static void WriteSolutionFile(TextWriter writer, List projects, string solutionFilePath) { WriteHeader(writer); WriteProjects(writer, projects, solutionFilePath); @@ -90,7 +91,7 @@ namespace ICSharpCode.Decompiler.Solution writer.WriteLine("MinimumVisualStudioVersion = 10.0.40219.1"); } - private static void WriteProjects(TextWriter writer, IEnumerable projects, string solutionFilePath) + static void WriteProjects(TextWriter writer, List projects, string solutionFilePath) { foreach (var project in projects) { @@ -103,7 +104,7 @@ namespace ICSharpCode.Decompiler.Solution } } - private static IEnumerable WriteSolutionConfigurations(TextWriter writer, IEnumerable projects) + static List WriteSolutionConfigurations(TextWriter writer, List projects) { var platforms = projects.GroupBy(p => p.PlatformName).Select(g => g.Key).ToList(); @@ -125,10 +126,10 @@ namespace ICSharpCode.Decompiler.Solution return platforms; } - private static void WriteProjectConfigurations( + static void WriteProjectConfigurations( TextWriter writer, - IEnumerable projects, - IEnumerable solutionPlatforms) + List projects, + List solutionPlatforms) { writer.WriteLine("\tGlobalSection(ProjectConfigurationPlatforms) = postSolution"); @@ -152,47 +153,80 @@ namespace ICSharpCode.Decompiler.Solution writer.WriteLine("\tEndGlobalSection"); } - private static void FixProjectReferences(IEnumerable projects) + static void FixAllProjectReferences(List projects) { - var projectsMap = projects.ToDictionary(p => p.ProjectName, p => p); + var projectsMap = projects.ToDictionary( + p => p.ProjectName, + p => p); foreach (var project in projects) { 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 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 - .Elements(ProjectFileNamespace + "ItemGroup") - .Where(e => e.Elements(ProjectFileNamespace + "Reference").Any()); + .Elements(itemGroupTagName) + .Where(e => e.Elements(referenceTagName).Any()) + .ToList(); foreach (var itemGroup in referencesItemGroups) { - FixProjectReferences(project.FilePath, itemGroup, projectsMap); + FixProjectReferences(project.FilePath, itemGroup, projectsMap, sdkStyle); } projectDoc.Save(project.FilePath); } } - private static void FixProjectReferences(string projectFilePath, XElement itemGroup, IDictionary projects) + static void FixProjectReferences(string projectFilePath, XElement itemGroup, + Dictionary 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; if (assemblyName != null && projects.TryGetValue(assemblyName, out var referencedProject)) { item.Remove(); - var projectReference = new XElement(ProjectFileNamespace + "ProjectReference", - new XElement(ProjectFileNamespace + "Project", referencedProject.Guid.ToString("B").ToUpperInvariant()), - new XElement(ProjectFileNamespace + "Name", referencedProject.ProjectName)); - projectReference.SetAttributeValue("Include", GetRelativePath(projectFilePath, referencedProject.FilePath)); + var projectReference = new XElement( + projectReferenceTagName, + new XAttribute("Include", GetRelativePath(projectFilePath, referencedProject.FilePath))); + + // SDK-style projects do not use the and 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); } } } - private static string GetRelativePath(string fromFilePath, string toFilePath) + static string GetRelativePath(string fromFilePath, string toFilePath) { Uri fromUri = new Uri(fromFilePath); Uri toUri = new Uri(toFilePath); diff --git a/ILSpy/Commands/SaveCodeContextMenuEntry.cs b/ILSpy/Commands/SaveCodeContextMenuEntry.cs index 071f29599..2950a3200 100644 --- a/ILSpy/Commands/SaveCodeContextMenuEntry.cs +++ b/ILSpy/Commands/SaveCodeContextMenuEntry.cs @@ -78,7 +78,7 @@ namespace ICSharpCode.ILSpy.TextView { var assemblies = selectedNodes.OfType() .Select(n => n.LoadedAssembly) - .Where(a => a.IsLoadedAsValidAssembly).ToArray(); + .Where(a => a.IsLoadedAsValidAssembly).ToList(); SolutionWriter.CreateSolution(tabPage, textView, selectedPath, currentLanguage, assemblies); } return; diff --git a/ILSpy/SolutionWriter.cs b/ILSpy/SolutionWriter.cs index 29e8db8b0..62f6ef6c2 100644 --- a/ILSpy/SolutionWriter.cs +++ b/ILSpy/SolutionWriter.cs @@ -56,7 +56,7 @@ namespace ICSharpCode.ILSpy /// Thrown when > or /// is null. public static void CreateSolution(TabPageModel tabPage, DecompilerTextView textView, string solutionFilePath, - Language language, IEnumerable assemblies) + Language language, List assemblies) { if (textView == null) { @@ -77,7 +77,7 @@ namespace ICSharpCode.ILSpy textView .RunWithCancellation(ct => writer.CreateSolution(tabPage, assemblies, language, ct)) - .Then(output => textView.ShowText(output)) + .Then(textView.ShowText) .HandleExceptions(); } @@ -94,41 +94,29 @@ namespace ICSharpCode.ILSpy projects = new ConcurrentBag(); } - async Task CreateSolution(TabPageModel tabPage, IEnumerable assemblies, Language language, CancellationToken ct) + async Task CreateSolution(TabPageModel tabPage, List allAssemblies, Language language, CancellationToken ct) { var result = new AvalonEditTextOutput(); - var assembliesByShortName = assemblies.ToLookup(_ => _.ShortName); + var assembliesByShortName = allAssemblies.GroupBy(_ => _.ShortName).ToDictionary(_ => _.Key, _ => _.ToList()); bool first = true; bool abort = false; - foreach (var item in assembliesByShortName) + foreach (var (shortName, assemblies) in assembliesByShortName) { - var enumerator = item.GetEnumerator(); - if (!enumerator.MoveNext()) - continue; - var firstAssembly = enumerator.Current; - if (!enumerator.MoveNext()) + if (assemblies.Count == 1) + { continue; + } + if (first) { result.WriteLine("Duplicate assembly names selected, cannot generate a solution:"); abort = true; + first = false; } - result.Write("- " + firstAssembly.Text + " conflicts with "); - - first = true; - do - { - var asm = enumerator.Current; - if (!first) - result.Write(", "); - result.Write(asm.Text); - first = false; - } while (enumerator.MoveNext()); - result.WriteLine(); - first = false; + result.WriteLine("- " + assemblies[0].Text + " conflicts with " + string.Join(", ", assemblies.Skip(1))); } if (abort) @@ -141,7 +129,7 @@ namespace ICSharpCode.ILSpy // 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 // 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 }, item => WriteProject(tabPage, item, language, solutionDirectory, ct))) .ConfigureAwait(false); @@ -153,8 +141,8 @@ namespace ICSharpCode.ILSpy } else { - await Task.Run(() => SolutionCreator.WriteSolutionFile(solutionFilePath, projects)) - .ConfigureAwait(false); + await Task.Run(() => SolutionCreator.WriteSolutionFile(solutionFilePath, projects.ToList()), ct) + .ConfigureAwait(false); } } catch (AggregateException ae) @@ -184,14 +172,14 @@ namespace ICSharpCode.ILSpy if (statusOutput.Count == 0) { 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(); - if (assemblies.Count() == projects.Count) + if (allAssemblies.Count == projects.Count) { result.WriteLine("Created the Visual Studio Solution file."); }