/// <summary> /// Attempts to resolve assembly references that were specified by the user. /// </summary> /// <param name="log">A <see cref="TaskLoggingHelper"/> used for logging.</param> /// <param name="taskInfo">A <see cref="RoslynCodeTaskFactoryTaskInfo"/> object containing details about the task.</param> /// <param name="items">Receives the list of full paths to resolved assemblies.</param> /// <returns><code>true</code> if all assemblies could be resolved, otherwise <code>false</code>.</returns> /// <remarks>The user can specify a short name like My.Assembly or My.Assembly.dll. In this case we'll /// attempt to look it up in the directory containing our reference assemblies. They can also specify a /// full path and we'll do no resolution. At this time, these are the only two resolution mechanisms. /// Perhaps in the future this could be more powerful by using NuGet to resolve assemblies but we think /// that is too complicated for a simple in-line task. If users have more complex requirements, they /// can compile their own task library.</remarks> internal static bool TryResolveAssemblyReferences(TaskLoggingHelper log, RoslynCodeTaskFactoryTaskInfo taskInfo, out ITaskItem[] items) { // Store the list of resolved assemblies because a user can specify a short name or a full path ISet <string> resolvedAssemblyReferences = new HashSet <string>(StringComparer.OrdinalIgnoreCase); // Keeps track if there were one or more unresolved assemblies bool hasInvalidReference = false; // Start with the user specified references and include all of the default references that are language agnostic IEnumerable <string> references = taskInfo.References.Union(DefaultReferences[String.Empty]); if (DefaultReferences.ContainsKey(taskInfo.CodeLanguage)) { // Append default references for the specific language references = references.Union(DefaultReferences[taskInfo.CodeLanguage]); } // Loop through the user specified references as well as the default references foreach (string reference in references) { // The user specified a full path to an assembly, so there is no need to resolve if (FileSystems.Default.FileExists(reference)) { // The path could be relative like ..\Assembly.dll so we need to get the full path resolvedAssemblyReferences.Add(Path.GetFullPath(reference)); continue; } // Attempt to "resolve" the assembly by getting a full path to our distributed reference assemblies string assemblyFileName = reference.EndsWith(".dll", StringComparison.OrdinalIgnoreCase) || reference.EndsWith(".exe", StringComparison.OrdinalIgnoreCase) ? reference : $"{reference}.dll"; string resolvedDir = new[] { Path.Combine(ThisAssemblyDirectoryLazy.Value, ReferenceAssemblyDirectoryName), ThisAssemblyDirectoryLazy.Value, } .Concat(MonoLibDirs) .FirstOrDefault(p => File.Exists(Path.Combine(p, assemblyFileName))); if (resolvedDir != null) { resolvedAssemblyReferences.Add(Path.Combine(resolvedDir, assemblyFileName)); continue; } // Could not resolve the assembly. We currently don't support looking things up the GAC so that in-line task // assemblies are portable across platforms log.LogErrorWithCodeFromResources("CodeTaskFactory.CouldNotFindReferenceAssembly", reference); hasInvalidReference = true; } // Transform the list of resolved assemblies to TaskItems if they were all resolved items = hasInvalidReference ? null : resolvedAssemblyReferences.Select(i => (ITaskItem) new TaskItem(i)).ToArray(); return(!hasInvalidReference); }
/// <summary> /// Attempts to compile the current source code and load the assembly into memory. /// </summary> /// <param name="buildEngine">An <see cref="IBuildEngine"/> to use give to the compiler task so that messages can be logged.</param> /// <param name="taskInfo">A <see cref="RoslynCodeTaskFactoryTaskInfo"/> object containing details about the task.</param> /// <param name="assembly">The <see cref="Assembly"/> if the source code be compiled and loaded, otherwise <code>null</code>.</param> /// <returns><code>true</code> if the source code could be compiled and loaded, otherwise <code>null</code>.</returns> private bool TryCompileInMemoryAssembly(IBuildEngine buildEngine, RoslynCodeTaskFactoryTaskInfo taskInfo, out Assembly assembly) { // First attempt to get a compiled assembly from the cache if (CompiledAssemblyCache.TryGetValue(taskInfo, out assembly)) { return(true); } if (!TryResolveAssemblyReferences(_log, taskInfo, out ITaskItem[] references))
/// <summary> /// Parses and validates the body of the <UsingTask />. /// </summary> /// <param name="log">A <see cref="TaskLoggingHelper"/> used to log events during parsing.</param> /// <param name="taskName">The name of the task.</param> /// <param name="taskBody">The raw inner XML string of the <UsingTask />> to parse and validate.</param> /// <param name="parameters">An <see cref="ICollection{TaskPropertyInfo}"/> containing parameters for the task.</param> /// <param name="taskInfo">A <see cref="RoslynCodeTaskFactoryTaskInfo"/> object that receives the details of the parsed task.</param> /// <returns><code>true</code> if the task body was successfully parsed, otherwise <code>false</code>.</returns> /// <remarks> /// The <paramref name="taskBody"/> will look like this: /// <![CDATA[ /// /// <Using Namespace="Namespace" /> /// <Reference Include="AssemblyName|AssemblyPath" /> /// <Code Type="Fragment|Method|Class" Language="cs|vb" Source="Path"> /// // Source code /// </Code> /// /// ]]> /// </remarks> internal static bool TryLoadTaskBody(TaskLoggingHelper log, string taskName, string taskBody, ICollection <TaskPropertyInfo> parameters, out RoslynCodeTaskFactoryTaskInfo taskInfo) { taskInfo = new RoslynCodeTaskFactoryTaskInfo { CodeLanguage = "CS", CodeType = RoslynCodeTaskFactoryCodeType.Fragment, Name = taskName, }; XDocument document; try { // For legacy reasons, the inner XML of the <UsingTask /> has no document element. So we have to add a top-level // element around it so it can be parsed. document = XDocument.Parse($"<Task>{taskBody}</Task>"); } catch (Exception e) { log.LogErrorWithCodeFromResources("CodeTaskFactory.InvalidTaskXml", e.Message); return(false); } if (document.Root == null) { log.LogErrorWithCodeFromResources("CodeTaskFactory.InvalidTaskXml", String.Empty); return(false); } XElement codeElement = null; // Loop through the children, ignoring ones we don't care about, parsing valid ones, and logging an error if we // encounter any element that is not recognized. foreach (XNode node in document.Root.Nodes() .Where(i => i.NodeType != XmlNodeType.Comment && i.NodeType != XmlNodeType.Whitespace)) { switch (node.NodeType) { case XmlNodeType.Element: XElement child = (XElement)node; // Parse known elements and go to the default case if its an unknown element if (child.Name.LocalName.Equals("Code")) { if (codeElement != null) { // Only one <Code /> element is allowed. log.LogErrorWithCodeFromResources("CodeTaskFactory.MultipleCodeNodes"); return(false); } codeElement = child; } else if (child.Name.LocalName.Equals("Reference")) { XAttribute includeAttribute = child.Attributes().FirstOrDefault(i => i.Name.LocalName.Equals("Include")); if (String.IsNullOrWhiteSpace(includeAttribute?.Value)) { // A <Reference Include="" /> is not allowed. log.LogErrorWithCodeFromResources("CodeTaskFactory.AttributeEmptyWithElement", "Include", "Reference"); return(false); } // Store the reference in the list taskInfo.References.Add(includeAttribute.Value.Trim()); } else if (child.Name.LocalName.Equals("Using")) { XAttribute namespaceAttribute = child.Attributes().FirstOrDefault(i => i.Name.LocalName.Equals("Namespace")); if (String.IsNullOrWhiteSpace(namespaceAttribute?.Value)) { // A <Using Namespace="" /> is not allowed log.LogErrorWithCodeFromResources("CodeTaskFactory.AttributeEmptyWithElement", "Namespace", "Using"); return(false); } // Store the using in the list taskInfo.Namespaces.Add(namespaceAttribute.Value.Trim()); } else { log.LogErrorWithCodeFromResources("CodeTaskFactory.InvalidElementLocation", child.Name.LocalName, document.Root.Name.LocalName); return(false); } break; default: log.LogErrorWithCodeFromResources("CodeTaskFactory.InvalidElementLocation", node.NodeType, document.Root.Name.LocalName); return(false); } } if (codeElement == null) { // <Code /> element is required so if we didn't find it then we need to error log.LogErrorWithCodeFromResources("CodeTaskFactory.CodeElementIsMissing", taskName); return(false); } // Copies the source code from the inner text of the <Code /> element. This might be override later if the user specified // a file instead. taskInfo.SourceCode = codeElement.Value; // Parse the attributes of the <Code /> element XAttribute languageAttribute = null; XAttribute sourceAttribute = null; XAttribute typeAttribute = null; // TODO: Unit test for this logic and the error message foreach (XAttribute attribute in codeElement.Attributes().Where(i => !i.IsNamespaceDeclaration)) { switch (attribute.Name.LocalName) { case "Language": languageAttribute = attribute; break; case "Source": sourceAttribute = attribute; break; case "Type": typeAttribute = attribute; break; default: log.LogErrorWithCodeFromResources("CodeTaskFactory.InvalidCodeElementAttribute", attribute.Name.LocalName); return(false); } } if (sourceAttribute != null) { if (String.IsNullOrWhiteSpace(sourceAttribute.Value)) { // A <Code Source="" /> is not allowed log.LogErrorWithCodeFromResources("CodeTaskFactory.AttributeEmptyWithElement", "Source", "Code"); return(false); } // Instead of using the inner text of the <Code /> element, read the specified file as source code taskInfo.CodeType = RoslynCodeTaskFactoryCodeType.Class; taskInfo.SourceCode = File.ReadAllText(sourceAttribute.Value.Trim()); } else if (typeAttribute != null) { if (String.IsNullOrWhiteSpace(typeAttribute.Value)) { // A <Code Type="" /> is not allowed log.LogErrorWithCodeFromResources("CodeTaskFactory.AttributeEmptyWithElement", "Type", "Code"); return(false); } // Attempt to parse the code type as a CodeTaskFactoryCodeType if (!Enum.TryParse(typeAttribute.Value.Trim(), ignoreCase: true, result: out RoslynCodeTaskFactoryCodeType codeType)) { log.LogErrorWithCodeFromResources("CodeTaskFactory.InvalidCodeType", typeAttribute.Value, String.Join(", ", Enum.GetNames(typeof(RoslynCodeTaskFactoryCodeType)))); return(false); } taskInfo.CodeType = codeType; } if (languageAttribute != null) { if (String.IsNullOrWhiteSpace(languageAttribute.Value)) { // A <Code Language="" /> is not allowed log.LogErrorWithCodeFromResources("CodeTaskFactory.AttributeEmptyWithElement", "Language", "Code"); return(false); } if (ValidCodeLanguages.ContainsKey(languageAttribute.Value)) { // The user specified one of the primary code languages using our vernacular taskInfo.CodeLanguage = languageAttribute.Value.ToUpperInvariant(); } else { bool foundValidCodeLanguage = false; // Attempt to map the user specified value as an alias to our vernacular for code languages foreach (string validLanguage in ValidCodeLanguages.Keys) { if (ValidCodeLanguages[validLanguage].Contains(languageAttribute.Value)) { taskInfo.CodeLanguage = validLanguage; foundValidCodeLanguage = true; break; } } if (!foundValidCodeLanguage) { // The user specified a code language we don't support log.LogErrorWithCodeFromResources("CodeTaskFactory.InvalidCodeLanguage", languageAttribute.Value, String.Join(", ", ValidCodeLanguages.Keys)); return(false); } } } if (String.IsNullOrWhiteSpace(taskInfo.SourceCode)) { // The user did not specify a path to source code or source code within the <Code /> element. log.LogErrorWithCodeFromResources("CodeTaskFactory.NoSourceCode"); return(false); } taskInfo.SourceCode = GetSourceCode(taskInfo, parameters); return(true); }
/// <summary> /// Gets the full source code by applying an appropriate template based on the current <see cref="RoslynCodeTaskFactoryCodeType"/>. /// </summary> internal static string GetSourceCode(RoslynCodeTaskFactoryTaskInfo taskInfo, ICollection <TaskPropertyInfo> parameters) { if (taskInfo.CodeType == RoslynCodeTaskFactoryCodeType.Class) { return(taskInfo.SourceCode); } CodeTypeDeclaration codeTypeDeclaration = new CodeTypeDeclaration { IsClass = true, Name = taskInfo.Name, TypeAttributes = TypeAttributes.Public, Attributes = MemberAttributes.Final }; codeTypeDeclaration.BaseTypes.Add("net.r_eg.IeXod.Utilities.Task"); foreach (TaskPropertyInfo propertyInfo in parameters) { CreateProperty(codeTypeDeclaration, propertyInfo.Name, propertyInfo.PropertyType); } if (taskInfo.CodeType == RoslynCodeTaskFactoryCodeType.Fragment) { CodeMemberProperty successProperty = CreateProperty(codeTypeDeclaration, "Success", typeof(bool), true); CodeMemberMethod executeMethod = new CodeMemberMethod { Name = "Execute", // ReSharper disable once BitwiseOperatorOnEnumWithoutFlags Attributes = MemberAttributes.Override | MemberAttributes.Public, ReturnType = new CodeTypeReference(typeof(Boolean)) }; executeMethod.Statements.Add(new CodeSnippetStatement(taskInfo.SourceCode)); executeMethod.Statements.Add(new CodeMethodReturnStatement(new CodePropertyReferenceExpression(null, successProperty.Name))); codeTypeDeclaration.Members.Add(executeMethod); } else { codeTypeDeclaration.Members.Add(new CodeSnippetTypeMember(taskInfo.SourceCode)); } CodeNamespace codeNamespace = new CodeNamespace("InlineCode"); codeNamespace.Imports.AddRange(DefaultNamespaces.Union(taskInfo.Namespaces, StringComparer.OrdinalIgnoreCase).Select(i => new CodeNamespaceImport(i)).ToArray()); codeNamespace.Types.Add(codeTypeDeclaration); CodeCompileUnit codeCompileUnit = new CodeCompileUnit(); codeCompileUnit.Namespaces.Add(codeNamespace); using (CodeDomProvider provider = CodeDomProvider.CreateProvider(taskInfo.CodeLanguage)) { using (StringWriter writer = new StringWriter(new StringBuilder(), CultureInfo.CurrentCulture)) { provider.GenerateCodeFromCompileUnit(codeCompileUnit, writer, new CodeGeneratorOptions { BlankLinesBetweenMembers = true, VerbatimOrder = true }); return(writer.ToString()); } } }