You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
367 lines
13 KiB
367 lines
13 KiB
// Copyright (c) AlphaSierraPapa for the SharpDevelop Team (for details please see \doc\copyright.txt) |
|
// This code is distributed under the GNU LGPL (for details please see \doc\license.txt) |
|
|
|
using System; |
|
using System.Collections.Generic; |
|
using System.IO; |
|
using System.Linq; |
|
using System.Text; |
|
using System.Text.RegularExpressions; |
|
using System.Windows.Media.Effects; |
|
using ICSharpCode.Core; |
|
using Microsoft.Build.Exceptions; |
|
|
|
namespace ICSharpCode.SharpDevelop.Project |
|
{ |
|
sealed class SolutionLoader : IDisposable |
|
{ |
|
readonly FileName fileName; |
|
readonly TextReader textReader; |
|
string currentLine; |
|
int lineNumber; |
|
|
|
public SolutionLoader(TextReader textReader) |
|
{ |
|
this.textReader = textReader; |
|
NextLine(); |
|
} |
|
|
|
public SolutionLoader(FileName fileName) |
|
{ |
|
this.fileName = fileName; |
|
// read solution files using system encoding, but detect UTF8 if BOM is present |
|
this.textReader = new StreamReader(fileName, Encoding.Default, true); |
|
NextLine(); |
|
} |
|
|
|
public void Dispose() |
|
{ |
|
textReader.Dispose(); |
|
} |
|
|
|
void NextLine() |
|
{ |
|
do { |
|
currentLine = textReader.ReadLine(); |
|
lineNumber++; |
|
} while (currentLine != null && (currentLine.Length == 0 || currentLine[0] == '#')); |
|
} |
|
|
|
InvalidProjectFileException Error() |
|
{ |
|
return Error("${res:SharpDevelop.Solution.InvalidSolutionFile}"); |
|
} |
|
|
|
InvalidProjectFileException Error(string message, params object[] formatItems) |
|
{ |
|
if (formatItems.Length > 0) |
|
message = StringParser.Format(message, formatItems); |
|
else |
|
message = StringParser.Parse(message); |
|
return new InvalidProjectFileException(fileName ?? string.Empty, lineNumber, 1, lineNumber, currentLine.Length + 1, message, string.Empty, string.Empty, string.Empty); |
|
} |
|
|
|
#region ReadSolution |
|
public void ReadSolution(Solution solution, IProgressMonitor progress) |
|
{ |
|
ReadFormatHeader(); |
|
|
|
// Read solution folder and project entries: |
|
var solutionEntries = new List<ProjectLoadInformation>(); |
|
var projectInfoDict = new Dictionary<Guid, ProjectLoadInformation>(); |
|
var solutionFolderDict = new Dictionary<Guid, SolutionFolder>(); |
|
int projectCount = 0; |
|
bool fixedGuidConflicts = false; |
|
|
|
ProjectLoadInformation information; |
|
while ((information = ReadProjectEntry(solution)) != null) { |
|
solutionEntries.Add(information); |
|
if (projectInfoDict.ContainsKey(information.IdGuid)) { |
|
// resolve GUID conflicts |
|
information.IdGuid = Guid.NewGuid(); |
|
fixedGuidConflicts = true; |
|
} |
|
projectInfoDict.Add(information.IdGuid, information); |
|
|
|
if (information.TypeGuid == ProjectTypeGuids.SolutionFolder) { |
|
solutionFolderDict.Add(information.IdGuid, CreateSolutionFolder(solution, information)); |
|
} else { |
|
projectCount++; |
|
} |
|
} |
|
|
|
progress.CancellationToken.ThrowIfCancellationRequested(); |
|
|
|
// Read global sections: |
|
if (currentLine != "Global") |
|
throw Error(); |
|
NextLine(); |
|
|
|
Dictionary<Guid, SolutionFolder> guidToParentFolderDict = null; |
|
|
|
SolutionSection section; |
|
while ((section = ReadSection(isGlobal: true)) != null) { |
|
switch (section.SectionName) { |
|
case "SolutionConfigurationPlatforms": |
|
var configurations = LoadSolutionConfigurations(section); |
|
foreach (var config in configurations.Select(c => c.Configuration).Distinct(ConfigurationAndPlatform.ConfigurationNameComparer)) |
|
solution.ConfigurationNames.Add(config, null); |
|
foreach (var platform in configurations.Select(c => c.Platform).Distinct(ConfigurationAndPlatform.ConfigurationNameComparer)) |
|
solution.PlatformNames.Add(platform, null); |
|
break; |
|
case "ProjectConfigurationPlatforms": |
|
LoadProjectConfigurations(section, projectInfoDict); |
|
break; |
|
case "NestedProjects": |
|
guidToParentFolderDict = LoadNesting(section, solutionFolderDict); |
|
break; |
|
default: |
|
solution.GlobalSections.Add(section); |
|
break; |
|
} |
|
} |
|
if (currentLine != "EndGlobal") |
|
throw Error(); |
|
NextLine(); |
|
if (currentLine != null) |
|
throw Error(); |
|
|
|
solution.LoadPreferences(); |
|
|
|
// Now that the project configurations have been set, we can actually load the projects: |
|
int projectsLoaded = 0; |
|
foreach (var projectInfo in solutionEntries) { |
|
ISolutionItem solutionItem; |
|
if (projectInfo.TypeGuid == ProjectTypeGuids.SolutionFolder) { |
|
solutionItem = solutionFolderDict[projectInfo.IdGuid]; |
|
} else { |
|
// Load project: |
|
projectInfo.ActiveProjectConfiguration = projectInfo.ConfigurationMapping.GetProjectConfiguration(solution.ActiveConfiguration); |
|
progress.TaskName = "Loading " + projectInfo.ProjectName; |
|
using (projectInfo.ProgressMonitor = progress.CreateSubTask(1.0 / projectCount)) { |
|
solutionItem = ProjectBindingService.LoadProject(projectInfo); |
|
} |
|
projectsLoaded++; |
|
progress.Progress = (double)projectsLoaded / projectCount; |
|
} |
|
// Add solutionItem to solution: |
|
SolutionFolder folder; |
|
if (guidToParentFolderDict != null && guidToParentFolderDict.TryGetValue(projectInfo.IdGuid, out folder)) { |
|
folder.Items.Add(solutionItem); |
|
} else { |
|
solution.Items.Add(solutionItem); |
|
} |
|
} |
|
|
|
solution.IsDirty = fixedGuidConflicts; // reset IsDirty=false unless we've fixed GUID conflicts |
|
} |
|
#endregion |
|
|
|
#region ReadFormatHeader |
|
static Regex versionPattern = new Regex(@"^Microsoft Visual Studio Solution File, Format Version\s+(?<Version>[\d\.]+)\s*$"); |
|
|
|
public SolutionFormatVersion ReadFormatHeader() |
|
{ |
|
Match match = versionPattern.Match(currentLine); |
|
if (!match.Success) |
|
throw Error(); |
|
|
|
SolutionFormatVersion version; |
|
switch (match.Result("${Version}")) { |
|
case "7.00": |
|
case "8.00": |
|
throw Error("${res:SharpDevelop.Solution.CannotLoadOldSolution}"); |
|
case "9.00": |
|
version = SolutionFormatVersion.VS2005; |
|
break; |
|
case "10.00": |
|
version = SolutionFormatVersion.VS2008; |
|
break; |
|
case "11.00": |
|
version = SolutionFormatVersion.VS2010; |
|
break; |
|
case "12.00": |
|
version = SolutionFormatVersion.VS2012; |
|
break; |
|
default: |
|
throw Error("${res:SharpDevelop.Solution.UnknownSolutionVersion}", match.Result("${Version}")); |
|
} |
|
NextLine(); |
|
return version; |
|
} |
|
#endregion |
|
|
|
#region ReadSection |
|
void ReadSectionEntries(SolutionSection section) |
|
{ |
|
while (currentLine != null) { |
|
int pos = currentLine.IndexOf('='); |
|
if (pos < 0) |
|
break; // end of section |
|
string key = currentLine.Substring(0, pos).Trim(); |
|
string value = currentLine.Substring(pos + 1).Trim(); |
|
section.Add(key, value); |
|
NextLine(); |
|
} |
|
} |
|
|
|
static readonly Regex sectionHeaderPattern = new Regex("^\\s*(Global|Project)Section\\((?<Name>.*)\\)\\s*=\\s*(?<Type>.*)\\s*$"); |
|
|
|
public SolutionSection ReadSection(bool isGlobal) |
|
{ |
|
if (currentLine == null) |
|
return null; |
|
Match match = sectionHeaderPattern.Match(currentLine); |
|
if (!match.Success) |
|
return null; |
|
NextLine(); |
|
|
|
SolutionSection section = new SolutionSection(match.Groups["Name"].Value, match.Groups["Type"].Value); |
|
ReadSectionEntries(section); |
|
string expectedLine = isGlobal ? "EndGlobalSection" : "EndProjectSection"; |
|
if ((currentLine ?? string.Empty).Trim() != expectedLine) |
|
throw Error("Expected " + expectedLine); |
|
NextLine(); |
|
return section; |
|
} |
|
#endregion |
|
|
|
#region ReadProjectEntry |
|
static readonly Regex projectLinePattern = new Regex("^\\s*Project\\(\"(?<TypeGuid>.*)\"\\)\\s+=\\s+\"(?<Title>.*)\",\\s*\"(?<Location>.*)\",\\s*\"(?<IdGuid>.*)\"\\s*$"); |
|
|
|
public ProjectLoadInformation ReadProjectEntry(ISolution parentSolution) |
|
{ |
|
if (currentLine == null) |
|
return null; |
|
Match match = projectLinePattern.Match(currentLine); |
|
if (!match.Success) |
|
return null; |
|
NextLine(); |
|
string title = match.Groups["Title"].Value; |
|
string location = match.Groups["Location"].Value; |
|
FileName projectFileName = FileName.Create(Path.Combine(parentSolution.Directory, location)); |
|
var loadInformation = new ProjectLoadInformation(parentSolution, projectFileName, title); |
|
loadInformation.TypeGuid = ParseGuidDefaultEmpty(match.Groups["TypeGuid"].Value); |
|
loadInformation.IdGuid = ParseGuidDefaultEmpty(match.Groups["IdGuid"].Value); |
|
SolutionSection section; |
|
while ((section = ReadSection(isGlobal: false)) != null) { |
|
loadInformation.ProjectSections.Add(section); |
|
} |
|
if (currentLine != "EndProject") |
|
throw Error(); |
|
NextLine(); |
|
return loadInformation; |
|
} |
|
|
|
static Guid ParseGuidDefaultEmpty(string value) |
|
{ |
|
Guid guid; |
|
if (Guid.TryParse(value, out guid)) |
|
return guid; |
|
else |
|
return Guid.Empty; |
|
} |
|
#endregion |
|
|
|
#region Load Configurations |
|
IEnumerable<ConfigurationAndPlatform> LoadSolutionConfigurations(IEnumerable<KeyValuePair<string, string>> section) |
|
{ |
|
// Entries in the section look like this: 'Debug|Any CPU = Debug|Any CPU' |
|
return section.Select(e => ConfigurationAndPlatform.FromKey(e.Key)); |
|
} |
|
|
|
void LoadProjectConfigurations(SolutionSection section, Dictionary<Guid, ProjectLoadInformation> projectInfoDict) |
|
{ |
|
foreach (var pair in section) { |
|
// pair is an entry like this: '{35CEF10F-2D4C-45F2-9DD1-161E0FEC583C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU' |
|
if (pair.Key.EndsWith(".ActiveCfg", StringComparison.OrdinalIgnoreCase)) { |
|
Guid guid; |
|
ConfigurationAndPlatform solutionConfig; |
|
if (!TryParseProjectConfigurationKey(pair.Key, out guid, out solutionConfig)) |
|
continue; |
|
ProjectLoadInformation projectInfo; |
|
if (!projectInfoDict.TryGetValue(guid, out projectInfo)) |
|
continue; |
|
var projectConfig = ConfigurationAndPlatform.FromKey(pair.Value); |
|
if (projectConfig == default(ConfigurationAndPlatform)) |
|
continue; |
|
projectInfo.ConfigurationMapping.SetProjectConfiguration(solutionConfig, projectConfig); |
|
// Disable build if we see a '.ActiveCfg' entry. |
|
projectInfo.ConfigurationMapping.SetBuildEnabled(solutionConfig, false); |
|
} |
|
} |
|
// Enable build/deploy if we see the corresponding entries: |
|
foreach (var pair in section) { |
|
// pair is an entry like this: '{35CEF10F-2D4C-45F2-9DD1-161E0FEC583C}.Debug|Any CPU.Build.0 = Debug|Any CPU' |
|
Guid guid; |
|
ConfigurationAndPlatform solutionConfig; |
|
if (!TryParseProjectConfigurationKey(pair.Key, out guid, out solutionConfig)) |
|
continue; |
|
ProjectLoadInformation projectInfo; |
|
if (!projectInfoDict.TryGetValue(guid, out projectInfo)) |
|
continue; |
|
if (pair.Key.EndsWith(".Build.0", StringComparison.OrdinalIgnoreCase)) { |
|
projectInfo.ConfigurationMapping.SetBuildEnabled(solutionConfig, true); |
|
} else if (pair.Key.EndsWith(".Deploy.0", StringComparison.OrdinalIgnoreCase)) { |
|
projectInfo.ConfigurationMapping.SetDeployEnabled(solutionConfig, true); |
|
} |
|
} |
|
} |
|
|
|
bool TryParseProjectConfigurationKey(string key, out Guid guid, out ConfigurationAndPlatform config) |
|
{ |
|
guid = default(Guid); |
|
config = default(ConfigurationAndPlatform); |
|
|
|
int firstDot = key.IndexOf('.'); |
|
int secondDot = key.IndexOf('.', firstDot + 1); |
|
if (firstDot < 0 || secondDot < 0) |
|
return false; |
|
|
|
string guidText = key.Substring(0, firstDot); |
|
if (!Guid.TryParse(guidText, out guid)) |
|
return false; |
|
|
|
string configKey = key.Substring(firstDot + 1, secondDot - (firstDot + 1)); |
|
config = ConfigurationAndPlatform.FromKey(configKey); |
|
return config != default(ConfigurationAndPlatform); |
|
} |
|
#endregion |
|
|
|
#region Load Nesting |
|
SolutionFolder CreateSolutionFolder(Solution solution, ProjectLoadInformation information) |
|
{ |
|
var folder = new SolutionFolder(solution, information.IdGuid); |
|
folder.Name = information.ProjectName; |
|
// Add solution items: |
|
var solutionItemsSection = information.ProjectSections.FirstOrDefault(s => s.SectionName == "SolutionItems"); |
|
if (solutionItemsSection != null) { |
|
foreach (string location in solutionItemsSection.Values) { |
|
var fileItem = new SolutionFileItem(solution); |
|
fileItem.FileName = FileName.Create(Path.Combine(information.Solution.Directory, location)); |
|
folder.Items.Add(fileItem); |
|
} |
|
} |
|
return folder; |
|
} |
|
|
|
/// <summary> |
|
/// Converts the 'NestedProjects' section into a dictionary from project GUID to parent solution folder. |
|
/// </summary> |
|
Dictionary<Guid, SolutionFolder> LoadNesting(SolutionSection section, IReadOnlyDictionary<Guid, SolutionFolder> solutionFolderDict) |
|
{ |
|
var result = new Dictionary<Guid, SolutionFolder>(); |
|
foreach (var entry in section) { |
|
Guid idGuid; |
|
Guid parentGuid; |
|
if (Guid.TryParse(entry.Key, out idGuid) && Guid.TryParse(entry.Value, out parentGuid)) { |
|
SolutionFolder parentFolder; |
|
if (solutionFolderDict.TryGetValue(parentGuid, out parentFolder)) |
|
result[idGuid] = parentFolder; |
|
} |
|
} |
|
return result; |
|
} |
|
#endregion |
|
} |
|
}
|
|
|