I first learned about Continuous Integration Servers about a year ago when I read a magazine article on the topic. After reading the article, I decided that there was no way I was willing to continue with manual production builds anymore. So I started to put together a CI Server. In doing so, I was pleasantly surprised to see that almost everything needed to complete the new CI Server was readily available with a quick internet search. However, there was one piece of the puzzle that was missing. I was unable to find a resource that could produce a Visual FoxPro build for Cruise Control.Net. Needless to say, I wasn’t about to let this minor setback deter me from my goal having a CI Server. After reviewing my options, I decided to create a custom MSBuild Task to build my Visual FoxPro Applications.
Creating the VFP MSBuild Task
I started off by creating a C# Class Library Project named VfpBuildTask. Then I added references for Microsoft.Build.Framework, Microsoft.Build.Utilities.v3.5, and Microsoft Visual FoxPro 9.0 Type Library. With the basic setup completed, the project was ready for developing the custom MSBuild Task.
To create the custom MSBuild Task, I added a new class named VfpBuild. The VfpBuild class inherits from the abstract class Microsoft.Build.Utilities.Task which includes an abstract method named Execute. The Execute method will handle the building of Visual FoxPro Applications. To start off with, I wanted to implement a C# version of the following VFP code:
oVfp = CREATEOBJECT("VisualFoxPro.Application")
oVfp.DoCmd("BUILD EXE <<MyApp>> FROM <<MyApp>>.pjx RECOMPILE")
oVfp.Quit()
Here is the C# code that I came up with for the VfpBuild class:
using System;
using System.IO;
using System.Threading;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace VfpBuildTask {
public class VfpBuild : Task {
[Required]
public ITaskItem[] Projects { get; set; }
public string BuildType { get; set; }
public override bool Execute() {
VisualFoxpro.FoxApplicationClass vfp = null;
try {
// vfp = CREATEOBJECT("VisualFoxPro.Application")
vfp = new VisualFoxpro.FoxApplicationClass();
// loop through all the vfp projects
for (int index = 0, total = this.Projects.Length; index < total; index++) {
// get the project full path
string project = this.Projects[index].ItemSpec;
// create the build command
string buildCommand = string.Format("build {0} '{1}' from '{2}' recompile",
(string.IsNullOrEmpty(this.BuildType) ? "exe" : this.BuildType),
Path.GetFileNameWithoutExtension(project),
project);
// execute the build command
vfp.DoCmd(buildCommand);
}
}
catch (Exception ex) {
this.Log.LogError(ex.Message);
}
finally {
if (vfp != null) {
vfp.Quit();
}
}
return !this.Log.HasLoggedErrors;
}
}
}
At this point, I’m ready to test this class to see if it will actually create the build. Now I needed to create and run a MS Build Project. I created a very simple MS Build Project with the file name Build1.proj.
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="C:VfpBuildTaskMSBuildResourcesVfpBuildTask.Targets" />
<Target Name="Build">
<VfpBuild Projects="C:VfpBuildTaskAppsExeProgram1.pjx" />
</Target>
</Project>
I ran this project from the command prompt using the following:
The VFP MSBuild Task built the application as expected. Great! But it also had a minor side effect which would prevent me from being able to add this to the CI Server Build Process. It seems that something was preventing Visual FoxPro from exiting… resulting in a very common dialog:
After looking into this issue, I found that the Task Pane was causing this dialog. I added code to close the Task Pane Window before calling the Quit method on the VFP instance. To do this I just added a Thread.Sleep command to give the window some time to load and then I executed the VFP command “Clear All".
Here is the modified code:
using System;
using System.IO;
using System.Threading;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace VfpBuildTask {
public class VfpBuild : Task {
[Required]
public ITaskItem[] Projects { get; set; }
public string BuildType { get; set; }
public override bool Execute() {
VisualFoxpro.FoxApplicationClass vfp = null;
try {
// vfp = CREATEOBJECT("VisualFoxPro.Application")
vfp = new VisualFoxpro.FoxApplicationClass();
// wait for the task pane window open
Thread.Sleep(2000);
// close the task pane window
vfp.DoCmd("CLEAR ALL");
// loop through all the vfp projects
for (int index = 0, total = this.Projects.Length; index < total; index++) {
// get the project full path
string project = this.Projects[index].ItemSpec;
// create the build command
string buildCommand = string.Format("build {0} '{1}' from '{2}' recompile",
(string.IsNullOrEmpty(this.BuildType) ? "exe" : this.BuildType),
Path.GetFileNameWithoutExtension(project),
project);
// execute the build command
vfp.DoCmd(buildCommand);
}
}
catch (Exception ex) {
this.Log.LogError(ex.Message);
}
finally {
if (vfp != null) {
vfp.Quit();
}
}
return !this.Log.HasLoggedErrors;
}
}
}
At this point, I’m technically done. I can modify the CI Server build process to include building my Visual FoxPro Applications. But now that I have the basics done… I can’t help but think of a few enhancements that I would like to include. Plus, I’m considering the fact that I have not handled a potential build time dialog which my hang up the CI Server. The dialog I’m referring to is the “Locate File” dialog.
Enhancing the VFP MSBuild Task - Fix “Locate File” dialog issue:
The first change that I would like to make is preventing the “Locate File” dialog. I can only think of two scenarios that would result in this dialog. The first scenario is that the project is referencing a file that does not exist in the file system. The second scenario is where the source code has a NewObject command which references a file that does not exist in the project or the file system. These two scenarios are similar but require two different solutions. In the first scenario, I can manually query the project file to see if any files are missing from the file system. The second scenario I cannot completely solve but I can get close. I can do this by querying the project file to generate a search path. Setting the search path should allow the build process to find a file that hasn’t been added to the solution. This will, of course, only work if the missing file is in a file location that has been referenced by another file in the project.
So how do I pull this off…? First I added a reference LinqToVfp.dll, IQtoolkit.dll, and IQToolkit.Data.dll. Then I create a class name VfpProjectItem to model the VFP Project table. I only selected the few fields that I needed.
namespace VfpBuildTask {
public class VfpProjectItem {
public string Name { get; set; }
public string Type { get; set; }
}
}
Next I created a new method ProjectSetup and modified the Execute method to call this method.
public bool ProjectSetup(string project) {
string connectionString = string.Format("Provider=VFPOLEDB.1;Data Source={0};", project);
bool hasError = false;
string projectFileName = null;
List<string> fileList = null;
using (VfpQueryProvider provider = VfpQueryProvider.Create(connectionString, null)) {
provider.Connection.Open();
projectFileName = Path.GetFileName(project);
// Get a list of files with their relative path
fileList = (from item in provider.GetTable<VfpProjectItem>(projectFileName)
// exclude the header item
where item.Type != "H"
select item.Name).ToList();
provider.Connection.Close();
}
string homeDir = Path.GetDirectoryName(project);
this.Log.LogMessage("Default Directory: " + homeDir);
this.vfp.DoCmd(string.Format("cd [{0}]", homeDir));
// change the relative paths to absolute paths
for (int index = 0, total = fileList.Count; index < total; index++) {
string tempDir = homeDir;
string itemPath = fileList[index];
while (itemPath.StartsWith(@"..")) {
// go up a directory level
tempDir = Path.GetDirectoryName(tempDir);
// remove ..
itemPath = itemPath.Substring(3);
}
fileList[index] = Path.Combine(tempDir, itemPath);
}
// create a FileInfo object for each file
List<FileInfo> fileInfoList = fileList.Select(file => new FileInfo(file)).ToList();
// log an error message for each file that does not exist
fileInfoList.Where(fi => !fi.Exists).ToList().ForEach(fi => {
this.Log.LogError(string.Format("Cannot Locate File: {0}", fi.FullName));
hasError = true;
});
if (!hasError) {
string searchPath = string.Empty;
// create the search path
fileInfoList.Select(fi => fi.DirectoryName.ToUpper()).Distinct().ToList().ForEach(dir => {
searchPath += dir + ";";
});
if (!string.IsNullOrEmpty(searchPath)) {
this.Log.LogMessage("Search Path: " + searchPath);
this.vfp.DoCmd(string.Format("SET PATH TO [{0}]", searchPath));
}
}
if (!hasError && !string.IsNullOrEmpty(this.Debug) && !string.IsNullOrEmpty(this.VersionNumber)) {
string command = string.Format("MODIFY PROJECT '{0}'", project);
this.Log.LogMessage(command);
this.vfp.DoCmd(command);
#region Debug
if (!string.IsNullOrEmpty(this.Debug)) {
bool value = false;
if (bool.TryParse(this.Debug, out value)) {
command = string.Format("_vfp.ActiveProject.Debug = {0}", value ? ".t." : ".f.");
this.Log.LogMessage(command);
this.vfp.DoCmd(command);
}
else {
this.Log.LogError("Debug property is invalid: {0}", this.Debug);
hasError = true;
}
}
#endregion
#region VersionNumber
if (!string.IsNullOrEmpty(this.VersionNumber)) {
// make sure auto increment is set to false if a version number was specified
command = string.Format("_vfp.ActiveProject.AutoIncrement = .f.");
this.Log.LogMessage(command);
this.vfp.DoCmd(command);
command = string.Format("_vfp.ActiveProject.VersionNumber = '{0}'", this.VersionNumber);
this.Log.LogMessage(command);
this.vfp.DoCmd(command);
}
#endregion
}
return !hasError;
}
With this method in place, the NewObject scenario should compile just fine. The missing project file scenario will no longer display the “Locate File” dialog. Instead, this error will be logged to the console as seen in red below.
Enhancing the VFP MSBuild Task – Other minor enhancments:
Added OutputDir property, Debug property, and VersonNumber property.
Example MSBuild Project using the VFP MSBuild Task:
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<MSBuildCommunityTasksPath>C:VfpBuildTaskMSBuildResources</MSBuildCommunityTasksPath>
</PropertyGroup>
<Import Project="$(MSBuildCommunityTasksPath)MSBuild.Community.Tasks.Targets"/>
<Import Project="C:VfpBuildTaskMSBuildResourcesVfpBuildTask.Targets" />
<Target Name="Build">
<PropertyGroup>
<GetVersionNumber>
<![CDATA[
public static string ScriptMain() {
return DateTime.Now.ToString("yyyy.MMdd.hhmm");
}
]]>
</GetVersionNumber>
</PropertyGroup>
<Script Language="C#" Code="$(GetVersionNumber)"><Output TaskParameter="ReturnValue" PropertyName="VersionNumber" /></Script><ItemGroup><VfpExeProjects Include="C:VfpBuildTaskAppsExeProgram*.pjx" /></ItemGroup><!-- build multiple Win32 executable build -->
<VfpBuild Projects="@(VfpExeProjects)"OutputDir="C:VfpBuildTaskDeploy"Debug="False"VersionNumber="$(VersionNumber)" /><!-- application build -->
<VfpBuild Projects="C:VfpBuildTaskAppsAppProgram1.pjx"OutputDir="C:VfpBuildTaskDeploy"BuildType="App"Debug="False"VersionNumber="$(VersionNumber)" /><!-- multi-threaded com server build -->
<VfpBuild Projects="C:VfpBuildTaskAppsDllPorgram1.pjx"OutputDir="C:VfpBuildTaskDeploy"BuildType="MTDLL"Debug="False"VersionNumber="$(VersionNumber)" /></Target></Project>
The download link on this page is broken as are some of the graphics…I am implementing CI with CruiseControl .Net and hoped to use your MSBuild task
Thanks!
Mike
Comment by Michael Barnett — April 21, 2011 @ 6:00 pm
I fixed both issues. Thanks for pointing that out.
Comment by Tom Brothers — April 22, 2011 @ 12:02 am
I am sorry but I do not know what should be in this file…
Comment by Michael Barnett — April 22, 2011 @ 3:57 pm
sorr again.. this file…
Import Project=”C:VfpBuildTaskMSBuildResourcesVfpBuildTask.Targets”
Comment by Michael Barnett — April 22, 2011 @ 3:58 pm
How do you generate or what should be in the Import Project=”C:VfpBuildTaskMSBuildResourcesVfpBuildTask.Targets” file?
Comment by Paul — January 14, 2013 @ 8:06 pm
I fixed the download link so you can view that file in the download zip.
Comment by Tom Brothers — January 15, 2013 @ 8:40 am