All,
I was working with
the vb6 task in a build file and found that the logic that determines if the
project should be compiled seems broken unless the project file exists in
the same directory as the build file. I don't really have time
to debug that issue right now, but as a quick fix I added a new "alwayscompile"
task attribute that, when true, insures that the project gets compiled,
regardless of the outcome of the NeedsCompiling() method. I like this
solution because I generally always want to rebuild all projects during the
build, and it's nice to be able to force a rebuild when you want. The
modified task is attached. If any maintainers wish to add this
modification to the source tree, feel free.
Terry Austin
Progressive Partnering
[EMAIL PROTECTED]
// // NAntContrib // Copyright (C) 2001-2002 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 //
// Aaron A. Anderson ([EMAIL PROTECTED] | [EMAIL PROTECTED])
// Kevin Dente ([EMAIL PROTECTED])
using System;
using System.Collections.Specialized;
using System.Globalization;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.Runtime.InteropServices;
using NAnt.Core;
using NAnt.Core.Attributes;
using NAnt.Core.Tasks;
using NAnt.Core.Types;
using NAnt.Core.Util;
namespace NAnt.Contrib.Tasks {
/// <summary>
/// Compiles Microsoft Visual Basic 6 programs.
/// </summary>
/// <remarks>
/// <para>Uses the VB6.EXE executable included with the Visual Basic 6
environment.</para>
/// <para>The compiler uses the settings and source files specified in the
project or group file.</para>
/// </remarks>
/// <example>
/// <para>Build the project <c>HelloWorld.vbp</c> in the <c>build</c>
directory.</para>
/// <code>
/// <![CDATA[
/// <vb6 project="HelloWorld.vbp" outdir="build" />
/// ]]>
/// </code>
/// </example>
[TaskName("vb6")]
public class Vb6Task : ExternalProgramBase {
#region Private Instance Fields
private FileInfo _projectFile;
private DirectoryInfo _outDir;
private string _programArguments = null;
private FileInfo _errorFile;
private bool _checkReferences = true;
private bool _alwaysCompile = false;
#endregion Private Instance Fields
#region Public Instance Properties
/// <summary>
/// Output directory for the compilation target.
/// </summary>
[TaskAttribute("outdir")]
public DirectoryInfo OutDir {
get {
if (_outDir == null) {
return new DirectoryInfo(Project.BaseDirectory);
}
return _outDir;
}
set { _outDir = value; }
}
/// <summary>
/// Visual Basic project or group file.
/// </summary>
[TaskAttribute("project", Required=true)]
public FileInfo ProjectFile {
get { return _projectFile; }
set { _projectFile = value; }
}
/// <summary>
/// Determines whether project references are checked when deciding
/// whether the project needs to be recompiled. The default is
/// <see langword="true" />.
/// </summary>
[TaskAttribute("checkreferences")]
[BooleanValidator()]
public bool CheckReferences {
get { return _checkReferences; }
set { _checkReferences = value; }
}
/// <summary>
/// Overrides the check to see if the project needs recompiling
/// and always recompiles the project. The default is
/// <see langword="false" />.
/// </summary>
[TaskAttribute("alwayscompile")]
[BooleanValidator()]
public bool AlwaysCompile {
get { return _alwaysCompile; }
set { _alwaysCompile = value; }
}
/// <summary>
/// The file to which the Visual Basic compiler should log errors.
/// </summary>
[TaskAttribute("errorfile")]
public FileInfo ErrorFile {
get { return _errorFile; }
set { _errorFile = value; }
}
#endregion Public Instance Properties
#region Override implementation of ExternalProgramBase
/// <summary>
/// Gets the filename of the external program to start.
/// </summary>
/// <value>
/// The filename of the external program.
/// </value>
public override string ProgramFileName {
get { return Name; }
}
/// <summary>
/// Gets the command-line arguments for the external program.
/// </summary>
/// <value>
/// The command-line arguments for the external program.
/// </value>
public override string ProgramArguments {
get { return _programArguments; }
}
/// <summary>
/// Compiles the Visual Basic project or project group.
/// </summary>
protected override void ExecuteTask() {
Log(Level.Info, LogPrefix + "Building project '{0}'.",
ProjectFile.FullName);
if (_alwaysCompile || NeedsCompiling()) {
//Using a stringbuilder vs. StreamWriter since this program will
// not accept response files.
StringBuilder writer = new StringBuilder();
writer.AppendFormat(" /make \"{0}\"", ProjectFile.FullName);
// make sure the output directory exists
if (!OutDir.Exists) {
OutDir.Create();
}
writer.AppendFormat(" /outdir \"{0}\"", OutDir.FullName);
if (ErrorFile != null) {
writer.AppendFormat(" /out \"{0}\"", ErrorFile.FullName);
}
_programArguments = writer.ToString();
// call base class to do the work
base.ExecuteTask();
}
}
#endregion Override implementation of ExternalProgramBase
#region Protected Instance Methods
protected virtual bool NeedsCompiling() {
// return true as soon as we know we need to compile
if (string.Compare(ProjectFile.Extension, ".VBG", true,
CultureInfo.InvariantCulture) == 0) {
// The project file is a Visual Basic group file (VBG).
// We need to check each subproject in the group
StringCollection projectFiles = ParseGroupFile(ProjectFile);
foreach (string projectFile in projectFiles) {
if (ProjectNeedsCompiling(projectFile)) {
return true;
}
}
} else {
// The project file is a Visual Basic project file (VBP)
return ProjectNeedsCompiling(ProjectFile.FullName);
}
return false;
}
/// <summary>
/// Parses a VB group file and extract the file names of the sub-projects
/// in the group.
/// </summary>
/// <param name="groupFile">The file name of the group file.</param>
/// <returns>
/// A string collection containing the list of sub-projects in the group.
/// </returns>
protected StringCollection ParseGroupFile(FileInfo groupFile) {
StringCollection projectFiles = new StringCollection();
if (!groupFile.Exists) {
throw new BuildException(string.Format(CultureInfo.InvariantCulture,
"Visual Basic group file '{0}' does not exist.",
groupFile.FullName), Location);
}
string fileLine = null;
// Regexp that extracts INI-style "key=value" entries used in the VBP
Regex keyValueRegEx = new Regex(@"(?<key>\w+)\s*=\s*(?<value>.*)\s*$");
string key = string.Empty;
string keyValue = string.Empty;
Match match = null;
using (StreamReader reader = new StreamReader(groupFile.FullName,
Encoding.ASCII)) {
while ((fileLine = reader.ReadLine()) != null) {
match = keyValueRegEx.Match(fileLine);
if (match.Success) {
key = match.Groups["key"].Value;
keyValue = match.Groups["value"].Value;
if (key == "StartupProject" || key == "Project") {
// This is a project file - get the file name and
// add it to the project list
projectFiles.Add(keyValue);
}
}
}
// close the reader
reader.Close();
}
return projectFiles;
}
/// <summary>
/// Determines if a VB project needs to be recompiled by comparing the
timestamp of
/// the project's files and references to the timestamp of the last built
version.
/// </summary>
/// <param name="projectFile">The file name of the project file.</param>
/// <returns>
/// <see langword="true" /> if the project should be compiled; otherwise,
/// <see langword="false" />.
/// </returns>
protected bool ProjectNeedsCompiling(string projectFile) {
// return true as soon as we know we need to compile
FileSet sources = new FileSet();
// shouldn't the base directory actually be the directory where
// the project file is stored ?
sources.BaseDirectory = new DirectoryInfo(Project.BaseDirectory);
FileSet references = new FileSet();
// shouldn't the base directory actually be the directory where
// the project file is stored ?
references.BaseDirectory = new DirectoryInfo(Project.BaseDirectory);
string outputFile = ParseProjectFile(projectFile, sources, references);
FileInfo outputFileInfo = new FileInfo(OutDir != null ?
Path.Combine(OutDir.FullName, outputFile) : outputFile);
if (!outputFileInfo.Exists) {
Log(Level.Info, LogPrefix + "Output file '{0}' does not exist,
recompiling.", outputFileInfo.FullName);
return true;
}
string fileName =
FileSet.FindMoreRecentLastWriteTime(outputFileInfo.FullName,
outputFileInfo.LastWriteTime);
if (fileName != null) {
Log(Level.Info, LogPrefix + "{0} is out of date, recompiling.",
fileName);
return true;
}
fileName = FileSet.FindMoreRecentLastWriteTime(sources.FileNames,
outputFileInfo.LastWriteTime);
if (fileName != null) {
Log(Level.Info, LogPrefix + "{0} is out of date, recompiling.",
fileName);
return true;
}
if (CheckReferences) {
fileName = FileSet.FindMoreRecentLastWriteTime(references.FileNames,
outputFileInfo.LastWriteTime);
if (fileName != null) {
Log(Level.Info, LogPrefix + "{0} is out of date, recompiling.",
fileName);
return true;
}
}
return false;
}
#endregion Protected Instance Methods
#region Private Instance Methods
/// <summary>
/// Parses a VB project file and extracts the source files, reference files,
and
/// the name of the compiled file for the project.
/// </summary>
/// <param name="projectFile">The filename of the project file.</param>
/// <param name="sources">
/// A fileset representing the source files of the project, which will
/// populated by the method.
/// </param>
/// <param name="references">
/// A fileset representing the references of the project, which will
/// populated by the method.
/// </param>
/// <returns>A string containing the output file name for the
project.</returns>
private string ParseProjectFile(string projectFile, FileSet sources, FileSet
references) {
if (!File.Exists(Project.GetFullPath(projectFile))) {
throw new BuildException(string.Format(CultureInfo.InvariantCulture,
"Visual Basic project file '{0}' does not exist.", projectFile),
Location);
}
string outputFile = null;
string fileLine = null;
string projectName = null;
string projectType = null;
//# Modified each regular expressioni to properly parse the project
references in the vbp file #
// Regexp that extracts INI-style "key=value" entries used in the VBP
Regex keyValueRegEx = new
Regex(@"(?<key>\w+)\s*=\s*(?<value>.*($^\.)*)\s*$");
// Regexp that extracts source file entries from the VBP
(Class=,Module=,Form=,UserControl=)
Regex codeRegEx = new
Regex(@"(Class|Module)\s*=\s*\w*;\s*(?<filename>.*($^\.)*)\s*$");
// Regexp that extracts reference entries from the VBP (Reference=)
Regex referenceRegEx = new
Regex(@"Reference\s*=\s*\*\\G{(?<tlbguid>[0-9\-A-Fa-f]*($^\.)*)}\#(?<majorver>[0-9\.0-9($^\.)*]*)\#(?<minorver>[0-9]($^\.)*)\#(?<tlbname>.*)\#");
string key = String.Empty;
string keyValue = String.Empty;
Match match = null;
using (StreamReader reader = new
StreamReader(Project.GetFullPath(projectFile), Encoding.ASCII)) {
while ((fileLine = reader.ReadLine()) != null) {
match = keyValueRegEx.Match(fileLine);
if (match.Success) {
key = match.Groups["key"].Value;
keyValue = match.Groups["value"].Value;
if (key == "Class" || key == "Module") {
// This is a class or module source file - extract the
file name and add it to the sources fileset
// The entry is of the form "Class=ClassName;ClassFile.cls"
match = codeRegEx.Match(fileLine);
if (match.Success) {
sources.Includes.Add(match.Groups["filename"].Value);
}
}
else if (key == "Form" || key == "UserControl" || key ==
"PropertyPage") {
// This is a form, control, or property page source file -
add the file name to the sources fileset
// The entry is of the form "Form=Form1.frm"
sources.Includes.Add(keyValue);
}
else if (key == "Reference") {
// This is a source file - extract the reference name and
add it to the references fileset
match = referenceRegEx.Match(fileLine);
if (match.Success) {
string tlbName = match.Groups["tlbname"].Value;
if (File.Exists(tlbName)) {
references.Includes.Add(tlbName);
}
else {
//the tlb filename embedded in the VBP file is just
//a hint about where to look for it. If the file
isn't
//at that location, the typelib ID is used to
lookup
//the file name
// # Added to properly cast the parts of the
version #
// Ensure that we use the correct cast option
string temp = match.Groups["majorver"].Value;
ushort majorVer = (ushort) double.Parse(temp,
CultureInfo.InvariantCulture);
temp = match.Groups["minorver"].Value;
ushort minorVer = (ushort) double.Parse(temp,
CultureInfo.InvariantCulture);
temp = match.Groups["lcid"].Value;
uint lcid = 0;
if (0 < temp.Length) {
lcid = (uint) double.Parse(temp,
CultureInfo.InvariantCulture);
}
string tlbGuid = match.Groups["tlbguid"].Value;
Guid guid = new Guid(tlbGuid);
try {
QueryPathOfRegTypeLib(ref guid, majorVer,
minorVer, lcid, out tlbName);
if (File.Exists(tlbName)) {
references.Includes.Add(tlbName);
}
} catch (COMException) {
//Typelib wasn't found - vb6 will barf
//when the compile happens, but we won't worry
about it.
}
}
}
} else if (key == "ExeName32") {
// Store away the built file name so that we can check
against it later
// If the project was never built in the IDE, or the
project file wasn't saved
// after the build occurred, this setting won't exist. In
that case, VB uses the
// ProjectName as the DLL/EXE name
outputFile = keyValue.Trim('"');
} else if (key == "Type") {
// Store away the project type - we may need it to
construct the built
// file name if ExeName32 doesn't exist
projectType = keyValue;
} else if (key == "Name") {
// Store away the project name - we may need it to
construct the built
// file name if ExeName32 doesn't exist
projectName = keyValue.Trim('"');
}
}
}
reader.Close();
}
if (outputFile == null) {
// The output file name wasn't specified in the project file, so
// We need to figure out the output file name from the project name
and type
if (projectType == "Exe" || projectType == "OleExe") {
outputFile = Path.ChangeExtension(projectName, ".exe");
} else if (projectType == "OleDll") {
outputFile = Path.ChangeExtension(projectName, ".dll");
} else if (projectType == "Control") {
outputFile = Path.ChangeExtension(projectName, ".ocx");
}
}
return outputFile;
}
#endregion Protected Instance Methods
#region Private Static Methods
[DllImport("oleaut32.dll", PreserveSig=false)]
private static extern void QueryPathOfRegTypeLib(
ref Guid guid,
ushort majorVer,
ushort minorVer,
uint lcid,
[MarshalAs(UnmanagedType.BStr)] out string path
);
#endregion Private Static Methods
}
}

