// 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) // 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.Runtime.Remoting.Lifetime; using System.Security.Cryptography; using System.Text; 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. /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// /// ]]> /// /// /// /// /// Create a C# AssemblyInfo file containing an attribute with multiple /// named properties by setting the /// attribute on the element to /// . /// /// /// /// /// /// /// /// /// /// /// /// /// /// ]]> /// /// [TaskName("asminfo")] [Serializable()] public class AssemblyInfoTask : Task { #region Private Instance Fields private FileInfo _output; private CodeLanguage _language = CodeLanguage.CSharp; private AssemblyAttributeCollection _attributes = new AssemblyAttributeCollection(); private NamespaceImportCollection _imports = new NamespaceImportCollection(); private AssemblyFileSet _references = new AssemblyFileSet(); #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)] public FileInfo Output { get { return _output; } set { _output = value; } } /// /// 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, ResourceUtils.GetString("NA2002"), 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. /// [BuildElement("imports")] public NamespaceImportCollection Imports { get { return _imports; } set { _imports = value; } } /// /// Assembly files used to locate the types of the specified attributes. /// [BuildElement("references")] public AssemblyFileSet References { get { return _references; } set { _references = value; } } #endregion Public Instance Properties #region Override implementation of Task /// /// Generates an AssemblyInfo file. /// protected override void ExecuteTask() { try { StringCollection imports = new StringCollection(); foreach (NamespaceImport import in Imports) { if (import.IfDefined && !import.UnlessDefined) { imports.Add(import.Namespace); } } // ensure base directory is set, even if fileset was not initialized // from XML if (References.BaseDirectory == null) { References.BaseDirectory = new DirectoryInfo(Project.BaseDirectory); } // write out code to memory stream, so we can compare it later // to what is already present (if necessary) MemoryStream generatedAsmInfoStream = new MemoryStream(); using (StreamWriter writer = new StreamWriter(generatedAsmInfoStream, Encoding.Default)) { // create new instance of CodeProviderInfo for specified CodeLanguage CodeProvider codeProvider = new CodeProvider(this, 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); // flush writer.Flush(); // check whether generated source should be persisted if (NeedsPersisting(generatedAsmInfoStream)) { using (FileStream fs = new FileStream(Output.FullName, FileMode.Create, FileAccess.Write)) { byte[] buffer = generatedAsmInfoStream.ToArray(); fs.Write(buffer, 0, buffer.Length); fs.Flush(); fs.Close(); generatedAsmInfoStream.Close(); } Log(Level.Info, ResourceUtils.GetString("String_GeneratedFile"), Output.FullName); } else { Log(Level.Verbose, ResourceUtils.GetString("String_FileUpToDate"), Output.FullName); } } } catch (Exception ex) { throw new BuildException(string.Format( CultureInfo.InvariantCulture, ResourceUtils.GetString("NA2004"), Output.FullName), Location, ex); } } #endregion Override implementation of Task #region Private Instance Methods /// /// Determines whether the specified AssemblyInfo file in the given /// needs to be persisted. /// /// holding the newly generated AssemblyInfo source. /// /// if the generated AssemblyInfo source needs /// to be persisted; otherwise, . /// private bool NeedsPersisting(Stream generatedAsmInfoStream) { // if output file doesn't exist, the stream will always need to be // persisted to the filesystem. if (!Output.Exists) { Log(Level.Verbose, ResourceUtils.GetString("String_OutputFileDoesNotExist"), Output.FullName); return true; } byte[] existingAssemblyInfoHash = null; byte[] generatedAssemblyInfoHash = null; SHA1 hasher = new SHA1CryptoServiceProvider(); // hash existing AssemblyInfo source using (FileStream fs = new FileStream(Output.FullName, FileMode.Open, FileAccess.Read)) { existingAssemblyInfoHash = hasher.ComputeHash(fs); } // hash generated AssemblyInfo source generatedAsmInfoStream.Position = 0; generatedAssemblyInfoHash = hasher.ComputeHash(generatedAsmInfoStream); // release all resources hasher.Clear(); //compare hash of generated source with of existing source if (Convert.ToBase64String(generatedAssemblyInfoHash) != Convert.ToBase64String(existingAssemblyInfoHash)) { Log(Level.Verbose, ResourceUtils.GetString("String_OutputFileNotUpToDate"), Output.FullName); return true; } else { return false; } } #endregion Private Instance Methods /// /// 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 readonly 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. /// The for which an instance of the class should be initialized. public CodeProvider(AssemblyInfoTask assemblyInfoTask, CodeLanguage codeLanguage) { CodeDomProvider provider = null; switch (codeLanguage) { case CodeLanguage.CSharp: provider = new Microsoft.CSharp.CSharpCodeProvider(); break; case CodeLanguage.JScript: throw new NotSupportedException(ResourceUtils.GetString("NA2008")); case CodeLanguage.VB: provider = new Microsoft.VisualBasic.VBCodeProvider(); break; default: throw new NotSupportedException(ResourceUtils.GetString("NA2007")); } _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, AppDomain.CurrentDomain.SetupInformation); 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 { #region Override implementation of MarshalByRefObject /// /// Obtains a lifetime service object to control the lifetime policy for /// this instance. /// /// /// An object of type used to control the lifetime /// policy for this instance. This is the current lifetime service object /// for this instance if one exists; otherwise, a new lifetime service /// object initialized with a lease that will never time out. /// public override Object InitializeLifetimeService() { ILease lease = (ILease) base.InitializeLifetimeService(); if (lease.CurrentState == LeaseState.Initial) { lease.InitialLeaseTime = TimeSpan.Zero; } return lease; } #endregion Override implementation of MarshalByRefObject #region Public Instance Methods /// /// 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); // 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, ResourceUtils.GetString("NA2005"), 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, ResourceUtils.GetString("NA2006"), value, parameters[0].ParameterType.FullName, type.FullName), Location.UnknownLocation, ex); } } } } if (typedValue == null) { throw new BuildException(string.Format( CultureInfo.InvariantCulture, ResourceUtils.GetString("NA2003"), typename), Location.UnknownLocation); } } return typedValue; } else { throw new BuildException(string.Format( CultureInfo.InvariantCulture, ResourceUtils.GetString("NA2001"),typename), Location.UnknownLocation); } } finally { // detach assembly resolver from the current domain assemblyResolver.Detach(); } } #endregion Public Instance Methods } } }