// NAnt - A .NET build tool
// Copyright (C) 2001-2002 Gerry Shaw
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// Sergey Chaban (serge@wildwestsoftware.com)
// Gerry Shaw (gerry_shaw@yahoo.com)
// Ian MacLean (ian at maclean.ms)
// Giuseppe Greco (giuseppe.greco@agamura.com)
using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Specialized;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Xml;
using NAnt.Core;
using NAnt.Core.Attributes;
using NAnt.Core.Types;
using NAnt.Core.Util;
using NAnt.DotNet.Types;
namespace NAnt.DotNet.Tasks {
///
/// Executes the code contained within the task. This code can include custom extension function definitions.
/// Once the script task has executed those custom functions will be available for use in the buildfile.
///
///
///
/// The must contain a single code
/// element, which in turn contains the script code.
///
///
/// A static entry point named ScriptMain is required if no custom functions have been defined. It must
/// have a single parameter.
///
///
/// The following namespaces are loaded by default:
///
///
/// -
/// System
///
/// -
/// System.Collections
///
/// -
/// System.Collections.Specialized
///
/// -
/// System.IO
///
/// -
/// System.Text
///
/// -
/// System.Text.RegularExpressions
///
/// -
/// NAnt.Core
///
///
///
///
/// Run C# code that writes a message to the build log.
///
/// <script language="C#">
/// <code>
/// <![CDATA[
/// public static void ScriptMain(Project project) {
/// project.Log(Level.Info, "Hello World from a script task using C#");
/// }
/// ]]>
/// </code>
/// </script>
///
///
///
/// Define a custom function and call it using C#.
///
/// <script language="C#" prefix="test" >
/// <code>
/// <![CDATA[
/// [Function("test-func")]
/// public static string Testfunc( ) {
/// return "some result !!!!!!!!";
/// }
/// ]]>
/// </code>
/// </script>
/// <echo message='${test::test-func()}'/>
///
///
///
/// Use a custom namespace in C# to create a database
///
/// <script language="C#" >
/// <references>
/// <include name="System.Data.dll" />
/// </references>
/// <imports>
/// <import namespace="System.Data.SqlClient" />
/// </imports>
/// <code>
/// <![CDATA[
/// public static void ScriptMain(Project project) {
/// string dbUserName = "nant";
/// string dbPassword = "nant";
/// string dbServer = "(local)";
/// string dbDatabaseName = "NAntSample";
/// string connectionString = String.Format("Server={0};uid={1};pwd={2};", dbServer, dbUserName, dbPassword);
///
/// SqlConnection connection = new SqlConnection(connectionString);
/// string createDbQuery = "CREATE DATABASE " + dbDatabaseName;
/// SqlCommand createDatabaseCommand = new SqlCommand(createDbQuery);
/// createDatabaseCommand.Connection = connection;
///
/// connection.Open();
///
/// try {
/// createDatabaseCommand.ExecuteNonQuery();
/// project.Log(Level.Info, "Database added successfully: " + dbDatabaseName);
/// } catch (Exception e) {
/// project.Log(Level.Error, e.ToString());
/// } finally {
/// connection.Close();
/// }
/// }
/// ]]>
/// </code>
/// </script>
///
///
///
///
/// Run Visual Basic.NET code that writes a message to the build log.
///
///
/// <script language="VB">
/// <code>
/// <![CDATA[
/// Public Shared Sub ScriptMain(project As Project)
/// project.Log(Level.Info, "Hello World from a script task using Visual Basic.NET")
/// End Sub
/// ]]>
/// </code>
/// </script>
///
///
///
/// Define a custom task and call it using C#.
///
/// <script language="C#" prefix="test" >
/// <code>
/// <![CDATA[
/// [TaskName("usertask")]
/// public class TestTask : Task {
/// #region Private Instance Fields
///
/// private string _message;
///
/// #endregion Private Instance Fields
///
/// #region Public Instance Properties
///
/// [TaskAttribute("message", Required=true)]
/// public string FileName {
/// get { return _message; }
/// set { _message = value; }
/// }
///
/// #endregion Public Instance Properties
///
/// #region Override implementation of Task
///
/// protected override void ExecuteTask() {
/// Log(Level.Info, _message.ToUpper());
/// }
/// #endregion Override implementation of Task
/// }
/// ]]>
/// </code>
/// </script>
/// <usertask message='Hello from UserTask'/>
///
///
///
///
/// Define a custom function and call it using Boo.
///
///
/// <script language="Boo.CodeDom.BooCodeProvider, Boo.CodeDom, Version=1.0.0.0, Culture=neutral, PublicKeyToken=32c39770e9a21a67"
/// failonerror="true">
/// <code>
/// <![CDATA[
///
/// [Function("test-func")]
/// def MyFunc():
/// return "Hello from Boo !!!!!!"
/// ]]>
/// </code>
/// </script>
/// <echo message='${script::test-func()}'/>
///
///
[TaskName("script")]
public class ScriptTask : Task {
#region Private Instance Fields
private string _language = null;
private AssemblyFileSet _references = new AssemblyFileSet();
private string _mainClass = "";
private string _rootClassName;
private string _prefix = "script";
private NamespaceImportCollection _imports = new NamespaceImportCollection();
private RawXml _code;
#endregion Private Instance Fields
#region Private Static Fields
private static readonly string[] _defaultNamespaces = {
"System",
"System.Collections",
"System.Collections.Specialized",
"System.IO",
"System.Text",
"System.Text.RegularExpressions",
"NAnt.Core",
"NAnt.Core.Attributes"};
#endregion Private Static Fields
#region Public Instance Properties
///
/// The language of the script block. Possible values are "VB", "vb", "VISUALBASIC", "C#", "c#", "CSHARP".
/// "JS", "js", "JSCRIPT" "VJS", "vjs", "JSHARP" or a fully-qualified name for a class implementing
/// .
///
[TaskAttribute("language", Required=true)]
public string Language {
get { return _language; }
set { _language = StringUtils.ConvertEmptyToNull(value); }
}
///
/// Any required references.
///
[BuildElement("references")]
public AssemblyFileSet References {
get { return _references; }
set { _references = value; }
}
///
/// The name of the main class containing the static ScriptMain
/// entry point.
///
[TaskAttribute("mainclass", Required=false)]
public string MainClass {
get { return _mainClass; }
set { _mainClass = StringUtils.ConvertEmptyToNull(value); }
}
///
/// The namespace prefix for any custom functions defined in the script.
/// If ommitted the prefix will default to 'script'
///
[TaskAttribute("prefix", Required=false)]
public string Prefix {
get { return _prefix; }
set { _prefix = StringUtils.ConvertEmptyToNull(value); }
}
///
/// The namespaces to import.
///
[BuildElement("imports")]
public NamespaceImportCollection Imports {
get { return _imports; }
set { _imports = value; }
}
///
/// The code to execute.
///
[BuildElement("code", Required=true)]
public RawXml Code {
get { return _code; }
set { _code = value; }
}
#endregion Public Instance Properties
#region Override implementation of Task
///
/// Initializes the task using the specified xml node.
///
protected override void InitializeTask(XmlNode taskNode) {
_rootClassName = "nant" + Guid.NewGuid().ToString("N",
CultureInfo.InvariantCulture);
}
///
/// Executes the script block.
///
protected override void ExecuteTask() {
// create compiler info for user-specified language
CompilerInfo compilerInfo = CreateCompilerInfo(Language);
// ensure base directory is set, even if fileset was not initialized
// from XML
if (References.BaseDirectory == null) {
References.BaseDirectory = new DirectoryInfo(Project.BaseDirectory);
}
ICodeCompiler compiler = compilerInfo.Compiler;
CompilerParameters options = new CompilerParameters();
options.GenerateExecutable = false;
options.GenerateInMemory = true;
options.MainClass = MainClass;
// add all available assemblies.
foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies()) {
try {
if (!StringUtils.IsNullOrEmpty(asm.Location)) {
options.ReferencedAssemblies.Add(asm.Location);
}
} catch (NotSupportedException) {
// Ignore - this error is sometimes thrown by asm.Location
// for certain dynamic assemblies
}
}
// add (and load) assemblies specified by user
foreach (string assemblyFile in References.FileNames) {
try {
// fetch name of assembly
AssemblyName aname = AssemblyName.GetAssemblyName(assemblyFile);
// don't bother adding assemblies that are already loaded
// in the AppDomain as they have already been added
if (IsAssemblyLoaded (aname)) {
continue;
}
// load the assembly into current AppDomain to ensure it is
// are available when executing the emitted assembly
Assembly asm = Assembly.LoadFrom(assemblyFile);
// add the location of the loaded assembly
if (!StringUtils.IsNullOrEmpty(asm.Location)) {
options.ReferencedAssemblies.Add(asm.Location);
}
} catch (Exception ex) {
throw new BuildException(string.Format(CultureInfo.InvariantCulture,
ResourceUtils.GetString("NA2028"), assemblyFile), Location, ex);
}
}
StringCollection imports = new StringCollection();
foreach (NamespaceImport import in Imports) {
if (import.IfDefined && !import.UnlessDefined) {
imports.Add(import.Namespace);
}
}
// generate the code
CodeCompileUnit compileUnit = compilerInfo.GenerateCode(_rootClassName,
Code.Xml.InnerText, imports, Prefix);
StringWriter sw = new StringWriter(CultureInfo.InvariantCulture);
compilerInfo.CodeGen.GenerateCodeFromCompileUnit(compileUnit, sw, null);
string code = sw.ToString();
Log(Level.Debug, ResourceUtils.GetString("String_GeneratedCodeLooksLike") + "\n{0}", code);
CompilerResults results = compiler.CompileAssemblyFromDom(options, compileUnit);
Assembly compiled = null;
if (results.Errors.Count > 0) {
string errors = ResourceUtils.GetString("NA2029") + Environment.NewLine;
foreach (CompilerError err in results.Errors) {
errors += err.ToString() + Environment.NewLine;
}
errors += code;
throw new BuildException(errors, Location);
} else {
compiled = results.CompiledAssembly;
}
// scan the new assembly for tasks, types and functions
// Its unlikely that tasks will be defined in buildfiles though.
bool extensionAssembly = TypeFactory.ScanAssembly(compiled, this);
string mainClass = _rootClassName;
if (!StringUtils.IsNullOrEmpty(MainClass)) {
mainClass += "+" + MainClass;
}
Type mainType = compiled.GetType(mainClass);
if (mainType == null) {
throw new BuildException(string.Format(CultureInfo.InvariantCulture,
ResourceUtils.GetString("NA2030"), mainClass), Location);
}
MethodInfo entry = mainType.GetMethod("ScriptMain");
// check for task or function definitions.
if (entry == null) {
if (!extensionAssembly) {
throw new BuildException(ResourceUtils.GetString("NA2031"), Location);
} else {
return; // no entry point so nothing to do here beyond loading task and function defs
}
}
if (!entry.IsStatic) {
throw new BuildException(ResourceUtils.GetString("NA2032"), Location);
}
ParameterInfo[] entryParams = entry.GetParameters();
if (entryParams.Length != 1) {
throw new BuildException(ResourceUtils.GetString("NA2033"), Location);
}
if (entryParams[0].ParameterType.FullName != typeof(Project).FullName) {
throw new BuildException(string.Format(CultureInfo.InvariantCulture,
ResourceUtils.GetString("NA2034"), entryParams[0].ParameterType.FullName,
typeof(Project).FullName), Location);
}
try {
// invoke Main method
entry.Invoke(null, new object[] {Project});
} catch (Exception ex) {
// this exception is not likely to tell us much, BUT the
// InnerException normally contains the runtime exception
// thrown by the executed script code.
throw new BuildException(ResourceUtils.GetString("NA2035"), Location,
ex.InnerException);
}
}
#endregion Override implementation of Task
#region Private Instance Methods
private CompilerInfo CreateCompilerInfo(string language) {
CodeDomProvider provider = null;
try {
switch (language) {
case "vb":
case "VB":
case "VISUALBASIC":
provider = CreateCodeDomProvider(
"Microsoft.VisualBasic.VBCodeProvider",
"System, Culture=neutral");
break;
case "c#":
case "C#":
case "CSHARP":
provider = CreateCodeDomProvider(
"Microsoft.CSharp.CSharpCodeProvider",
"System, Culture=neutral");
break;
case "js":
case "JS":
case "JSCRIPT":
provider = CreateCodeDomProvider(
"Microsoft.JScript.JScriptCodeProvider",
"Microsoft.JScript, Culture=neutral");
break;
case "vjs":
case "VJS":
case "JSHARP":
provider = CreateCodeDomProvider(
"Microsoft.VJSharp.VJSharpCodeProvider",
"VJSharpCodeProvider, Culture=neutral");
break;
default:
// if its not one of the above then it must be a fully
// qualified provider class name
provider = CreateCodeDomProvider(language);
break;
}
return new CompilerInfo(provider);
} catch (Exception ex) {
throw new BuildException(string.Format(CultureInfo.InvariantCulture,
ResourceUtils.GetString("NA2036"), language), Location, ex);
}
}
#endregion Private Instance Methods
#region Private Static Methods
private static CodeDomProvider CreateCodeDomProvider(string typeName, string assemblyName) {
Assembly providerAssembly = Assembly.LoadWithPartialName(assemblyName);
if (providerAssembly == null) {
throw new ArgumentException(string.Format(CultureInfo.InvariantCulture,
ResourceUtils.GetString("NA2037"), assemblyName));
}
Type providerType = providerAssembly.GetType(typeName, true, true);
return CreateCodeDomProvider(providerType);
}
private static CodeDomProvider CreateCodeDomProvider(string assemblyQualifiedTypeName) {
Type providerType = Type.GetType(assemblyQualifiedTypeName, true, true);
return CreateCodeDomProvider(providerType);
}
private static CodeDomProvider CreateCodeDomProvider(Type providerType) {
object provider = Activator.CreateInstance(providerType);
if (!(provider is CodeDomProvider)) {
throw new ArgumentException(string.Format(CultureInfo.InvariantCulture,
ResourceUtils.GetString("NA2038"), providerType.FullName));
}
return (CodeDomProvider) provider;
}
private static bool IsAssemblyLoaded (AssemblyName aname) {
foreach (Assembly asm in AppDomain.CurrentDomain.GetAssemblies()) {
if (asm.FullName == aname.FullName) {
return true;
}
}
return false;
}
#endregion Private Static Methods
internal class CompilerInfo {
public readonly ICodeCompiler Compiler;
public readonly ICodeGenerator CodeGen;
public CompilerInfo(CodeDomProvider provider) {
Compiler = provider.CreateCompiler();
CodeGen = provider.CreateGenerator();
}
public CodeCompileUnit GenerateCode(string typeName, string codeBody,
StringCollection imports,
string prefix) {
CodeCompileUnit compileUnit = new CodeCompileUnit();
CodeTypeDeclaration typeDecl = new CodeTypeDeclaration(typeName);
typeDecl.IsClass = true;
typeDecl.TypeAttributes = TypeAttributes.Public;
// create constructor
CodeConstructor constructMember = new CodeConstructor();
constructMember.Attributes = MemberAttributes.Public;
constructMember.Parameters.Add(new CodeParameterDeclarationExpression("NAnt.Core.Project", "project"));
constructMember.Parameters.Add(new CodeParameterDeclarationExpression("NAnt.Core.PropertyDictionary", "propDict"));
constructMember.BaseConstructorArgs.Add(new CodeVariableReferenceExpression("project"));
constructMember.BaseConstructorArgs.Add(new CodeVariableReferenceExpression ("propDict"));
typeDecl.Members.Add(constructMember);
typeDecl.BaseTypes.Add(typeof(FunctionSetBase));
// add FunctionSet attribute
CodeAttributeDeclaration attrDecl = new CodeAttributeDeclaration("FunctionSet");
attrDecl.Arguments.Add(new CodeAttributeArgument(
new CodeVariableReferenceExpression("\"" + prefix + "\"")));
attrDecl.Arguments.Add(new CodeAttributeArgument(
new CodeVariableReferenceExpression("\"" + prefix + "\"")));
typeDecl.CustomAttributes.Add(attrDecl);
// pump in the user specified code as a snippet
CodeSnippetTypeMember literalMember =
new CodeSnippetTypeMember(codeBody);
typeDecl.Members.Add( literalMember );
CodeNamespace nspace = new CodeNamespace();
//Add default imports
foreach (string nameSpace in ScriptTask._defaultNamespaces) {
nspace.Imports.Add(new CodeNamespaceImport(nameSpace));
}
foreach (string nameSpace in imports) {
nspace.Imports.Add(new CodeNamespaceImport(nameSpace));
}
compileUnit.Namespaces.Add( nspace );
nspace.Types.Add(typeDecl);
return compileUnit;
}
}
}
}