This blog shows how to create a private build process using Rake and assumes you’ll be using some type of continuous integration (CI) build such as Azure DevOps pipelines. 

Rake is an Open Source project that uses a build script written in
Ruby. It is structured around tasks that are chained together through dependency relationships.

The script will usually have multiple, high-level tasks that allow the developer to run quick builds, clean builds for before committing, and ones for the CI system.

The basic steps for creating a Rake build

The following list are the basic steps to get building with Rake.  Each one is discussed below.

  1. Ensure the Solution builds in Visual Studio
  2. Install Ruby
  3. Install Rake
  4. Create Rakefile at the solution file level with the tasks such as the following:
    a.Init task for cleanup of artifacts
    b.Compile task to build the code
    c.Test task to run the unit tests
    d.Database task deploy the database if applicable

Ensure it Builds

Obviously you must be able to cleanly build your code.  The important word here is cleanly since the build pipeline will be building in a clean environment.  Forgetting to add a file to VCS is a common mistake that will let you successfully build locally, but will lose a cycle on the build.

Install Ruby

For Windows, there is a Ruby installer.  On *nix, you can use the appropriate package manager, which are on the same link.

Install Rake

Once Ruby is installed correctly, simply install Rake with gem.  On *nix you may have to use sudo.

gem install rake

Create the Rakefile

Usually the Rakefile is in the same folder as the solution to make finding and running it easy.  This file should be committed to VCS so it can be run by anyone, including Azure DevOps.  

Rakefileis a Ruby source file that is run by the rake gem.  It is comprised of multiple task methods that define dependencies, and can optionally do some action for each step of the build process.

Prolog

At the top of the file, you can load Ruby modules that you want to use.  For example to load file utilities.

require "fileutils"

Variables and Arguments

Also at the top you define variables that you can reference in of the methods, like a global.  To get arguments into the script, you can use args, or ENV['name'].  To use args in a task, the calling task, must also take args.  In the example below the init task needs the parameters, but since default has it as a dependency, it must have the parameters, too.

task :default,[:version,:runOctopack] => [:init,:test] do |task, args|
end
# set variables
version = "1.0.0.0"
runOctopack = false
user = ""

desc "get parameters into variables"
task :init,[:version,:runOctopack] do |task, args|
    args.with_defaults(:version => "1.0.0.0")
    args.with_defaults(:runOctopack => false)
    runOctopack = args.runOctopack
    version = args.version
    user = ENV['USER']

    puts "runoctopak is #{runOctopack}"
    puts "version is #{version}"
    puts "user is #{user}"

    # add cleanup code here
end

Helper Functions

You can write any helper functions you may want to include.  It’s just a Ruby file, so have at it.

Tasks

Each call to task takes the name of the task, any parameters, and a list of dependencies.  An optional do/end block does the processing. Tasks without processing high-level tasks just to setup dependencies.

At the top, you usually have a default task that will be executed without when no task is specified, and other high-level tasks to set up dependencies.

task :default,[:version,:runOctopack] => [:init,:compile,:test] do |task, args|
end

task :ci,[:version,:runOctopack] => [:init,:compile,:test,:database] do |task, args|
end

The init task does cleanup and setup needed to run the build.  An example was show above.

Other tasks do the compile, setup test database, run tests etc.  Below are a couple more examples.  Since any task can be executed when invoking rake, make sure that each one has appropriate dependencies.  For example test depends on compile that in turn depends on init.

desc "Compile project with MSBuild"
task :compile => [:init] 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 "Run unit tests"
task :test => [:build] do

Running the Build

At this point every developer can run a build and local tests to make sure that when they commit, it will compile and run in Azure DevOps.   rake is the command that actually does the work. If no arguments are specified for it, rake will build the task called default. Passing in a different target name will run that one.  In our example above, at the high level we have default and ci, each of which has different dependent tasks. 

To pass parameters into rake pass in positional parameters as shown below. Here we pass in the version and runOctopack positional parameters show in the example above.

$ rake default['1.3',true]

You will usually have a high-level task for devs to run for a quick build, and one for a build with tests that they should run before committing to VCS.  Then you may have others that are specific to an environment, with the idea of having as much common code run in the private build as runs in the build pipeline.

Debugging

Since it’s just running Ruby, you can use your favorite debugger.  In addition you can do a dry run, or turn on tracing.

rake —-dry-run default
rake -—trace default

Summary

It is virtually impossible to have the build environment in Azure DevOps match that of the developer, but using a shared build script can help avoid red builds and a developer scrambling to find that missing file or fix a bug they introduced to the unit test suite.  Cake is one way to solve this problem.  In other blog posts we looked at psake, which is the PowerShell cousin of Rake, and Cake, the C# relative. 

References