// NAnt - A .NET build tool // Copyright (C) 2002-2003 Gordon Weakliem // // 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 // // Gordon Weakliem (gweakliem@oddpost.com) // Gert Driesen (gert.driesen@ardatis.com) // Ian MacLean (ian_maclean@another.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.Security.Cryptography; using NAnt.Core; using NAnt.Core.Attributes; using NAnt.Core.Types; using NAnt.Core.Util; using NAnt.DotNet.Types; namespace NAnt.DotNet.Tasks { /// /// Generates an AssemblyInfo file using the attributes given. /// /// /// Create a C# AssemblyInfo file containing the specified assembly-level attributes. /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// ]]> /// /// [TaskName("asminfo")] public class AssemblyInfoTask : Task { #region Private Instance Fields private string _output; private CodeLanguage _language = CodeLanguage.CSharp; private AssemblyAttributeCollection _attributes = new AssemblyAttributeCollection(); private NamespaceImportCollection _imports = new NamespaceImportCollection(); private FileSet _references = new FileSet(); #endregion Private Instance Fields #region Public Instance Properties /// /// Name of the AssemblyInfo file to generate. /// /// /// The name of the AssemblyInfo file to generate. /// [TaskAttribute("output", Required=true)] [StringValidator(AllowEmpty=false)] public string Output { get { return (_output != null) ? Project.GetFullPath(_output) : null; } set { _output = StringUtils.ConvertEmptyToNull(value); } } /// /// The code language in which the AssemblyInfo file should be /// generated - either or /// . /// /// /// The code language in which the AssemblyInfo file should be /// generated. /// [TaskAttribute("language", Required=true)] [StringValidator(AllowEmpty=false)] public CodeLanguage Language { get { return _language; } set { if (!Enum.IsDefined(typeof(CodeLanguage), value)) { throw new ArgumentException(string.Format( CultureInfo.InvariantCulture, "An invalid type {0} was specified.", value)); } else { _language = value; } } } /// /// The assembly-level attributes to generate. /// /// /// The assembly-level attributes to generate. /// [BuildElementCollection("attributes", "attribute")] public AssemblyAttributeCollection AssemblyAttributes { get { return _attributes; } } /// /// The namespaces to import. /// /// /// The namespaces to import. /// [BuildElementCollection("imports", "import")] public NamespaceImportCollection Imports { get { return _imports; } } /// /// Assembly files used to locate the types of the specified attributes. /// [BuildElement("references")] public FileSet References { get { return _references; } set { _references = value; } } #endregion Public Instance Properties #region Override implementation of Task /// /// Generates an AssemblyInfo file. /// protected override void ExecuteTask() { if (References.BaseDirectory == null) { References.BaseDirectory = Project.BaseDirectory; } try { StringCollection imports = new StringCollection(); foreach (NamespaceImport import in Imports) { if (import.IfDefined && !import.UnlessDefined) { imports.Add(import.Namespace); } } // fix references to system assemblies if (Project.CurrentFramework != null) { foreach (string pattern in References.Includes) { if (Path.GetFileName(pattern) == pattern) { string frameworkDir = Project.CurrentFramework.FrameworkAssemblyDirectory.FullName; string localPath = Path.Combine(References.BaseDirectory, pattern); string fullPath = Path.Combine(frameworkDir, pattern); if (!File.Exists(localPath) && File.Exists(fullPath)) { // found a system reference References.FileNames.Add(fullPath); } } } } //write out code to memory stream //so we can compare it later to what is already present if necessary MemoryStream ms = new MemoryStream(); StreamWriter writer = new StreamWriter(ms,System.Text.Encoding.Default); // create new instance of CodeProviderInfo for specified CodeLanguage CodeProvider codeProvider = new CodeProvider(Language); // only generate imports here for C#, for VB we create the // imports as part of the assembly attributes compile unit if (Language == CodeLanguage.CSharp) { // generate imports code codeProvider.GenerateImportCode(imports, writer); } // generate code for assembly attributes codeProvider.GenerateAssemblyAttributesCode(AssemblyAttributes, imports, References.FileNames, writer); writer.Flush(); byte[] previousAsmHash = null; byte[] genAsmHash = null; if(File.Exists(Output)) { SHA1 hasher = new SHA1CryptoServiceProvider(); //hash previous using(FileStream fs = new FileStream(Output,FileMode.Open,FileAccess.Read)) { previousAsmHash = hasher.ComputeHash(fs); } //hash generated ms.Position = 0; genAsmHash = hasher.ComputeHash(ms); } //compare what is generated with what is present if(previousAsmHash != null && Convert.ToBase64String(genAsmHash) != Convert.ToBase64String(previousAsmHash)) { using (FileStream fs = new FileStream(Output, FileMode.OpenOrCreate, FileAccess.Write)) { byte[] buf = ms.ToArray(); fs.Write(buf,0,buf.Length); fs.Flush(); fs.Close(); ms.Close(); } } else { Log(Level.Verbose, LogPrefix + "AssemblyInfo generated was not different than the file already present."); } } catch (Exception ex) { throw new BuildException(string.Format( CultureInfo.InvariantCulture, "AssemblyInfo file '{0}' could not be generated.", Output), Location, ex); } } #endregion Override implementation of Task /// /// Defines the supported code languages for generating an AssemblyInfo /// file. /// public enum CodeLanguage : int { /// /// A value for generating C# code. /// CSharp = 0, /// /// A value for generating JScript code. /// JScript = 1, /// /// A value for generating Visual Basic code. /// VB = 2, } /// /// Encapsulates functionality to generate a code file with imports /// and assembly-level attributes. /// internal class CodeProvider { #region Private Instance Fields private CodeLanguage _language; private readonly ICodeGenerator _generator; #endregion Private Instance Fields #region Public Instance Constructors /// /// Initializes a new instance of the /// for the specified . /// /// The for which an instance of the class should be initialized. public CodeProvider(CodeLanguage codeLanguage) { CodeDomProvider provider = null; switch (codeLanguage) { case CodeLanguage.CSharp: provider = new Microsoft.CSharp.CSharpCodeProvider(); break; case CodeLanguage.JScript: throw new NotSupportedException("Generating a JSCript AssemblyInfo file is not supported at this moment."); case CodeLanguage.VB: provider = new Microsoft.VisualBasic.VBCodeProvider(); break; default: throw new NotSupportedException("The specified code language is not supported."); } _generator = provider.CreateGenerator(); _language = codeLanguage; } #endregion Public Instance Constructors #region Private Instance Properties /// /// Gets the in which the AssemblyInfo /// code will be generated. /// private CodeLanguage Language { get { return _language; } } /// /// Gets the that will be used to /// generate the AssemblyInfo code. /// private ICodeGenerator Generator { get { return _generator; } } #endregion Private Instance Properties #region Public Instance Methods /// /// Generates code for the specified imports. /// /// The imports for which code should be generated. /// The to which the generated code will be written. public void GenerateImportCode(StringCollection imports, TextWriter writer) { CodeNamespace codeNamespace = new CodeNamespace(); foreach (string import in imports) { codeNamespace.Imports.Add(new CodeNamespaceImport(import)); } Generator.GenerateCodeFromNamespace(codeNamespace, writer, new CodeGeneratorOptions()); } /// /// Generates code for the specified assembly attributes. /// /// The assembly attributes for which code should be generated. /// Imports used to resolve the assembly attribute names to fully qualified type names. /// Assembly that will be used to resolve the attribute names to instances. /// The to which the generated code will be written. public void GenerateAssemblyAttributesCode(AssemblyAttributeCollection assemblyAttributes, StringCollection imports, StringCollection assemblies, TextWriter writer) { CodeCompileUnit codeCompileUnit = new CodeCompileUnit(); // for C# the imports were already generated, as the # generator // will otherwise output the imports after the assembly attributes if (Language == CodeLanguage.VB) { CodeNamespace codeNamespace = new CodeNamespace(); foreach (string import in imports) { codeNamespace.Imports.Add(new CodeNamespaceImport(import)); } codeCompileUnit.Namespaces.Add(codeNamespace); } foreach(AssemblyAttribute assemblyAttribute in assemblyAttributes) { if (assemblyAttribute.IfDefined && !assemblyAttribute.UnlessDefined) { // create new assembly-level attribute CodeAttributeDeclaration codeAttributeDeclaration = new CodeAttributeDeclaration(assemblyAttribute.TypeName); if (assemblyAttribute.AsIs) { codeAttributeDeclaration.Arguments.Add(new CodeAttributeArgument(new CodeSnippetExpression(assemblyAttribute.Value))); } else { // convert string value to type expected by attribute constructor object typedValue = GetTypedValue(assemblyAttribute, assemblies, imports); if (typedValue != null) { // add typed value to attribute arguments codeAttributeDeclaration.Arguments.Add(new CodeAttributeArgument(new CodePrimitiveExpression(typedValue))); } } // add assembly-level argument to code compile unit codeCompileUnit.AssemblyCustomAttributes.Add(codeAttributeDeclaration); } } Generator.GenerateCodeFromCompileUnit(codeCompileUnit, writer, new CodeGeneratorOptions()); } #endregion Public Instance Methods #region Private Instance Methods private object GetTypedValue(AssemblyAttribute attribute, StringCollection assemblies, StringCollection imports) { // locate type assuming TypeName is fully qualified typename AppDomain newDomain = AppDomain.CreateDomain("TypeGatheringDomain", AppDomain.CurrentDomain.Evidence, new AppDomainSetup()); TypedValueGatherer typedValueGatherer = (TypedValueGatherer) newDomain.CreateInstanceAndUnwrap(typeof(TypedValueGatherer).Assembly.FullName, typeof(TypedValueGatherer).FullName, false, BindingFlags.Public | BindingFlags.Instance, null, new object[0], CultureInfo.InvariantCulture, new object[0], AppDomain.CurrentDomain.Evidence); object typedValue = typedValueGatherer.GetTypedValue( assemblies, imports, attribute.TypeName, attribute.Value); // unload newly created AppDomain AppDomain.Unload(newDomain); return typedValue; } #endregion Private Instance Methods } /// /// Responsible for returning the specified value converted to a /// accepted by a constructor for a given /// . /// private class TypedValueGatherer : MarshalByRefObject { /// /// Retrieves the specified corresponding with the specified /// type name from a list of assemblies. /// /// The collection of assemblies that the type should tried to be instantiated from. /// The list of imports that can be used to resolve the typename to a full typename. /// The typename that should be used to determine the type to which the specified value should be converted. /// The value that should be converted to a typed value. /// /// /// is and the identified by has no default public constructor. /// -or- /// cannot be converted to a value that's suitable for one of the constructors of the identified by . /// -or- /// The identified by has no suitable constructor. /// -or- /// A identified by could not be located or loaded. /// public object GetTypedValue(StringCollection assemblies, StringCollection imports, string typename, string value) { // create assembly resolver AssemblyResolver assemblyResolver = new AssemblyResolver(); // attach assembly resolver to the current domain assemblyResolver.Attach(); try { Type type = null; // load each assembly and try to get type from it foreach (string assemblyFileName in assemblies) { // load assembly from filesystem Assembly assembly = Assembly.LoadFrom(assemblyFileName, AppDomain.CurrentDomain.Evidence); // try to load type from assembly type = assembly.GetType(typename, false, false); if (type == null) { foreach (string import in imports) { type = assembly.GetType(import + "." + typename, false, false); if (type != null) { break; } } } if (type != null) { break; } } // try to load type from all assemblies loaded from disk, if // it was not loaded from the references assemblies if (type == null) { type = Type.GetType(typename, false, false); if (type == null) { foreach (string import in imports) { type = Type.GetType(import + "." + typename, false, false); if (type != null) { break; } } } } if (type != null) { object typedValue = null; if (value == null) { ConstructorInfo defaultConstructor = type.GetConstructor( BindingFlags.Public | BindingFlags.Instance, null, new Type[0], new ParameterModifier[0]); if (defaultConstructor != null) { throw new BuildException(string.Format( CultureInfo.InvariantCulture, "Assembly attribute {0} has no default public constructor.", type.FullName), Location.UnknownLocation); } typedValue = null; } else { ConstructorInfo[] constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance); for (int counter = 0; counter < constructors.Length; counter++) { ParameterInfo[] parameters = constructors[counter].GetParameters(); if (parameters.Length == 1) { if (parameters[0].ParameterType.IsPrimitive || parameters[0].ParameterType == typeof(string)) { try { // convert value to type of constructor parameter typedValue = Convert.ChangeType(value, parameters[0].ParameterType, CultureInfo.InvariantCulture); break; } catch (Exception ex) { throw new BuildException(string.Format( CultureInfo.InvariantCulture, "Value '{0}' cannot be converted to type '{1}' of assembly attribute {2}.", value, parameters[0].ParameterType.FullName, type.FullName), Location.UnknownLocation, ex); } } } } if (typedValue == null) { throw new BuildException(string.Format( CultureInfo.InvariantCulture, "Value of assembly attribute {0} cannot be set as it has no constructor accepting a primitive type or string.", typename), Location.UnknownLocation); } } return typedValue; } else { throw new BuildException(string.Format( CultureInfo.InvariantCulture, "Assembly attribute with type {0} could not be loaded.", typename), Location.UnknownLocation); } } finally { // detach assembly resolver from the current domain assemblyResolver.Detach(); } } } } }