Шалом тебе, дражайший читатель.
В этом посте речь пойдёт о вещах сугубо технических, и мало кому интересных. Однако, довольно насущных в сфере разработки всяческого программного обеспечения. Имеется ввиду автоматизированный «выкат» (ну, или «сборка») какого-либо проекта, путём написания для этого скриптов развёртки. Всё ещё хотите читать?)) Ну ладно, ладно.
Так вот, о чем это я. Систем выката, как известно, существует великое множество — например самой распространённой в *nix мире является make — по крайней мере если не брать красноглазиков из мира gentoo. Как вариант некоторые рассматривают cmake. Возможно довольно неплохие вещи, но скажем перенос их на другую платформу как правило означает переписывание всех скриптов заново. Да и синтаксис их написания практически один в один напоминает unix-shell, который я лично стараюсь использовать по минимуму. Ужасен он, ИМХО, что бы по этому не думали труЪ-адепты секты линуксоидов, хотя надо признать, что он предоставляет широкие возможности.
Так вот, есть более-менее нормальная такая альтернатива. Называется она Apache Ant, и в принципе с задачей она справиться вполне способна. Единственный её минус — очень мало информации на русском языке. Но для начала, давайте подумаем — зачем вообще может понадобиться делать лишнюю работу, и писать ещё какие-то билд-скрипты, ант устанавливать, и вообще, фигнёй страдать?
В принципе, если у вас небольшой проект, и весь процесс его развёртки, это просто svn checkout, то заморачиваться не стоит. Зачем вам лишние проблемы? А вот если у вас проект хранится в нескольких репозиториях (распространённый вариант — фреймворк в одном месте, а проекты его использующие — в другом, и разработка их ведётся параллельно), либо есть несколько конфигураций готового развёрнутого проекта (Dev-версия, или версия для разработки, Test — версия для тестирования в условиях, максимально приближенных к боевым, Production — версия для боевой работы), то вам уже надо как-то их упорядочить — на уровне развёртки. Видели ли вы конфиги, от которых зависит поведение веб-решений, которые занимают хотя бы 15-20 мегабайт кода (проекты, не конфиги, разумеется, от конфига на 20 мегабайт я бы лично кирпичную будку выложил)? Жуткое зрелище, поверьте. В этом случае вам очень пригодится система конфигурирования во время разворачивания, которая будет генерировать, скажем, файлы с пресетами из шаблонов, выкатывать разные компоненты из репозиториев, вызывать всяческие программы и выполнять команды, что в итоге должно дать работающий продукт.
Так вот, возвращаясь к ANT’у (кстати, ant в переводе с английского — муравей). Чем приглянулось это решение? Во-первых, синтаксис билд скриптов хотя и ограничен в некоторых аспектах, тем не менее он куда как проще и понятнее, чем гиковский shell, подобие которого используется в make. Во-вторых, всё-таки шелл-команды из него вызывать вполне можно. По сути, билд-скрипты для ant — это xml-файлы, в котором описана последовательность команд, которые требуется выполнить, чтобы получить рабочее решение. В третьих, для ANT Существует довольно много модулей, расширяющих его функциональность. В четвёртых — ант имеет встроенную поддежку svn. В пятых — он кросс-платформенный, т.е. одинаково хорошо будет работать и на Windows и на Linux машинах (заметьте, я нигде не сказал, что билды будут одинаково работать, только о самом ant’е речь пока). Ах да, и в шестых — идейные ненавистники такой технологии как Java, смело идут на хуй, так как ANT написан на Java. Surprise, suckers! :)
В нашей работе мы используем именно ANT, и, как уже упоминалось, очень сложно найти по нему информацию на русском языке. В некоторых проектов есть разделение билд-скриптов по уровням: «билды верхнего уровня, билды проектного уровня». Что это значит? Это разделение труда — билд верхнего уровня выкатывает из репозиториев требуемые файлы (движок, проекты), а затем передаёт управление соответственно билдам проекта или билдам движка (фреймворка), которые отрабатывают уже свою задачу — конфигурируют, скажем, движок, в соответствии с заложенным в них алгоритмом, но при этом используя часть конфигурации «головных» билдов. В данной статье подобные схемы описаны не будут, потому как объём получится совсем невообразимый, но в целом додумать не сложно — главное знать, что такие возможности присутствуют.
Так вот, для начала — ну, чтобы попробовать то, что тут описано — надо иметь сам ANT установленный. Скачать его можно на
Итак, сердце «билдов» (билд-скриптов) это файл build.xml, который содержит начальные инструкции по сборке. Можно, конечно, всё хранить в нём, но это будет ужасно — всё в куче, в одном месте… В общем — нехорошо. Намного лучше описать в build.xml алгоритм выбора конфигурации, а сами конфигурационные файлы — хранить по отдельности. Скажем, у нас будет отдельная папка build, в которой хранится всё, что требуется для разворачивания проекта. И предположим, она будет иметь следующую структуру:
/build /properties /scripts /templates build.xml
Здесь в папке properties хранятся конфигурационные файлы (для простоты всего две конфигурации — Develop & Production, плюс общий для всех файл Common в котором хранятся настройки единые для всех конфигураций). Соответственно, для того, чтобы построить проект, необходимо будет войти в директорию build, чтобы она стала текущей, и в ней выполнить команду например:
ant -Dbt=D install
Дальше ант должен подцепить файл build.xml, и начать выполнять инструкции в нём описанные. Собственно, вот и сам build:
<?xml version="1.0"?> <!-- Обратите внимание на параметр basedir - в дальнейшем он активно используется --> <project name="Some Project" basedir="." default="install"> <!-- BEGIN:Load of properties... --> <echo>Loading properties of build...</echo> <!-- Здесь мы тащемта выбираем, какую конфигурацию мы будем разворачивать. Develop or Prod --> <!-- Property ${bt} MUST BE set in command line --> <property name="bt" value="!" /> <condition property="build.type" value="Develop"> <or> <equals arg1="d" arg2="${bt}" casesensitive="false" trim="true" /> <equals arg1="dev" arg2="${bt}" casesensitive="false" trim="true" /> <equals arg1="develop" arg2="${bt}" casesensitive="false" trim="true" /> </or> </condition> <condition property="build.type" value="Production"> <or> <equals arg1="p" arg2="${bt}" casesensitive="false" trim="true" /> <equals arg1="prod" arg2="${bt}" casesensitive="false" trim="true" /> <equals arg1="production" arg2="${bt}" casesensitive="false" trim="true" /> </or> </condition> <property name="build.type" value="${bt}" /> <!-- Сия конструкция подгружает файл с пресетами, в зависимости от выбранной конфигурации сборки --> <property name="build.properties.filename" value="${basedir}/properties/${build.type}" /> <condition property="incorrect.build.type"> <not> <available file="${build.properties.filename}" /> </not> </condition> <!-- А нет у нас такого конфига! --> <fail if="incorrect.build.type" message="Incorrect build type [${build.type}] was received - build stopped!" /> <property file="${build.properties.filename}" /> <!-- И что бы ни случилось - подгружаем настройки Common, общие для всех конфигураций --> <property file="${basedir}/properties/Common" /> <!-- END:Load of properties --> <!-- А дальше конфигурируем SVN --> <!-- BEGIN:SVN Configure --> <echo>Configuring subversion...</echo> <!-- SETUP of SVN-Action type --> <property name="sa" value="checkout" /> <property name="svn.action" value="${sa}" /> <condition property="svn.checkout" value="true"> <or> <equals arg1="co" arg2="${svn.action}" casesensitive="false" trim="true" /> <equals arg1="checkout" arg2="${svn.action}" casesensitive="false" trim="true" /> </or> </condition> <condition property="svn.update" value="true"> <or> <equals arg1="up" arg2="${svn.action}" casesensitive="false" trim="true" /> <equals arg1="update" arg2="${svn.action}" casesensitive="false" trim="true" /> </or> </condition> <property name="project.svn.password" value="${sp}" /> <!-- SvnAnt Setup --> <typedef resource="org/tigris/subversion/svnant/svnantlib.xml" classpath="svnant.jar" /> <!-- END:SVN Configure --> <!-- А вот тут мы как раз в зависимости от заданного "таргета" выбираем, что делать будем - устанавливать, линковать, или просто переконфигурировать. Разница в следующем: в install мы просто вытягиваем все требуемые файлы из репозиториев, и размещаем их так, как нам нужно, с учётом всяческих иерархий и прочего. В link мы можем много чего. Например самое простое - в UNIX системах понапроставлять симлинков на нужные директории в проекте, или там вызвать какой-нибудь внешний линковщик, или ещё что-нибудь... В общем логически данный уровень выката проекта - наиболее абстрактен, вы можете в нём по идее творить что угодно. Ну и наконец configure в данном случае просто генерирует конфигурационные файлы - для развёрнутого продукта. Важно помнить, что вы можете задавать любые цели, и как угодно разделять логику сборки - что-то у вас может быть, чего-то не быть - в общем, всё на ваш вкус. --> <!-- BEGIN: TARGETS --> <target name="install"> <ant antfile="${basedir}/scripts/project_install.xml" /> </target> <target name="configure"> <ant antfile="${basedir}/scripts/project_configure.xml" /> </target> <target name="link"> <ant antfile="${basedir}/scripts/project_link.xml" /> <ant antfile="${basedir}/scripts/project_configure.xml" /> </target> <!-- А сюда, собственно, мы помещаем информацию о том, что мы вообще выкатили. В дальнейшем эта информация помогает тому же анту ориентироваться в версиях выкаченного, да и самим вполне можно будет посмотреть, что там происходило --> <!-- BEGIN: DO NOT place any spaces at the beginning of lines in that target --> <target name="save.branchname"> <echo file="installed_branches_project.txt" append="true" level="info">${forum.svn.project.branch} </echo> </target> <!-- END. --> <!-- END: TARGETS --> </project>
Как пример — project_configure.xml для одного форума:
<?xml version="1.0"?> <project name="Some Project" default="configure"> <target name="configure"> <echo> Configure: START </echo> <!-- Forum config --> <delete file="${forum.install.path}/conf_global.php" quiet="true" /> <copy file="${basedir}/templates/conf_global.php.tpl" tofile="${forum.install.path}/conf_global.php" /> <replace file="${forum.install.path}/conf_global.php" token="#ANT:forum.sql.driver#" value="${forum.sql.driver}" /> <replace file="${forum.install.path}/conf_global.php" token="#ANT:forum.sql.host#" value="${forum.sql.host}" /> <replace file="${forum.install.path}/conf_global.php" token="#ANT:forum.sql.port#" value="${forum.sql.port}" /> <replace file="${forum.install.path}/conf_global.php" token="#ANT:forum.sql.database#" value="${forum.sql.database}" /> <replace file="${forum.install.path}/conf_global.php" token="#ANT:forum.sql.user#" value="${forum.sql.user}" /> <replace file="${forum.install.path}/conf_global.php" token="#ANT:forum.sql.pass#" value="${forum.sql.pass}" /> <replace file="${forum.install.path}/conf_global.php" token="#ANT:forum.sql.tbl_prefix#" value="${forum.sql.tbl_prefix}" /> <replace file="${forum.install.path}/conf_global.php" token="#ANT:forum.sql.debug#" value="${forum.sql.debug}" /> <replace file="${forum.install.path}/conf_global.php" token="#ANT:forum.host#" value="${forum.host}" /> <replace file="${forum.install.path}/conf_global.php" token="#ANT:forum.settings.group.banned#" value="${forum.settings.group.banned}" /> <replace file="${forum.install.path}/conf_global.php" token="#ANT:forum.settings.group.admin#" value="${forum.settings.group.admin}" /> <replace file="${forum.install.path}/conf_global.php" token="#ANT:forum.settings.group.guest#" value="${forum.settings.group.guest}" /> <replace file="${forum.install.path}/conf_global.php" token="#ANT:forum.settings.group.member#" value="${forum.settings.group.member}" /> <replace file="${forum.install.path}/conf_global.php" token="#ANT:forum.settings.group.auth#" value="${forum.settings.group.auth}" /> <replace file="${forum.install.path}/conf_global.php" token="#ANT:forum.locale#" value="${forum.locale}" /> <replace file="${forum.install.path}/conf_global.php" token="#ANT:forum.date_start#" value="${forum.date_start}" /> <replace file="${forum.install.path}/conf_global.php" token="#ANT:forum.hooks.disabled#" value="${forum.hooks.disabled}" /> <!-- / Forum config --> <echo> Configure: END </echo> </target> </project>
Ах да, чуть не забыл. Ещё требуется конфиг. В данном случае он лежит в подпапке properties/ — это файл Develop или Production. В зависимости от того, что в них — будет конфигурироваться проект. Скажем, если мы разворачиваем проект строкой ant -Dbt=D install — подцепится Debug конфиг (=D). Если бы мы собирали его -Dbt=P — подцепился бы Production конфиг. Вот вам пример конфига — естественно, некоторые значения я там заменил :) А, кстати — вот вверху есть configure таргет — в нем происходит следующее. Сначала удаляется старый файл (который генерируется при развёртке проекта, скажем это файл с настройками). Затем, на его место пишется шаблон, простым копированием. И затем, в этом скопированном шаблоне заменяются токены вида #ANT:forum.settings.group.auth# на соответствующие (см. выше) значения. Итак, пример конфигурационного файла Develop:
#----------------------------------------------------------------- # Properties for Dev-build #----------------------------------------------------------------- # Forum Settings forum.name=Some Forum Name forum.host=www.host.ru forum.path=/www/host.ru forum.install.path=/www/host.ru/htdocs forum.locale=ru_RU.CP1251 forum.date_start=1234567890 forum.settings.group.banned=1 forum.settings.group.admin=2 forum.settings.group.guest=3 forum.settings.group.member=4 forum.settings.group.auth=5 # SVN Settings forum.svn.user=svn-user # Репозиторий движка. forum.svn.engine=svn+ssh://host.ru/ipb forum.svn.engine.branch=branches/Dev_1.0.0 forum.engine.dir=${forum.path}/engine # Проектный репозиторий forum.svn.project=svn+ssh://host.ru/project forum.svn.project.branch=Dev_0.0.3 forum.project.dir=${forum.path}/project # uploads dir forum.uploads.dir=${forum.path}/uploads # DB Settings forum.sql.driver=pgsql forum.sql.host=host.ru forum.sql.port=6505 forum.sql.database=forum forum.sql.user=forum_user forum.sql.pass=password forum.sql.tbl_prefix=prefix_ forum.sql.debug=1
И файлик, собственно, с шаблоном для конфигурационного файла:
<?php $INFO['sql_driver'] = '#ANT:forum.sql.driver#'; $INFO['sql_host'] = '#ANT:forum.sql.host#'; $INFO['sql_port'] = '#ANT:forum.sql.port#'; $INFO['sql_database'] = '#ANT:forum.sql.database#'; $INFO['sql_user'] = '#ANT:forum.sql.user#'; $INFO['sql_pass'] = '#ANT:forum.sql.pass#'; $INFO['sql_tbl_prefix'] = '#ANT:forum.sql.tbl_prefix#'; $INFO['sql_debug'] = '#ANT:forum.sql.debug#'; $INFO['board_start'] = '#ANT:forum.date_start#'; $INFO['installed'] = '1'; $INFO['php_ext'] = 'php'; $INFO['safe_mode'] = '0'; $INFO['board_url'] = 'http://#ANT:forum.host#'; $INFO['banned_group'] = '#ANT:forum.settings.group.banned#'; $INFO['admin_group'] = '#ANT:forum.settings.group.admin#'; $INFO['guest_group'] = '#ANT:forum.settings.group.guest#'; $INFO['member_group'] = '#ANT:forum.settings.group.member#'; $INFO['auth_group'] = '#ANT:forum.settings.group.auth#'; $INFO['locale'] = '#ANT:forum.locale#'; ?>
Да, и кстати. Если вы поменяли что-то в конфигурационном файле, либо заменили нечто в шаблоне — вам не обязательно (в рамках приведённого примера) выкатывать всё заново. Достаточно вызвать команду: ant -Dbt=D configure. В результате будет выполнен таргет configure, в котором произойдёт перегенерация всех шаблонно-генерируемых файлов, вот.
Вот, в общем-то и всё… Надеюсь, удалось осветить как и что. Хотя концовка получилась немного скомканной — дописывалось уже через пару недель, после того, как основной текст был порождён. Так что в спешке. Но если не дописать сейчас — то наверное уже не дойдут руки никогда :) Вот. Все вопросы, если они возникнут, как обычно — в каменты.