Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(GH-17) Use dynamic loading of addins #127

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Cake.Issues.Recipe/Content/addins.cake
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
#addin nuget:?package=Cake.Issues.DupFinder&version=0.9.0
#addin nuget:?package=Cake.Issues.Markdownlint&version=0.9.0
#addin nuget:?package=Cake.Issues.Reporting&version=0.9.0
#addin nuget:?package=Cake.Issues.Reporting.Generic&version=0.9.0
#addin nuget:?package=Cake.Issues.PullRequests&version=0.9.0
#addin nuget:?package=Cake.Issues.PullRequests.AppVeyor&version=0.9.0
#addin nuget:?package=Cake.Issues.PullRequests.AzureDevOps&version=0.9.0
#addin nuget:?package=Cake.AzureDevOps&version=0.5.0
#addin nuget:?package=Cake.AzureDevOps&version=0.5.0

public const string CakeIssuesReportingGenericVersion = "0.8.2";
29 changes: 19 additions & 10 deletions Cake.Issues.Recipe/Content/build.cake
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
#load IssuesBuildTasksDefinitions.cake
#load version.cake
#load data/data.cake
#load loader/loader.cake
#load parameters/parameters.cake
#load reporters/reporters.cake

///////////////////////////////////////////////////////////////////////////////
// GLOBAL VARIABLES
Expand Down Expand Up @@ -128,16 +130,23 @@ IssuesBuildTasks.CreateFullIssuesReportTask = Task("Create-FullIssuesReport")
IssuesParameters.OutputDirectory.CombineWithFilePath(reportFileName);
EnsureDirectoryExists(IssuesParameters.OutputDirectory);

// Create HTML report using DevExpress template.
var settings =
GenericIssueReportFormatSettings
.FromEmbeddedTemplate(GenericIssueReportTemplate.HtmlDxDataGrid)
.WithOption(HtmlDxDataGridOption.Theme, DevExtremeTheme.MaterialBlueLight);
CreateIssueReport(
data.Issues,
GenericIssueReportFormat(settings),
data.BuildRootDirectory,
data.FullIssuesReport);
var issueFormats = new List<IIssueReportFormat>();

if (!Context.Environment.Runtime.IsCoreClr)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Loading of addins should mainly be triggered by parameters. Meaning that if any parameter is set which requires the reporting addin it should be loaded. In case of the reporting addin which is currently not compatible with .NET Core we can decide what to do in this case (IMHO it should be an error, since the user says he want's report and the script is not capable of creating one).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, I didn't want to implement any changes to the options class for this PR, which is why I just added a check if we are running on .NET Core or not.

But I do agree that it should be triggered by parameters.

{
Information("Creating report format using Generic Reporter");
issueFormats.Add(GenericReporterData.CreateIssueFormatFromEmbeddedTemplate(Context, IssuesParameters.Reporting));
}

foreach (var issueFormat in issueFormats)
{
CreateIssueReport(
data.Issues,
issueFormat,
data.BuildRootDirectory,
data.FullIssuesReport
);
}
});

IssuesBuildTasks.PublishIssuesArtifactsTask = Task("Publish-IssuesArtifacts")
Expand Down
289 changes: 289 additions & 0 deletions Cake.Issues.Recipe/Content/loader/AddinData.cake
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
using System.Reflection;
using Cake.Core.Annotations;

public class AddinData
{
private readonly ICakeContext _context;

public AddinData(ICakeContext context, string packageName, string packageVersion, string assemblyName = null)
{
this._context = context;
this.Initialize(context, packageName, packageVersion, assemblyName);
}

public Assembly AddinAssembly { get; private set; }
public IList<Type> _declaredEnums = new List<Type>();
public IList<TypeInfo> _definedClasses = new List<TypeInfo>();
private IList<MethodInfo> _definedMethods = new List<MethodInfo>();

public ClassWrapper CreateClass(string classTypeString, params object[] parameters)
{
var possibleClass = _definedClasses.FirstOrDefault(c => string.Compare(c.Name, classTypeString, StringComparison.OrdinalIgnoreCase) == 0);

if (possibleClass is null)
{
throw new NullReferenceException($"No loaded class named {classTypeString} was found in this assembly.");
}

return CreateClass(possibleClass, parameters);
}

public ClassWrapper CreateClass(TypeInfo classType, params object[] parameters)
{
parameters = parameters ?? new object[0];
var constructors = classType.DeclaredConstructors.Where(c => c.IsPublic && !c.IsStatic && c.GetParameters().Length == parameters.Length);
ConstructorInfo constructor = null;

foreach (var ctx in constructors)
{
var ctxParams = ctx.GetParameters();
bool useCtx = ParametersMatch(ctxParams, parameters);

if (useCtx)
{
constructor = ctx;
break;
}
}

if (constructor is null)
{
throw new NullReferenceException("No valid constructor was found!");
}

var result = constructor.Invoke(parameters ?? new object[0]);

return new ClassWrapper(result, this);
}

public TType CallStaticMethod<TType>(string methodName, params object[] parameters)
{
var result = CallStaticMethod(methodName, parameters);

if (result.GetType().IsClass)
{
return (TType)result.ToActual();
}

return (TType)result;
}

public dynamic CallStaticMethod(string methodName, params object[] parameters)
{
parameters = TransformParameters(parameters);

var methods = this._definedMethods.Where(m => m.IsPublic && m.IsStatic && string.Compare(m.Name, methodName, StringComparison.OrdinalIgnoreCase) == 0);
MethodInfo method = null;

foreach (var m in methods.Where(m => m.GetParameters().Length == parameters.Length))
{
var methodParams = m.GetParameters();
bool useMethod = ParametersMatch(methodParams, parameters);

if (useMethod)
{
method = m;
break;
}
}

if (method is null)
{
throw new NullReferenceException($"No method with the name '{methodName}' was found!");
}

var result = method.Invoke(null, parameters);

if (result.GetType().IsClass)
{
return new ClassWrapper(result, this);
}

return result;
}

public object[] TransformParameters(params object[] parameters)
{
var newParameters = new List<object>();
if (parameters is null)
{
return newParameters.ToArray();
}

foreach (var parameter in parameters)
{
object value = parameter;
if (parameter is string sParam)
{
int index = sParam.IndexOf('.');
if (index >= 0)
{
var enumOrClass = sParam.Substring(0, index);
var subValue = sParam.Substring(index+1);
var enumType = _declaredEnums.FirstOrDefault(e => string.Compare(e.Name, enumOrClass, StringComparison.OrdinalIgnoreCase) == 0);
var classType = _definedClasses.FirstOrDefault(c => string.Compare(c.Name, enumOrClass, StringComparison.OrdinalIgnoreCase) == 0);
if (enumType is object)
{
value = Enum.Parse(enumType, subValue);
}
else if (classType is object)
{
var property = classType.GetProperty(subValue, BindingFlags.GetProperty | BindingFlags.Public | BindingFlags.Static);
value = property.GetValue(null);
}
}
}
else if (parameter is ClassWrapper wrapper)
{
value = wrapper.ToActual();
}

newParameters.Add(value);
}

return newParameters.ToArray();
}

public static bool ParametersMatch(ParameterInfo[] methodParameters, object[] parameters)
{
bool useMethod = true;
for (int i = 0; i < methodParameters.Length && useMethod; i++)
{
var methodParamType = methodParameters[i].ParameterType;
var optionParamType = parameters[i].GetType();
if (methodParamType.IsEnum && optionParamType == typeof(string))
{
try
{
var parsedValue = Enum.Parse(methodParamType, parameters[i].ToString());
if (parsedValue is object)
{
parameters[i] = parsedValue;
}
else
useMethod = false;
}
catch
{
useMethod = false;
}
}
else if (methodParamType == typeof(Enum) && optionParamType.IsEnum)
{
useMethod = true;
}
else
{
useMethod = methodParamType == optionParamType || methodParamType.IsAssignableFrom(optionParamType);
}
}

return useMethod;
}

protected void Initialize(ICakeContext context, string packageName, string packageVersion, string assemblyName = null)
{
if (string.IsNullOrEmpty(assemblyName))
{
assemblyName = packageName;
}

var assembly = LoadAddinAssembly(context, packageName, packageVersion, assemblyName);

if (assembly is null)
{
return;
}

AddinAssembly = assembly;

foreach (var ti in assembly.DefinedTypes.Where(ti => ti.IsPublic))
{
if (ti.IsEnum)
{
_declaredEnums.Add(ti.AsType());
}
else if(ti.IsClass && (!ti.IsAbstract || ti.IsStatic()) && !ti.IsGenericTypeDefinition)
{
_definedClasses.Add(ti);
ParseClass(context, ti);
}
}
}

protected void ParseClass(ICakeContext context, TypeInfo classTypeInfo)
{
var aliases = new List<MethodInfo>();
var methods = new List<MethodInfo>();

foreach (var mi in classTypeInfo.DeclaredMethods.Where(i => i.IsPublic))
{
_definedMethods.Add(mi);
}
}

private static Assembly LoadAddinAssembly(ICakeContext context, string packageName, string packageVersion, string assemblyName)
{
var dllPath = GetAssemblyPath(context, packageName, packageVersion, assemblyName);

if (dllPath is null)
{
var script = $"#tool nuget:?package={packageName}&version={packageVersion}&prerelease";
var tempFile = Guid.NewGuid() + ".cake";

System.IO.File.WriteAllText(tempFile, script);

try
{
context.CakeExecuteScript(tempFile);
}
finally
{
if (context.FileExists(tempFile))
{
context.DeleteFile(tempFile);
}
}
}

dllPath = GetAssemblyPath(context, packageName, packageVersion, assemblyName);

if (dllPath is null)
{
context.Warning("Unable to find path to the {0} package assembly!", packageName);
return null;
}

var assembly = Assembly.LoadFrom(dllPath.FullPath);
return assembly;
}

private static FilePath GetAssemblyPath(ICakeContext context, string packageName, string packageVersion, string assemblyName)
{
FilePath dllPath = null;
const string pathFormat = "{0}.{1}/**/{2}*/{3}.dll";

var possibleFrameworks = new List<String>();

if (context.Environment.Runtime.IsCoreClr)
{
possibleFrameworks.Add("netcoreapp");
}
else
{
possibleFrameworks.Add("net4");
}
possibleFrameworks.Add("netstandard");

foreach (var framework in possibleFrameworks)
{
dllPath = context.Tools.Resolve(string.Format(pathFormat, packageName, packageVersion, framework, assemblyName));
if (dllPath is null)
dllPath = context.Tools.Resolve(string.Format(pathFormat, packageName, "*", framework, assemblyName));
if (dllPath is object)
break;
}

return dllPath;
}
}
Loading