Skip to content

Commit

Permalink
Added some more features (#8)
Browse files Browse the repository at this point in the history
* generating API info
* diff-ing from an API info
* merging assemblies
  • Loading branch information
mattleibow authored Nov 12, 2019
1 parent 0af330a commit 41daca9
Show file tree
Hide file tree
Showing 7 changed files with 442 additions and 21 deletions.
117 changes: 117 additions & 0 deletions api-tools/ApiInfoCommand.cs
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;
}
}
}
171 changes: 171 additions & 0 deletions api-tools/AssemblyMerger.cs
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.");
}
}
}
}
Loading

0 comments on commit 41daca9

Please sign in to comment.