Михаил Овчинников

Основы автоматизации сборки при помощи Rake

Эта статья была впервые опубликована в журнале “Хакер”, здесь публикуется исходная версия, до отправки в редакцию.

С Rake знакомы многие кто писал веб-приложения на Ruby on Rails. Но этот инструмент для автоматизации сборки может оказаться весьма полезен и вне контекста популярного веб-фреймворка. Предлагаю разобраться подробнее с его базовыми принципами и подумать где еще он может пригодиться.

Введение

Разработка любого проекта постоянно связана с автоматизацией сопутствующих рутинных задач. Сначала хватает средств IDE + пары ручных операций. Затем количество телодвижений начинает расти: требуется выполнять несколько наборов тестов, подкладывать какие-нибудь сертификаты, прогонять скрипты на базе данных, генерировать документацию к коду и т.д. Также нужно выполнять эти и другие операции на Continuous Integration сервере, а возможно и осуществлять деплой приложения на продакшн-сервера (если говорить о каком-либо клиент-серверном решении). Иногда это автоматизируют при помощи набора самописных batch- или shell-скриптов, но чаще всего в командах разработчиков приходят к какому-то консолидированному решению.

Для каждой платформы существует уже устоявшийся набор инструментов автоматизации, они довольно разнообразны и каждый использует свой синтаксис и философию. Тем не менее в том или ином виде они используют некие конфигурационные файлы, в котором описаны задания, зависимости между ними и порядок выполнения. В зависимости от используемого языка описания такие инструменты можно условно разделить на декларативные, где, как правило на XML, описываются необходимые действия и императивные где задания реализуются в виде кода, который пишется с учетом некоторых соглашений.

На первый взгляд, декларативный подход выглядит более выгодным, так как позволяет добиться желаемых результатов описав задания в сравнительно несложном XML или ином синтаксисе. С другой стороны, рано или поздно упираешься в ограничения реализации самого инструмента (например, скопировать/удалить файлы по какой-нибудь сложной маске), плюс когда требуется реализовать сложный воркфлоу (к примеру, в зависимости от версии приложения и платформы под которую оно собирается подкладывать нужные конфиги и скрипты), приходится писать собственные экстеншены, которые в свою очередь приходится тоже мантейнить, тестировать и т.д. А раз написание кода все равно неизбежно, то почему бы не реализовать необходимые действия сразу в коде, причем в удобном формате?

Именно об одном из таких инструментов сегодня пойдет речь. Rake широко используется в известном веб-фреймворке Ruby on Rails и других Ruby-проектах, но у меня вполне удачно получалось его сочитать с другими технологиями, в частности с .NET, что мы рассмотрим сегодня в качестве примера.

Rake - это инструмент автоматизации сборки программного кода, написанный на Ruby. Это проект с открытым исходным кодом, автором которого являлся ныне покойный Джим Вайрих. Своей целью он ставил создание инструмента подобного распространенным Make, Ant и MSBuild, сделав его более простым и гибким. Сам автор выделяет следующие особенности Rake:

  • Простой DSL на основе Ruby позволяет использовать всю мощь языка и не требует составления сложных XML и других конфигурационных файлов;
  • Параллельное выполнение нескольких заданий;
  • Шаблоны правил для синтеза неявных заданий;
  • Библиотека готовых заданий, для выполнения типичных задач;
  • Возможность указывать начальные условия для заданий.

Установка

Rake распространяется в виде библиотеки для Ruby. Версии языка, начиная от 1.9 и выше, уже включают его в себя, не требуя дополнительной установки. Если используется Ruby версии 1.8, или есть необходимость установить более свежую версию Rake, чем идет в комплекте с языком, то можно воспользоваться системой управления пакетами RubyGems, выполнив команду:

gem install rake

Задания

Конфигурационный файл в контексте Rake называется Rakefile. Его основными “строительными блоками” являются задания (tasks). Задание обладает именем, набором начальных условий и списоком действий, которые должны быть выполнены.

task name: [:prereq1, :prereq2]

Действия передаются в конструкции, которая в Ruby называется блок (block).

task name: [:prereq1, :prereq2] do |t|
  # actions
end

Условно задания в Rake можно разделить на два типа: обычные и файловые. Обычное задание - это просто набор действий, который нужно выполнить. Такое задание объявляется методом task.

Назначением файлового задания является генерация файла (как правило на основе одного или более уже существующих). Если файл уже существует, то такое задание не выполняется (пропускается). Файловое задание объявляется методом file.

Имя задания, которое нужно выполнить передается в качестве параметра утилите rake.

rake task_name

Если запустить rake без параметров, то он будет пытаться найти в Rakefile задание с именем “default”. Если задание с таким именем не будет найдено, то rake выдаст соотвествующую ошибку.

>rake
rake aborted!
Don't know how to build task 'default'

Комментарии

Хотя в Rake разрешаются ruby-комментарии при помощи символа “#”, хорошим тоном считается писать комментарии к заданиям при помощи ключевого слова desc.

В этом случае, запустив команду rake -T мы получим список заданий с описаниями. Таким образом, хорошо составленные описания избавляют от необходимости дополнительно документировать rake-скрипты.

Пример вывода команды 'rake -T'

Пространства имен

В Rake есть концепция пространств имен (namespaces) для объединения заданий в группы. К примеру, любой, кто писал на Ruby on Rails, использовал команду rake db:migrate, в данном случае db - это пространство имен, а migrate задание в которое в него входит. Пространство имен объявляется ключевым словом namespace.

namespace :namespace_name do
     # tasks
end

Организация Rake-файлов

Утилита Rake ищет задания в файле с названием Rakefile. Очень быстро этот файл разрастается и возникает необходимость его разбить на несколько файлов по функциональному назначению. Для этого нужно воспользоваться следующим соглашением: создаем папку с названием rakelib, а в ней файлы с расширением *.rake. На все задания и пространства имен объявленные в *.rake-файлах можно ссылаться из основного Rakefile.

Сборка .NET проекта при помощи Rake

Ожидаемо, что чаще всего использование Rake встречается в Ruby/Rails проектах. Тем не менее никто не запрещает нам применить его в других языках. В качестве примера рассмотрим сборку простого “Hello world” на C#. Использование Rake в .NET-проектах может быть вполне оправдано, когда требуется реализовать сложную логику для сборки/тестирования/развертывания приложения, что может оказаться весьма нетривиально реализовать в XML-декларациях привычных NAnt или MSBuild.

Исходник будет представлять собой файл с простейшим кодом и файл с проектом для того чтобы воспользоваться MSBuild. Здесь он избыточно простой, но подход будет тот же, что и при билде больших солюшенов.

using System;

public class HelloWorld
{
    static void Main()
    {
        Console.WriteLine("Hello world!");
        Console.ReadLine();
    }
}

Файл проекта в этом случае будет выглядеть таким образом:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <ItemGroup>
        <Compile Include="hello.cs" />
    </ItemGroup>
    <Target Name="Build">
        <Csc Sources="@(Compile)"/>
    </Target>
</Project>

Так как Rake-код получается достаточно простой, я приведу его полностью, а затем поясню.

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

У нашего проекта определены следующие стадии сборки:

  • Очистка (Clean) - на этой стадии будут чиститься артефакты от предыдущего билда. В данном случае удаляются все *.exe файлы и папка out;
  • Сборка (Build) - все что касается компиляции приложения. В данном случае мы передаем MSBuild в качестве параметра путь к proj-файлу и выполняем эту команду;
  • Подготовка к поставке (Package) - здесь собранный exe-файл перемещается в папку “out”, а также подкладываться файл с ReadMe.

Задание default не выполняет никаких действий, но зависит от остальных объявленных заданий, таким образом, выполнив команду rake без параметров мы запустим друг за другом задания clean, build и package. Также хочу отметить, что в Rakefile, как и в обычных Ruby-скриптах можно пользоваться оператором require для того чтобы загружать и использовать любые библиотеки языка.

Результат выполнения Rake-скрипта из примера

Albacore

Я не единственный, кто захотел использовать Rake для сборки .NET-проектов. Для этого существует довольно популярный проект под названием Albacore, который расширяет DSL Rake, специальными ключевыми словами для автоматизации типичных задач с которыми сталкиваются разработчики для платформы от Microsoft.

Установка осуществляется командой:

gem install albacore

Давайте рассмотрим, как будет выглядеть задание сборки с использованием Albacore:

require "albacore"

desc "Compile project with MSBuild using Albacore"
build :alba_build do |b|
  b.file = "#{proj_root}/hello.proj"
end

Согласитесь, код стал выглядеть гораздо компактнее. Полную информацию по синтаксису Albacore можно найти в wiki проекта на Github.

Заключение

Rake - это отличный, гибкий инструмент для создания различных скриптов для сборки и обслуживания ваших проектов. Его использование совершенно не ограничивается Ruby-экосистемой, мы смогли убедиться, что ему найдется место даже в .NET-инфраструре.

Конечно не стоит вносить лишнюю зависимость на Ruby в небольшие проекты, где легко можно обойтись средствами, которые предоставляет платформа. В сложных же проектах, особенно при поддержке legacy-систем, где требуется при сборке разрешать сложные зависимости, подготавливать окружение для тестов (к примеру регистрировать COM-объекты в системе по набору определенных условий) и многое другое, Rake поможет сэкономить большое количество времени при минимальном количестве кода.

Альтернативы Rake:

Thor

Thor за авторством Yehuda Katz - это тулкит для создания консольных приложений на Ruby. В последнее время он также становится все популярнее в Ruby-сообществе в качестве замены Rake, так как имея схожую философию позволяет писать код на чистом Ruby, не требуя освоения специфичного Rake DSL и следования его соглашениям. Часто это позволяет облегчить внедрение инструмента в существующую инфраструктуру, покрыть скрипты автоматизации тестами и т.д.

Grunt

Grunt широко используется в мире фронтенд веб-разработки и позволяет реализовать задания на Javascript, a также обладает большим количеством плагинов. Мне также встречалось его использование вне контекста фронтенд-разработки.

Paver

Если для автоматизации рутинных задач ты предпочитаешь использовать Python вместо Ruby, то всю мощь и простоту Rake тебе сможет принести Paver. Используя схожий с Rake подход и философию, он позволяет также решать специфичные для Python задачи вроде разворачивания виртуальных окружений, содержит врапперы для генераторов документации и многое другое.

Gradle

В мире Java даже относительно простые проекты редко обходятся без использования инструментов сборки, обычно это Ant или Maven использующие конфиги в виде *.xml-файлов. Gradle позволяет отойти от декларативного подхода и реализовать задания в виде кода на Groovy DSL.

Ссылки

Примеры кода из статьи

Ключевые слова