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
    }
}

Reply via email to