Wednesday, September 26, 2007

Using Rake to Automate Windows Desktop App Builds

We've recently talked about compiling your Ruby app with RubyScript2Exe, and creating an install package with Inno Setup.

I was inspired by RoW reader Luis Lebron, who recently shared with me his Rakefile for automating the running of RubyScript2Exe from within the NetBeans Ruby IDE.

My process for packaging an application for distribution involves, among other things:


  • Compiling the code with RubyScript2Exe
  • Replacing the default EXE icon with my own (using Resource Hacker)
  • Moving the EXE file to my Install folder
  • Updating the ReadMe file with the new version number
  • Updating the Inno Setup script with the new version number
  • Running the Inno Setup script to create a new Setup.exe file

This isn't a particularly time-consuming list of manual tasks. But I spend my workdays developing tools to automate my fellow employees' manual tasks. So it was inevitable that I would seek to do the same for myself.

I had no prior experience with Rake, a DSL created by Jim Weirich to automate project builds. I'm neither a C programmer nor a web developer, so I wasn't really motivated to investigate what Rake might have to offer. But after receiving Luis' email, I looked into using Rake, and then researched the options for running Resource Hacker and the Inno Setup compiler from the command line.

I still know almost nothing about Rake, but I learned enough to create a simple Rakefile to automate all of the above tasks. This could, of course, be done in Ruby without the using Rake, but NetBeans' Rake integration and templates make it handy. Now I can right-click on my project icon in NetBeans, select Run Rake Task => create_setup, and Rake takes care of the rest.

In case you're interested, here's an example of my Rakefile. It can no doubt be improved upon, but should give you an example to start from (Beware of line-wrap):

require 'rake'
require 'fileutils'
include FileUtils

# set constant values:
LIB_FOLDER = File.expand_path('../lib')
INSTALL_FOLDER = File.expand_path('../install')
ISCC = "C:/Program Files/Inno Setup 5/iscc.exe"
RESHACKER = "C:/Program Files/ResHacker/ResHacker.exe"
ISS_FILE = "#{INSTALL_FOLDER}/Setup.iss"
README_FILE = "#{INSTALL_FOLDER}/ReadMe.txt"

# extract values from main.rb file:
main_rb = open('../lib/main.rb').read
APP_TITLE = main_rb.scan(/APP_TITLE = '(.+)'/)[0][0]
EXE_NAME = main_rb.scan(/EXE_NAME = '(.+)'/)[0][0]
EXE_BASENAME = EXE_NAME.gsub('.exe', '')
APP_VERSION = main_rb.scan(/APP_VERSION = '(.+)'/)[0][0]

# rake tasks:
task :default => [:create_setup]

desc "Create setup.exe"
task :create_setup => [:move_exe, :modify_icon, :create_iss_script,
:edit_readme] do
puts "Creating setup.exe"
Dir.chdir(INSTALL_FOLDER)
system(ISCC, ISS_FILE)
end

desc "Create ISS script"
task :create_iss_script => [:move_exe] do
puts "Creating ISS script"
Dir.chdir(INSTALL_FOLDER)
data = ISS_TEXT.gsub('[APP_TITLE]', APP_TITLE)
.gsub('[APP_VERSION]', APP_VERSION)
.gsub('[EXE_NAME]', EXE_NAME)
.gsub('[EXE_BASENAME]', EXE_BASENAME)
File.open(ISS_FILE, 'w') do |f|
f.puts(data)
end
end

desc "Edit ReadMe.txt"
task :edit_readme do
puts "Updating ReadMe.txt file"
Dir.chdir(INSTALL_FOLDER)
txt = nil
open(README_FILE) do |f|
txt = f.read
end
old_version = txt.scan(/Version (\d\d\.\d\d\.\d\d)/)[0][0]
txt = txt.gsub(old_version, APP_VERSION)
File.delete(README_FILE)
open(README_FILE, 'w') do |f|
f.puts(txt)
end
end

desc "Modify EXE icon"
task :modify_icon => [:move_exe] do
puts "Modifying EXE icon"
Dir.chdir (INSTALL_FOLDER)
arg = " -addoverwrite #{EXE_NAME}, #{EXE_NAME}, application.ico,
icongroup, appicon, 0"
system(RESHACKER + arg)
end

desc "Move EXE to install folder"
task :move_exe => [:compile_code] do
puts "Moving EXE to install folder"
mv("#{LIB_FOLDER}/main.exe", "#{INSTALL_FOLDER}/#{EXE_NAME}")
end

desc "Compile code into EXE"
task :compile_code do
puts "Compiling main.rb into EXE"
system("rubyscript2exe.cmd", "#{LIB_FOLDER}/main.rb")
end

# text of Inno Setup script:
ISS_TEXT =<<-END_OF_ISS

[Setup]
AppName=[APP_TITLE]
AppVerName=[APP_TITLE] version [APP_VERSION]
AppPublisher=David L. Mullet
AppPublisherURL=http://davidmulletcom
AppContact= david.mullet@gmail.com
AppVersion=[APP_VERSION]
DefaultDirName=C:\\[APP_TITLE]
DefaultGroupName=[APP_TITLE]
UninstallDisplayIcon={app}\\[EXE_NAME]
Compression=lzma
SolidCompression=yes
OutputDir=.
OutputBaseFilename="[EXE_BASENAME]_Setup"

[Files]
Source: "[EXE_NAME]"; DestDir: "{app}"; Flags: ignoreversion
Source: "Readme.txt"; DestDir: "{app}"; Flags: isreadme ignoreversion

[Icons]
Name: "{group}\\[APP_TITLE]"; Filename: "{app}\\[EXE_NAME]";
WorkingDir: "{app}"
Name: "{group}\\View ReadMe File"; Filename: "{app}\\ReadMe.txt";
WorkingDir: "{app}"

END_OF_ISS

As always, post a comment here or send me an email with questions, comments, or suggestions.

Thanks for stopping by!

Digg my article

2 comments:

Emmanuel Oga said...

Excelent!

PS: Have you considered using pastie.caboo.se for pasting your code? Also i highly recommend using wordpress instead of blogger for, mmm, blogging :). Try it! Everything from the ui to the syntax highliter support is better.

PS2: THIS HAS NOTHING TO DO WITH THE PREVIOUS PS OR YOUR POST, but, i need to ask you. Do you think that it would be posible to "paste" a file to a windows shared folder from ruby_on_linux without using samba or mounting my own ftp server (or even drb server) on the pc with the shared folder?

Thank you very much!

engtech said...

It seems overly convoluted with what you are doing with the ISS script.

This seems simpler:

task ISS_FILE do
File.open(ISS_FILE, 'w') do |f|
f.puts(ISS_TEXT)
end
end

# text of Inno Setup script:
ISS_TEXT =<<-END_OF_ISS

[Setup]
AppName=#{APP_TITLE}
AppVerName=#{APP_TITLE} version #{APP_VERSION}
...