1278 lines
46 KiB
C#
1278 lines
46 KiB
C#
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics.CodeAnalysis;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text.RegularExpressions;
|
|
using System.Xml;
|
|
|
|
namespace EpicGames.Core
|
|
{
|
|
/// <summary>
|
|
/// Exception parsing a csproj file
|
|
/// </summary>
|
|
public sealed class CsProjectParseException : Exception
|
|
{
|
|
internal CsProjectParseException(string? message, Exception? exception = null) : base(message, exception)
|
|
{
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Basic information from a preprocessed C# project file. Supports reading a project file, expanding simple conditions in it, parsing property values, assembly references and references to other projects.
|
|
/// </summary>
|
|
public class CsProjectInfo
|
|
{
|
|
/// <summary>
|
|
/// Evaluated properties from the project file
|
|
/// </summary>
|
|
public Dictionary<string, string> Properties { get; }
|
|
|
|
/// <summary>
|
|
/// Mapping of referenced assemblies to their 'CopyLocal' (aka 'Private') setting.
|
|
/// </summary>
|
|
public Dictionary<FileReference, bool> References { get; } = [];
|
|
|
|
/// <summary>
|
|
/// Mapping of referenced projects to their 'CopyLocal' (aka 'Private') setting.
|
|
/// </summary>
|
|
public Dictionary<FileReference, bool> ProjectReferences { get; } = [];
|
|
|
|
/// <summary>
|
|
/// List of compile references in the project.
|
|
/// </summary>
|
|
public List<FileReference> CompileReferences { get; } = [];
|
|
|
|
/// <summary>
|
|
/// Mapping of content IF they are flagged Always or Newer
|
|
/// </summary>
|
|
public Dictionary<FileReference, bool> ContentReferences { get; } = [];
|
|
|
|
/// <summary>
|
|
/// Path to the CSProject file
|
|
/// </summary>
|
|
public FileReference ProjectPath { get; }
|
|
|
|
/// <summary>
|
|
/// Constructor
|
|
/// </summary>
|
|
/// <param name="inProperties">Initial mapping of property names to values</param>
|
|
/// <param name="inProjectPath"></param>
|
|
CsProjectInfo(Dictionary<string, string> inProperties, FileReference inProjectPath)
|
|
{
|
|
ProjectPath = inProjectPath;
|
|
Properties = new Dictionary<string, string>(inProperties);
|
|
Properties.TryAdd("MSBuildProjectFile", inProjectPath.FullName);
|
|
Properties.TryAdd("MSBuildThisFileDirectory", inProjectPath.Directory.FullName);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the ouptut file for this project
|
|
/// </summary>
|
|
/// <param name="file">If successful, receives the assembly path</param>
|
|
/// <returns>True if the output file was found</returns>
|
|
public bool TryGetOutputFile([NotNullWhen(true)] out FileReference? file)
|
|
{
|
|
DirectoryReference? outputDir;
|
|
if(!TryGetOutputDir(out outputDir))
|
|
{
|
|
file = null;
|
|
return false;
|
|
}
|
|
|
|
string? assemblyName;
|
|
if(!TryGetAssemblyName(out assemblyName))
|
|
{
|
|
file = null;
|
|
return false;
|
|
}
|
|
|
|
file = FileReference.Combine(outputDir, assemblyName + ".dll");
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns whether or not this project is a modern dotnet project
|
|
/// </summary>
|
|
/// <returns>True if this is a netcore project</returns>
|
|
private bool IsNetCoreProject()
|
|
{
|
|
string? framework;
|
|
return Properties.TryGetValue("TargetFramework", out framework)
|
|
&& (framework.StartsWith("netcoreapp", StringComparison.Ordinal) || Regex.Match(framework, @"net\d+\.0").Success);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolve the project's output directory
|
|
/// </summary>
|
|
/// <param name="baseDirectory">Base directory to resolve relative paths to</param>
|
|
/// <returns>The configured output directory</returns>
|
|
public DirectoryReference GetOutputDir(DirectoryReference baseDirectory)
|
|
{
|
|
string? outputPath;
|
|
if (Properties.TryGetValue("OutputPath", out outputPath))
|
|
{
|
|
return DirectoryReference.Combine(baseDirectory, outputPath);
|
|
}
|
|
else if (IsNetCoreProject())
|
|
{
|
|
string configuration = Properties.TryGetValue("Configuration", out string? value) ? value : "Development";
|
|
return !Properties.TryGetValue("AppendTargetFrameworkToOutputPath", out string? appendFrameworkStr) || appendFrameworkStr.Equals("true", StringComparison.OrdinalIgnoreCase)
|
|
? DirectoryReference.Combine(baseDirectory, "bin", configuration, Properties["TargetFramework"])
|
|
: DirectoryReference.Combine(baseDirectory, "bin", configuration);
|
|
}
|
|
else
|
|
{
|
|
return baseDirectory;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Resolve the project's output directory
|
|
/// </summary>
|
|
/// <param name="outputDir">If successful, receives the output directory</param>
|
|
/// <returns>True if the output directory was found</returns>
|
|
public bool TryGetOutputDir([NotNullWhen(true)] out DirectoryReference? outputDir)
|
|
{
|
|
string? outputPath;
|
|
if (Properties.TryGetValue("OutputPath", out outputPath))
|
|
{
|
|
outputDir = DirectoryReference.Combine(ProjectPath.Directory, outputPath);
|
|
return true;
|
|
}
|
|
else if (IsNetCoreProject())
|
|
{
|
|
string configuration = Properties.ContainsKey("Configuration") ? Properties["Configuration"] : "Development";
|
|
outputDir = DirectoryReference.Combine(ProjectPath.Directory, "bin", configuration, Properties["TargetFramework"]);
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
outputDir = null;
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the assembly name used by this project
|
|
/// </summary>
|
|
/// <returns></returns>
|
|
public bool TryGetAssemblyName([NotNullWhen(true)] out string? assemblyName)
|
|
{
|
|
if (Properties.TryGetValue("AssemblyName", out assemblyName))
|
|
{
|
|
return true;
|
|
}
|
|
else if (IsNetCoreProject())
|
|
{
|
|
assemblyName = ProjectPath.GetFileNameWithoutExtension();
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds all build products from this project. This includes content and other assemblies marked to be copied local.
|
|
/// </summary>
|
|
/// <param name="outputDir">The output directory</param>
|
|
/// <param name="buildProducts">Receives the set of build products</param>
|
|
/// <param name="projectFileToInfo">Map of project file to information, to resolve build products from referenced projects copied locally</param>
|
|
public void FindBuildProducts(DirectoryReference outputDir, HashSet<FileReference> buildProducts, Dictionary<FileReference, CsProjectInfo> projectFileToInfo)
|
|
{
|
|
// Add the standard build products
|
|
FindCompiledBuildProducts(outputDir, buildProducts);
|
|
|
|
// Add the referenced assemblies which are marked to be copied into the output directory. This only happens for the main project, and does not happen for referenced projects.
|
|
foreach(KeyValuePair<FileReference, bool> reference in References)
|
|
{
|
|
if (reference.Value)
|
|
{
|
|
FileReference outputFile = FileReference.Combine(outputDir, reference.Key.GetFileName());
|
|
AddReferencedAssemblyAndSupportFiles(outputFile, buildProducts);
|
|
}
|
|
}
|
|
|
|
// Copy the build products for any referenced projects. Note that this does NOT operate recursively.
|
|
foreach(KeyValuePair<FileReference, bool> projectReference in ProjectReferences)
|
|
{
|
|
CsProjectInfo? otherProjectInfo;
|
|
if(projectFileToInfo.TryGetValue(projectReference.Key, out otherProjectInfo))
|
|
{
|
|
otherProjectInfo.FindCompiledBuildProducts(outputDir, buildProducts);
|
|
}
|
|
}
|
|
|
|
// Add any copied content. This DOES operate recursively.
|
|
FindCopiedContent(outputDir, buildProducts, projectFileToInfo);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines all the compiled build products (executable, etc...) directly built from this project.
|
|
/// </summary>
|
|
/// <param name="outputDir">The output directory</param>
|
|
/// <param name="buildProducts">Receives the set of build products</param>
|
|
public void FindCompiledBuildProducts(DirectoryReference outputDir, HashSet<FileReference> buildProducts)
|
|
{
|
|
string? outputType, assemblyName;
|
|
if (Properties.TryGetValue("OutputType", out outputType) && TryGetAssemblyName(out assemblyName))
|
|
{
|
|
switch (outputType)
|
|
{
|
|
case "Exe":
|
|
case "WinExe":
|
|
string executableExtension = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : "";
|
|
buildProducts.Add(FileReference.Combine(outputDir, assemblyName + executableExtension));
|
|
// dotnet outputs a apphost executable and a dll with the actual assembly
|
|
AddOptionalBuildProduct(FileReference.Combine(outputDir, assemblyName + ".dll"), buildProducts);
|
|
AddOptionalBuildProduct(FileReference.Combine(outputDir, assemblyName + ".pdb"), buildProducts);
|
|
AddOptionalBuildProduct(FileReference.Combine(outputDir, assemblyName + ".exe.config"), buildProducts);
|
|
AddOptionalBuildProduct(FileReference.Combine(outputDir, assemblyName + ".exe.mdb"), buildProducts);
|
|
break;
|
|
case "Library":
|
|
buildProducts.Add(FileReference.Combine(outputDir, assemblyName + ".dll"));
|
|
AddOptionalBuildProduct(FileReference.Combine(outputDir, assemblyName + ".pdb"), buildProducts);
|
|
AddOptionalBuildProduct(FileReference.Combine(outputDir, assemblyName + ".dll.config"), buildProducts);
|
|
AddOptionalBuildProduct(FileReference.Combine(outputDir, assemblyName + ".dll.mdb"), buildProducts);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds all content which will be copied into the output directory for this project. This includes content from any project references as "copy local" recursively (though MSBuild only traverses a single reference for actual binaries, in such cases)
|
|
/// </summary>
|
|
/// <param name="outputDir">The output directory</param>
|
|
/// <param name="outputFiles">Receives the set of build products</param>
|
|
/// <param name="projectFileToInfo">Map of project file to information, to resolve build products from referenced projects copied locally</param>
|
|
private void FindCopiedContent(DirectoryReference outputDir, HashSet<FileReference> outputFiles, Dictionary<FileReference, CsProjectInfo> projectFileToInfo)
|
|
{
|
|
// Copy any referenced projects too.
|
|
foreach(KeyValuePair<FileReference, bool> projectReference in ProjectReferences)
|
|
{
|
|
CsProjectInfo? otherProjectInfo;
|
|
if(projectFileToInfo.TryGetValue(projectReference.Key, out otherProjectInfo))
|
|
{
|
|
otherProjectInfo.FindCopiedContent(outputDir, outputFiles, projectFileToInfo);
|
|
}
|
|
}
|
|
|
|
// Add the content which is copied to the output directory
|
|
foreach (KeyValuePair<FileReference, bool> contentReference in ContentReferences)
|
|
{
|
|
FileReference contentFile = contentReference.Key;
|
|
if (contentReference.Value)
|
|
{
|
|
outputFiles.Add(FileReference.Combine(outputDir, contentFile.GetFileName()));
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds the given file and any additional build products to the output set
|
|
/// </summary>
|
|
/// <param name="outputFile">The assembly to add</param>
|
|
/// <param name="outputFiles">Set to receive the file and support files</param>
|
|
public static void AddReferencedAssemblyAndSupportFiles(FileReference outputFile, HashSet<FileReference> outputFiles)
|
|
{
|
|
outputFiles.Add(outputFile);
|
|
|
|
FileReference symbolFile = outputFile.ChangeExtension(".pdb");
|
|
if (FileReference.Exists(symbolFile))
|
|
{
|
|
outputFiles.Add(symbolFile);
|
|
}
|
|
|
|
FileReference documentationFile = outputFile.ChangeExtension(".xml");
|
|
if (FileReference.Exists(documentationFile))
|
|
{
|
|
outputFiles.Add(documentationFile);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if a TargetFramework is a .NET core framework
|
|
/// </summary>
|
|
/// <returns>True if the TargetFramework is a .NET core framework</returns>
|
|
static bool IsDotNETCoreFramework(string targetFramework)
|
|
{
|
|
if (targetFramework.ToLower().Contains("netstandard", StringComparison.Ordinal) || targetFramework.ToLower().Contains("netcoreapp", StringComparison.Ordinal))
|
|
{
|
|
return true;
|
|
}
|
|
else if (targetFramework.StartsWith("net", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
string[] versionSplit = targetFramework.Substring(3).Split('.');
|
|
if (versionSplit.Length >= 1 && Int32.TryParse(versionSplit[0], out int majorVersion))
|
|
{
|
|
return majorVersion >= 5;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Determines if this project is a .NET core project
|
|
/// </summary>
|
|
/// <returns>True if the project is a .NET core project</returns>
|
|
public bool IsDotNETCoreProject()
|
|
{
|
|
if (Properties.TryGetValue("TargetFramework", out string? targetFramework))
|
|
{
|
|
return IsDotNETCoreFramework(targetFramework);
|
|
}
|
|
|
|
if (Properties.TryGetValue("TargetFrameworks", out string? targetFrameworks))
|
|
{
|
|
return targetFramework?.Split(';', StringSplitOptions.RemoveEmptyEntries).Any(x => IsDotNETCoreFramework(x.Trim())) ?? false;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a build product to the output list if it exists
|
|
/// </summary>
|
|
/// <param name="buildProduct">The build product to add</param>
|
|
/// <param name="buildProducts">List of output build products</param>
|
|
public static void AddOptionalBuildProduct(FileReference buildProduct, HashSet<FileReference> buildProducts)
|
|
{
|
|
if (FileReference.Exists(buildProduct))
|
|
{
|
|
buildProducts.Add(buildProduct);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses supported elements from the children of the provided note. May recurse
|
|
/// for conditional elements.
|
|
/// </summary>
|
|
/// <param name="node"></param>
|
|
/// <param name="projectInfo"></param>
|
|
static void ParseNode(XmlNode node, CsProjectInfo projectInfo)
|
|
{
|
|
foreach (XmlElement element in node.ChildNodes.OfType<XmlElement>())
|
|
{
|
|
switch (element.Name)
|
|
{
|
|
case "Import":
|
|
if (EvaluateCondition(element, projectInfo))
|
|
{
|
|
ParseImportProject(element, projectInfo);
|
|
}
|
|
break;
|
|
case "PropertyGroup":
|
|
if (EvaluateCondition(element, projectInfo))
|
|
{
|
|
ParsePropertyGroup(element, projectInfo);
|
|
}
|
|
break;
|
|
case "ItemGroup":
|
|
if (EvaluateCondition(element, projectInfo))
|
|
{
|
|
ParseItemGroup(projectInfo.ProjectPath.Directory, element, projectInfo);
|
|
}
|
|
break;
|
|
case "Choose":
|
|
case "When":
|
|
if (EvaluateCondition(element, projectInfo))
|
|
{
|
|
ParseNode(element, projectInfo);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads project information for the given file.
|
|
/// </summary>
|
|
/// <param name="file">The project file to read</param>
|
|
/// <param name="properties">Initial set of property values</param>
|
|
/// <returns>The parsed project info</returns>
|
|
public static CsProjectInfo Read(FileReference file, Dictionary<string, string> properties)
|
|
{
|
|
CsProjectInfo? project;
|
|
if(!TryRead(file, properties, out project))
|
|
{
|
|
throw new Exception(String.Format("Unable to read '{0}'", file));
|
|
}
|
|
return project;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Attempts to read project information for the given file.
|
|
/// </summary>
|
|
/// <param name="file">The project file to read</param>
|
|
/// <param name="properties">Initial set of property values</param>
|
|
/// <param name="outProjectInfo">If successful, the parsed project info</param>
|
|
/// <returns>True if the project was read successfully, false otherwise</returns>
|
|
public static bool TryRead(FileReference file, Dictionary<string, string> properties, [NotNullWhen(true)] out CsProjectInfo? outProjectInfo)
|
|
{
|
|
// Read the project file
|
|
XmlDocument document = new XmlDocument();
|
|
document.Load(file.FullName);
|
|
|
|
// Check the root element is the right type
|
|
// HashSet<FileReference> ProjectBuildProducts = new HashSet<FileReference>();
|
|
if (document.DocumentElement!.Name != "Project")
|
|
{
|
|
outProjectInfo = null;
|
|
return false;
|
|
}
|
|
|
|
// Parse the basic structure of the document, updating properties and recursing into other referenced projects as we go
|
|
CsProjectInfo projectInfo = new CsProjectInfo(properties, file);
|
|
|
|
// Parse elements in the root node
|
|
try
|
|
{
|
|
ParseNode(document.DocumentElement, projectInfo);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
throw new CsProjectParseException($"Error parsing {file}: {ex.Message}", ex);
|
|
}
|
|
|
|
// Return the complete project
|
|
outProjectInfo = projectInfo;
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a 'Import' emenebt
|
|
/// </summary>
|
|
/// <param name="element">The element</param>
|
|
/// <param name="projectInfo">Dictionary mapping property names to values</param>
|
|
static void ParseImportProject(XmlElement element, CsProjectInfo projectInfo)
|
|
{
|
|
string importProjectStr = element.GetAttribute("Project");
|
|
foreach (Match match in Regex.Matches(importProjectStr, @"(\$\(.*?\))").OfType<Match>())
|
|
{
|
|
importProjectStr = importProjectStr.Replace(match.Value, projectInfo.Properties.GetValueOrDefault(match.Value.Substring(2, match.Length - 3), String.Empty), StringComparison.Ordinal);
|
|
}
|
|
FileReference importProject = Path.IsPathFullyQualified(importProjectStr) ? new FileReference(importProjectStr) : FileReference.Combine(projectInfo.ProjectPath.Directory, importProjectStr);
|
|
if (!FileReference.Exists(importProject))
|
|
{
|
|
return;
|
|
}
|
|
XmlDocument document = new XmlDocument();
|
|
document.Load(importProject.FullName);
|
|
if (document.DocumentElement?.Name == "Project")
|
|
{
|
|
projectInfo.Properties.TryGetValue("MSBuildThisFileDirectory", out string? msBuildThisFileDirectory);
|
|
projectInfo.Properties.Remove("MSBuildThisFileDirectory");
|
|
projectInfo.Properties.Add("MSBuildThisFileDirectory", importProject.Directory.FullName);
|
|
try
|
|
{
|
|
ParseNode(document.DocumentElement, projectInfo);
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// Ignore issues in imported .props files
|
|
}
|
|
projectInfo.Properties.Remove("MSBuildThisFileDirectory");
|
|
if (msBuildThisFileDirectory != null)
|
|
{
|
|
projectInfo.Properties.Add("MSBuildThisFileDirectory", msBuildThisFileDirectory);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a 'PropertyGroup' element.
|
|
/// </summary>
|
|
/// <param name="parentElement">The parent 'PropertyGroup' element</param>
|
|
/// <param name="projectInfo">Dictionary mapping property names to values</param>
|
|
static void ParsePropertyGroup(XmlElement parentElement, CsProjectInfo projectInfo)
|
|
{
|
|
// We need to know the overridden output type and output path for the selected configuration.
|
|
foreach (XmlElement element in parentElement.ChildNodes.OfType<XmlElement>())
|
|
{
|
|
// Common properties used by UnrealEngine.csproj.props, manually handle because the parsing is awful
|
|
switch (element.Name)
|
|
{
|
|
case "IsLinux": projectInfo.Properties[element.Name] = OperatingSystem.IsLinux().ToString(); continue;
|
|
case "IsMacOS": projectInfo.Properties[element.Name] = OperatingSystem.IsMacOS().ToString(); continue;
|
|
case "IsWindows": projectInfo.Properties[element.Name] = OperatingSystem.IsWindows().ToString(); continue;
|
|
case "IsX64": projectInfo.Properties[element.Name] = (RuntimeInformation.OSArchitecture == Architecture.X64).ToString(); continue;
|
|
case "IsArm64": projectInfo.Properties[element.Name] = (RuntimeInformation.OSArchitecture == Architecture.Arm64).ToString(); continue;
|
|
case "OSArchitecture": projectInfo.Properties[element.Name] = RuntimeInformation.OSArchitecture.ToString(); continue;
|
|
case "EngineDirectory": projectInfo.Properties.TryAdd(element.Name, String.Empty); continue;
|
|
case "EngineDir":
|
|
case "EnginePath":
|
|
{
|
|
projectInfo.Properties[element.Name] = String.Empty;
|
|
if (projectInfo.Properties.TryGetValue("EngineDirectory", out string? engineDirectory) && !String.IsNullOrEmpty(engineDirectory))
|
|
{
|
|
projectInfo.Properties[element.Name] = engineDirectory;
|
|
}
|
|
continue;
|
|
}
|
|
case "IsEngineProject":
|
|
{
|
|
projectInfo.Properties[element.Name] = "False";
|
|
if (projectInfo.Properties.TryGetValue("EngineDirectory", out string? engineDirectory) && !String.IsNullOrEmpty(engineDirectory))
|
|
{
|
|
projectInfo.Properties[element.Name] = projectInfo.ProjectPath.IsUnderDirectory(new DirectoryReference(engineDirectory)).ToString();
|
|
}
|
|
continue;
|
|
}
|
|
case "IsAutomationProject": projectInfo.Properties[element.Name] = projectInfo.ProjectPath.FullName.EndsWith(".automation.csproj", StringComparison.OrdinalIgnoreCase).ToString(); continue;
|
|
case "HasAssemblyInfo": projectInfo.Properties[element.Name] = FileReference.Exists(FileReference.Combine(projectInfo.ProjectPath.Directory, "Properties", "AssemblyInfo.cs")).ToString(); continue;
|
|
default: break;
|
|
}
|
|
|
|
if (EvaluateCondition(element, projectInfo))
|
|
{
|
|
projectInfo.Properties[element.Name] = ExpandProperties(element.InnerText, projectInfo.Properties);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses an 'ItemGroup' element.
|
|
/// </summary>
|
|
/// <param name="baseDirectory">Base directory to resolve relative paths against</param>
|
|
/// <param name="parentElement">The parent 'ItemGroup' element</param>
|
|
/// <param name="projectInfo">Project info object to be updated</param>
|
|
static void ParseItemGroup(DirectoryReference baseDirectory, XmlElement parentElement, CsProjectInfo projectInfo)
|
|
{
|
|
// Parse any external assembly references
|
|
foreach (XmlElement itemElement in parentElement.ChildNodes.OfType<XmlElement>())
|
|
{
|
|
switch (itemElement.Name)
|
|
{
|
|
case "Reference":
|
|
// Reference to an external assembly
|
|
if (EvaluateCondition(itemElement, projectInfo))
|
|
{
|
|
ParseReference(baseDirectory, itemElement, projectInfo.References);
|
|
}
|
|
break;
|
|
case "ProjectReference":
|
|
// Reference to another project
|
|
if (EvaluateCondition(itemElement, projectInfo))
|
|
{
|
|
ParseProjectReference(baseDirectory, itemElement, projectInfo.Properties, projectInfo.ProjectReferences);
|
|
}
|
|
break;
|
|
case "Compile":
|
|
// Reference to a file
|
|
if (EvaluateCondition(itemElement, projectInfo))
|
|
{
|
|
ParseCompileReference(baseDirectory, itemElement, projectInfo.CompileReferences);
|
|
}
|
|
break;
|
|
case "Content":
|
|
case "None":
|
|
// Reference to a file
|
|
if (EvaluateCondition(itemElement, projectInfo))
|
|
{
|
|
ParseContent(baseDirectory, itemElement, projectInfo.ContentReferences);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses an assembly reference from a given 'Reference' element
|
|
/// </summary>
|
|
/// <param name="baseDirectory">Directory to resolve relative paths against</param>
|
|
/// <param name="parentElement">The parent 'Reference' element</param>
|
|
/// <param name="references">Dictionary of project files to a bool indicating whether the assembly should be copied locally to the referencing project.</param>
|
|
static void ParseReference(DirectoryReference baseDirectory, XmlElement parentElement, Dictionary<FileReference, bool> references)
|
|
{
|
|
string? hintPath = UnescapeString(GetChildElementString(parentElement, "HintPath", null));
|
|
if (!String.IsNullOrEmpty(hintPath))
|
|
{
|
|
// Don't include embedded assemblies; they aren't referenced externally by the compiled executable
|
|
bool bEmbedInteropTypes = GetChildElementBoolean(parentElement, "EmbedInteropTypes", false);
|
|
if(!bEmbedInteropTypes)
|
|
{
|
|
if (!OperatingSystem.IsWindows() && hintPath.Contains('\\', StringComparison.Ordinal))
|
|
{
|
|
hintPath = hintPath.Replace('\\', Path.DirectorySeparatorChar);
|
|
}
|
|
FileReference assemblyFile = Path.IsPathFullyQualified(hintPath) ? new FileReference(hintPath) : FileReference.Combine(baseDirectory, hintPath);
|
|
bool bPrivate = GetChildElementBoolean(parentElement, "Private", true);
|
|
references.Add(assemblyFile, bPrivate);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a project reference from a given 'ProjectReference' element
|
|
/// </summary>
|
|
/// <param name="baseDirectory">Directory to resolve relative paths against</param>
|
|
/// <param name="parentElement">The parent 'ProjectReference' element</param>
|
|
/// <param name="properties">Dictionary of properties for parsing the file</param>
|
|
/// <param name="projectReferences">Dictionary of project files to a bool indicating whether the outputs of the project should be copied locally to the referencing project.</param>
|
|
static void ParseProjectReference(DirectoryReference baseDirectory, XmlElement parentElement, Dictionary<string, string> properties, Dictionary<FileReference, bool> projectReferences)
|
|
{
|
|
string? includePath = UnescapeString(parentElement.GetAttribute("Include"));
|
|
if (!String.IsNullOrEmpty(includePath))
|
|
{
|
|
includePath = ExpandProperties(includePath, properties);
|
|
if (!OperatingSystem.IsWindows() && includePath.Contains('\\', StringComparison.Ordinal))
|
|
{
|
|
includePath = includePath.Replace('\\', Path.DirectorySeparatorChar);
|
|
}
|
|
FileReference projectFile = Path.IsPathFullyQualified(includePath) ? new FileReference(includePath) : FileReference.Combine(baseDirectory, includePath);
|
|
bool bPrivate = GetChildElementBoolean(parentElement, "Private", true);
|
|
projectReferences[projectFile] = bPrivate;
|
|
}
|
|
}
|
|
|
|
/// recursive helper used by the function below that will append RemainingComponents one by one to ExistingPath,
|
|
/// expanding wildcards as necessary. The complete list of files that match the complete path is returned out OutFoundFiles
|
|
static void ProcessPathComponents(DirectoryReference existingPath, IEnumerable<string> remainingComponents, List<FileReference> outFoundFiles)
|
|
{
|
|
if (!remainingComponents.Any())
|
|
{
|
|
return;
|
|
}
|
|
|
|
// take a look at the first component
|
|
string currentComponent = remainingComponents.First();
|
|
remainingComponents = remainingComponents.Skip(1);
|
|
|
|
// If no other components then this is either a file pattern or a greedy pattern
|
|
if (!remainingComponents.Any())
|
|
{
|
|
// ** means include all files under this tree, so enumerate them all
|
|
if (currentComponent.Contains("**", StringComparison.Ordinal))
|
|
{
|
|
outFoundFiles.AddRange(DirectoryReference.EnumerateFiles(existingPath, "*", SearchOption.AllDirectories));
|
|
}
|
|
else
|
|
{
|
|
// easy, a regular path with a file that may or may not be a wildcard
|
|
outFoundFiles.AddRange(DirectoryReference.EnumerateFiles(existingPath, currentComponent));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// new component contains a wildcard, and based on the above we know there are more entries so find
|
|
// matching directories
|
|
if (currentComponent.Contains('*', StringComparison.Ordinal))
|
|
{
|
|
// ** means all directories, no matter how deep
|
|
SearchOption option = currentComponent == "**" ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly;
|
|
|
|
IEnumerable<DirectoryReference> directories = DirectoryReference.EnumerateDirectories(existingPath, currentComponent, option);
|
|
|
|
// if we searched all directories regardless of depth, the rest of the components other than the last (file) are irrelevant
|
|
if (option == SearchOption.AllDirectories)
|
|
{
|
|
remainingComponents = [remainingComponents.Last()];
|
|
|
|
// ** includes files in the current directory too
|
|
directories = directories.Concat([existingPath]);
|
|
}
|
|
|
|
foreach (DirectoryReference dir in directories)
|
|
{
|
|
ProcessPathComponents(dir, remainingComponents, outFoundFiles);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// add this component to our path and recurse.
|
|
existingPath = DirectoryReference.Combine(existingPath, currentComponent);
|
|
|
|
// but... we can just take all the next components that don't have wildcards in them instead of recursing
|
|
// into each one!
|
|
IEnumerable<string> nonWildCardComponents = remainingComponents.TakeWhile(c => !c.Contains('*', StringComparison.Ordinal));
|
|
remainingComponents = remainingComponents.Skip(nonWildCardComponents.Count());
|
|
|
|
existingPath = DirectoryReference.Combine(existingPath, nonWildCardComponents.ToArray());
|
|
|
|
if (Directory.Exists(existingPath.FullName))
|
|
{
|
|
ProcessPathComponents(existingPath, remainingComponents, outFoundFiles);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Finds all files in the provided path, which may be a csproj wildcard specification.
|
|
/// E.g. The following are all valid
|
|
/// Foo/Bar/Item.cs
|
|
/// Foo/Bar/*.cs
|
|
/// Foo/*/Item.cs
|
|
/// Foo/*/*.cs
|
|
/// Foo/**
|
|
/// (the last means include all files under the path).
|
|
/// </summary>
|
|
/// <param name="inPath">Path specifier to process</param>
|
|
/// <returns></returns>
|
|
static IEnumerable<FileReference> FindMatchingFiles(FileReference inPath)
|
|
{
|
|
List<FileReference> foundFiles = [];
|
|
|
|
// split off the drive root
|
|
string driveRoot = Path.GetPathRoot(inPath.FullName)!;
|
|
|
|
// break the rest of the path into components
|
|
string[] pathComponents = inPath.FullName.Substring(driveRoot.Length).Split([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar]);
|
|
|
|
// Process all the components recursively
|
|
ProcessPathComponents(new DirectoryReference(driveRoot), pathComponents, foundFiles);
|
|
|
|
return foundFiles;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a a compile file path from a given 'Compile' element
|
|
/// </summary>
|
|
/// <param name="baseDirectory">Directory to resolve relative paths against</param>
|
|
/// <param name="parentElement">The parent 'ProjectReference' element</param>
|
|
/// <param name="compileReferences">List of source files.</param>
|
|
static void ParseCompileReference(DirectoryReference baseDirectory, XmlElement parentElement, List<FileReference> compileReferences)
|
|
{
|
|
string? includePath = UnescapeString(parentElement.GetAttribute("Include"));
|
|
if (!String.IsNullOrEmpty(includePath))
|
|
{
|
|
if (!OperatingSystem.IsWindows() && includePath.Contains('\\', StringComparison.Ordinal))
|
|
{
|
|
includePath = includePath.Replace('\\', Path.DirectorySeparatorChar);
|
|
}
|
|
FileReference sourceFile = Path.IsPathFullyQualified(includePath) ? new FileReference(includePath) : FileReference.Combine(baseDirectory, includePath);
|
|
|
|
if (sourceFile.FullName.Contains('*', StringComparison.Ordinal))
|
|
{
|
|
compileReferences.AddRange(FindMatchingFiles(sourceFile));
|
|
}
|
|
else
|
|
{
|
|
compileReferences.Add(sourceFile);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a file path from a given 'Content' element
|
|
/// </summary>
|
|
/// <param name="baseDirectory">Directory to resolve relative paths against</param>
|
|
/// <param name="parentElement">The parent 'Content' element</param>
|
|
/// <param name="contents">Dictionary of project files to a bool indicating whether the assembly should be copied locally to the referencing project.</param>
|
|
static void ParseContent(DirectoryReference baseDirectory, XmlElement parentElement, Dictionary<FileReference, bool> contents)
|
|
{
|
|
string? includePath = UnescapeString(parentElement.GetAttribute("Include"));
|
|
if (!String.IsNullOrEmpty(includePath))
|
|
{
|
|
string? copyTo = GetChildElementString(parentElement, "CopyToOutputDirectory", null);
|
|
bool shouldCopy = !String.IsNullOrEmpty(copyTo) && (copyTo.Equals("Always", StringComparison.OrdinalIgnoreCase) || copyTo.Equals("PreserveNewest", StringComparison.OrdinalIgnoreCase));
|
|
if (!OperatingSystem.IsWindows() && includePath.Contains('\\', StringComparison.Ordinal))
|
|
{
|
|
includePath = includePath.Replace('\\', Path.DirectorySeparatorChar);
|
|
}
|
|
FileReference contentFile = Path.IsPathFullyQualified(includePath) ? new FileReference(includePath) : FileReference.Combine(baseDirectory, includePath);
|
|
contents.TryAdd(contentFile, shouldCopy);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reads the inner text of a child XML element
|
|
/// </summary>
|
|
/// <param name="parentElement">The parent element to check</param>
|
|
/// <param name="name">Name of the child element</param>
|
|
/// <param name="defaultValue">Default value to return if the child element is missing</param>
|
|
/// <returns>The contents of the child element, or default value if it's not present</returns>
|
|
static string? GetChildElementString(XmlElement parentElement, string name, string? defaultValue)
|
|
{
|
|
XmlElement? childElement = parentElement.ChildNodes.OfType<XmlElement>().FirstOrDefault(x => x.Name == name);
|
|
if (childElement == null)
|
|
{
|
|
return defaultValue;
|
|
}
|
|
else
|
|
{
|
|
return childElement.InnerText ?? defaultValue;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read a child XML element with the given name, and parse it as a boolean.
|
|
/// </summary>
|
|
/// <param name="parentElement">Parent element to check</param>
|
|
/// <param name="name">Name of the child element to look for</param>
|
|
/// <param name="defaultValue">Default value to return if the element is missing or not a valid bool</param>
|
|
/// <returns>The parsed boolean, or the default value</returns>
|
|
static bool GetChildElementBoolean(XmlElement parentElement, string name, bool defaultValue)
|
|
{
|
|
string? value = GetChildElementString(parentElement, name, null);
|
|
if (value == null)
|
|
{
|
|
return defaultValue;
|
|
}
|
|
else if (value.Equals("True", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return true;
|
|
}
|
|
else if (value.Equals("False", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
return defaultValue;
|
|
}
|
|
}
|
|
|
|
record class ConditionContext(DirectoryReference BaseDir);
|
|
|
|
/// <summary>
|
|
/// Evaluate whether the optional MSBuild condition on an XML element evaluates to true. Currently only supports 'ABC' == 'DEF' style expressions, but can be expanded as needed.
|
|
/// </summary>
|
|
/// <param name="element">The XML element to check</param>
|
|
/// <param name="projectInfo">Dictionary mapping from property names to values.</param>
|
|
/// <returns></returns>
|
|
static bool EvaluateCondition(XmlElement element, CsProjectInfo projectInfo)
|
|
{
|
|
// Read the condition attribute. If it's not present, assume it evaluates to true.
|
|
string condition = element.GetAttribute("Condition");
|
|
if (String.IsNullOrEmpty(condition))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Expand all the properties
|
|
condition = ExpandProperties(condition, projectInfo.Properties);
|
|
|
|
// Parse literal true/false values
|
|
bool outResult;
|
|
if (Boolean.TryParse(condition, out outResult))
|
|
{
|
|
return outResult;
|
|
}
|
|
|
|
// Tokenize the condition
|
|
string[] tokens = Tokenize(condition);
|
|
|
|
// Try to evaluate it. We only support a very limited class of condition expressions at the moment, but it's enough to parse standard projects
|
|
int tokenIdx = 0;
|
|
|
|
ConditionContext context = new ConditionContext(projectInfo.ProjectPath.Directory);
|
|
try
|
|
{
|
|
bool bResult = CoerceToBool(EvaluateLogicalAnd(context, tokens, ref tokenIdx));
|
|
if (tokenIdx != tokens.Length)
|
|
{
|
|
throw new CsProjectParseException("Unexpected tokens at end of condition");
|
|
}
|
|
return bResult;
|
|
}
|
|
catch (CsProjectParseException ex)
|
|
{
|
|
throw new CsProjectParseException($"{ex.Message} while parsing {element} in project file {projectInfo.ProjectPath}", ex);
|
|
}
|
|
}
|
|
|
|
static string EvaluateLogicalAnd(ConditionContext context, string[] tokens, ref int tokenIdx)
|
|
{
|
|
string result = EvaluateLogicalOr(context, tokens, ref tokenIdx);
|
|
while (tokenIdx < tokens.Length && tokens[tokenIdx].Equals("And", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
tokenIdx++;
|
|
string rhs = EvaluateEquality(context, tokens, ref tokenIdx);
|
|
result = (CoerceToBool(result) && CoerceToBool(rhs)).ToString();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static string EvaluateLogicalOr(ConditionContext context, string[] tokens, ref int tokenIdx)
|
|
{
|
|
string result = EvaluateEquality(context, tokens, ref tokenIdx);
|
|
while (tokenIdx < tokens.Length && tokens[tokenIdx].Equals("Or", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
tokenIdx++;
|
|
string rhs = EvaluateEquality(context, tokens, ref tokenIdx);
|
|
result = (CoerceToBool(result) || CoerceToBool(rhs)).ToString();
|
|
}
|
|
return result;
|
|
}
|
|
|
|
static string EvaluateEquality(ConditionContext context, string[] tokens, ref int tokenIdx)
|
|
{
|
|
// Otherwise try to parse an equality or inequality expression
|
|
string lhs = EvaluateValue(context, tokens, ref tokenIdx);
|
|
if (tokenIdx < tokens.Length)
|
|
{
|
|
if (tokens[tokenIdx] == "==")
|
|
{
|
|
tokenIdx++;
|
|
string rhs = EvaluateValue(context, tokens, ref tokenIdx);
|
|
return lhs.Equals(rhs, StringComparison.OrdinalIgnoreCase).ToString();
|
|
}
|
|
else if (tokens[tokenIdx] == "!=")
|
|
{
|
|
tokenIdx++;
|
|
string rhs = EvaluateValue(context, tokens, ref tokenIdx);
|
|
return lhs.Equals(rhs, StringComparison.OrdinalIgnoreCase).ToString();
|
|
}
|
|
}
|
|
return lhs;
|
|
}
|
|
|
|
static string EvaluateValue(ConditionContext context, string[] tokens, ref int tokenIdx)
|
|
{
|
|
// Handle Exists('Platform\Windows\Gauntlet.TargetDeviceWindows.cs')
|
|
if (tokens[tokenIdx].Equals("Exists", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
if (tokenIdx + 3 >= tokens.Length || !tokens[tokenIdx + 1].Equals("(", StringComparison.Ordinal) || !tokens[tokenIdx + 3].Equals(")", StringComparison.Ordinal))
|
|
{
|
|
throw new CsProjectParseException("Invalid 'Exists' expression", null);
|
|
}
|
|
|
|
// remove all quotes, apostrophes etc that are either tokens or wrap tokens (The Tokenize() function is a bit suspect).
|
|
string path = tokens[tokenIdx + 2].Trim('\'', '(', ')', '{', '}', '[', ']');
|
|
tokenIdx += 4;
|
|
|
|
FileSystemReference dependency = DirectoryReference.Combine(context.BaseDir, path);
|
|
bool exists = File.Exists(dependency.FullName) || Directory.Exists(dependency.FullName);
|
|
return exists.ToString();
|
|
}
|
|
|
|
// Handle negation
|
|
if (tokens[tokenIdx].Equals("!", StringComparison.Ordinal))
|
|
{
|
|
tokenIdx++;
|
|
bool value = CoerceToBool(EvaluateValue(context, tokens, ref tokenIdx));
|
|
return (!value).ToString();
|
|
}
|
|
|
|
// Handle subexpressions
|
|
if (tokens[tokenIdx].Equals("(", StringComparison.Ordinal))
|
|
{
|
|
tokenIdx++;
|
|
|
|
string result = EvaluateLogicalAnd(context, tokens, ref tokenIdx);
|
|
if (!tokens[tokenIdx].Equals(")", StringComparison.Ordinal))
|
|
{
|
|
throw new CsProjectParseException("Missing ')'", null);
|
|
}
|
|
tokenIdx++;
|
|
|
|
return result;
|
|
}
|
|
|
|
return tokens[tokenIdx++];
|
|
}
|
|
|
|
static bool CoerceToBool(string value)
|
|
{
|
|
return !value.Equals("false", StringComparison.OrdinalIgnoreCase) && !value.Equals("0", StringComparison.Ordinal);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Expand MSBuild properties within a string. If referenced properties are not in this dictionary, the process' environment variables are expanded. Unknown properties are expanded to an empty string.
|
|
/// </summary>
|
|
/// <param name="text">The input string to expand</param>
|
|
/// <param name="properties">Dictionary mapping from property names to values.</param>
|
|
/// <returns>String with all properties expanded.</returns>
|
|
static string ExpandProperties(string text, Dictionary<string, string> properties)
|
|
{
|
|
string newText = text;
|
|
for (int idx = newText.IndexOf("$(", StringComparison.Ordinal); idx != -1; idx = newText.IndexOf("$(", idx, StringComparison.Ordinal))
|
|
{
|
|
// Find the end of the variable name, accounting for changes in scope
|
|
int endIdx = idx + 2;
|
|
for(int depth = 1; depth > 0; endIdx++)
|
|
{
|
|
if(endIdx == newText.Length)
|
|
{
|
|
throw new Exception("Encountered end of string while expanding properties");
|
|
}
|
|
else if(newText[endIdx] == '(')
|
|
{
|
|
depth++;
|
|
}
|
|
else if(newText[endIdx] == ')')
|
|
{
|
|
depth--;
|
|
}
|
|
}
|
|
|
|
// Convert the property name to tokens
|
|
string[] tokens = Tokenize(newText.Substring(idx + 2, (endIdx - 1) - (idx + 2)));
|
|
|
|
// Make sure the first token is a valid property name
|
|
if(tokens.Length == 0 || !(Char.IsLetter(tokens[0][0]) || tokens[0][0] == '_' || tokens[0][0] == '[' ))
|
|
{
|
|
throw new Exception(String.Format("Invalid property name '{0}' in .csproj file", tokens[0]));
|
|
}
|
|
|
|
// Find the value for it, either from the dictionary or the environment block
|
|
string value;
|
|
if (properties.TryGetValue(tokens[0], out string? retrievedValue))
|
|
{
|
|
value = retrievedValue;
|
|
}
|
|
else
|
|
{
|
|
value = Environment.GetEnvironmentVariable(tokens[0]) ?? "";
|
|
}
|
|
|
|
// Evaluate any functions within it
|
|
int tokenIdx = 1;
|
|
while(tokenIdx + 3 < tokens.Length && tokens[tokenIdx] == "." && tokens[tokenIdx + 2] == "(")
|
|
{
|
|
// Read the method name
|
|
string methodName = tokens[tokenIdx + 1];
|
|
|
|
// Skip to the first argument
|
|
tokenIdx += 3;
|
|
|
|
// Parse any arguments
|
|
List<object> arguments = [];
|
|
if(tokens[tokenIdx] != ")")
|
|
{
|
|
arguments.Add(ParseArgument(tokens[tokenIdx]));
|
|
tokenIdx++;
|
|
|
|
while(tokenIdx + 1 < tokens.Length && tokens[tokenIdx] == ",")
|
|
{
|
|
arguments.Add(ParseArgument(tokens[tokenIdx + 2]));
|
|
tokenIdx += 2;
|
|
}
|
|
|
|
if(tokens[tokenIdx] != ")")
|
|
{
|
|
throw new Exception("Missing closing parenthesis in condition");
|
|
}
|
|
}
|
|
|
|
// Skip over the closing parenthesis
|
|
tokenIdx++;
|
|
|
|
// Execute the method
|
|
try
|
|
{
|
|
value = typeof(string).InvokeMember(methodName, System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.InvokeMethod, Type.DefaultBinder, value, [.. arguments])!.ToString()!;
|
|
}
|
|
catch(Exception ex)
|
|
{
|
|
throw new Exception(String.Format("Unable to evaluate condition '{0}'", text), ex);
|
|
}
|
|
}
|
|
|
|
if (tokenIdx < tokens.Length && tokens[tokenIdx] == ":")
|
|
{
|
|
tokenIdx = tokens.Length;
|
|
}
|
|
|
|
// Make sure there's nothing left over
|
|
if(tokenIdx != tokens.Length)
|
|
{
|
|
throw new Exception(String.Format("Unable to parse token '{0}'", newText));
|
|
}
|
|
|
|
// Replace the variable with its value
|
|
newText = newText.Substring(0, idx) + value + newText.Substring(endIdx);
|
|
|
|
// Make sure we skip over the expanded variable; we don't want to recurse on it.
|
|
idx += value.Length;
|
|
}
|
|
return newText;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse an argument into a framework type
|
|
/// </summary>
|
|
/// <param name="token">The token to parse</param>
|
|
/// <returns>The argument object</returns>
|
|
static object ParseArgument(string token)
|
|
{
|
|
// Try to parse a string
|
|
if(token.Length > 2 && token[0] == '\'' && token[^1] == '\'')
|
|
{
|
|
return token.Substring(1, token.Length - 2);
|
|
}
|
|
|
|
// Try to parse an integer
|
|
int value;
|
|
if(Int32.TryParse(token, out value))
|
|
{
|
|
return value;
|
|
}
|
|
|
|
// Otherwise throw an exception
|
|
throw new Exception(String.Format("Unable to parse token '{0}' into a .NET framework type", token));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Split an MSBuild condition into tokens
|
|
/// </summary>
|
|
/// <param name="condition">The condition expression</param>
|
|
/// <returns>Array of the parsed tokens</returns>
|
|
static string[] Tokenize(string condition)
|
|
{
|
|
List<string> tokens = [];
|
|
for (int idx = 0; idx < condition.Length; )
|
|
{
|
|
if(Char.IsWhiteSpace(condition[idx]))
|
|
{
|
|
// Whitespace
|
|
idx++;
|
|
}
|
|
else if (idx + 1 < condition.Length && condition[idx] == '=' && condition[idx + 1] == '=')
|
|
{
|
|
// "==" operator
|
|
idx += 2;
|
|
tokens.Add("==");
|
|
}
|
|
else if (idx + 1 < condition.Length && condition[idx] == '!' && condition[idx + 1] == '=')
|
|
{
|
|
// "!=" operator
|
|
idx += 2;
|
|
tokens.Add("!=");
|
|
}
|
|
else if (condition[idx] == '\'')
|
|
{
|
|
// Quoted string
|
|
int startIdx = idx++;
|
|
for(;;idx++)
|
|
{
|
|
if(idx == condition.Length)
|
|
{
|
|
throw new Exception(String.Format("Missing end quote in condition string ('{0}')", condition));
|
|
}
|
|
if(condition[idx] == '\'')
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
idx++;
|
|
tokens.Add(condition.Substring(startIdx, idx - startIdx));
|
|
}
|
|
else if (condition[idx] == '[')
|
|
{
|
|
#pragma warning disable IDE0059 // Unnecessary assignment of a value
|
|
// static property function invoke
|
|
// format: [Class]::Property
|
|
// alternatively: [Class]::Method()
|
|
// we consider the entire invocation to be a single token
|
|
int startIdx = idx++;
|
|
int classEndIdx = 0;
|
|
int methodEndIdx = 0;
|
|
int methodArgsEndIdx = 0;
|
|
for (; ; idx++)
|
|
{
|
|
while (idx < condition.Length && (Char.IsLetterOrDigit(condition[idx]) || condition[idx] == '_'))
|
|
{
|
|
idx++;
|
|
}
|
|
|
|
if (idx == condition.Length)
|
|
{
|
|
throw new Exception(String.Format("Found end of condition when searching for end of static property function for condition string ('{0}')", condition));
|
|
}
|
|
if (condition[idx] == ']')
|
|
{
|
|
classEndIdx = idx;
|
|
idx++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// skip ::
|
|
if (condition[idx] != ':')
|
|
{
|
|
throw new Exception(String.Format("Unexpected format of static property function, expected :: after class declaration in condition string ('{0}')", condition));
|
|
}
|
|
idx += 2;
|
|
|
|
while (idx < condition.Length && (Char.IsLetterOrDigit(condition[idx]) || condition[idx] == '_'))
|
|
{
|
|
idx++;
|
|
}
|
|
|
|
methodEndIdx = idx;
|
|
|
|
if (idx < condition.Length && condition[idx] == '(')
|
|
{
|
|
// a method invoke
|
|
for (; ; idx++)
|
|
{
|
|
while (idx < condition.Length && (Char.IsLetterOrDigit(condition[idx]) || condition[idx] == '_'))
|
|
{
|
|
idx++;
|
|
}
|
|
|
|
if (idx == condition.Length)
|
|
{
|
|
throw new Exception(String.Format("Found end of condition when searching for ) to indicate end of arguments to static property function for condition string ('{0}')", condition));
|
|
}
|
|
if (condition[idx] == ')')
|
|
{
|
|
methodArgsEndIdx = idx;
|
|
idx++;
|
|
break;
|
|
}
|
|
}
|
|
idx++;
|
|
}
|
|
#pragma warning restore IDE0059 // Unnecessary assignment of a value
|
|
|
|
tokens.Add(condition.Substring(startIdx, idx - startIdx));
|
|
}
|
|
else if(Char.IsLetterOrDigit(condition[idx]) || condition[idx] == '_')
|
|
{
|
|
// Identifier or number
|
|
int startIdx = idx++;
|
|
while(idx < condition.Length && (Char.IsLetterOrDigit(condition[idx]) || condition[idx] == '_'))
|
|
{
|
|
idx++;
|
|
}
|
|
tokens.Add(condition.Substring(startIdx, idx - startIdx));
|
|
}
|
|
else
|
|
{
|
|
// Other token; assume a single character.
|
|
string token = condition.Substring(idx++, 1);
|
|
tokens.Add(token);
|
|
}
|
|
}
|
|
return [.. tokens];
|
|
}
|
|
|
|
/// <summary>
|
|
/// Un-escape an MSBuild string (see https://msdn.microsoft.com/en-us/library/bb383819.aspx)
|
|
/// </summary>
|
|
/// <param name="text">String to remove escape characters from</param>
|
|
/// <returns>Unescaped string</returns>
|
|
static string? UnescapeString(string? text)
|
|
{
|
|
const string HexChars = "0123456789abcdef";
|
|
|
|
string? newText = text;
|
|
if(newText != null)
|
|
{
|
|
for(int idx = 0; idx + 2 < newText.Length; idx++)
|
|
{
|
|
if(newText[idx] == '%')
|
|
{
|
|
int upperDigitIdx = HexChars.IndexOf(Char.ToLowerInvariant(newText[idx + 1]), StringComparison.Ordinal);
|
|
if(upperDigitIdx != -1)
|
|
{
|
|
int lowerDigitIdx = HexChars.IndexOf(Char.ToLowerInvariant(newText[idx + 2]), StringComparison.Ordinal);
|
|
if(lowerDigitIdx != -1)
|
|
{
|
|
char newChar = (char)((upperDigitIdx << 4) | lowerDigitIdx);
|
|
newText = newText.Substring(0, idx) + newChar + newText.Substring(idx + 3);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return newText;
|
|
}
|
|
}
|
|
}
|