using System;
using System.Data;
using System.Diagnostics;
using System.IO;
using System.Text;
using Agile.Common;
using Agile.Common.UI;
using Agile.Genie.Descriptors;
using GeneratorCustomization;
namespace Agile.Genie.Generators
{
/// <summary>
/// Generates a NAnt build file for a given project file.
/// </summary>
[GUIDetails("Build File")]
public class BuildFileGenerator : Generator
{
#region Constructors and Factories
/// <summary>
/// Constructor
/// </summary>
private BuildFileGenerator(FileInfo projectFile)
{
_projectFile = AgileFileInfo.Build(projectFile);
}
/// <summary>
/// Instantiate a new Build file generator for the given solution file.
/// </summary>
/// <remarks>File type must be a project file, if it is not,
/// null will be returned.
/// <p>Will also return null if the project file is in the 'excluded' list.</p></remarks>
/// <returns>Returns null if the file is not a project file, otherwise returns the instantiated generator.</returns>
public static BuildFileGenerator Build(FileInfo projectFile)
{
if (!Files.IsTheRightFileType(projectFile, "csproj", "vbproj"))
return null;
if (!projectFile.Exists)
return null;
// If an exception occurs during instantiation, just return null
try
{
var generator = new BuildFileGenerator(projectFile);
if (generator.IsExcludedProject)
return null;
return generator;
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
return null;
}
}
#endregion
private readonly AgileFileInfo _projectFile;
private VisualStudioProjectFile _visualStudioProjectFile;
private DataTable serverTestingProjects;
/// <summary>
/// Checks if the project file is in the excluded projects list in the Generator Details Dataset.
/// </summary>
/// <remarks>Excluded projects are ones that we do not want to generate a build file for
/// (and also do not want included in the master.build).
/// </remarks>
/// <returns>True if the project has been listed explicitly to be excluded.</returns>
private bool IsExcludedProject
{
get
{
// Ignore all .Tests projects
if (ProjectFile.Name.Contains(".Test"))
return true;
foreach (DataRow row in GeneratorsData.All.Tables["ProjectsExcludedFromBuild"].Rows)
{
if (ProjectFile.NameExcludingExtension == row["ProjectName"].ToString())
return true;
}
return false;
}
}
/// <summary>
/// Gets the project system file that we are generating the .build file for.
/// </summary>
public FileInfo ProjectFileInfo
{
get { return ProjectFile.FileInfo; }
}
/// <summary>
/// Gets the AgileFileInfo object that encapsulates the project file.
/// </summary>
public AgileFileInfo ProjectFile
{
get { return _projectFile; }
}
/// <summary>
/// Gets the visual studio project files.
/// </summary>
private VisualStudioProjectFile VisualStudioProjectFile
{
get
{
if (_visualStudioProjectFile == null)
{
_visualStudioProjectFile = VisualStudioProjectFileFactory.Build(ProjectFileInfo);
}
return _visualStudioProjectFile;
}
}
/// <summary>
/// Returns the name of the output file that will be generated by the msbuild process
/// </summary>
private static string OutputFileName
{
get
{
// Don't append anything for Compact framework...doesn't matter if it overwrites (if the CF results overwrites the normal build results it means the normal build was successful)
// Cruise Control can only grab one results file.
return "buildResults.xml";
}
}
/// <summary>
/// Generates the build file core code
/// </summary>
/// <returns></returns>
public string GenerateBuildFileCode()
{
string code =string.Format(@"
<project name=""{0}"" default=""go"" basedir=""."">
<property name=""TestsBaseDirectory"" value=""{7}"" />
<property name=""TestsAssemblyDirectory"" value=""${{TestsBaseDirectory}}\bin\Release"" />
<property name=""RunDirectory"" value=""${{project::get-base-directory()}}\${{project::get-name()}}"" />
<property name=""BuildResultsDirectory"" value=""${{directory::get-current-directory()}}\Build"" />
<property name=""nant.onfailure"" value=""OnFailure"" />
<!-- Go
Runs both the build and test targets.
-->
<target name=""go"" depends=""build{6}, test, UpdateResults""/>
<!-- Build
Builds the project and test project (if there is one).
-->
{4}
{5}
<!-- test
Tests are compulsory - every project must have them. All test results are copied into
C:\build\testresults (or whatever your RootDirectory is)
-->
{1}
{2}
{3}
</project>
"
, FileName
, GenerateTestSection()
, GenerateCopyResults()
, GenerateOnFailure()
, GenerateBuildTarget()
, GenerateBuildTestTarget()
, (!VisualStudioProjectFile.IsUnitTested || !VisualStudioProjectFile.HasTestProject) ? string.Empty : ", buildTest"
, GetTestProjectFileDirectoryOnly()
);
return code;
}
/// <summary>
/// Gets the command line args for the msbuild execution.
/// </summary>
/// <returns></returns>
private string GenerateBuildTarget()
{
// Set the command line details (for msbuild) differently for CF and normal projects.
string commandLine = VisualStudioProjectFile.IsCompactFramework
? string.Format(@"{0}", FileName.Replace("Compact", "Compact") + ProjectFile.FileInfo.Extension)
: string.Format(@"{0}", string.Format("{0}{1}", FileName, ProjectFile.FileInfo.Extension));
return GetBuildTarget("build", commandLine);
}
/// <summary>
/// Gets the command line args for the msbuild execution.
/// </summary>
/// <returns></returns>
private string GenerateBuildTestTarget()
{
// If it does not have a unit test project, dont create the 'buildTest' target
if (!VisualStudioProjectFile.IsUnitTested || !VisualStudioProjectFile.HasTestProject)
return string.Empty;
// Default to building the test project (doing this ensures the test project gets built which triggers a build of the project itself if required)
return GetBuildTarget("buildTest", GetTestProjectFileLocation());
// return GetBuildTarget("buildTest", string.Format(@"""..\${{project::get-name()}}.Test\{0}"
// , string.Format("{0}.Test{1}", FileName, ProjectFile.FileInfo.Extension)));
}
/// <summary>
/// Returns the directory that the test project file is in
/// </summary>
private string GetTestProjectFileLocation()
{
FileInfo testProject = VisualStudioProjectFile.GetTestProjectFile();
if (testProject == null || testProject.Directory == null)
return string.Empty;
// don't get the full path as build files get checked in and will likely be in a different dir on the build box and other developers machines.
string testProjectName = testProject.Name;
return string.Format(@"{0}\{1}", GetTestProjectFileDirectoryOnly(), testProjectName);
}
/// <summary>
/// Returns the directory that the test project file is in
/// </summary>
private string GetTestProjectFileDirectoryOnly()
{
FileInfo testProject = VisualStudioProjectFile.GetTestProjectFile();
if (testProject == null || testProject.Directory == null)
return string.Empty;
// don't get the full path as build files get checked in and will likely be in a different dir on the build box and other developers machines.
string testProjectDirectory = testProject.Directory.Name;
return string.Format(@"..\{0}", testProjectDirectory);
}
private string GetBuildTarget(string targetName, string commandLineCode)
{
const string rootDriveEnvironmentalVariableName = "RootDrive";
string platform = VisualStudioProjectFile.IsCompactFramework ? "AnyCPU" : "AnyCPU"; // need tobe different for CF?
string stuffAtTheEnd =
string.Format(
@" /verbosity:quiet /nologo /p:Configuration=Release;Platform={2} /logger:${{environment::get-variable('{1}')}}:\Build\CCLogger\ThoughtWorks.CruiseControl.MSBuild.dll;{0}""/>""/>"
, OutputFileName
, rootDriveEnvironmentalVariableName
, platform);
return string.Format(@"
<target name=""{0}"">
<delete file=""{2}"" failonerror=""false""/>
<echo message=""*** Building {1}...""/>
<exec
program=""msbuild""
commandline=""{1}{3}
<echo message=""*** Build of {1} Completed Successfully!""/>
</target>
"
, targetName
, commandLineCode
, OutputFileName
, stuffAtTheEnd
);
}
/// <summary>
/// Generate the CopyResults target
/// </summary>
/// <returns></returns>
private static string GenerateCopyResults()
{
return string.Format(@"
<target name=""UpdateResults"">
<mkdir dir=""${{BuildResultsDirectory}}"" failonerror=""false""/>
<copy verbose=""true"" todir=""${{BuildResultsDirectory}}"" overwrite=""true"">
<fileset basedir=""${{project::get-base-directory()}}"">
<include name=""buildResults.xml""/>
</fileset>
</copy>
</target>
<target name=""CopyResults"">
<mkdir dir=""${{BuildResultsDirectory}}\TestResults"" failonerror=""true""/>
<copy verbose=""true"" todir=""${{BuildResultsDirectory}}\TestResults"">
<fileset basedir=""${{TestsAssemblyDirectory}}"">
<include name=""*results.xml""/>
</fileset>
</copy>
<echo message=""copied test results from: ${{TestsAssemblyDirectory}}...TO --> ${{BuildResultsDirectory}}""></echo>
</target>
");
}
/// <summary>
/// Generate the OnFailure target
/// </summary>
/// <returns></returns>
private string GenerateOnFailure()
{
var onFailure =
new StringBuilder(
string.Format(
@"
<target name=""OnFailure"" description=""Called when build fails"">
<property name=""message"" value=""OnFailure""/>
<call target=""CopyResults"" />
<call target=""UpdateResults"" />
"
, OutputFileName));
if (ProjectIsServerTesting(VisualStudioProjectFile.ProjectName))
{
onFailure.Append(
string.Format(
@"
<mail
from=""isisclient@aus.fujixerox.com""
tolist=""mark.wallis@aus.fujixerox.com; ali.shafai@aus.fujixerox.com; dave.wheeler@aus.fujixerox.com; craig.hunter@aus.fujixerox.com""
subject=""Tests failed in ${{project::get-name()}}""
mailhost=""ausnry-ex01.pilot.local""
message=""Note: this mail is also generated when a compile error occurs."">
<attachments>
<include name=""${{TestsAssemblyDirectory}}\*Results.xml"" />
</attachments>
</mail>
"));
}
onFailure.Append(@"
</target>
");
return onFailure.ToString();
}
/// <summary>
/// Returns true if the tests for the project test the server.
/// </summary>
/// <remarks>COPIED IN MasterBuildFileGenerator</remarks>
/// <returns></returns>
private bool ProjectIsServerTesting(string projectName)
{
if (serverTestingProjects == null)
serverTestingProjects = GeneratorsData.GetDataTable("ServerTestingProjects");
foreach (DataRow row in serverTestingProjects.Rows)
{
if (row["ProjectName"].ToString() == projectName)
return true;
}
return false;
}
/// <summary>
/// Generates the test section of the build file
/// </summary>
/// <returns></returns>
public string GenerateTestSection()
{
if (VisualStudioProjectFile.OutputType.EndsWith("Exe"))
return GenerateTestsNotRun("TESTS NOT CURRENTLY RUN FOR PROJECTS WITH AN Exe Output");
if (!VisualStudioProjectFile.IsUnitTested)
return GenerateTestsNotRun("PROJECTS OF THIS TYPE ARE NOT UNIT TESTED!");
if (!VisualStudioProjectFile.HasTestProject)
return GenerateTestsNotRun("THIS PROJECT DOES NOT HAVE A .TEST PROJECT!");
return GenerateTestSectionDetails();
}
/// <summary>
/// Generates the test section of the build file for VS2003 projects
/// </summary>
/// <returns></returns>
public string GenerateTestSectionDetails()
{
FileInfo testProjectFile = VisualStudioProjectFile.GetTestProjectFile();
string code = string.Format(
@" <target name=""test"">
<echo message=""*******************************************************""/>
<echo message="" RUNNING TEST SUITES *""/>
<echo message="" - {0}\{1}.dll""/>
<echo message="" *""/>
<echo message=""*******************************************************""/>
<nunit2 verbose=""true"" failonerror=""true"">
<formatter type=""Plain"" />
<formatter type=""Xml"" usefile=""true"" extension="".xml"" />
<test assemblyname=""${{TestsAssemblyDirectory}}\{1}.dll""/>
</nunit2>
<call target=""CopyResults"" />
<echo message=""""/>
<echo message=""*** ${{project::get-name()}} SUCCESSFULLY BUILT AND TESTED! ***""/>
<echo message=""""/>
</target>"
, GetTestProjectFileDirectoryOnly()
, testProjectFile.Name.Replace(".csproj", string.Empty).Replace(".vbproj", string.Empty)
);
return code;
}
/// <summary>
/// Generates the test section but just reports in NAnt that tests are not run.
/// </summary>
/// <returns></returns>
public string GenerateTestsNotRun(string reason)
{
string code =
string.Format(
@"
<target name=""test"">
<echo message=""*******************************************************""/>
<echo message="" {0} *""/>
<echo message="" *""/>
<echo message="" !!TESTS NOT RUN!! *""/>
<echo message=""*******************************************************""/>
</target>",
reason);
return code;
}
/// <summary>
/// Gets the list of comma separated dependencies.
/// </summary>
/// <example>For Generators:
/// <p>'Common, Descriptors'</p>
/// </example>
/// <returns>string containing comma separated list of the projects dependencies.</returns>
public string GetCommaSeparatedDependencies()
{
var dependencies = new StringBuilder();
try
{
foreach (VisualStudioProjectFile referencedAssembly in VisualStudioProjectFile.ReferencedProjects)
{
if (!IsAnExcludedAssembly(referencedAssembly.ProjectAssemblyName))
dependencies.Append(string.Format(@", {0}", referencedAssembly.ProjectName));
}
}
catch (Exception ex)
{
dependencies.Append(string.Format(" An Exception occured trying to get dependencies [{0}]",
ex.Message));
}
return Strings.RemoveFirstInstanceOf(",", dependencies.ToString()).Trim();
}
/// <summary>
/// Determines if the assembly is in the list of assemblies that are not to be
/// included in the build file dependency list.
/// </summary>
/// <remarks>This is important because we don't want to include system and third
/// party assemblies in our dependency list for build files (we only want to know
/// which of OUR assemblies must be built first!)</remarks>
/// <param name="assemblyName">Check if this assembly is to be excluded.</param>
/// <returns>Returns true if it is to be excluded.</returns>
public bool IsAnExcludedAssembly(string assemblyName)
{
foreach (DataRow excluded in GeneratorsData.GetDataRowsFor("BuildFileGeneratorExcludedDependencies"))
{
string excludeAssembliesStartingWith = excluded["StartingWith"].ToString();
if (assemblyName.StartsWith(excludeAssembliesStartingWith))
return true;
}
return false;
}
#region Overrides
/// <summary>
/// Gets the string that is used as the project dependency 'heading'.
/// </summary>
/// <remarks>Declared as public static because the MasterBuildFileGenerator accesses the value.
/// Don't normally like to do this but that generator uses BuildFile generated by this
/// class to do it's generation work. However if it needs to get any other info, better
/// to make a Base BuildFileGen class...</remarks>
public static string ProjectDependencyString = "This Project is dependent on:";
/// <summary>
/// Gets the base directory for file generation.
/// </summary>
/// <remarks>i.e. The base directory that the file will be generated in.</remarks>
public override DirectoryInfo FileGenerationDirectory
{
get { return ProjectFile.FileInfo.Directory; }
}
/// <summary>
/// Gets the additional information that is required to save the generated file
/// in the correct directory.
/// e.g. "Northwind\SQL\StoredProcs"
/// </summary>
/// <remarks>i.e. The base directory that the file will be generated in.</remarks>
protected override string FileGenerationDirectoryDetails
{
get { return "NOT USED FOR BUILD FILE GENERATOR, FOR THIS WE JUST USE THE DIR OF THE SLN FILE."; }
}
/// <summary>
/// Gets the description of the purpose of the file.
/// </summary>
protected override string FilePurpose
{
get
{
return string.Format(@"{0} Build File
Compiles and tests the project in Release mode.
{1}
{2}"
, FileName
, ProjectDependencyString
, GetCommaSeparatedDependencies());
}
}
/// <summary>
/// Gets a string containing additional notes about the files contents.
/// </summary>
protected override string FileNotes
{
get { return string.Empty; }
}
/// <summary>
/// Gets the file extension of the file that is to be created.
/// </summary>
/// <example>.sql</example>
public override string FileExtension
{
get { return ".build"; }
}
/// <summary>
/// Gets the file name of the file that is to be created, not including the file extension.
/// </summary>
/// <example>fileThatIsBeingGenerated</example>
public override string FileName
{
get { return ProjectFileInfo.Name.Replace(ProjectFileInfo.Extension, ""); }
}
/// <summary>
/// Overrides the GeneratedStamp to put the comment tags in front.
/// </summary>
public override string GeneratedStamp
{
get { return "<!-- " + base.GeneratedStamp + " -->"; }
}
/// <summary>
/// Generate all of the code.
/// </summary>
/// <returns></returns>
public override string Generate()
{
try
{
string baseCode = base.Generate();
var generatedCode = new StringBuilder(baseCode);
generatedCode.Append(GenerateBuildFileCode());
Debug.WriteLine(string.Format(@"Generated build file for {0}", VisualStudioProjectFile.ProjectName));
return generatedCode.ToString();
}
catch (Exception ex)
{
Debug.WriteLine(ex.Message);
return new StringBuilder(
string.Format("<!-- An exception occurred whilst generating:- \r\n \r\n{0} -->", ex.Message)).
ToString();
}
}
/// <summary>
/// Generates the comments section for the file
/// </summary>
/// <returns></returns>
public override string GenerateFileHeaderComments()
{
return string.Format(@"
<!-- {0}
-->
"
, Strings.SplitMultiLineString(FilePurpose, " "));
}
/// <summary>
/// Initialize all delegates for code generation.
/// i.e. set the strategy pattern handlers.
/// </summary>
protected override void InitializeDelegates()
{
// nothing to initialize for the build file generator.
}
#endregion
#region Old code that was used to get the project file from sln file details. Now just using proj file directly
// /// <summary>
// /// Gets the path of the project file from the solution file.
// /// NOTE: Assumes that the project and solution files have the same name
// /// (not including the extension) and the project is in the solution.
// /// </summary>
// /// <returns></returns>
// private string GetProjectFileFullPath()
// {
// string solutionText = Files.GetFileContents(ProjectFileInfo);
// string initialReference = string.Format(@"""{0}"","
// , ProjectFile.NameExcludingExtension);
// int initialReferenceIndex = solutionText.ToLower().IndexOf(initialReference.ToLower());
// int pathStartIndex = solutionText.ToLower().IndexOf(ProjectFile.NameExcludingExtension.ToLower()
// , initialReferenceIndex + initialReference.Length);
// int pathEndIndex = solutionText.IndexOf(@"""", pathStartIndex);
// string projectFileSubPath = solutionText.Substring(pathStartIndex, pathEndIndex - pathStartIndex);
// return Path.Combine(ProjectFileInfo.DirectoryName, projectFileSubPath);
//// FileInfo file = new FileInfo(Path.Combine(ProjectFileInfo.DirectoryName, projectFileSubPath));
//// return AgileFileInfo.Build(file);
// }
// /// <summary>
// /// Gets the path of the project file from the solution file.
// /// NOTE: Assumes that the project and solution files have the same name
// /// (not including the extension) and the project is in the solution.
// /// </summary>
// /// <returns></returns>
// private string GetProjectFileDirectory()
// {
// return
// Strings.RemoveFirstInstanceOf(ProjectFile.NameExcludingExtension + ".csproj", GetProjectFileFullPath());
// }
#endregion
/// <summary>
/// Generate the copy configs section for projects that have config files.
/// </summary>
/// <returns></returns>
[Obsolete]
private string GenerateCopyConfigs()
{
// if (!VisualStudioProjectFile.NeedsConfigFilesCopied)
return string.Empty;
//
// return
// @"
// <echo message=""
//*** Copying Config files...from: ${directory::get-parent-directory(GrandParent)}\Build\Configs"" />
// <copy verbose=""true"" todir=""${RunDirectory}"" overwrite=""true"" failonerror=""true"">
// <fileset basedir=""${directory::get-parent-directory(GrandParent)}\Build\Configs"">
// <include name=""*configuration.config""/>
// </fileset>
// </copy>
//";
}
}
}