// 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
//
// Matthew Mastracci (mmastrac@canada.com)
// Sascha Andres (sa@programmers-world.com)
// Gert Driesen (gert.driesen@ardatis.com)
// Giuseppe Greco (giuseppe.greco@agamura.com)
using System;
using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Runtime.Remoting.Lifetime;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.Xml;
using NAnt.Core;
using NAnt.Core.Attributes;
using NAnt.Core.Tasks;
using NAnt.Core.Types;
using NAnt.Core.Util;
using NAnt.DotNet.Types;
namespace NAnt.DotNet.Tasks {
///
/// Generates a .licence file from a .licx file.
///
///
///
/// If no output file is specified, the default filename is the name of the
/// target file with the extension .licenses appended.
///
///
///
///
/// Generate the file component.exe.licenses file from component.licx.
///
///
///
/// ]]>
///
///
[Serializable()]
[TaskName("license")]
[ProgramLocation(LocationType.FrameworkSdkDir)]
public class LicenseTask : ExternalProgramBase {
#region Private Instance Fields
private AssemblyFileSet _assemblies = new AssemblyFileSet();
private FileInfo _inputFile;
private FileInfo _outputFile;
private string _target;
private string _programFileName;
private DirectoryInfo _workingDirectory;
// framework configuration settings
private bool _supportsAssemblyReferences;
private bool _hasCommandLineCompiler = true;
#endregion Private Instance Fields
#region Public Instance Properties
///
/// Input file to process.
///
[TaskAttribute("input", Required=true)]
public FileInfo InputFile {
get { return _inputFile; }
set { _inputFile = value; }
}
///
/// Name of the license file to output.
///
[TaskAttribute("output", Required=false)]
public FileInfo OutputFile {
get { return _outputFile; }
set { _outputFile = value; }
}
///
/// Names of the references to scan for the licensed component.
///
[BuildElement("assemblies")]
public AssemblyFileSet Assemblies {
get { return _assemblies; }
set { _assemblies = value; }
}
///
/// Specifies the executable for which the .licenses file is generated.
///
[TaskAttribute("licensetarget", Required=false)]
[StringValidator(AllowEmpty=false)]
[Obsolete("Use the \"target\" attribute instead.", false)]
public string LicenseTarget {
get { return Target; }
set { Target = value; }
}
// TODO: we need to mark "target" as required after the release of
// NAnt 0.85
//
// We can't do this right now, as it would be a major breaking change
// (build authors still use "licensetarget" right now).
///
/// Specifies the executable for which the .licenses file is generated.
///
[TaskAttribute("target", Required=false)]
[StringValidator(AllowEmpty=false)]
public string Target {
get { return _target; }
set { _target = StringUtils.ConvertEmptyToNull(value); }
}
///
/// Indicates whether assembly references are supported by the current
/// target framework. The default is .
///
///
/// Applies only to frameworks having a command line tool for compiling
/// licenses files.
///
[FrameworkConfigurable("supportsassemblyreferences")]
public bool SupportsAssemblyReferences {
get { return _supportsAssemblyReferences; }
set { _supportsAssemblyReferences = value; }
}
///
/// Indicates whether the current target framework has a command line
/// tool for compiling licenses files. The default is
/// .
///
[FrameworkConfigurable("hascommandlinecompiler")]
public bool HasCommandLineCompiler {
get { return _hasCommandLineCompiler; }
set { _hasCommandLineCompiler = value; }
}
#endregion Public Instance Properties
#region Override implementation of Task
///
/// Initializes the class.
///
/// The used to initialize the task.
protected override void InitializeTask(XmlNode taskNode) {
// check if target was specified
// TODO: remove this check after NAnt 0.85 as this should be handled
// by setting Required to "true"
if (Target == null) {
throw new BuildException(string.Format(CultureInfo.InvariantCulture,
ResourceUtils.GetString("NA2013"), Name), Location);
}
// check if input file exists
if (!InputFile.Exists) {
throw new BuildException(string.Format(CultureInfo.InvariantCulture,
ResourceUtils.GetString("NA2014"), InputFile.FullName),
Location);
}
}
#endregion Override implementation of Task
#region Override implementation of ExternalProgramBase
///
/// Gets the working directory for the application.
///
///
/// The working directory for the application.
///
public override DirectoryInfo BaseDirectory {
get {
if (_workingDirectory == null) {
return base.BaseDirectory;
}
return _workingDirectory;
}
set {
_workingDirectory = value;
}
}
///
/// The command-line arguments for the external program.
///
///
/// Override to avoid exposing these elements in build file.
///
public override ArgumentCollection Arguments {
get { return base.Arguments; }
}
///
/// Gets the command-line arguments for the external program.
///
///
/// The command-line arguments for the external program.
///
public override string ProgramArguments {
get { return string.Empty; }
}
///
/// Gets the filename of the external program to start.
///
///
/// The filename of the external program.
///
///
/// Override in derived classes to explicitly set the location of the
/// external tool.
///
public override string ProgramFileName {
get {
if (_programFileName == null) {
_programFileName = base.ProgramFileName;
}
return _programFileName;
}
}
///
/// Updates the of the specified
/// .
///
/// The of which the should be updated.
protected override void PrepareProcess(Process process) {
if (!SupportsAssemblyReferences) {
// create instance of Copy task
CopyTask ct = new CopyTask();
// inherit project from current task
ct.Project = Project;
// inherit namespace manager from current task
ct.NamespaceManager = NamespaceManager;
// parent is current task
ct.Parent = this;
// inherit verbose setting from license task
ct.Verbose = Verbose;
// only output warning messages or higher, unless we're running
// in verbose mode
if (!ct.Verbose) {
ct.Threshold = Level.Warning;
}
// make sure framework specific information is set
ct.InitializeTaskConfiguration();
// set parent of child elements
ct.CopyFileSet.Parent = ct;
// inherit project from solution task for child elements
ct.CopyFileSet.Project = ct.Project;
// inherit namespace manager from solution task
ct.CopyFileSet.NamespaceManager = ct.NamespaceManager;
// set base directory of fileset
ct.CopyFileSet.BaseDirectory = Assemblies.BaseDirectory;
// copy all files to base directory itself
ct.Flatten = true;
// copy referenced assemblies
foreach (string file in Assemblies.FileNames) {
ct.CopyFileSet.Includes.Add(file);
}
// copy command line tool to working directory
ct.CopyFileSet.Includes.Add(base.ProgramFileName);
// set destination directory
ct.ToDirectory = BaseDirectory;
// increment indentation level
ct.Project.Indent();
try {
// execute task
ct.Execute();
} finally {
// restore indentation level
ct.Project.Unindent();
}
// change program to execute the tool in working directory as
// that will allow this tool to resolve assembly references
// using assemblies stored in the same directory
_programFileName = Path.Combine(BaseDirectory.FullName,
Path.GetFileName(base.ProgramFileName));
// determine target directory
string targetDir = Path.GetDirectoryName(Path.Combine(
BaseDirectory.FullName, Target));
// ensure target directory exists
if (!StringUtils.IsNullOrEmpty(targetDir) && !Directory.Exists(targetDir)) {
Directory.CreateDirectory(targetDir);
}
} else {
foreach (string assembly in Assemblies.FileNames) {
Arguments.Add(new Argument(string.Format(CultureInfo.InvariantCulture,
"/i:\"{0}\"", assembly)));
}
}
// further delegate preparation to base class
base.PrepareProcess(process);
}
///
/// Generates the license file.
///
protected override void ExecuteTask() {
FileInfo licensesFile = null;
// ensure base directory is set, even if fileset was not initialized
// from XML
if (Assemblies.BaseDirectory == null) {
Assemblies.BaseDirectory = new DirectoryInfo(Project.BaseDirectory);
}
// get the output .licenses file
if (OutputFile == null) {
try {
licensesFile = new FileInfo(Project.GetFullPath(Target + ".licenses"));
} catch (Exception ex) {
throw new BuildException(string.Format(CultureInfo.InvariantCulture,
ResourceUtils.GetString("NA2015"), Target), Location, ex);
}
} else {
licensesFile = OutputFile;
}
// make sure the directory for the .licenses file exists
if (!licensesFile.Directory.Exists) {
licensesFile.Directory.Create();
}
// determine whether .licenses file need to be recompiled
if (!NeedsCompiling(licensesFile)) {
return;
}
Log(Level.Verbose, ResourceUtils.GetString("String_CompilingLicenseUsingTarget"),
InputFile.FullName, licensesFile.FullName, Target);
if (HasCommandLineCompiler) {
// the command line compiler does not allow us to specify the
// full path to the output file, so we have it create the licenses
// file in a temp directory, and copy it to its actual output
// location
// use a newly created temporary directory as working directory
BaseDirectory = FileUtils.GetTempDirectory();
try {
// set target assembly for generated licenses file
Arguments.Add(new Argument(string.Format(CultureInfo.InvariantCulture,
"/target:\"{0}\"", Target)));
// set input filename
Arguments.Add(new Argument(string.Format(CultureInfo.InvariantCulture,
"/complist:\"{0}\"", InputFile.FullName)));
// set output directory
Arguments.Add(new Argument(string.Format(CultureInfo.InvariantCulture,
"/outdir:\"{0}\"", BaseDirectory.FullName)));
// suppress display of startup banner
Arguments.Add(new Argument("/nologo"));
// adjust verbosity of tool if necessary
if (Verbose) {
Arguments.Add(new Argument("/v"));
}
// use command line tool to compile licenses file
base.ExecuteTask();
// delete any existing output file
if (File.Exists(licensesFile.FullName)) {
File.Delete(licensesFile.FullName);
}
// copy licenses file to output file (with overwrite)
File.Copy(Path.Combine(BaseDirectory.FullName, Target + ".licenses"),
licensesFile.FullName, true);
} finally {
// delete temporary directory and all files in it
DeleteTask deleteTask = new DeleteTask();
deleteTask.Project = Project;
deleteTask.Parent = this;
deleteTask.InitializeTaskConfiguration();
deleteTask.Directory = BaseDirectory;
deleteTask.Threshold = Level.None; // no output in build log
deleteTask.Execute();
}
} else {
// create new domain
AppDomain newDomain = AppDomain.CreateDomain("LicenseGatheringDomain",
AppDomain.CurrentDomain.Evidence);
LicenseGatherer licenseGatherer = (LicenseGatherer)
newDomain.CreateInstanceAndUnwrap(typeof(LicenseGatherer).Assembly.FullName,
typeof(LicenseGatherer).FullName, false, BindingFlags.Public | BindingFlags.Instance,
null, new object[0], CultureInfo.InvariantCulture, new object[0],
AppDomain.CurrentDomain.Evidence);
licenseGatherer.CreateLicenseFile(this, licensesFile.FullName);
// unload newly created domain
AppDomain.Unload(newDomain);
}
}
#endregion Override implementation of ExternalProgramBase
#region Private Instance Methods
///
/// Determines whether the .licenses file needs to be recompiled
/// or is uptodate.
///
/// The .licenses file.
///
/// if the .licenses file needs compiling;
/// otherwise, .
///
private bool NeedsCompiling(FileInfo licensesFile) {
if (!licensesFile.Exists) {
Log(Level.Verbose, ResourceUtils.GetString("String_OutputFileDoesNotExist"),
licensesFile.FullName);
return true;
}
// check if assembly references were updated
string fileName = FileSet.FindMoreRecentLastWriteTime(Assemblies.FileNames, licensesFile.LastWriteTime);
if (fileName != null) {
Log(Level.Verbose, ResourceUtils.GetString("String_FileHasBeenUpdated"),
fileName);
return true;
}
// check if input file was updated
if (InputFile != null) {
fileName = FileSet.FindMoreRecentLastWriteTime(InputFile.FullName, licensesFile.LastWriteTime);
if (fileName != null) {
Log(Level.Verbose, ResourceUtils.GetString("String_FileHasBeenUpdated"),
fileName);
return true;
}
}
// if we made it here then we don't have to recompile
return false;
}
#endregion Private Instance Methods
///
/// Responsible for reading the license and writing them to a license
/// file.
///
private class LicenseGatherer : 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
///
/// Creates the whole license file.
///
/// The instance for which the license file should be created.
/// The .licenses file to create.
public void CreateLicenseFile(LicenseTask licenseTask, string licensesFile) {
ArrayList assemblies = new ArrayList();
// create assembly resolver
AssemblyResolver assemblyResolver = new AssemblyResolver(licenseTask);
// attach assembly resolver to the current domain
assemblyResolver.Attach();
licenseTask.Log(Level.Verbose, ResourceUtils.GetString("String_LoadingAssemblies"));
try {
// first, load all the assemblies so that we can search for the
// licensed component
foreach (string assemblyFileName in licenseTask.Assemblies.FileNames) {
Assembly assembly = Assembly.LoadFrom(assemblyFileName);
if (assembly != null) {
// output assembly filename to build log
licenseTask.Log(Level.Verbose, ResourceUtils.GetString("String_AssemblyLoaded"),
assemblyFileName);
// add assembly to list of loaded assemblies
assemblies.Add(assembly);
}
}
DesigntimeLicenseContext dlc = new DesigntimeLicenseContext();
LicenseManager.CurrentContext = dlc;
// read the input file
using (StreamReader sr = new StreamReader(licenseTask.InputFile.FullName)) {
Hashtable licenseTypes = new Hashtable();
licenseTask.Log(Level.Verbose, ResourceUtils.GetString("String_CreatingLicenses"));
while (true) {
string line = sr.ReadLine();
if (line == null) {
break;
}
line = line.Trim();
// Skip comments, empty lines and already processed assemblies
if (line.StartsWith("#") || line.Length == 0 || licenseTypes.Contains(line)) {
continue;
}
licenseTask.Log(Level.Verbose, line + ": ");
// Strip off the assembly name, if it exists
string typeName;
if (line.IndexOf(',') != -1) {
typeName = line.Split(',')[0];
} else {
typeName = line;
}
Type tp = null;
// try to locate the type in each assembly
foreach (Assembly assembly in assemblies) {
if (tp == null) {
tp = assembly.GetType(typeName, false, true);
}
if (tp != null) {
break;
}
}
if (tp == null) {
try {
// final attempt, assuming line contains
// assembly qualfied name
tp = Type.GetType(line, false, false);
} catch {
// ignore error, we'll report the load
// failure later
}
}
if (tp == null) {
throw new BuildException(string.Format(CultureInfo.InvariantCulture,
ResourceUtils.GetString("NA2016"), typeName), licenseTask.Location);
} else {
// add license type to list of processed license types
licenseTypes[line] = tp;
}
// ensure that we've got a licensed component
if (tp.GetCustomAttributes(typeof(LicenseProviderAttribute), true).Length == 0) {
throw new BuildException(string.Format(CultureInfo.InvariantCulture,
ResourceUtils.GetString("NA2017"), tp.FullName),
licenseTask.Location);
}
try {
LicenseManager.CreateWithContext(tp, dlc);
} catch (Exception ex) {
if (IsSerializable(ex)) {
throw new BuildException(string.Format(CultureInfo.InvariantCulture,
ResourceUtils.GetString("NA2018"), tp.FullName),
licenseTask.Location, ex);
}
// do not directly pass the exception as inner
// exception to BuildException as the exception
// is not serializable, so construct a new
// exception with message set to message of
// original exception
throw new BuildException(string.Format(CultureInfo.InvariantCulture,
ResourceUtils.GetString("NA2018"), tp.FullName),
licenseTask.Location, new Exception(ex.Message));
}
}
}
// overwrite the existing file, if it exists - is there a better way?
if (File.Exists(licensesFile)) {
File.SetAttributes(licensesFile, FileAttributes.Normal);
File.Delete(licensesFile);
}
// write out the license file, keyed to the appropriate output
// target filename
// this .license file will only be valid for this exe/dll
using (FileStream fs = new FileStream(licensesFile, FileMode.Create)) {
DesigntimeLicenseContextSerializer.Serialize(fs, licenseTask.Target, dlc);
licenseTask.Log(Level.Verbose, ResourceUtils.GetString("String_CreatedNewLicense"),
licensesFile);
}
dlc = null;
} catch (BuildException) {
// re-throw exception
throw;
} catch (Exception ex) {
if (IsSerializable(ex)) {
throw new BuildException(string.Format(CultureInfo.InvariantCulture,
ResourceUtils.GetString("NA2019"), licenseTask.InputFile.FullName),
licenseTask.Location, ex);
} else {
// do not directly pass the exception as inner exception to
// BuildException as the exception might not be serializable,
// so construct a
// new exception with message set to message of
// original exception
throw new BuildException(string.Format(CultureInfo.InvariantCulture,
ResourceUtils.GetString("NA2019"), licenseTask.InputFile.FullName),
licenseTask.Location, new Exception(ex.Message));
}
} finally {
// detach assembly resolver from the current domain
assemblyResolver.Detach();
}
}
#endregion Public Instance Methods
#region Private Instance Methods
///
/// Determines whether the given object is serializable in binary
/// format.
///
/// The object to check.
///
/// if is
/// serializable in binary format; otherwise, .
///
private bool IsSerializable(object value) {
BinaryFormatter formatter = new BinaryFormatter();
MemoryStream stream = new MemoryStream();
try {
formatter.Serialize(stream, value);
return true;
} catch (SerializationException) {
return false;
} finally {
stream.Close();
}
}
#endregion Private Instance Methods
}
}
}