2020-12-21 15:58:58 -04:00
// Copyright Epic Games, Inc. All Rights Reserved.
using System ;
using System.Collections.Generic ;
2021-11-18 14:37:34 -05:00
using System.Diagnostics.CodeAnalysis ;
2020-12-21 15:58:58 -04:00
using System.IO ;
using System.Linq ;
using System.Runtime.InteropServices ;
using System.Xml ;
2020-12-21 23:07:37 -04:00
namespace EpicGames.Core
2020-12-21 15:58:58 -04:00
{
/// <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>
2022-03-24 16:35:00 -04:00
public Dictionary < string , string > Properties { get ; }
2020-12-21 15:58:58 -04:00
/// <summary>
/// Mapping of referenced assemblies to their 'CopyLocal' (aka 'Private') setting.
/// </summary>
2022-03-24 16:35:00 -04:00
public Dictionary < FileReference , bool > References { get ; } = new Dictionary < FileReference , bool > ( ) ;
2020-12-21 15:58:58 -04:00
/// <summary>
/// Mapping of referenced projects to their 'CopyLocal' (aka 'Private') setting.
/// </summary>
2022-03-24 16:35:00 -04:00
public Dictionary < FileReference , bool > ProjectReferences { get ; } = new Dictionary < FileReference , bool > ( ) ;
2020-12-21 15:58:58 -04:00
/// <summary>
/// List of compile references in the project.
/// </summary>
2022-03-24 16:35:00 -04:00
public List < FileReference > CompileReferences { get ; } = new List < FileReference > ( ) ;
2020-12-21 15:58:58 -04:00
/// <summary>
/// Mapping of content IF they are flagged Always or Newer
/// </summary>
2022-03-24 16:35:00 -04:00
public Dictionary < FileReference , bool > ContentReferences { get ; } = new Dictionary < FileReference , bool > ( ) ;
2020-12-21 15:58:58 -04:00
/// <summary>
/// Path to the CSProject file
/// </summary>
2022-03-24 16:35:00 -04:00
public FileReference ProjectPath { get ; }
2020-12-21 15:58:58 -04:00
/// <summary>
/// Constructor
/// </summary>
2022-03-24 16:35:00 -04:00
/// <param name="inProperties">Initial mapping of property names to values</param>
CsProjectInfo ( Dictionary < string , string > inProperties , FileReference inProjectPath )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
ProjectPath = inProjectPath ;
Properties = new Dictionary < string , string > ( inProperties ) ;
2020-12-21 15:58:58 -04:00
}
/// <summary>
/// Get the ouptut file for this project
/// </summary>
2022-03-24 16:35:00 -04:00
/// <param name="file">If successful, receives the assembly path</param>
2020-12-21 15:58:58 -04:00
/// <returns>True if the output file was found</returns>
2022-03-24 16:35:00 -04:00
public bool TryGetOutputFile ( [ NotNullWhen ( true ) ] out FileReference ? file )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
DirectoryReference ? outputDir ;
if ( ! TryGetOutputDir ( out outputDir ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
file = null ;
2020-12-21 15:58:58 -04:00
return false ;
}
2022-03-24 16:35:00 -04:00
string? assemblyName ;
if ( ! TryGetAssemblyName ( out assemblyName ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
file = null ;
2020-12-21 15:58:58 -04:00
return false ;
}
2022-03-24 16:35:00 -04:00
file = FileReference . Combine ( outputDir , assemblyName + ".dll" ) ;
2020-12-21 15:58:58 -04:00
return true ;
}
2021-10-25 20:05:28 -04:00
/// <summary>
/// Returns whether or not this project is a netcore project (as opposed to full .net sdk) project
/// </summary>
/// <returns>True if this is a netcore project</returns>
private bool IsNetCoreProject ( )
{
2022-03-24 16:35:00 -04:00
string? framework ;
return Properties . TryGetValue ( "TargetFramework" , out framework ) & & framework . StartsWith ( "netcoreapp" ) ;
2021-10-25 20:05:28 -04:00
}
2020-12-21 15:58:58 -04:00
/// <summary>
/// Resolve the project's output directory
/// </summary>
2022-03-24 16:35:00 -04:00
/// <param name="baseDirectory">Base directory to resolve relative paths to</param>
2020-12-21 15:58:58 -04:00
/// <returns>The configured output directory</returns>
2022-03-24 16:35:00 -04:00
public DirectoryReference GetOutputDir ( DirectoryReference baseDirectory )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
string? outputPath ;
if ( Properties . TryGetValue ( "OutputPath" , out outputPath ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
return DirectoryReference . Combine ( baseDirectory , outputPath ) ;
2020-12-21 15:58:58 -04:00
}
2021-10-25 20:05:28 -04:00
else if ( IsNetCoreProject ( ) )
{
2022-03-24 16:35:00 -04:00
string configuration = Properties . ContainsKey ( "Configuration" ) ? Properties [ "Configuration" ] : "Development" ;
return DirectoryReference . Combine ( baseDirectory , "bin" , configuration , Properties [ "TargetFramework" ] ) ;
2021-10-25 20:05:28 -04:00
}
2020-12-21 15:58:58 -04:00
else
{
2022-03-24 16:35:00 -04:00
return baseDirectory ;
2020-12-21 15:58:58 -04:00
}
}
/// <summary>
/// Resolve the project's output directory
/// </summary>
2022-03-24 16:35:00 -04:00
/// <param name="outputDir">If successful, receives the output directory</param>
2020-12-21 15:58:58 -04:00
/// <returns>True if the output directory was found</returns>
2022-03-24 16:35:00 -04:00
public bool TryGetOutputDir ( [ NotNullWhen ( true ) ] out DirectoryReference ? outputDir )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
string? outputPath ;
if ( Properties . TryGetValue ( "OutputPath" , out outputPath ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
outputDir = DirectoryReference . Combine ( ProjectPath . Directory , outputPath ) ;
2020-12-21 15:58:58 -04:00
return true ;
}
2021-10-25 20:05:28 -04:00
else if ( IsNetCoreProject ( ) )
{
2022-03-24 16:35:00 -04:00
string configuration = Properties . ContainsKey ( "Configuration" ) ? Properties [ "Configuration" ] : "Development" ;
outputDir = DirectoryReference . Combine ( ProjectPath . Directory , "bin" , configuration , Properties [ "TargetFramework" ] ) ;
2021-10-25 20:05:28 -04:00
return true ;
}
2020-12-21 15:58:58 -04:00
else
{
2022-03-24 16:35:00 -04:00
outputDir = null ;
2020-12-21 15:58:58 -04:00
return false ;
}
}
/// <summary>
/// Returns the assembly name used by this project
/// </summary>
/// <returns></returns>
2022-03-24 16:35:00 -04:00
public bool TryGetAssemblyName ( [ NotNullWhen ( true ) ] out string? assemblyName )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
if ( Properties . TryGetValue ( "AssemblyName" , out assemblyName ) )
2021-10-25 20:05:28 -04:00
{
return true ;
}
else if ( IsNetCoreProject ( ) )
{
2022-03-24 16:35:00 -04:00
assemblyName = ProjectPath . GetFileNameWithoutExtension ( ) ;
2021-10-25 20:05:28 -04:00
return true ;
}
return false ;
2020-12-21 15:58:58 -04:00
}
/// <summary>
/// Finds all build products from this project. This includes content and other assemblies marked to be copied local.
/// </summary>
2022-03-24 16:35:00 -04:00
/// <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 )
2020-12-21 15:58:58 -04:00
{
// Add the standard build products
2022-03-24 16:35:00 -04:00
FindCompiledBuildProducts ( outputDir , buildProducts ) ;
2020-12-21 15:58:58 -04:00
// 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.
2022-03-24 16:35:00 -04:00
foreach ( KeyValuePair < FileReference , bool > reference in References )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
if ( reference . Value )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
FileReference outputFile = FileReference . Combine ( outputDir , reference . Key . GetFileName ( ) ) ;
AddReferencedAssemblyAndSupportFiles ( outputFile , buildProducts ) ;
2020-12-21 15:58:58 -04:00
}
}
// Copy the build products for any referenced projects. Note that this does NOT operate recursively.
2022-03-24 16:35:00 -04:00
foreach ( KeyValuePair < FileReference , bool > projectReference in ProjectReferences )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
CsProjectInfo ? otherProjectInfo ;
if ( projectFileToInfo . TryGetValue ( projectReference . Key , out otherProjectInfo ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
otherProjectInfo . FindCompiledBuildProducts ( outputDir , buildProducts ) ;
2020-12-21 15:58:58 -04:00
}
}
// Add any copied content. This DOES operate recursively.
2022-03-24 16:35:00 -04:00
FindCopiedContent ( outputDir , buildProducts , projectFileToInfo ) ;
2020-12-21 15:58:58 -04:00
}
/// <summary>
/// Determines all the compiled build products (executable, etc...) directly built from this project.
/// </summary>
2022-03-24 16:35:00 -04:00
/// <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 )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
string? outputType , assemblyName ;
if ( Properties . TryGetValue ( "OutputType" , out outputType ) & & TryGetAssemblyName ( out assemblyName ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
switch ( outputType )
2020-12-21 15:58:58 -04:00
{
case "Exe" :
case "WinExe" :
2022-03-24 16:35:00 -04:00
string executableExtension = RuntimeInformation . IsOSPlatform ( OSPlatform . Windows ) ? ".exe" : "" ;
buildProducts . Add ( FileReference . Combine ( outputDir , assemblyName + executableExtension ) ) ;
2020-12-21 15:58:58 -04:00
// dotnet outputs a apphost executable and a dll with the actual assembly
2022-03-24 16:35:00 -04:00
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 ) ;
2020-12-21 15:58:58 -04:00
break ;
case "Library" :
2022-03-24 16:35:00 -04:00
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 ) ;
2020-12-21 15:58:58 -04:00
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>
2022-03-24 16:35:00 -04:00
/// <param name="outputDir">The output directory</param>
2020-12-21 15:58:58 -04:00
/// <param name="BuildProducts">Receives the set of build products</param>
2022-03-24 16:35:00 -04:00
/// <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 )
2020-12-21 15:58:58 -04:00
{
// Copy any referenced projects too.
2022-03-24 16:35:00 -04:00
foreach ( KeyValuePair < FileReference , bool > projectReference in ProjectReferences )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
CsProjectInfo ? otherProjectInfo ;
if ( projectFileToInfo . TryGetValue ( projectReference . Key , out otherProjectInfo ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
otherProjectInfo . FindCopiedContent ( outputDir , outputFiles , projectFileToInfo ) ;
2020-12-21 15:58:58 -04:00
}
}
// Add the content which is copied to the output directory
2022-03-24 16:35:00 -04:00
foreach ( KeyValuePair < FileReference , bool > contentReference in ContentReferences )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
FileReference contentFile = contentReference . Key ;
if ( contentReference . Value )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
outputFiles . Add ( FileReference . Combine ( outputDir , contentFile . GetFileName ( ) ) ) ;
2020-12-21 15:58:58 -04:00
}
}
}
/// <summary>
/// Adds the given file and any additional build products to the output set
/// </summary>
2022-03-24 16:35:00 -04:00
/// <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 )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
outputFiles . Add ( outputFile ) ;
2020-12-21 15:58:58 -04:00
2022-03-24 16:35:00 -04:00
FileReference symbolFile = outputFile . ChangeExtension ( ".pdb" ) ;
if ( FileReference . Exists ( symbolFile ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
outputFiles . Add ( symbolFile ) ;
2020-12-21 15:58:58 -04:00
}
2022-03-24 16:35:00 -04:00
FileReference documentationFile = outputFile . ChangeExtension ( ".xml" ) ;
if ( FileReference . Exists ( documentationFile ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
outputFiles . Add ( documentationFile ) ;
2020-12-21 15:58:58 -04:00
}
}
/// <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 ( )
{
bool bIsDotNetCoreProject = false ;
2022-03-24 16:35:00 -04:00
string? targetFramework ;
if ( Properties . TryGetValue ( "TargetFramework" , out targetFramework ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
bIsDotNetCoreProject = targetFramework . ToLower ( ) . Contains ( "netstandard" ) | | targetFramework . ToLower ( ) . Contains ( "netcoreapp" ) ;
2020-12-21 15:58:58 -04:00
}
return bIsDotNetCoreProject ;
}
/// <summary>
/// Adds a build product to the output list if it exists
/// </summary>
2022-03-24 16:35:00 -04:00
/// <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 )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
if ( FileReference . Exists ( buildProduct ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
buildProducts . Add ( buildProduct ) ;
2020-12-21 15:58:58 -04:00
}
}
/// <summary>
/// Parses supported elements from the children of the provided note. May recurse
/// for conditional elements.
/// </summary>
2022-03-24 16:35:00 -04:00
/// <param name="node"></param>
/// <param name="projectInfo"></param>
static void ParseNode ( XmlNode node , CsProjectInfo projectInfo )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
foreach ( XmlElement element in node . ChildNodes . OfType < XmlElement > ( ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
switch ( element . Name )
2020-12-21 15:58:58 -04:00
{
case "PropertyGroup" :
2022-03-24 16:35:00 -04:00
if ( EvaluateCondition ( element , projectInfo ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
ParsePropertyGroup ( element , projectInfo ) ;
2020-12-21 15:58:58 -04:00
}
break ;
case "ItemGroup" :
2022-03-24 16:35:00 -04:00
if ( EvaluateCondition ( element , projectInfo ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
ParseItemGroup ( projectInfo . ProjectPath . Directory , element , projectInfo ) ;
2020-12-21 15:58:58 -04:00
}
break ;
case "Choose" :
case "When" :
2022-03-24 16:35:00 -04:00
if ( EvaluateCondition ( element , projectInfo ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
ParseNode ( element , projectInfo ) ;
2020-12-21 15:58:58 -04:00
}
break ;
}
}
}
/// <summary>
/// Reads project information for the given file.
/// </summary>
2022-03-24 16:35:00 -04:00
/// <param name="file">The project file to read</param>
/// <param name="properties">Initial set of property values</param>
2020-12-21 15:58:58 -04:00
/// <returns>The parsed project info</returns>
2022-03-24 16:35:00 -04:00
public static CsProjectInfo Read ( FileReference file , Dictionary < string , string > properties )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
CsProjectInfo ? project ;
if ( ! TryRead ( file , properties , out project ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
throw new Exception ( String . Format ( "Unable to read '{0}'" , file ) ) ;
2020-12-21 15:58:58 -04:00
}
2022-03-24 16:35:00 -04:00
return project ;
2020-12-21 15:58:58 -04:00
}
/// <summary>
/// Attempts to read project information for the given file.
/// </summary>
2022-03-24 16:35:00 -04:00
/// <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>
2020-12-21 15:58:58 -04:00
/// <returns>True if the project was read successfully, false otherwise</returns>
2022-03-24 16:35:00 -04:00
public static bool TryRead ( FileReference file , Dictionary < string , string > properties , [ NotNullWhen ( true ) ] out CsProjectInfo ? outProjectInfo )
2020-12-21 15:58:58 -04:00
{
// Read the project file
2022-03-24 16:35:00 -04:00
XmlDocument document = new XmlDocument ( ) ;
document . Load ( file . FullName ) ;
2020-12-21 15:58:58 -04:00
// Check the root element is the right type
// HashSet<FileReference> ProjectBuildProducts = new HashSet<FileReference>();
2022-03-24 16:35:00 -04:00
if ( document . DocumentElement . Name ! = "Project" )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
outProjectInfo = null ;
2020-12-21 15:58:58 -04:00
return false ;
}
// Parse the basic structure of the document, updating properties and recursing into other referenced projects as we go
2022-03-24 16:35:00 -04:00
CsProjectInfo projectInfo = new CsProjectInfo ( properties , file ) ;
2020-12-21 15:58:58 -04:00
// Parse elements in the root node
2022-03-24 16:35:00 -04:00
ParseNode ( document . DocumentElement , projectInfo ) ;
2020-12-21 15:58:58 -04:00
// Return the complete project
2022-03-24 16:35:00 -04:00
outProjectInfo = projectInfo ;
2020-12-21 15:58:58 -04:00
return true ;
}
/// <summary>
/// Parses a 'PropertyGroup' element.
/// </summary>
2022-03-24 16:35:00 -04:00
/// <param name="parentElement">The parent 'PropertyGroup' element</param>
2020-12-21 15:58:58 -04:00
/// <param name="Properties">Dictionary mapping property names to values</param>
2022-03-24 16:35:00 -04:00
static void ParsePropertyGroup ( XmlElement parentElement , CsProjectInfo projectInfo )
2020-12-21 15:58:58 -04:00
{
// We need to know the overridden output type and output path for the selected configuration.
2022-03-24 16:35:00 -04:00
foreach ( XmlElement element in parentElement . ChildNodes . OfType < XmlElement > ( ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
if ( EvaluateCondition ( element , projectInfo ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
projectInfo . Properties [ element . Name ] = ExpandProperties ( element . InnerText , projectInfo . Properties ) ;
2020-12-21 15:58:58 -04:00
}
}
}
/// <summary>
/// Parses an 'ItemGroup' element.
/// </summary>
2022-03-24 16:35:00 -04:00
/// <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 )
2020-12-21 15:58:58 -04:00
{
// Parse any external assembly references
2022-03-24 16:35:00 -04:00
foreach ( XmlElement itemElement in parentElement . ChildNodes . OfType < XmlElement > ( ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
switch ( itemElement . Name )
2020-12-21 15:58:58 -04:00
{
case "Reference" :
// Reference to an external assembly
2022-03-24 16:35:00 -04:00
if ( EvaluateCondition ( itemElement , projectInfo ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
ParseReference ( baseDirectory , itemElement , projectInfo . References ) ;
2020-12-21 15:58:58 -04:00
}
break ;
case "ProjectReference" :
// Reference to another project
2022-03-24 16:35:00 -04:00
if ( EvaluateCondition ( itemElement , projectInfo ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
ParseProjectReference ( baseDirectory , itemElement , projectInfo . Properties , projectInfo . ProjectReferences ) ;
2020-12-21 15:58:58 -04:00
}
break ;
case "Compile" :
// Reference to another project
2022-03-24 16:35:00 -04:00
if ( EvaluateCondition ( itemElement , projectInfo ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
ParseCompileReference ( baseDirectory , itemElement , projectInfo . CompileReferences ) ;
2020-12-21 15:58:58 -04:00
}
break ;
case "Content" :
case "None" :
// Reference to another project
2022-03-24 16:35:00 -04:00
if ( EvaluateCondition ( itemElement , projectInfo ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
ParseContent ( baseDirectory , itemElement , projectInfo . ContentReferences ) ;
2020-12-21 15:58:58 -04:00
}
break ;
}
}
}
/// <summary>
/// Parses an assembly reference from a given 'Reference' element
/// </summary>
2022-03-24 16:35:00 -04:00
/// <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 )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
string? hintPath = UnescapeString ( GetChildElementString ( parentElement , "HintPath" , null ) ) ;
if ( ! String . IsNullOrEmpty ( hintPath ) )
2020-12-21 15:58:58 -04:00
{
// Don't include embedded assemblies; they aren't referenced externally by the compiled executable
2022-03-24 16:35:00 -04:00
bool bEmbedInteropTypes = GetChildElementBoolean ( parentElement , "EmbedInteropTypes" , false ) ;
2020-12-21 15:58:58 -04:00
if ( ! bEmbedInteropTypes )
{
2022-03-24 16:35:00 -04:00
FileReference assemblyFile = FileReference . Combine ( baseDirectory , hintPath ) ;
bool bPrivate = GetChildElementBoolean ( parentElement , "Private" , true ) ;
references . Add ( assemblyFile , bPrivate ) ;
2020-12-21 15:58:58 -04:00
}
}
}
/// <summary>
/// Parses a project reference from a given 'ProjectReference' element
/// </summary>
2022-03-24 16:35:00 -04:00
/// <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 )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
string? includePath = UnescapeString ( parentElement . GetAttribute ( "Include" ) ) ;
if ( ! String . IsNullOrEmpty ( includePath ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
FileReference projectFile = FileReference . Combine ( baseDirectory , ExpandProperties ( includePath , properties ) ) ;
bool bPrivate = GetChildElementBoolean ( parentElement , "Private" , true ) ;
projectReferences [ projectFile ] = bPrivate ;
2020-12-21 15:58:58 -04:00
}
}
/// 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
2022-03-24 16:35:00 -04:00
static void ProcessPathComponents ( DirectoryReference existingPath , IEnumerable < string > remainingComponents , List < FileReference > outFoundFiles )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
if ( ! remainingComponents . Any ( ) )
2020-12-21 15:58:58 -04:00
{
return ;
}
// take a look at the first component
2022-03-24 16:35:00 -04:00
string currentComponent = remainingComponents . First ( ) ;
remainingComponents = remainingComponents . Skip ( 1 ) ;
2020-12-21 15:58:58 -04:00
// If no other components then this is either a file pattern or a greedy pattern
2022-03-24 16:35:00 -04:00
if ( ! remainingComponents . Any ( ) )
2020-12-21 15:58:58 -04:00
{
// ** means include all files under this tree, so enumerate them all
2022-03-24 16:35:00 -04:00
if ( currentComponent . Contains ( "**" ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
outFoundFiles . AddRange ( DirectoryReference . EnumerateFiles ( existingPath , "*" , SearchOption . AllDirectories ) ) ;
2020-12-21 15:58:58 -04:00
}
else
{
// easy, a regular path with a file that may or may not be a wildcard
2022-03-24 16:35:00 -04:00
outFoundFiles . AddRange ( DirectoryReference . EnumerateFiles ( existingPath , currentComponent ) ) ;
2020-12-21 15:58:58 -04:00
}
}
else
{
// new component contains a wildcard, and based on the above we know there are more entries so find
// matching directories
2022-03-24 16:35:00 -04:00
if ( currentComponent . Contains ( "*" ) )
2020-12-21 15:58:58 -04:00
{
// ** means all directories, no matter how deep
2022-03-24 16:35:00 -04:00
SearchOption option = currentComponent = = "**" ? SearchOption . AllDirectories : SearchOption . TopDirectoryOnly ;
2020-12-21 15:58:58 -04:00
2022-03-24 16:35:00 -04:00
IEnumerable < DirectoryReference > directories = DirectoryReference . EnumerateDirectories ( existingPath , currentComponent , option ) ;
2020-12-21 15:58:58 -04:00
// if we searched all directories regardless of depth, the rest of the components other than the last (file) are irrelevant
2022-03-24 16:35:00 -04:00
if ( option = = SearchOption . AllDirectories )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
remainingComponents = new [ ] { remainingComponents . Last ( ) } ;
2020-12-21 15:58:58 -04:00
// ** includes files in the current directory too
2022-03-24 16:35:00 -04:00
directories = directories . Concat ( new [ ] { existingPath } ) ;
2020-12-21 15:58:58 -04:00
}
2022-03-24 16:35:00 -04:00
foreach ( DirectoryReference dir in directories )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
ProcessPathComponents ( dir , remainingComponents , outFoundFiles ) ;
2020-12-21 15:58:58 -04:00
}
}
else
{
// add this component to our path and recurse.
2022-03-24 16:35:00 -04:00
existingPath = DirectoryReference . Combine ( existingPath , currentComponent ) ;
2020-12-21 15:58:58 -04:00
// but... we can just take all the next components that don't have wildcards in them instead of recursing
// into each one!
2022-03-24 16:35:00 -04:00
IEnumerable < string > nonWildCardComponents = remainingComponents . TakeWhile ( c = > ! c . Contains ( "*" ) ) ;
remainingComponents = remainingComponents . Skip ( nonWildCardComponents . Count ( ) ) ;
2020-12-21 15:58:58 -04:00
2022-03-24 16:35:00 -04:00
existingPath = DirectoryReference . Combine ( existingPath , nonWildCardComponents . ToArray ( ) ) ;
2020-12-21 15:58:58 -04:00
2022-03-24 16:35:00 -04:00
if ( Directory . Exists ( existingPath . FullName ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
ProcessPathComponents ( existingPath , remainingComponents , outFoundFiles ) ;
2020-12-21 15:58:58 -04:00
}
}
}
}
/// <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>
2022-03-24 16:35:00 -04:00
/// <param name="inPath">Path specifier to process</param>
2020-12-21 15:58:58 -04:00
/// <returns></returns>
2022-03-24 16:35:00 -04:00
static IEnumerable < FileReference > FindMatchingFiles ( FileReference inPath )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
List < FileReference > foundFiles = new List < FileReference > ( ) ;
2020-12-21 15:58:58 -04:00
// split off the drive root
2022-03-24 16:35:00 -04:00
string driveRoot = Path . GetPathRoot ( inPath . FullName ) ! ;
2020-12-21 15:58:58 -04:00
// break the rest of the path into components
2022-03-24 16:35:00 -04:00
string [ ] pathComponents = inPath . FullName . Substring ( driveRoot . Length ) . Split ( new char [ ] { Path . DirectorySeparatorChar , Path . AltDirectorySeparatorChar } ) ;
2020-12-21 15:58:58 -04:00
// Process all the components recursively
2022-03-24 16:35:00 -04:00
ProcessPathComponents ( new DirectoryReference ( driveRoot ) , pathComponents , foundFiles ) ;
2020-12-21 15:58:58 -04:00
2022-03-24 16:35:00 -04:00
return foundFiles ;
2020-12-21 15:58:58 -04:00
}
/// <summary>
/// Parses a project reference from a given 'ProjectReference' element
/// </summary>
2022-03-24 16:35:00 -04:00
/// <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 )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
string? includePath = UnescapeString ( parentElement . GetAttribute ( "Include" ) ) ;
if ( ! String . IsNullOrEmpty ( includePath ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
FileReference sourceFile = FileReference . Combine ( baseDirectory , includePath ) ;
2020-12-21 15:58:58 -04:00
2022-03-24 16:35:00 -04:00
if ( sourceFile . FullName . Contains ( "*" ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
compileReferences . AddRange ( FindMatchingFiles ( sourceFile ) ) ;
2020-12-21 15:58:58 -04:00
}
else
{
2022-03-24 16:35:00 -04:00
compileReferences . Add ( sourceFile ) ;
2020-12-21 15:58:58 -04:00
}
}
}
/// <summary>
/// Parses an assembly reference from a given 'Content' element
/// </summary>
2022-03-24 16:35:00 -04:00
/// <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 )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
string? includePath = UnescapeString ( parentElement . GetAttribute ( "Include" ) ) ;
if ( ! String . IsNullOrEmpty ( includePath ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
string? copyTo = GetChildElementString ( parentElement , "CopyToOutputDirectory" , null ) ;
bool shouldCopy = ! String . IsNullOrEmpty ( copyTo ) & & ( copyTo . Equals ( "Always" , StringComparison . InvariantCultureIgnoreCase ) | | copyTo . Equals ( "PreserveNewest" , StringComparison . InvariantCultureIgnoreCase ) ) ;
FileReference contentFile = FileReference . Combine ( baseDirectory , includePath ) ;
contents . Add ( contentFile , shouldCopy ) ;
2020-12-21 15:58:58 -04:00
}
}
/// <summary>
/// Reads the inner text of a child XML element
/// </summary>
2022-03-24 16:35:00 -04:00
/// <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>
2020-12-21 15:58:58 -04:00
/// <returns>The contents of the child element, or default value if it's not present</returns>
2022-03-24 16:35:00 -04:00
static string? GetChildElementString ( XmlElement parentElement , string name , string? defaultValue )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
XmlElement childElement = parentElement . ChildNodes . OfType < XmlElement > ( ) . FirstOrDefault ( x = > x . Name = = name ) ;
if ( childElement = = null )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
return defaultValue ;
2020-12-21 15:58:58 -04:00
}
else
{
2022-03-24 16:35:00 -04:00
return childElement . InnerText ? ? defaultValue ;
2020-12-21 15:58:58 -04:00
}
}
/// <summary>
/// Read a child XML element with the given name, and parse it as a boolean.
/// </summary>
2022-03-24 16:35:00 -04:00
/// <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>
2020-12-21 15:58:58 -04:00
/// <returns>The parsed boolean, or the default value</returns>
2022-03-24 16:35:00 -04:00
static bool GetChildElementBoolean ( XmlElement parentElement , string name , bool defaultValue )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
string? value = GetChildElementString ( parentElement , name , null ) ;
if ( value = = null )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
return defaultValue ;
2020-12-21 15:58:58 -04:00
}
2022-03-24 16:35:00 -04:00
else if ( value . Equals ( "True" , StringComparison . InvariantCultureIgnoreCase ) )
2020-12-21 15:58:58 -04:00
{
return true ;
}
2022-03-24 16:35:00 -04:00
else if ( value . Equals ( "False" , StringComparison . InvariantCultureIgnoreCase ) )
2020-12-21 15:58:58 -04:00
{
return false ;
}
else
{
2022-03-24 16:35:00 -04:00
return defaultValue ;
2020-12-21 15:58:58 -04:00
}
}
/// <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>
2022-03-24 16:35:00 -04:00
/// <param name="element">The XML element to check</param>
2020-12-21 15:58:58 -04:00
/// <param name="Properties">Dictionary mapping from property names to values.</param>
/// <returns></returns>
2022-03-24 16:35:00 -04:00
static bool EvaluateCondition ( XmlElement element , CsProjectInfo projectInfo )
2020-12-21 15:58:58 -04:00
{
// Read the condition attribute. If it's not present, assume it evaluates to true.
2022-03-24 16:35:00 -04:00
string condition = element . GetAttribute ( "Condition" ) ;
if ( String . IsNullOrEmpty ( condition ) )
2020-12-21 15:58:58 -04:00
{
return true ;
}
// Expand all the properties
2022-03-24 16:35:00 -04:00
condition = ExpandProperties ( condition , projectInfo . Properties ) ;
2020-12-21 15:58:58 -04:00
// Parse literal true/false values
2022-03-24 16:35:00 -04:00
bool outResult ;
if ( Boolean . TryParse ( condition , out outResult ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
return outResult ;
2020-12-21 15:58:58 -04:00
}
// Tokenize the condition
2022-03-24 16:35:00 -04:00
string [ ] tokens = Tokenize ( condition ) ;
2020-12-21 15:58:58 -04:00
2022-03-24 16:35:00 -04:00
char [ ] tokenQuotes = new [ ] { '\'' , '(' , ')' , '{' , '}' , '[' , ']' } ;
2020-12-21 15:58:58 -04:00
// 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
bool bResult ;
// Handle Exists('Platform\Windows\Gauntlet.TargetDeviceWindows.cs')
2022-03-24 16:35:00 -04:00
if ( tokens [ 0 ] = = "Exists" )
2020-12-21 15:58:58 -04:00
{
// remove all quotes, apostrophes etc that are either tokens or wrap tokens (The Tokenize() function is a bit suspect).
2022-03-24 16:35:00 -04:00
string [ ] arguments = tokens . Select ( s = > s . Trim ( tokenQuotes ) ) . Where ( s = > s . Length > 0 ) . ToArray ( ) ;
2020-12-21 15:58:58 -04:00
2022-03-24 16:35:00 -04:00
if ( tokens . Length > 1 )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
FileSystemReference dependency = DirectoryReference . Combine ( projectInfo . ProjectPath . Directory , arguments [ 1 ] ) ;
2020-12-21 15:58:58 -04:00
2022-03-24 16:35:00 -04:00
if ( File . Exists ( dependency . FullName ) | | Directory . Exists ( dependency . FullName ) )
2020-12-21 15:58:58 -04:00
{
return true ;
}
return false ;
}
}
2022-03-24 16:35:00 -04:00
if ( tokens . Length = = 3 & & tokens [ 0 ] . StartsWith ( "'" ) & & tokens [ 1 ] = = "==" & & tokens [ 2 ] . StartsWith ( "'" ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
bResult = String . Compare ( tokens [ 0 ] , tokens [ 2 ] , StringComparison . InvariantCultureIgnoreCase ) = = 0 ;
2020-12-21 15:58:58 -04:00
}
2022-03-24 16:35:00 -04:00
else if ( tokens . Length = = 3 & & tokens [ 0 ] . StartsWith ( "'" ) & & tokens [ 1 ] = = "!=" & & tokens [ 2 ] . StartsWith ( "'" ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
bResult = String . Compare ( tokens [ 0 ] , tokens [ 2 ] , StringComparison . InvariantCultureIgnoreCase ) ! = 0 ;
2020-12-21 15:58:58 -04:00
}
else
{
2022-03-24 16:35:00 -04:00
string msg = String . Format ( "Couldn't parse condition {0} in project file {1}" , element . ToString ( ) , projectInfo . ProjectPath ) ;
throw new Exception ( msg ) ;
2020-12-21 15:58:58 -04:00
}
return bResult ;
}
/// <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>
2022-03-24 16:35:00 -04:00
/// <param name="text">The input string to expand</param>
/// <param name="properties">Dictionary mapping from property names to values.</param>
2020-12-21 15:58:58 -04:00
/// <returns>String with all properties expanded.</returns>
2022-03-24 16:35:00 -04:00
static string ExpandProperties ( string text , Dictionary < string , string > properties )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
string newText = text ;
for ( int idx = newText . IndexOf ( "$(" ) ; idx ! = - 1 ; idx = newText . IndexOf ( "$(" , idx ) )
2020-12-21 15:58:58 -04:00
{
// Find the end of the variable name, accounting for changes in scope
2022-03-24 16:35:00 -04:00
int endIdx = idx + 2 ;
for ( int depth = 1 ; depth > 0 ; endIdx + + )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
if ( endIdx = = newText . Length )
2020-12-21 15:58:58 -04:00
{
throw new Exception ( "Encountered end of string while expanding properties" ) ;
}
2022-03-24 16:35:00 -04:00
else if ( newText [ endIdx ] = = '(' )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
depth + + ;
2020-12-21 15:58:58 -04:00
}
2022-03-24 16:35:00 -04:00
else if ( newText [ endIdx ] = = ')' )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
depth - - ;
2020-12-21 15:58:58 -04:00
}
}
// Convert the property name to tokens
2022-03-24 16:35:00 -04:00
string [ ] tokens = Tokenize ( newText . Substring ( idx + 2 , ( endIdx - 1 ) - ( idx + 2 ) ) ) ;
2020-12-21 15:58:58 -04:00
// Make sure the first token is a valid property name
2022-03-24 16:35:00 -04:00
if ( tokens . Length = = 0 | | ! ( Char . IsLetter ( tokens [ 0 ] [ 0 ] ) | | tokens [ 0 ] [ 0 ] = = '_' | | tokens [ 0 ] [ 0 ] = = '[' ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
throw new Exception ( String . Format ( "Invalid property name '{0}' in .csproj file" , tokens [ 0 ] ) ) ;
2020-12-21 15:58:58 -04:00
}
// Find the value for it, either from the dictionary or the environment block
2022-03-24 16:35:00 -04:00
string value ;
if ( properties . TryGetValue ( tokens [ 0 ] , out string? retrievedValue ) )
2021-11-18 14:37:34 -05:00
{
2022-03-24 16:35:00 -04:00
value = retrievedValue ;
2021-11-18 14:37:34 -05:00
}
else
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
value = Environment . GetEnvironmentVariable ( tokens [ 0 ] ) ? ? "" ;
2020-12-21 15:58:58 -04:00
}
// Evaluate any functions within it
2022-03-24 16:35:00 -04:00
int tokenIdx = 1 ;
while ( tokenIdx + 3 < tokens . Length & & tokens [ tokenIdx ] = = "." & & tokens [ tokenIdx + 2 ] = = "(" )
2020-12-21 15:58:58 -04:00
{
// Read the method name
2022-03-24 16:35:00 -04:00
string methodName = tokens [ tokenIdx + 1 ] ;
2020-12-21 15:58:58 -04:00
// Skip to the first argument
2022-03-24 16:35:00 -04:00
tokenIdx + = 3 ;
2020-12-21 15:58:58 -04:00
// Parse any arguments
2022-03-24 16:35:00 -04:00
List < object > arguments = new List < object > ( ) ;
if ( tokens [ tokenIdx ] ! = ")" )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
arguments . Add ( ParseArgument ( tokens [ tokenIdx ] ) ) ;
tokenIdx + + ;
2020-12-21 15:58:58 -04:00
2022-03-24 16:35:00 -04:00
while ( tokenIdx + 1 < tokens . Length & & tokens [ tokenIdx ] = = "," )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
arguments . Add ( ParseArgument ( tokens [ tokenIdx + 2 ] ) ) ;
tokenIdx + = 2 ;
2020-12-21 15:58:58 -04:00
}
2022-03-24 16:35:00 -04:00
if ( tokens [ tokenIdx ] ! = ")" )
2020-12-21 15:58:58 -04:00
{
throw new Exception ( "Missing closing parenthesis in condition" ) ;
}
}
// Skip over the closing parenthesis
2022-03-24 16:35:00 -04:00
tokenIdx + + ;
2020-12-21 15:58:58 -04:00
// Execute the method
try
{
2022-03-24 16:35:00 -04:00
value = typeof ( string ) . InvokeMember ( methodName , System . Reflection . BindingFlags . Instance | System . Reflection . BindingFlags . Public | System . Reflection . BindingFlags . InvokeMethod , Type . DefaultBinder , value , arguments . ToArray ( ) ) ! . ToString ( ) ! ;
2020-12-21 15:58:58 -04:00
}
2022-03-24 16:35:00 -04:00
catch ( Exception ex )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
throw new Exception ( String . Format ( "Unable to evaluate condition '{0}'" , text ) , ex ) ;
2020-12-21 15:58:58 -04:00
}
}
2022-03-24 16:35:00 -04:00
if ( tokenIdx < tokens . Length & & tokens [ tokenIdx ] = = ":" )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
tokenIdx = tokens . Length ;
2020-12-21 15:58:58 -04:00
}
// Make sure there's nothing left over
2022-03-24 16:35:00 -04:00
if ( tokenIdx ! = tokens . Length )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
throw new Exception ( String . Format ( "Unable to parse token '{0}'" , newText ) ) ;
2020-12-21 15:58:58 -04:00
}
// Replace the variable with its value
2022-03-24 16:35:00 -04:00
newText = newText . Substring ( 0 , idx ) + value + newText . Substring ( endIdx ) ;
2020-12-21 15:58:58 -04:00
// Make sure we skip over the expanded variable; we don't want to recurse on it.
2022-03-24 16:35:00 -04:00
idx + = value . Length ;
2020-12-21 15:58:58 -04:00
}
2022-03-24 16:35:00 -04:00
return newText ;
2020-12-21 15:58:58 -04:00
}
/// <summary>
/// Parse an argument into a framework type
/// </summary>
2022-03-24 16:35:00 -04:00
/// <param name="token">The token to parse</param>
2020-12-21 15:58:58 -04:00
/// <returns>The argument object</returns>
2022-03-24 16:35:00 -04:00
static object ParseArgument ( string token )
2020-12-21 15:58:58 -04:00
{
// Try to parse a string
2022-03-24 16:35:00 -04:00
if ( token . Length > 2 & & token [ 0 ] = = '\'' & & token [ ^ 1 ] = = '\'' )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
return token . Substring ( 1 , token . Length - 2 ) ;
2020-12-21 15:58:58 -04:00
}
// Try to parse an integer
2022-03-24 16:35:00 -04:00
int value ;
if ( Int32 . TryParse ( token , out value ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
return value ;
2020-12-21 15:58:58 -04:00
}
// Otherwise throw an exception
2022-03-24 16:35:00 -04:00
throw new Exception ( String . Format ( "Unable to parse token '{0}' into a .NET framework type" , token ) ) ;
2020-12-21 15:58:58 -04:00
}
/// <summary>
/// Split an MSBuild condition into tokens
/// </summary>
2022-03-24 16:35:00 -04:00
/// <param name="condition">The condition expression</param>
2020-12-21 15:58:58 -04:00
/// <returns>Array of the parsed tokens</returns>
2022-03-24 16:35:00 -04:00
static string [ ] Tokenize ( string condition )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
List < string > tokens = new List < string > ( ) ;
for ( int idx = 0 ; idx < condition . Length ; )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
if ( Char . IsWhiteSpace ( condition [ idx ] ) )
2020-12-21 15:58:58 -04:00
{
// Whitespace
2022-03-24 16:35:00 -04:00
idx + + ;
2020-12-21 15:58:58 -04:00
}
2022-03-24 16:35:00 -04:00
else if ( idx + 1 < condition . Length & & condition [ idx ] = = '=' & & condition [ idx + 1 ] = = '=' )
2020-12-21 15:58:58 -04:00
{
// "==" operator
2022-03-24 16:35:00 -04:00
idx + = 2 ;
tokens . Add ( "==" ) ;
2020-12-21 15:58:58 -04:00
}
2022-03-24 16:35:00 -04:00
else if ( idx + 1 < condition . Length & & condition [ idx ] = = '!' & & condition [ idx + 1 ] = = '=' )
2020-12-21 15:58:58 -04:00
{
// "!=" operator
2022-03-24 16:35:00 -04:00
idx + = 2 ;
tokens . Add ( "!=" ) ;
2020-12-21 15:58:58 -04:00
}
2022-03-24 16:35:00 -04:00
else if ( condition [ idx ] = = '\'' )
2020-12-21 15:58:58 -04:00
{
// Quoted string
2022-03-24 16:35:00 -04:00
int startIdx = idx + + ;
for ( ; ; idx + + )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
if ( idx = = condition . Length )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
throw new Exception ( String . Format ( "Missing end quote in condition string ('{0}')" , condition ) ) ;
2020-12-21 15:58:58 -04:00
}
2022-03-24 16:35:00 -04:00
if ( condition [ idx ] = = '\'' )
2020-12-21 15:58:58 -04:00
{
break ;
}
}
2022-03-24 16:35:00 -04:00
idx + + ;
tokens . Add ( condition . Substring ( startIdx , idx - startIdx ) ) ;
2020-12-21 15:58:58 -04:00
}
2022-03-24 16:35:00 -04:00
else if ( condition [ idx ] = = '[' )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
#pragma warning disable IDE0059 // Unnecessary assignment of a value
2020-12-21 15:58:58 -04:00
// static property function invoke
// format: [Class]::Property
// alternatively: [Class]::Method()
// we consider the entire invocation to be a single token
2022-03-24 16:35:00 -04:00
int startIdx = idx + + ;
int classEndIdx = 0 ;
int methodEndIdx = 0 ;
int methodArgsEndIdx = 0 ;
for ( ; ; idx + + )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
while ( idx < condition . Length & & ( Char . IsLetterOrDigit ( condition [ idx ] ) | | condition [ idx ] = = '_' ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
idx + + ;
2020-12-21 15:58:58 -04:00
}
2022-03-24 16:35:00 -04:00
if ( idx = = condition . Length )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
throw new Exception ( String . Format ( "Found end of condition when searching for end of static property function for condition string ('{0}')" , condition ) ) ;
2020-12-21 15:58:58 -04:00
}
2022-03-24 16:35:00 -04:00
if ( condition [ idx ] = = ']' )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
classEndIdx = idx ;
idx + + ;
2020-12-21 15:58:58 -04:00
break ;
}
}
// skip ::
2022-03-24 16:35:00 -04:00
if ( condition [ idx ] ! = ':' )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
throw new Exception ( String . Format ( "Unexpected format of static property function, expected :: after class declaration in condition string ('{0}')" , condition ) ) ;
2020-12-21 15:58:58 -04:00
}
2022-03-24 16:35:00 -04:00
idx + = 2 ;
2020-12-21 15:58:58 -04:00
2022-03-24 16:35:00 -04:00
while ( idx < condition . Length & & ( Char . IsLetterOrDigit ( condition [ idx ] ) | | condition [ idx ] = = '_' ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
idx + + ;
2020-12-21 15:58:58 -04:00
}
2022-03-24 16:35:00 -04:00
methodEndIdx = idx ;
2020-12-21 15:58:58 -04:00
2022-03-24 16:35:00 -04:00
if ( condition [ idx ] = = '(' )
2020-12-21 15:58:58 -04:00
{
// a method invoke
2022-03-24 16:35:00 -04:00
for ( ; ; idx + + )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
while ( idx < condition . Length & & ( Char . IsLetterOrDigit ( condition [ idx ] ) | | condition [ idx ] = = '_' ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
idx + + ;
2020-12-21 15:58:58 -04:00
}
2022-03-24 16:35:00 -04:00
if ( idx = = condition . Length )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
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 ) ) ;
2020-12-21 15:58:58 -04:00
}
2022-03-24 16:35:00 -04:00
if ( condition [ idx ] = = ')' )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
methodArgsEndIdx = idx ;
idx + + ;
2020-12-21 15:58:58 -04:00
break ;
}
}
}
2022-03-24 16:35:00 -04:00
#pragma warning restore IDE0059 // Unnecessary assignment of a value
2020-12-21 15:58:58 -04:00
2022-03-24 16:35:00 -04:00
idx + + ;
tokens . Add ( condition . Substring ( startIdx , idx - startIdx ) ) ;
2020-12-21 15:58:58 -04:00
}
2022-03-24 16:35:00 -04:00
else if ( Char . IsLetterOrDigit ( condition [ idx ] ) | | condition [ idx ] = = '_' )
2020-12-21 15:58:58 -04:00
{
// Identifier or number
2022-03-24 16:35:00 -04:00
int startIdx = idx + + ;
while ( idx < condition . Length & & ( Char . IsLetterOrDigit ( condition [ idx ] ) | | condition [ idx ] = = '_' ) )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
idx + + ;
2020-12-21 15:58:58 -04:00
}
2022-03-24 16:35:00 -04:00
tokens . Add ( condition . Substring ( startIdx , idx - startIdx ) ) ;
2020-12-21 15:58:58 -04:00
}
else
{
// Other token; assume a single character.
2022-03-24 16:35:00 -04:00
string token = condition . Substring ( idx + + , 1 ) ;
tokens . Add ( token ) ;
2020-12-21 15:58:58 -04:00
}
}
2022-03-24 16:35:00 -04:00
return tokens . ToArray ( ) ;
2020-12-21 15:58:58 -04:00
}
/// <summary>
/// Un-escape an MSBuild string (see https://msdn.microsoft.com/en-us/library/bb383819.aspx)
/// </summary>
2022-03-24 16:35:00 -04:00
/// <param name="text">String to remove escape characters from</param>
2020-12-21 15:58:58 -04:00
/// <returns>Unescaped string</returns>
2022-03-24 16:35:00 -04:00
static string? UnescapeString ( string? text )
2020-12-21 15:58:58 -04:00
{
const string HexChars = "0123456789abcdef" ;
2022-03-24 16:35:00 -04:00
string? newText = text ;
if ( newText ! = null )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
for ( int idx = 0 ; idx + 2 < newText . Length ; idx + + )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
if ( newText [ idx ] = = '%' )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
int upperDigitIdx = HexChars . IndexOf ( Char . ToLowerInvariant ( newText [ idx + 1 ] ) ) ;
if ( upperDigitIdx ! = - 1 )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
int lowerDigitIdx = HexChars . IndexOf ( Char . ToLowerInvariant ( newText [ idx + 2 ] ) ) ;
if ( lowerDigitIdx ! = - 1 )
2020-12-21 15:58:58 -04:00
{
2022-03-24 16:35:00 -04:00
char newChar = ( char ) ( ( upperDigitIdx < < 4 ) | lowerDigitIdx ) ;
newText = newText . Substring ( 0 , idx ) + newChar + newText . Substring ( idx + 3 ) ;
2020-12-21 15:58:58 -04:00
}
}
}
}
}
2022-03-24 16:35:00 -04:00
return newText ;
2020-12-21 15:58:58 -04:00
}
}
}