-
Notifications
You must be signed in to change notification settings - Fork 5
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
AdmiringWorm
wants to merge
3
commits into
cake-contrib:develop
Choose a base branch
from
AdmiringWorm:load-with-reflection
base: develop
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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
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,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; | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.