-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* generating API info * diff-ing from an API info * merging assemblies
- Loading branch information
1 parent
0af330a
commit 41daca9
Showing
7 changed files
with
442 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
using Mono.Options; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.IO; | ||
using System.Linq; | ||
using System.Text; | ||
|
||
namespace Mono.ApiTools | ||
{ | ||
public class ApiInfoCommand : BaseCommand | ||
{ | ||
private const int DefaultSaveBufferSize = 1024; | ||
|
||
private static readonly Encoding UTF8NoBOM = new UTF8Encoding(false, true); | ||
|
||
public ApiInfoCommand() | ||
: base("api-info", "ASSEMBLY ...", "Generate API info XML for assemblies.") | ||
{ | ||
} | ||
|
||
public List<string> Assemblies { get; set; } = new List<string>(); | ||
|
||
public string OutputPath { get; set; } | ||
|
||
public bool IgnoreNonbreaking { get; set; } | ||
|
||
protected override OptionSet OnCreateOptions() => new OptionSet | ||
{ | ||
{ "o|output=", "The output file path", v => OutputPath = v }, | ||
{ "ignore-nonbreaking", "Ignore the non-breaking changes and just output breaking changes", v => IgnoreNonbreaking = true }, | ||
}; | ||
|
||
protected override bool OnValidateArguments(IEnumerable<string> extras) | ||
{ | ||
var hasError = false; | ||
|
||
var assemblies = extras.Where(p => !string.IsNullOrWhiteSpace(p)).ToArray(); | ||
foreach (var assemblyOrDir in assemblies.ToArray()) | ||
{ | ||
if (Directory.Exists(assemblyOrDir)) | ||
{ | ||
Assemblies.AddRange(Directory.GetFiles(assemblyOrDir, "*.dll")); | ||
} | ||
else if (File.Exists(assemblyOrDir)) | ||
{ | ||
Assemblies.Add(assemblyOrDir); | ||
} | ||
else | ||
{ | ||
Console.Error.WriteLine($"{Program.Name}: File does not exist: `{assemblyOrDir}`."); | ||
hasError = true; | ||
} | ||
} | ||
|
||
if (Assemblies.Count == 0) | ||
{ | ||
Console.Error.WriteLine($"{Program.Name}: At least one assembly is required."); | ||
hasError = true; | ||
} | ||
|
||
if (!string.IsNullOrWhiteSpace(OutputPath)) | ||
{ | ||
var dir = Path.GetDirectoryName(OutputPath); | ||
if (!string.IsNullOrWhiteSpace(dir) && !Directory.Exists(dir)) | ||
Directory.CreateDirectory(dir); | ||
} | ||
|
||
return !hasError; | ||
} | ||
|
||
protected override bool OnInvoke(IEnumerable<string> extras) | ||
{ | ||
foreach (var assembly in Assemblies) | ||
{ | ||
if (Program.Verbose) | ||
Console.WriteLine($"Generating API information for '{assembly}'..."); | ||
|
||
using var stream = File.OpenRead(assembly); | ||
using var info = GenerateAssemblyApiInfo(stream); | ||
|
||
var path = OutputPath; | ||
if (string.IsNullOrWhiteSpace(path)) | ||
{ | ||
if (assembly.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) | ||
path = Path.ChangeExtension(assembly, ".api-info.xml"); | ||
else | ||
path = assembly + ".api-info.xml"; | ||
} | ||
|
||
using var output = File.Create(path); | ||
info.CopyTo(output); | ||
} | ||
|
||
return true; | ||
} | ||
|
||
private Stream GenerateAssemblyApiInfo(Stream assemblyStream) | ||
{ | ||
var config = new ApiInfoConfig | ||
{ | ||
IgnoreResolutionErrors = true | ||
}; | ||
|
||
var info = new MemoryStream(); | ||
|
||
using (var writer = new StreamWriter(info, UTF8NoBOM, DefaultSaveBufferSize, true)) | ||
{ | ||
ApiInfo.Generate(assemblyStream, writer, config); | ||
} | ||
|
||
assemblyStream.Position = 0; | ||
info.Position = 0; | ||
|
||
return info; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
using ILRepacking; | ||
using Mono.Cecil; | ||
using Mono.Cecil.Cil; | ||
using System; | ||
using System.Collections.Generic; | ||
using System.IO; | ||
using System.Linq; | ||
|
||
namespace Mono.ApiTools | ||
{ | ||
public class AssemblyMerger | ||
{ | ||
public List<string> SearchDirectories { get; set; } = new List<string>(); | ||
|
||
public bool InjectAssemblyNames { get; set; } | ||
|
||
public string InjectedAttributeFullName { get; set; } = "Mono.ApiTools.Internal.InjectedAssemblyNameAttribute"; | ||
|
||
public bool Verbose { get; set; } | ||
|
||
public void Merge(IEnumerable<string> assemblies, string outputPath) | ||
{ | ||
var assembliesToMerge = assemblies?.ToList() ?? throw new ArgumentNullException(nameof(assemblies)); | ||
|
||
var assemblyResolver = new DefaultAssemblyResolver(); | ||
if (SearchDirectories != null) | ||
{ | ||
foreach (var dir in SearchDirectories) | ||
{ | ||
assemblyResolver.AddSearchDirectory(dir); | ||
} | ||
} | ||
|
||
if (Verbose) | ||
{ | ||
Console.WriteLine("Merging:"); | ||
foreach (var include in assembliesToMerge) | ||
Console.WriteLine($" - {include}"); | ||
} | ||
|
||
var tempRoot = Path.Combine(Path.GetTempPath(), "Mono.ApiTools", "AssemblyMerger", Guid.NewGuid().ToString()); | ||
|
||
if (InjectAssemblyNames) | ||
{ | ||
if (!string.IsNullOrWhiteSpace(tempRoot) && !Directory.Exists(tempRoot)) | ||
Directory.CreateDirectory(tempRoot); | ||
|
||
assembliesToMerge = assembliesToMerge.ToList(); | ||
|
||
for (int i = 0; i < assembliesToMerge.Count; i++) | ||
{ | ||
var ass = assembliesToMerge[i]; | ||
var temp = Path.Combine(tempRoot, Guid.NewGuid().ToString() + ".dll"); | ||
InjectAssemblyName(assemblyResolver, ass, temp); | ||
assembliesToMerge[i] = temp; | ||
} | ||
|
||
if (Verbose) | ||
{ | ||
Console.WriteLine("Temporary assemblies:"); | ||
foreach (var include in assembliesToMerge) | ||
Console.WriteLine($" - {include}"); | ||
} | ||
} | ||
|
||
var options = new RepackOptions | ||
{ | ||
InputAssemblies = assembliesToMerge.ToArray(), | ||
OutputFile = outputPath, | ||
SearchDirectories = SearchDirectories.ToArray(), | ||
CopyAttributes = true, | ||
AllowMultipleAssemblyLevelAttributes = true, | ||
LogVerbose = Verbose | ||
}; | ||
options.AllowedDuplicateTypes.Add(InjectedAttributeFullName, InjectedAttributeFullName); | ||
|
||
var repacker = new ILRepack(options); | ||
repacker.Repack(); | ||
|
||
if (InjectAssemblyNames) | ||
{ | ||
if (Directory.Exists(tempRoot)) | ||
{ | ||
try | ||
{ | ||
Directory.Delete(tempRoot, true); | ||
} | ||
catch | ||
{ | ||
} | ||
} | ||
} | ||
} | ||
|
||
private void InjectAssemblyName(IAssemblyResolver assemblyResolver, string assemblyPath, string outputPath) | ||
{ | ||
var assemblyName = Path.GetFileNameWithoutExtension(assemblyPath); | ||
|
||
using var assembly = AssemblyDefinition.ReadAssembly(assemblyPath); | ||
var module = assembly.MainModule; | ||
|
||
var attributeType = ResolveSystemAttribute(module); | ||
var baseCtor = attributeType.Methods.FirstOrDefault(m => m.Name == ".ctor"); | ||
|
||
var ns = InjectedAttributeFullName.Substring(0, InjectedAttributeFullName.LastIndexOf(".")); | ||
var tn = InjectedAttributeFullName.Substring(InjectedAttributeFullName.LastIndexOf(".") + 1); | ||
var iana = new TypeDefinition(ns, tn, TypeAttributes.Class); | ||
iana.BaseType = module.ImportReference(attributeType); | ||
|
||
var field = new FieldDefinition("assemblyName", FieldAttributes.Private | FieldAttributes.InitOnly, module.TypeSystem.String); | ||
|
||
var getterAttributes = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName; | ||
var getter = new MethodDefinition("get_AssemblyName", getterAttributes, module.TypeSystem.String); | ||
getter.DeclaringType = iana; | ||
getter.HasThis = true; | ||
getter.IsGetter = true; | ||
getter.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_0)); | ||
getter.Body.Instructions.Add(Instruction.Create(OpCodes.Ldfld, field)); | ||
getter.Body.Instructions.Add(Instruction.Create(OpCodes.Ret)); | ||
|
||
var property = new PropertyDefinition("AssemblyName", PropertyAttributes.None, module.TypeSystem.String); | ||
property.HasThis = true; | ||
property.GetMethod = getter; | ||
|
||
var methodAttributes = MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName; | ||
var ctor = new MethodDefinition(".ctor", methodAttributes, module.TypeSystem.Void); | ||
var param = new ParameterDefinition("assemblyName", ParameterAttributes.None, module.TypeSystem.String); | ||
ctor.Parameters.Add(param); | ||
|
||
ctor.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_0)); | ||
ctor.Body.Instructions.Add(Instruction.Create(OpCodes.Call, module.ImportReference(baseCtor))); | ||
ctor.Body.Instructions.Add(Instruction.Create(OpCodes.Nop)); | ||
ctor.Body.Instructions.Add(Instruction.Create(OpCodes.Nop)); | ||
ctor.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_0)); | ||
ctor.Body.Instructions.Add(Instruction.Create(OpCodes.Ldarg_1)); | ||
ctor.Body.Instructions.Add(Instruction.Create(OpCodes.Stfld, field)); | ||
ctor.Body.Instructions.Add(Instruction.Create(OpCodes.Ret)); | ||
|
||
iana.Fields.Add(field); | ||
iana.Properties.Add(property); | ||
iana.Methods.Add(ctor); | ||
iana.Methods.Add(getter); | ||
module.Types.Add(iana); | ||
|
||
foreach (var type in module.Types) | ||
{ | ||
if (type.FullName == InjectedAttributeFullName) | ||
continue; | ||
|
||
type.CustomAttributes.Add(new CustomAttribute(ctor) | ||
{ | ||
ConstructorArguments = { new CustomAttributeArgument(module.TypeSystem.String, assemblyName) } | ||
}); | ||
} | ||
|
||
assembly.Write(outputPath); | ||
|
||
TypeDefinition ResolveSystemAttribute(ModuleDefinition module) | ||
{ | ||
foreach (var reference in module.AssemblyReferences) | ||
{ | ||
var resolved = assemblyResolver.Resolve(reference); | ||
var attributeType = resolved.MainModule.GetType("System.Attribute"); | ||
if (attributeType != null) | ||
return attributeType; | ||
} | ||
throw new Exception("Unable to locate System.Attribute in any of the referenced assemblies."); | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.