Rakefiles for Delphi


It’s not a secret that I’m a big advocate of Continuous Delivery and I’ve also found that I’ve been using a lot of Ruby and Python development workflow tooling recently because I like those languages in this setting and I find the tools to be relatively easy to use (I recently wrote about Fabric for push deployments).
I wanted a build tool to run the builds of my multi-platform targeted Delphi XE2 application because it was getting tedious to publish. As with any Continuous Delivery pipeline, the first step is to map out the steps involved in getting the application to the customer. For this particular application, there were several steps:

Build the application in Release configuration for each platform (Win32 and MacOS32)
Compile a zip file for each platform
Create a manifest XML file with the version for auto-update purposes
Upload the applications binary zip files to Amazon S3
Upload the manifest to Amazon S3

Rake is a fairly mature Ruby based build tool which Martin Fowler has written about in the past. There were a couple of articles about using Rake with Delphi, one from Ed Vander Hoek and another from Shawn Oster but they were both a little out of date for my purposes. So I thought I’d cover enough Rake to get you started with a Delphi Rakefile. The msbuild driven versions of Delphi make building your Delphi project considerably simpler than trying to wrangle the appropriate dcc32 flags.
If you’re already familiar with Ruby or Python then you will find building Rakefiles much easier. If you’re not familiar with ruby, I’d suggest RubyMonk.
To start with, we define a function which will run msbuild after running rsvars.bat to setup the environment:

def release_project(project_file, platform)
buildcmd = "msbuild #{project_file} /t:Rebuild /p:Config=Release /p:Platform=#{platform}";
result = `"#{RAD_STUDIO_BIN_PATH}\rsvars.bat"&#{buildcmd_win}`
if result.include? ‘Build succeeded.’
puts "Successfully built Windows 32 project #{project_file}"
puts result
puts "Our build failed, there were build errors!"
raise "Errors during Windows build"

We can then call put this into a simple rake task to build the Win32 version like this:

desc "Builds MyProject.dproj for Win32 in release configuration"
task :build_release do
puts ‘Building Project MyProject.dproj..’
release_project(‘MyProject.dproj’, ‘Win32’)

We can then call this via:
> rake build_release
We can also list the tasks that rake has via:
> rake -T
rake build_release # Builds MyProject.dproj for Win32 in release configuration

We can adapt this method to enable us to run any DUnit tests and code coverage tests.
In order to find the version of our executable, we need to call GetFileVersionInfo on the Windows API. Luckily, there was a StackOverflow question on calling GetFileVersionInfo with Ruby. This gives us something like this:

def get_version(artefact_path)
require "Win32API"
vsize=Win32API.new(‘version.dll’, ‘GetFileVersionInfoSize’,
[‘P’, ‘P’], ‘L’).call(artefact_path, s)
if (vsize > 0)
result = ‘ ‘*vsize
Win32API.new(‘version.dll’, ‘GetFileVersionInfo’,
[‘P’, ‘L’, ‘L’, ‘P’], ‘L’).call(artefact_path, 0, vsize, result)
rstring = result.unpack(‘v*’).map{|s| s.chr if s<256}*”
r = /FileVersion..(.*?)\000/.match(rstring)
"#{r ? r[1] : ‘??’ }"
raise "GetFileVersionInfoSize returned 0 for #{artefact_path}"

We can create our zip file in two different ways, we can either use a zip gem for Ruby native creation or we can shell out to a command line version. This function uses the zip gem and takes an array of files to zip and a name for the zip file.

def make_zip_distro(files_to_zip, zip_name)
require ‘zip/zip’

Zip::ZipFile.open(zip_name, Zip::ZipFile::CREATE) { |zipfile|
files_to_zip.each { |file_to_zip|
if File.exists?(file_to_zip)
zipfile.get_output_stream(File.basename(file_to_zip)) { |f|
File.open(file_to_zip) do |in_file|
while blk = in_file.read(1024**2)
f << blk
raise "Could not find #{file_to_zip}"

For the manifest file, we can either generate it using Ruby string interpolation or a template system like erb, depending on how complex your requirements are.
The final step is the upload all artefacts (the manifest, the artefacts):

def upload_to_s3(file, s3_destination)
require "aws/s3"
if File.exists?(file)
:access_key_id => AWS_ACCESS_KEY_ID,
:secret_access_key => AWS_SECRET_ACCESS_KEY
:access => :public_read)
puts "Uploaded #{file} to #{s3_destination}"
puts "Could not file #{file} to upload to S3"

You will need to define the access keys, secret keys, endpoints and bucket.
The next step is to put it into composite parts and draw up a set of tasks. Rake tasks can be standalone or can have prerequisite dependencies:

task :my_task => [:dependent_task_1, :dependent_task_2] do
# task implementation

Rake tasks can also have parallel prerequisite dependencies like this:

multitask :my_task => [:build_task1, :build_task2] do
puts "Built all the things!"

Rake tasks can also have parameters, this might be useful if you wanted to pass in a version number manually at build or publish time.
Back to our project pipeline from earlier, here are a selection of tasks that wrap everything together:

desc "Builds the Win32 release of MyProject.dproj"
task :build_win_release do
puts ‘Building Project MyProject.dproj for Windows’
release_project(‘MyProject.dproj’, ‘Win32’)

desc "Builds the Mac OS X release of MyProject.dproj"
task :build_mac_release do
puts ‘Building Project MyProject.dproj for Mac’
release_project(‘MyProject.dproj’, ‘MacOS32’)

desc "Writes the manifest out to manifest.xml in the current working dir"
task :write_manifest => [:build_win_release] do
puts ‘Writing the update manifest’
v = get_version(get_executable_path(:Win32))
write_update_manifest_to(File.join(get_cwd(), ‘manifest.xml’), v)

desc "Uploads the manifest to Amazon S3"
task :upload_manifest => [:write_manifest] do
puts ‘Uploading the manifest..’
upload_to_s3(File.join(get_cwd(), ‘manifest.xml’), ‘myproject/manifests/manifest.xml’)

desc "Builds the Windows zip distributable"
task :make_win_zip => [:build_win_release] do
exe = get_executable_path(:Win32)
v = get_version(exe)
zip_name = ‘MyProject-win-‘ + v + ‘.zip’
make_zip_distro([exe], zip_name)
puts "Create zip: " + zip_name

desc "Builds the Mac zip distributable"
task :make_mac_zip => [:build_mac_release] do
exe = get_executable_path(:MacOS32)
# Call get_version on the Win32 executable. If the Win and Mac OS X version numbers
# differ, you will need to extract the version from the generated Info.plist instead.
v = get_version(get_executable_path(:Win32))
zip_name = ‘MyProject-mac-‘ + v + ‘.zip’
make_zip_distro([exe], zip_name)
puts "Create zip: " + zip_name

desc "Uploads all of the zip files"
task :upload_zips => [:make_win_zip, :make_mac_zip] do
v = get_version(get_executable_path(:Win32))
win_name = ‘MyProject-win-‘ + v + ‘.zip’
mac_name = ‘MyProject-mac-‘ + v + ‘.zip’
puts ‘Uploading the win zip version .. ‘ + v
upload_to_s3(win_name, ‘myproject/downloads/’ + win_name)
puts ‘Uploading the mac zip version .. ‘ + v
upload_to_s3(mac_name, ‘myproject/downloads/’ + mac_name)

desc "Builds, zips and uploads the artefacts and manifests"
task :release_all => [:upload_zips, :upload_manifest]

You can build your releases, create the manifest and upload all with:
> rake release_all
Simple, repeatable and easy to extend when you have more steps to your pipeline. Further steps for your project might be adding in your acceptance tests or automatically generating and publishing some release notes from your git logs.
Other Links

Using Rake – An introduction to Rake by Thoughtworker Martin Fowler.
Rake RDocs – The official rake documentation.
Using rake to automate tasks – A pretty comprehensive introduction to rake tasks.
Building Delphi with Ruby – the EDN article from Ed Vander Hoek, including a generic framework for Delphi Rakefiles but which is limited in version support.
A Simple Delphi Rakefile – A straightforward rake file for Delphi apps from Shawn Oster.

Comments are closed.