PowerShell/src/TypeCatalogGen/TypeCatalogGen.cs
2021-08-05 16:21:46 -07:00

475 lines
22 KiB
C#

// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
/*
* This is the source code for the tool 'TypeCatalogGen.exe', which has been checked in %SDXROOT%\tools\managed\v4.0\TypeCatalogGen.
* The tool 'TypeCatalogGen.exe' is used when building 'Microsoft.PowerShell.CoreCLR.AssemblyLoadContext.dll' for OneCore powershell
* to generate the CoreCLR type catalog initialization code, which will then be compiled into the same DLL.
*
* See files 'makefile.inc' and 'sources' under directory 'PSAssemblyLoadContext' to learn how the tool and the auto-generated CSharp
* file is used.
*
* Compilation Note:
* .NET Fx Version - 4.5
* Special Dependency - System.Reflection.Metadata.dll, System.Collections.Immutable.dll (Available as nuget package: https://www.nuget.org/packages/System.Reflection.Metadata)
* To compile the code, create a VS project and get the 'System.Reflection.Metadata' package from nuget. Then add this file to the VS
* project and compile it.
*/
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Reflection.Metadata;
using System.Reflection.PortableExecutable;
using System.Security.Cryptography;
using System.Text;
namespace Microsoft.PowerShell.CoreCLR
{
public static class TypeCatalogGen
{
// Help messages
private const string Param_TargetCSharpFilePath = "TargetCSharpFilePath";
private const string Param_ReferenceListPath = "ReferenceListPath";
private const string Param_PrintDebugMessage = "-debug";
private const string HelpMessage = @"
Usage: TypeCatalogGen.exe <{0}> <{1}> [{2}]
- {0}: Path of the target C# source file to generate.
- {1}: Path of the file containing all reference assembly paths, separated by semicolons.
- [{2}]: Write out debug messages. Optional.
";
// Error messages
private const string TargetSourceDirNotFound = "Cannot find the target source directory. The path '{0}' doesn't exist.";
private const string ReferenceListFileNotFound = "Cannot find the file that contains the reference list. The path '{0}' doesn't exist.";
private const string RefAssemblyNotFound = "Reference assembly '{0}' is declared in the reference list file '{1}', but the assembly doesn't exist.";
private const string UnexpectedFileExtension = "Cannot process '{0}' because its extension is neither '.DLL' nor '.METADATA_DLL'. Please make sure the file is an reference assembly.";
// Format strings for constructing type names
private const string Format_RegularType = "{0}.{1}";
private const string Format_SingleLevelNestedType = "{0}.{1}+{2}";
private const string Format_MultiLevelNestedType = "{0}+{1}";
/*
* Go through all reference assemblies of .NET Core and generate the type catalog -> Dictionary<NamespaceQualifiedTypeName, TPAStrongName>
* Then auto-generate the partial class 'PowerShellAssemblyLoadContext' that has the code to initialize the type catalog cache.
*
* In CoreCLR, there is no way to get all loaded TPA assemblies (.NET Framework Assemblies). In order to get type based on type name, powershell needs to know what .NET
* types are available and in which TPA assemblies. So we have to generate the type catalog based on the reference assemblies of .NET Core.
*/
public static void Main(string[] args)
{
if (args.Length < 2 || args.Length > 3)
{
string message = string.Format(CultureInfo.CurrentCulture, HelpMessage,
Param_TargetCSharpFilePath,
Param_ReferenceListPath,
Param_PrintDebugMessage);
Console.WriteLine(message);
return;
}
bool printDebugMessage = args.Length == 3 && string.Equals(Param_PrintDebugMessage, args[2], StringComparison.OrdinalIgnoreCase);
string targetFilePath = ResolveTargetFilePath(args[0]);
List<string> refAssemblyFiles = ResolveReferenceAssemblies(args[1]);
Dictionary<string, TypeMetadata> typeNameToAssemblyMap = new Dictionary<string, TypeMetadata>(StringComparer.OrdinalIgnoreCase);
// mscorlib.metadata_dll doesn't contain any type definition.
foreach (string filePath in refAssemblyFiles)
{
if (!filePath.EndsWith(".METADATA_DLL", StringComparison.OrdinalIgnoreCase) &&
!filePath.EndsWith(".DLL", StringComparison.OrdinalIgnoreCase))
{
string message = string.Format(CultureInfo.CurrentCulture, UnexpectedFileExtension, filePath);
throw new InvalidOperationException(message);
}
using (Stream stream = File.OpenRead(filePath))
using (PEReader peReader = new PEReader(stream))
{
MetadataReader metadataReader = peReader.GetMetadataReader();
string strongAssemblyName = GetAssemblyStrongName(metadataReader);
foreach (TypeDefinitionHandle typeHandle in metadataReader.TypeDefinitions)
{
// We only care about public types
TypeDefinition typeDefinition = metadataReader.GetTypeDefinition(typeHandle);
// The visibility mask is used to mask out the bits that contain the visibility.
// The visibilities are not combineable, e.g. you can't be both public and private, which is why these aren't independent powers of two.
TypeAttributes visibilityBits = typeDefinition.Attributes & TypeAttributes.VisibilityMask;
if (visibilityBits != TypeAttributes.Public && visibilityBits != TypeAttributes.NestedPublic)
{
continue;
}
string fullName = GetTypeFullName(metadataReader, typeDefinition);
bool isTypeObsolete = IsTypeObsolete(metadataReader, typeDefinition);
if (!typeNameToAssemblyMap.ContainsKey(fullName))
{
// Add unique type.
typeNameToAssemblyMap.Add(fullName, new TypeMetadata(strongAssemblyName, isTypeObsolete));
}
else if (typeNameToAssemblyMap[fullName].IsObsolete && !isTypeObsolete)
{
// Duplicate types found defined in different assemblies, but the previous one is obsolete while the current one is not.
// Replace the existing type with the current one.
if (printDebugMessage)
{
var existingTypeMetadata = typeNameToAssemblyMap[fullName];
Console.WriteLine($@"
REPLACE '{fullName}' from '{existingTypeMetadata.AssemblyName}' (IsObsolete? {existingTypeMetadata.IsObsolete})
WITH '{strongAssemblyName}' (IsObsolete? {isTypeObsolete})");
}
typeNameToAssemblyMap[fullName] = new TypeMetadata(strongAssemblyName, isTypeObsolete);
}
else if (printDebugMessage)
{
// Duplicate types found defined in different assemblies, and fall into one of the following conditions:
// - both are obsolete
// - both are not obsolete
// - the existing type is not obsolete while the new one is obsolete
var existingTypeMetadata = typeNameToAssemblyMap[fullName];
Console.WriteLine($@"
DUPLICATE key '{fullName}' from '{strongAssemblyName}' (IsObsolete? {isTypeObsolete}).
-- Already exist in '{existingTypeMetadata.AssemblyName}' (IsObsolete? {existingTypeMetadata.IsObsolete})");
}
}
}
}
WritePowerShellAssemblyLoadContextPartialClass(targetFilePath, typeNameToAssemblyMap);
}
/// <summary>
/// Check if the type is obsolete.
/// </summary>
private static bool IsTypeObsolete(MetadataReader reader, TypeDefinition typeDefinition)
{
const string obsoleteFullTypeName = "System.ObsoleteAttribute";
foreach (var customAttributeHandle in typeDefinition.GetCustomAttributes())
{
var customAttribute = reader.GetCustomAttribute(customAttributeHandle);
if (IsAttributeOfType(reader, customAttribute, obsoleteFullTypeName))
{
return true;
}
}
return false;
}
/// <summary>
/// Check if the attribute type name is what we expected.
/// </summary>
private static bool IsAttributeOfType(MetadataReader reader, CustomAttribute customAttribute, string expectedTypeName)
{
string attributeFullName = null;
switch (customAttribute.Constructor.Kind)
{
case HandleKind.MethodDefinition:
// Attribute is defined in the same module
MethodDefinition methodDef = reader.GetMethodDefinition((MethodDefinitionHandle)customAttribute.Constructor);
TypeDefinitionHandle declaringTypeDefHandle = methodDef.GetDeclaringType();
if (declaringTypeDefHandle.IsNil) { /* Global method */ return false; }
TypeDefinition declaringTypeDef = reader.GetTypeDefinition(declaringTypeDefHandle);
attributeFullName = GetTypeFullName(reader, declaringTypeDef);
break;
case HandleKind.MemberReference:
MemberReference memberRef = reader.GetMemberReference((MemberReferenceHandle)customAttribute.Constructor);
switch (memberRef.Parent.Kind)
{
case HandleKind.TypeReference:
TypeReference typeRef = reader.GetTypeReference((TypeReferenceHandle)memberRef.Parent);
attributeFullName = GetTypeFullName(reader, typeRef);
break;
case HandleKind.TypeDefinition:
TypeDefinition typeDef = reader.GetTypeDefinition((TypeDefinitionHandle)memberRef.Parent);
attributeFullName = GetTypeFullName(reader, typeDef);
break;
default:
// constructor is global method, vararg method, or from a generic type.
return false;
}
break;
default:
throw new BadImageFormatException("Invalid custom attribute.");
}
return string.Equals(attributeFullName, expectedTypeName, StringComparison.Ordinal);
}
/// <summary>
/// Get the strong name of a reference assembly represented by the 'metadataReader'
/// </summary>
private static string GetAssemblyStrongName(MetadataReader metadataReader)
{
AssemblyDefinition assemblyDefinition = metadataReader.GetAssemblyDefinition();
string asmName = metadataReader.GetString(assemblyDefinition.Name);
string asmVersion = assemblyDefinition.Version.ToString();
string asmCulture = metadataReader.GetString(assemblyDefinition.Culture);
asmCulture = (asmCulture == string.Empty) ? "neutral" : asmCulture;
AssemblyHashAlgorithm hashAlgorithm = assemblyDefinition.HashAlgorithm;
BlobHandle blobHandle = assemblyDefinition.PublicKey;
BlobReader blobReader = metadataReader.GetBlobReader(blobHandle);
byte[] publickey = blobReader.ReadBytes(blobReader.Length);
HashAlgorithm hashImpl = null;
switch (hashAlgorithm)
{
case AssemblyHashAlgorithm.Sha1:
hashImpl = SHA1.Create();
break;
case AssemblyHashAlgorithm.MD5:
hashImpl = MD5.Create();
break;
case AssemblyHashAlgorithm.Sha256:
hashImpl = SHA256.Create();
break;
case AssemblyHashAlgorithm.Sha384:
hashImpl = SHA384.Create();
break;
case AssemblyHashAlgorithm.Sha512:
hashImpl = SHA512.Create();
break;
default:
throw new NotSupportedException();
}
byte[] publicKeyHash = hashImpl.ComputeHash(publickey);
byte[] publicKeyTokenBytes = new byte[8];
// Note that, the low 8 bytes of the hash of public key in reverse order is the public key tokens.
for (int i = 1; i <= 8; i++)
{
publicKeyTokenBytes[i - 1] = publicKeyHash[publicKeyHash.Length - i];
}
// Convert bytes to hex format strings in lower case.
string publicKeyTokenString = BitConverter.ToString(publicKeyTokenBytes).Replace("-", string.Empty).ToLowerInvariant();
string strongAssemblyName = string.Format(CultureInfo.InvariantCulture,
"{0}, Version={1}, Culture={2}, PublicKeyToken={3}",
asmName, asmVersion, asmCulture, publicKeyTokenString);
return strongAssemblyName;
}
/// <summary>
/// Get the full name of a Type reference.
/// </summary>
private static string GetTypeFullName(MetadataReader metadataReader, TypeReference typeReference)
{
string fullName;
string typeName = metadataReader.GetString(typeReference.Name);
string nsName = metadataReader.GetString(typeReference.Namespace);
EntityHandle resolutionScope = typeReference.ResolutionScope;
if (resolutionScope.IsNil || resolutionScope.Kind != HandleKind.TypeReference)
{
fullName = string.Format(CultureInfo.InvariantCulture, Format_RegularType, nsName, typeName);
}
else
{
// It's a nested type.
fullName = typeName;
while (!resolutionScope.IsNil && resolutionScope.Kind == HandleKind.TypeReference)
{
TypeReference declaringTypeRef = metadataReader.GetTypeReference((TypeReferenceHandle)resolutionScope);
resolutionScope = declaringTypeRef.ResolutionScope;
if (resolutionScope.IsNil || resolutionScope.Kind != HandleKind.TypeReference)
{
fullName = string.Format(CultureInfo.InvariantCulture, Format_SingleLevelNestedType,
metadataReader.GetString(declaringTypeRef.Namespace),
metadataReader.GetString(declaringTypeRef.Name),
fullName);
}
else
{
fullName = string.Format(CultureInfo.InvariantCulture, Format_MultiLevelNestedType,
metadataReader.GetString(declaringTypeRef.Name),
fullName);
}
}
}
return fullName;
}
/// <summary>
/// Get the full name of a Type definition.
/// </summary>
private static string GetTypeFullName(MetadataReader metadataReader, TypeDefinition typeDefinition)
{
string fullName;
string typeName = metadataReader.GetString(typeDefinition.Name);
string nsName = metadataReader.GetString(typeDefinition.Namespace);
// Get the enclosing type if the type is nested
TypeDefinitionHandle declaringTypeHandle = typeDefinition.GetDeclaringType();
if (declaringTypeHandle.IsNil)
{
fullName = string.Format(CultureInfo.InvariantCulture, Format_RegularType, nsName, typeName);
}
else
{
fullName = typeName;
while (!declaringTypeHandle.IsNil)
{
TypeDefinition declaringTypeDef = metadataReader.GetTypeDefinition(declaringTypeHandle);
declaringTypeHandle = declaringTypeDef.GetDeclaringType();
if (declaringTypeHandle.IsNil)
{
fullName = string.Format(CultureInfo.InvariantCulture, Format_SingleLevelNestedType,
metadataReader.GetString(declaringTypeDef.Namespace),
metadataReader.GetString(declaringTypeDef.Name),
fullName);
}
else
{
fullName = string.Format(CultureInfo.InvariantCulture, Format_MultiLevelNestedType,
metadataReader.GetString(declaringTypeDef.Name),
fullName);
}
}
}
return fullName;
}
/// <summary>
/// Resolve the target file path.
/// </summary>
private static string ResolveTargetFilePath(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
throw new ArgumentNullException(Param_TargetCSharpFilePath);
}
string targetPath = Path.GetFullPath(path);
string targetParentFolder = Path.GetDirectoryName(targetPath);
if (!Directory.Exists(targetParentFolder))
{
string message = string.Format(CultureInfo.CurrentCulture, TargetSourceDirNotFound, targetParentFolder ?? "null");
throw new ArgumentException(message);
}
return targetPath;
}
/// <summary>
/// Resolve the reference assembly file paths.
/// </summary>
private static List<string> ResolveReferenceAssemblies(string path)
{
if (string.IsNullOrWhiteSpace(path))
{
throw new ArgumentNullException(Param_ReferenceListPath);
}
string referenceListPath = Path.GetFullPath(path);
if (!File.Exists(referenceListPath))
{
string message = string.Format(CultureInfo.CurrentCulture, ReferenceListFileNotFound, referenceListPath ?? "null");
throw new ArgumentException(message);
}
string allText = File.ReadAllText(referenceListPath);
string[] references = allText.Split(';', StringSplitOptions.RemoveEmptyEntries);
List<string> refAssemblyFiles = new List<string>(120);
for (int i = 0; i < references.Length; i++)
{
// Ignore entries that only contain white spaces
if (string.IsNullOrWhiteSpace(references[i]))
{
continue;
}
string refAssemblyPath = references[i].Trim();
if (File.Exists(refAssemblyPath))
{
refAssemblyFiles.Add(refAssemblyPath);
}
else
{
string message = string.Format(CultureInfo.CurrentCulture, RefAssemblyNotFound, refAssemblyPath, referenceListPath);
throw new InvalidDataException(message);
}
}
return refAssemblyFiles;
}
/// <summary>
/// Generate the CSharp source code that initialize the type catalog.
/// </summary>
private static void WritePowerShellAssemblyLoadContextPartialClass(string targetFilePath, Dictionary<string, TypeMetadata> typeNameToAssemblyMap)
{
const string SourceFormat = "{2} {{\"{0}\", \"{1}\"}},";
const string SourceHead = @"//
// This file is auto-generated by TypeCatalogGen.exe during build of Microsoft.PowerShell.CoreCLR.AssemblyLoadContext.dll.
// This file will be compiled into Microsoft.PowerShell.CoreCLR.AssemblyLoadContext.dll.
//
// In CoreCLR, there is no way to get all loaded TPA assemblies (.NET Framework Assemblies). In order to get type based on type
// name, powershell needs to know what .NET types are available and in which TPA assemblies. So we have to generate this type
// catalog based on the reference assemblies of .NET Core.
//
using System.Collections.Generic;
namespace System.Management.Automation
{{
internal partial class PowerShellAssemblyLoadContext
{{
private static Dictionary<string, string> InitializeTypeCatalog()
{{
return new Dictionary<string, string>({0}, StringComparer.OrdinalIgnoreCase) {{";
const string SourceEnd = @"
};
}
}
}
";
StringBuilder sourceCode = new StringBuilder(string.Format(CultureInfo.InvariantCulture, SourceHead, typeNameToAssemblyMap.Count));
foreach (KeyValuePair<string, TypeMetadata> pair in typeNameToAssemblyMap)
{
sourceCode.Append(string.Format(CultureInfo.InvariantCulture, SourceFormat, pair.Key, pair.Value.AssemblyName, Environment.NewLine));
}
sourceCode.Append(SourceEnd);
using (FileStream stream = new FileStream(targetFilePath, FileMode.Create, FileAccess.Write))
using (StreamWriter writer = new StreamWriter(stream, Encoding.ASCII))
{
writer.Write(sourceCode.ToString());
}
}
/// <summary>
/// Helper class to keep the metadata of a type.
/// </summary>
private sealed class TypeMetadata
{
internal readonly string AssemblyName;
internal readonly bool IsObsolete;
internal TypeMetadata(string assemblyName, bool isTypeObsolete)
{
this.AssemblyName = assemblyName;
this.IsObsolete = isTypeObsolete;
}
}
}
}