[Estimated Reading Time: 5 minutes]

A comment from Kevin P brought a build automation tool to my attention this evening, called Train.

Train is a JavaScript based build automation tool from a little company called RemObjects. It is written in Oxygene but the API provides specific support for Delphi. It’s an open source project and free (as in beer).

So I decided to check it out.

After cloning the git repo containing the source, getting up and running was simplicity itself, following some simple instructions in the wiki of the repo itself.

It helped that I had Oxygene sitting ready to build the solution, but only a little. I am sure the process is just as smooth with the free command line Oxygene compiler (or the Visual Studio trial).

At this stage I just wanted to see how easy it was going to be to get a simple script up and running to build one of my test suites using, say, three different versions of Delphi.

It turned out to be surprisingly and delightfully simple.

The Train API (also documented in the wiki) describe two methods for working with Delphi. One returns the base path of the installation for a specified Delphi version.

The other invokes a Delphi build and is the one I am more interested with right now:

  delphi.build( "project.dpr", {options object});

The project.dpr parameter obviously identifies the project to be built.

The options object is a lot more fun, containing everything we need to identify the version of the Delphi compiler to be used and the settings to be passed to that compiler.

Obviously, the command line compiler is oblivious to all my carefully crafted IDE environment variables and settings. Train does not currently – as far as I can tell – provide an option or a mechanism to retrieve compiler settings such as library path etc from IDE entries in the registry.

So I need to set some basic things up in my script first, to take the place of these settings. First, some variables to stand in for my IDE environment variables:

SRC     = "\\\\psf\\home\\dropbox\\dev\\src\\delphi\\";
DELTICS = SRC + "libs\\";
FASTMM  = SRC + "vendor\\fastmm\\4.99.1";

Being JavaScript, path delimiters need to be escaped of course.

Now for the compiler options object. Again, being JavaScript I simply declare an object with some suitable initial properties:

CFG = {
         platform             : "win32",
         conditionalDefines   : ["FullDebugMode", "CONSOLE"],
         includeSearchPath    : DELTICS + "+ inc",
         unitSearchPath       : FASTMM + ";"
                              + DELTICS + "rtl;"
                              + DELTICS + "smoketest;"
                              + DELTICS + "vcl",
         dcuDestinationFolder : "c:\\dev\\dcu\\~train",
         destinationFolder    : "c:\\dev\\bin\\train"
      };

This isn’t a fully specified Delphi build object, but is (almost) enough to get me started.

This configuration reproduces the basic set of compiler settings applied to one of my test projects. In fact, the test project that tests my test framework itself (smoketest).

The DCU and output folders are going to be specific to my Train build process. As I flesh this script out I shall clean out the dcu\~train folder after building each project and before building the next so I don’t need to be concerned with platform or selected configurations ($(platform) / $(config)).

One last bit of preparation is required.

I declare a variable that identifies the filename of the project I am going to build. Or most of it at least:

DPR = DELTICS + "~ tests\\smoketest\\projects\\selftest";

There is a Delphi version specific component in the project name which I will append as required before making any changes that may also be required for initiating a build with that Delphi compiler version.

Which brings us to the first actual build – for Delphi 7:

CFG.delphi = "7";
delphi.build(DPR + ".d7.dpr", CFG);

The “delphi” property in the CFG object identifies the Delphi version that we wish to use to build the project, and I complete the project name passed to the build method accordingly, along with the CFG object.

That’s it.

To build the Delphi 2006 version of the same project (not literally the same project – there is that version specific suffix) I can re-use the CFG object, simply changing the “delphi” property to the required version:

CFG.delphi = "2006";
delphi.build(DPR + ".d2006.dpr", CFG);

And for one final flourish, let’s build for the absolute latest version of Delphi, XE5.

For later versions of Delphi we must complete another part of the CFG object, the “namespaces” list and due to a small bug in the delphi.build() method of the Train API (the benefit of open source – I can already see what the bug is and both how to avoid it and fix it at some point), we must specify the Delphi version using the internal Delphi version number (19) rather than the branded version on the box (XE5):

CFG.namespaces = "System;Xml;Data;Datasnap;Web;Soap;Winapi;System.Win;Vcl;$(DCC_Namespace)";

CFG.delphi = "19";
delphi.build(DPR + ".XE5.dpr", CFG);

So, just to bring the whole thing together, my initial thrown together build script looks like this:

SRC     = "\\\\psf\\home\\dropbox\\dev\\src\\delphi\\";
DELTICS = SRC + "libs\\";
FASTMM  = SRC + "vendor\\fastmm\\4.99.1";

CFG = {
         platform : "win32",
         conditionalDefines   : ["FullDebugMode", "CONSOLE"],
         includeSearchPath    : DELTICS + "+ inc",
	 unitSearchPath       : FASTMM + ";" + DELTICS + "rtl;" + DELTICS + "smoketest;" + DELTICS + "vcl",
	 dcuDestinationFolder : "c:\\dev\\dcu\\~train",
	 destinationFolder    : "c:\\dev\\bin\\train"
      };

DPR = DELTICS + "~ tests\\smoketest\\projects\\selftest";

CFG.delphi = "7";
delphi.build(DPR + ".d7.dpr", CFG);

CFG.delphi = "2006";
delphi.build(DPR + ".d2006.dpr", CFG);


// Additional configuration for Namespace support:

CFG.namespaces = "System;Xml;Data;Datasnap;Web;Soap;Winapi;System.Win;Vcl";

CFG.delphi = "19";
delphi.build(DPR + ".XE5.dpr", CFG);

Some utility/wrapper functions will make writing scripts even more straightforward.

I still need to implement the cleanup of the Train build products folders and I still need to look into the capabilities around error handling and responding appropriately to build failures. There is some way to go before this could be called a usable build script, but actually not that far. It’s an impressive start given the amount of time (and money) I was required to invest to get to this point (very little and zero – if you don’t count Delphi itself – respectively).

Error handling and logging is an area where tools like FinalBuilder clearly have a great deal more to offer, but having said that, if a build fails then the build fails. I use FinalBuilder in my day job and it’s great, but actually, on those rare occasions when there is a build failure sometimes the log feels like an awful lot of noise that has to be waded through to get to the nub of the problem.

Which is why our build process captures and emails me the compiler logs separately from the build log itself. 9 times out of 10 those compiler logs are all I need, no matter how pretty the full, HTML formatted FinalBuilder log may be.

Having lots of great logging facilities is all well and good, but as long as the facilities that are provided are enough to identify the problem, the rest is not actually adding very much in terms of value. The output from Train (bearing in mind that I have not as yet spent any time investigating what control over things or options I may have in this area) is certainly functional enough on that score:

Computer Says No
Computer Says No

The API in Train is also relatively sparse (though not necessarily limited) but, again, if it meets my needs then I am not too concerned at the length of the feature list. And, being open source, I can always consider contributing anything that I feel is missing and would be worthwhile.

7 thoughts on “Build Automation With Train”

    1. This may be possible but it is not desirable.

      A scripted build is not produced for the same reasons as a build produced within the IDE. Having script specific project settings is positively desirable. Being able to load global settings would be a minor additional benefit but since I try as far as possible to not be dependent upon such settings, it’s no big deal.

      And in any event, I certainly wouldn’t want my build scripts to be bogged down by having to load up every single IDE in it’s entirety just to do a build for each version (remember my scripts will be producing builds for every version of Delphi since 7).

  1. Instead of escaping path names, have you tried using *nix style path delimiters? e.g. SRC= “//psf/home/dropbox/dev/src/delphi/”;

    I find that Later version of windows do recognize this style of delimiter.

    Also, I believe there is a blog article or two on RO’s website about Train that may be useful.

    1. Windows might recognise them but some of those paths end up being passed through to the compilers and I’m not so sure that the earlier versions of Delphi will be so flexible. Besides, strictly speaking “/” should be escaped in JavaScript as well, so that path would be \/\/psf\/home\/dropbox\/dev\/src\/delphi'

      I already had to change the names of some of my folders to accommodate Delphi 7’s inability to deal with Unicode filenames.

  2. At least for the newer versions of Delphi, since the switch to Galileo, Delphi itself uses MSBuild internally.

    Personally, I don’t see a benefit in encapsulating MSBuild (in itself extremely powerful, extensible and very mature) with another build tooling. Especially not, when most CI environments do work pretty fine with MSBuild and, on the other hand, don’t know anything of Train.

    That of course is a different thing, when you start to integrate other platforms like builds for OS X (MSBuild, or better the mono counterpart XBuild on OS X still has some major drawbacks), but when solely talking about Delphi on Windows, I don’t see a reason to introduce jet another layer of complexity to your build process.

    1. Key point: newer versions of Delphi.

      I need to support Delphi 7, 2006 and 2007 which do not use MSBuild. As can be seen from my build script so far, supporting these older versions is not exactly difficult, and treating all versions the same way adds nothing in the way of complexity.

      Not having looked at the impact of the use of MSBuild outside of the IDE, would it actually be any easier ? Or just different ? Note that I specifically do not want all of the same settings applied in the IDE to my scripted builds.

      Of course, should I ever wish to incorporate MS Build for those projects that do support it Train has API support for that as well. 🙂

      Second point: I’m not looking to incorporate Train into some other CI environment. Train will itself be my CI environment (or at least the core component). 🙂

      1. FWIW, We also have a proper distributed CI infrastructure that goes around Train. One of these days, i’ll get around to cleaning, abstracting and open-sourcing this as well. 😉

Comments are closed.