//// NAnt - A .NET build tool
// Copyright (C) 2001-2003 Gerry Shaw
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Lesser General Public
// License as published by the Free Software Foundation; either
// version 2.1 of the License, or (at your option) any later version.
//
// This library 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
// Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
//
// Joe Jones (joejo@microsoft.com)
// Gerry Shaw (gerry_shaw@yahoo.com)
// Klemen Zagar (klemen@zagar.ws)
// Ian MacLean (ian_maclean@another.com)
// Gert Driesen (drieseng@ardatis.com)
// Giuseppe Greco (giuseppe.greco@agamura.com)
using System;
using System.Collections;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Text;
using System.Xml;
using System.Xml.XPath;
using NAnt.Core;
using NAnt.Core.Tasks;
using NAnt.Core.Attributes;
using NAnt.Core.Types;
using NAnt.Core.Util;
using NAnt.DotNet.Types;
namespace NAnt.DotNet.Tasks {
///
/// Converts files from one resource format to another.
///
///
///
/// If no is specified, the resource file will
/// be created next to the input file.
///
///
///
///
/// Convert a resource file from the .resx to the .resources
/// format.
///
///
///
/// ]]>
///
///
///
///
/// Convert a set of .resx files to the .resources format.
///
///
///
///
///
///
///
/// ]]>
///
///
[TaskName("resgen")]
[ProgramLocation(LocationType.FrameworkSdkDir)]
public class ResGenTask : ExternalProgramBase {
#region Private Instance Fields
private StringBuilder _arguments = new StringBuilder();
private AssemblyFileSet _assemblies = new AssemblyFileSet();
private FileInfo _inputFile;
private FileInfo _outputFile;
private string _programFileName;
private ResourceFileSet _resources = new ResourceFileSet();
private string _targetExt = "resources";
private DirectoryInfo _toDir;
private DirectoryInfo _workingDirectory;
private bool _useSourcePath;
private ArrayList _qualifiedResources = new ArrayList();
// framework configuration settings
private bool _supportsAssemblyReferences;
private bool _supportsExternalFileReferences;
#endregion Private Instance Fields
#region Private Static Fields
private const int _maxCmdLineLength = 30000;
#endregion Private Static Fields
#region Public Instance Properties
///
/// Input file to process.
///
///
/// The full path to the input file.
///
[TaskAttribute("input", Required=false)]
public FileInfo InputFile {
get { return _inputFile; }
set { _inputFile = value; }
}
///
/// The resource file to output.
///
[TaskAttribute("output", Required=false)]
public FileInfo OutputFile {
get { return _outputFile; }
set { _outputFile = value; }
}
///
/// The target type. The default is resources.
///
[TaskAttribute("target", Required=false)]
public string TargetExt {
get { return _targetExt; }
set { _targetExt = StringUtils.ConvertEmptyToNull(value); }
}
///
/// The directory to which outputs will be stored.
///
[TaskAttribute("todir", Required=false)]
public DirectoryInfo ToDirectory {
get { return _toDir; }
set { _toDir = value; }
}
///
/// Use each source file's directory as the current directory for
/// resolving relative file paths. The default is .
/// Only supported when targeting .NET 2.0 (or higher).
///
[TaskAttribute("usesourcepath", Required=false)]
public bool UseSourcePath {
get { return _useSourcePath; }
set { _useSourcePath = value; }
}
///
/// Takes a list of .resx or .txt files to convert to .resources files.
///
[BuildElement("resources")]
public ResourceFileSet Resources {
get { return _resources; }
set { _resources = value; }
}
///
/// Reference metadata from the specified assembly files.
///
[BuildElement("assemblies")]
public AssemblyFileSet Assemblies {
get { return _assemblies; }
set { _assemblies = value; }
}
///
/// Indicates whether assembly references are supported by the
/// resgen tool for the current target framework. The default
/// is .
///
[FrameworkConfigurable("supportsassemblyreferences")]
public bool SupportsAssemblyReferences {
get { return _supportsAssemblyReferences; }
set { _supportsAssemblyReferences = value; }
}
///
/// Indicates whether external file references are supported by the
/// resgen tool for the current target framework. The default
/// is .
///
[FrameworkConfigurable("supportsexternalfilereferences")]
public bool SupportsExternalFileReferences {
get { return _supportsExternalFileReferences; }
set { _supportsExternalFileReferences = value; }
}
///
/// For internal use only !
///
public ArrayList QualifiedResources {
get { return _qualifiedResources; }
}
#endregion Public Instance Properties
#region Private Instance Properties
private bool RequiresAssemblyReferences {
get {
if (Resources.FileNames.Count > 0 || QualifiedResources.Count > 0) {
foreach (string resourceFile in Resources.FileNames) {
if (ReferencesThirdPartyAssemblies(resourceFile)) {
return true;
}
}
foreach (QualifiedResource resource in QualifiedResources) {
if (ReferencesThirdPartyAssemblies(resource.Input.FullName)) {
return true;
}
}
} else if (InputFile != null) {
return ReferencesThirdPartyAssemblies(InputFile.FullName);
}
return false;
}
}
#endregion Private Instance Properties
#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;
}
}
///
/// Gets the command line arguments for the external program.
///
///
/// The command line arguments for the external program.
///
public override string ProgramArguments {
get { return _arguments.ToString(); }
}
///
/// 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) {
// use a newly created temporary directory as working directory
BaseDirectory = FileUtils.GetTempDirectory();
// avoid copying the assembly references (and resgen) to a
// temporary directory if not necessary
if (Assemblies.FileNames.Count == 0 || !RequiresAssemblyReferences) {
// further delegate preparation to base class
base.PrepareProcess(process);
// no further processing required
return;
}
// 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 resgen 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));
}
// further delegate preparation to base class
base.PrepareProcess(process);
}
///
/// Converts a single file or group of files.
///
protected override void ExecuteTask() {
// ensure base directory is set, even if fileset was not initialized
// from XML
if (Assemblies.BaseDirectory == null) {
Assemblies.BaseDirectory = new DirectoryInfo(Project.BaseDirectory);
}
if (Resources.BaseDirectory == null) {
Resources.BaseDirectory = new DirectoryInfo(Project.BaseDirectory);
}
// clear buffer
_arguments.Length = 0;
if (Resources.FileNames.Count > 0 || QualifiedResources.Count > 0) {
if (OutputFile != null) {
throw new BuildException(ResourceUtils.GetString("NA2026"), Location);
}
foreach (string fileName in Resources.FileNames) {
FileInfo inputFile = new FileInfo(fileName);
FileInfo outputFile = GetOutputFile(new FileInfo(Path.Combine(
inputFile.DirectoryName, Resources.GetManifestResourceName(fileName))));
WriteCommandLineOptions(inputFile, outputFile);
}
// used by task
foreach (QualifiedResource resource in QualifiedResources) {
WriteCommandLineOptions(resource.Input, resource.Output);
}
} else {
// Single file situation
if (InputFile == null) {
throw new BuildException(ResourceUtils.GetString("NA2027"), Location);
}
FileInfo outputFile = GetOutputFile(InputFile);
if (NeedsCompiling(InputFile, outputFile)) {
// ensure output directory exists
if (!outputFile.Directory.Exists) {
outputFile.Directory.Create();
}
if (UseSourcePath) {
if (SupportsExternalFileReferences) {
_arguments.Append("/useSourcePath");
} else {
Log(Level.Warning, ResourceUtils.GetString(
"String_ResourceCompilerDoesNotSupportExternalReferences"),
Project.TargetFramework.Description);
}
}
_arguments.Append(string.Format(CultureInfo.InvariantCulture,
" \"{0}\" \"{1}\"", InputFile.FullName, outputFile.FullName));
}
}
if (_arguments.Length != 0) {
try {
// call base class to do the work
base.ExecuteTask();
} finally {
// we only need to remove temporary directory if it was
// actually created
if (_workingDirectory != null) {
// delete temporary directory and all files in it
DeleteTask deleteTask = new DeleteTask();
deleteTask.Project = Project;
deleteTask.Parent = this;
deleteTask.InitializeTaskConfiguration();
deleteTask.Directory = _workingDirectory;
deleteTask.Threshold = Level.None; // no output in build log
deleteTask.Execute();
}
}
}
}
#endregion Override implementation of ExternalProgramBase
#region Public Instance Methods
///
/// Cleans up generated files.
///
public void RemoveOutputs() {
foreach (string filename in Resources.FileNames) {
FileInfo outputFile = GetOutputFile(new FileInfo(Path.Combine(
Path.GetDirectoryName(filename), Resources.GetManifestResourceName(filename))));
if (filename != outputFile.FullName) {
outputFile.Delete();
}
}
foreach (QualifiedResource resource in QualifiedResources) {
resource.Output.Delete();
}
if (InputFile != null) {
FileInfo outputFile = GetOutputFile(InputFile);
if (InputFile.FullName != outputFile.FullName) {
outputFile.Delete();
}
}
}
#endregion Public Instance Methods
#region Protected Instance Methods
///
/// Determines whether the specified input file needs to be compiled.
///
/// The input file.
/// The output file.
///
/// if the input file need to be compiled;
/// otherwise .
///
protected virtual bool NeedsCompiling(FileInfo inputFile, FileInfo outputFile) {
if (!outputFile.Exists) {
Log(Level.Verbose, ResourceUtils.GetString("String_OutputFileDoesNotExist"),
outputFile.FullName);
return true;
}
// check if input file was updated
string fileName = FileSet.FindMoreRecentLastWriteTime(inputFile.FullName, outputFile.LastWriteTime);
if (fileName != null) {
Log(Level.Verbose, ResourceUtils.GetString("String_FileHasBeenUpdated"),
fileName);
return true;
}
// check if reference assemblies were updated
fileName = FileSet.FindMoreRecentLastWriteTime(Assemblies.FileNames, outputFile.LastWriteTime);
if (fileName != null) {
Log(Level.Verbose, ResourceUtils.GetString("String_FileHasBeenUpdated"),
fileName);
return true;
}
// check if we're dealing with a resx file
if (string.Compare(inputFile.Extension, ".resx", true, CultureInfo.InvariantCulture) == 0) {
StringCollection externalFileReferences = GetExternalFileReferences(inputFile);
if (externalFileReferences != null) {
fileName = FileSet.FindMoreRecentLastWriteTime(externalFileReferences, outputFile.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 Protected Instance Methods
#region Private Instance Methods
///
/// Determines the full path and extension for the output file.
///
/// The output file for which the full path and extension should be determined.
///
/// The full path (with extensions) for the specified file.
///
private FileInfo GetOutputFile(FileInfo file) {
FileInfo outputFile;
// if output is empty just change the extension
if (OutputFile == null) {
if (ToDirectory == null) {
outputFile = file;
} else {
outputFile = new FileInfo(Path.Combine(ToDirectory.FullName, file.Name));
}
outputFile = new FileInfo(Path.ChangeExtension(outputFile.FullName, TargetExt));
} else {
outputFile = OutputFile;
}
return outputFile;
}
///
/// Determines whether the specified resource file references third
/// party assemblies by checking whether a <data> element exists
/// with a "type" attribute that does not start with
/// "System.".
///
/// The resource file to check.
///
/// if the resource file references third party
/// assemblies, or an error occurred; otherwise, .
///
///
/// This check will only be accurate for 1.0 resource file, but the
/// 2.0 resx files can only be compiled with a resgen tool that supported
/// assembly references, so this method will not be used anyway.
///
private bool ReferencesThirdPartyAssemblies(string resourceFile) {
try {
if (!File.Exists(resourceFile)) {
return false;
}
// only resx files require assembly references
if (string.Compare(Path.GetExtension(resourceFile), ".resx", true, CultureInfo.InvariantCulture) != 0) {
return false;
}
using (StreamReader sr = new StreamReader(resourceFile, true)) {
XPathDocument xpathDoc = new XPathDocument(new XmlTextReader(sr));
// determine the number of elements that either have
// a "mimetype" attribute (meaning it contains a serialized
// instance that might be of a referenced assembly) or a
// "type" attribute with a value that does not start with
// "System." and is not fully qualified
int count = xpathDoc.CreateNavigator().Select("/root/data[@mimetype or (@type and not(starts-with(@type, 'System.') and contains(@type,'PublicKeyToken=')))]").Count;
// if there are no elements of a third party type, we
// assume that the resource file does not reference types from
// third party assemblies
return count > 0;
}
} catch (Exception) {
// have the resgen tool deal with issues (eg. invalid xml)
return true;
}
}
///
/// Returns a list of external file references for the specified file.
///
/// The resx file for which a list of external file references should be returned.
///
/// A list of external file references for the specified file, or
/// if does not
/// exist or does not support external file references.
///
private StringCollection GetExternalFileReferences(FileInfo resxFile) {
if (!resxFile.Exists) {
return null;
}
using (StreamReader sr = new StreamReader(resxFile.FullName, true)) {
XPathDocument xpathDoc = new XPathDocument(new XmlTextReader(sr));
XPathNavigator xpathNavigator = xpathDoc.CreateNavigator();
// check resheader version
xpathNavigator.Select("/root/resheader[@name = 'version']/value");
XPathNodeIterator nodeIterator = xpathNavigator.Select("/root/resheader[@name = 'version']/value");
if (nodeIterator.MoveNext()) {
string version = nodeIterator.Current.Value;
// 1.0 resx files do not support external file references
if (version == "1.0.0.0") {
return null;
}
}
StringCollection externalFiles = new StringCollection();
string baseExternalFileDirectory = UseSourcePath ? resxFile.DirectoryName
: Project.BaseDirectory;
// determine the number of elements that have a "type"
// attribute with a value that does not start with "System."
XPathNodeIterator xfileIterator = xpathNavigator.Select("/root/data[@type = 'System.Resources.ResXFileRef, System.Windows.Forms']/value");
while (xfileIterator.MoveNext()) {
string[] parts = xfileIterator.Current.Value.Split(';');
if (parts.Length <= 1) {
continue;
}
externalFiles.Add(Path.Combine(baseExternalFileDirectory, parts[0]));
}
return externalFiles;
}
}
private void WriteCommandLineOptions(FileInfo inputFile, FileInfo outputFile) {
if (!NeedsCompiling(inputFile, outputFile)) {
return;
}
// ensure output directory exists
if (!outputFile.Directory.Exists) {
outputFile.Directory.Create();
}
string cmdLineArg = string.Format(CultureInfo.InvariantCulture,
"\"{0},{1}\" ", inputFile, outputFile.FullName);
// check if adding arguments to compile current resx to
// total command line would cause it to exceed maximum
// length
bool maxCmdLineExceeded = (_arguments.Length + cmdLineArg.Length > _maxCmdLineLength);
// if this is the first resx that we're compiling, or the
// first one of the next execution of the resgen tool, then
// add options to command line
if (_arguments.Length == 0 || maxCmdLineExceeded) {
if (UseSourcePath) {
if (SupportsExternalFileReferences) {
cmdLineArg = "/useSourcePath /compile " + cmdLineArg;
} else {
cmdLineArg = "/compile " + cmdLineArg;
Log(Level.Warning, ResourceUtils.GetString(
"String_ResourceCompilerDoesNotSupportExternalReferences"),
Project.TargetFramework.Description);
}
} else {
StringBuilder sb = new StringBuilder ();
// bug #1415272: first write assembly references, to make sure these
// are taken into account when calculating the length of the command
// line
if (SupportsAssemblyReferences) {
foreach (string assembly in Assemblies.FileNames) {
sb.AppendFormat (CultureInfo.InvariantCulture,
"/r:\"{0}\" ", assembly);
}
}
sb.Append ("/compile ");
sb.Append (cmdLineArg);
cmdLineArg = sb.ToString ();
}
}
// if maximum length would have been exceeded by compiling
// the current resx file, then first execute the resgen
// tool
if (maxCmdLineExceeded) {
try {
// call base class to do the work
base.ExecuteTask();
} catch {
// we only need to remove temporary directory when
// an error occurred and if it was actually created
if (_workingDirectory != null) {
// delete temporary directory and all files in it
DeleteTask deleteTask = new DeleteTask();
deleteTask.Project = Project;
deleteTask.Parent = this;
deleteTask.InitializeTaskConfiguration();
deleteTask.Directory = _workingDirectory;
deleteTask.Threshold = Level.None; // no output in build log
deleteTask.Execute();
}
// rethrow exception
throw;
}
// reset command line arguments as we've processed them
_arguments.Length = 0;
}
// append command line arguments to compile current resx
// file to the total command line
_arguments.Append(cmdLineArg);
}
#endregion Private Instance Methods
}
///
/// For internal use only !
///
public class QualifiedResource {
#region Private Instance Fields
private FileInfo _inputFile;
private FileInfo _outputFile;
#endregion Private Instance Fields
#region Public Instance Constructors
///
/// Initializes a new instance of the
/// class for a given input and output file.
///
/// The resource to compile.
/// The compiled resource.
public QualifiedResource(FileInfo input, FileInfo output) {
_inputFile = input;
_outputFile = output;
}
#endregion Public Instance Constructors
#region Public Instance Properties
///
/// Gets the resource file to compile.
///
///
/// The resource file to compile.
///
public FileInfo Input {
get { return _inputFile; }
}
///
/// Gets the compiled resource file.
///
///
/// The compiled resource file.
///
public FileInfo Output {
get { return _outputFile; }
}
#endregion Public Instance Properties
}
}