Any software development project is always associated with the automation of related routine tasks. Initially, IDE and a pair of manual operations will be enough for you. Then, the number of body movements begins to grow: you need to perform multiple sets of tests, embed various certificates, execute scripts in the database, generate documentation on the code, and so on. You also need to perform these and other operations on the Continuous Integration server. In addition, you may need to deploy applications on production servers (if we’re talking about a client-server solution). To automate such tasks, programmers sometimes create sets of batch or shell scripts, but more often, the team of developers comes to some consolidated decision.
For every single platform, there is a well-established set of automation tools that are quite diverse, and each set has its own syntax and philosophy. However, they use configuration files in one form or another in order to describe the tasks, relationships between them and order of execution. In terms of the language that is used to describe the tasks, such tools can be divided into declarative, which describe the necessary actions (just like the rules in XML), and imperative that describe the task as a fragment of code being developed in accordance with specific agreements.
At first glance, the declarative approach is more advantageous as it allows you to achieve the desired results by describing the task using a relatively simple syntax (XML or other). On the other hand, sooner or later you will face restrictions of the implementation of the tool itself (for example, if you need to copy/delete files according to some complex mask). Besides, if you need to implement a complex workflow (for example, depending on the version of the application and the platform for which the proper configuration files and scripts must be provided), then you will have to write your own extensions, which, in turn, also need technical support, testing and so on. It turns out that you have to write a code in any case, so why not to implement the necessary actions in the code itself, and in a comfortable format?
And today, I’ll tell you about one of these tools. Rake is widely used in the famous Ruby on Rails web framework and other Ruby projects. In addition, I am quite successful in combining it with other technologies such as .NET, and such integration will be our example today.
Rake is a build automation tool that was written in Ruby. This is an open source project, now-deceased Jim Weirich was its author. His purpose was to create a tool similar to the popular Make, Ant and MSBuild, but the new tool had to be simpler and more flexible. The author focused on the following features of Rake:
- Simple Ruby-based DSL allows you to use the full power of the language without complex XML or other configuration files.
- Parallel execution of multiple tasks.
- Rule templates allowing to generate implicit tasks.
- A library of typical tasks.
- Possibility to specify the initial conditions for tasks.
Installation
Rake is available as a library for Ruby. Ruby 1.9 (and more recent versions) already has embedded Rake. If you use Ruby 1.8 or there is a need to install a more recent version of Rake, you can run the following command in the package management system RubyGems:
gem install rake
Tasks
The configuration file in Rake is called Rakefile. Tasks are its main “building blocks.” Each task has a name, a set of initial conditions and a list of actions to be performed.
task name: [:prereq1, :prereq2]
Actions are transmitted to a structure which is called “block” in Ruby.
task name: [:prereq1, :prereq2] do |t| # actions end
Tasks in Rake can be divided into two types: conventional and file. Conventional task is a set of actions to be performed. Such tasks are declared using the “task” method.
A file task is assigned in the form of a file that is usually created on the basis of one or more existing ones. If such a file already exists, the corresponding task is not executed and is skipped. File tasks are declared using the “file” method.
The name of the task to be performed is sent to the “rake” tool as a parameter.
rake task_name
If you execute “rake” without parameters, it will try to find a task named “default” in the Rakefile. If the “default” task is not found, “rake” will generate an error message.
>rake rake aborted! Don’t know how to build task 'default'
Comments
Although Rake allows you to use Ruby-like comments prefixed with #, it is recommended to use the keyword “desc” to comment your tasks.
In this case, the “rake -T” command will help us get the list of tasks along with descriptions. Well thought-out descriptions eliminate the need to document the Rake scripts.
Namespaces
Rake offers the concept of namespaces to group the tasks. For example, anyone who has written code in Ruby on Rails previously, knows the “rake db: migrate” command; in this case, “db” is the namespace, and “migrate” is a task that belongs to it. A namespace is declared using the “namespace” keyword.
namespace :namespace_name do # tasks end
Managing the Rake files
The “Rake” utility is searching for tasks in the “Rakefile” file. This file is growing very fast, so there is a need to split it into several files according to their functions. To do this, the following agreement should be used: create “rakelib” directory and “.rake” files in it. The main Rakefile may contain links to all the tasks and namespaces declared in other Rake files.
Building a .NET project with the help of Rake
Not surprisingly, Rake is most commonly used in Ruby/Rails projects. Nevertheless, we can freely use it in other languages. Let’s take a simple “Hello world” build in C# as an example. It is advisable to use Rake in .NET projects, especially when it is necessary to implement complex logic to build/test/deploy an application, as it is quite non-trivial, if XML declarations of NAnt or MSBuild are used.
The source will be a file with a simple code and a project file (in order to use MSBuild). The code is too simple, but the approach itself is the same as that when assembling large-scale solutions.
using System; public class HelloWorld { static void Main() { Console.WriteLine("Hello world!"); Console.ReadLine(); } }
The project file would look something like this:
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <ItemGroup> <Compile Include="hello.cs" /> </ItemGroup> <Target Name="Build"> <Csc Sources="@(Compile)"/> </Target> </Project>
Since the Rake code is rather simple, I’ll give comments later.
require "fileutils" task :default => [:clean, :build, :pkg] msbuild = "#{ENV['WINDIR']}\\Microsoft.NET\\Framework\\v3.5\\msbuild.exe" proj_root = File.dirname(__FILE__) out_dir = "#{proj_root}/out" desc "Clean the artefacts from previous build" task :clean do rm Dir.glob('*.exe') rm_rf(out_dir) if Dir.exists?(out_dir) end desc "Compile project with MSBuild" task :build do mkdir_p(out_dir) if !Dir.exists?(out_dir) project = "#{proj_root}/hello.proj" cmd = "\"#{msbuild}\" #{project}" sh cmd do |ok, res| raise "*** BUILD FAILED! ***" if !ok end end desc "Prepare deploy package" task :pkg do artefacts = ["#{proj_root}/hello.exe", "#{proj_root}/readme.txt"] cp_r(artefacts, out_dir) end
The following build stages are defined in our project:
- Clean — artifacts of the previous build are deleted at this stage. In this case, all the “*.exe” files and the “out” directory are deleted.
- Build — everything associated with compilation of the application. In this case, MSBuild gets the path to the .proj file as a parameter, and we execute this command.
- Package — the .exe file that has been built is moved to the “out” directory, a “Read Me” is also copied there.
The “default” task does nothing, but it depends on the rest of the declared tasks. Thus, “rake” without parameters will execute the “clean,” “build,” and “package” tasks in sequence. It should also be mentioned that Rakefile, as any conventional Ruby script, allows to use the “require” operator in order to upload and use any libraries of this language.
Albacore
I’m not the only one who wants to use Rake to build .NET projects. To do this, we can use quite a popular project called Albacore, which extends DSL Rake by adding special keywords to automate common tasks faced by developers of software for the Microsoft platform.
Set by the following command:
gem install albacore
Let’s look at a build task declared using Albacore:
require "albacore" desc "Compile project with MSBuild using Albacore" build :alba_build do |b| b.file = "#{proj_root}/hello.proj" end
The code has become much neater, hasn’t it? For detailed information on Albacore, please see the project’s Wiki on GitHub.
Conclusion
Rake is a great, flexible tool for creating various scripts to build and maintain your projects. Its use is not limited by the Ruby ecosystem, and due to this article, it’s obvious that it can be used even in the .NET infrastructure.
But of course, it’s inappropriate to use Ruby in small projects that can be implemented with the help of the tools provided by the platform itself. However, Rake can save a lot of time and minimize the amount of code in complex projects, particularly those that include support for legacy systems, when you have to deal with complex dependencies, prepare test environments (for example, in order to register COM objects in the system according to a set of specific conditions), etc.