Предположим, у вас есть веб-приложение, которое установлено на многих хостах. Каждая установка может иметь собственную конфигурацию, и приложение все еще находится в активной разработке. Вам нужен простой способ развертывания новых функций и исправления ошибок на всех хостах.
В своей предыдущей статье « Использование Phing», инструмента сборки PHP , Shameer дал вам базовое понимание Phing. Вы узнали, как читать и писать файл сборки и каковы его основные компоненты: project
, targets
, tasks
и properties
.
В этой статье я покажу вам, как использовать эти знания для написания стандартного файла сборки, который можно настраивать и повторно использовать в реальных приложениях. Мы будем использовать задачи Phing Subversion для управления хранилищем и расширение FileSync для синхронизации вашей локальной установки с удаленным сервером.
Подготовка окружающей среды
Чтобы использовать задачи, связанные с Subversion, вам нужны два пакета PEAR VersionControl_SVN
и NET_FTP
. Оба могут быть легко установлены с помощью следующих команд:
$ sudo pear установить VersionControl_SVN $ sudo pear установить NET_FTP
Расширение FileSyncTask является дружественной оболочкой для команды rsync
. Он поддерживается Федерико Каргнелутти и может быть загружен с его сайта . После загрузки скопируйте файл FileSyncTask.php
в каталог расширений Phing (путь по умолчанию в unix-подобных системах — /opt/phing/tasks/ext
, но ваша конфигурация может отличаться).
Я создал пустой репозиторий Subversion под названием «helloworld» и дал ему базовую структуру каталогов с каталогами trunk
, branches
и tags
. Я проверил транк локально и импортировал файлы моего приложения, используя следующую структуру каталогов:
Как видите, есть каталог с именем build, который будет содержать файлы, связанные с Phing.
Основной файл build.xml
выглядит так:
<?xml version="1.0" encoding="UTF-8"?> <project name="HelloWorld" default="hello" basedir="../"> <!-- Load project settings from external file --> <property file="build/config/project.properties" /> <!-- Default empty target --> <target name="hello" description="Displays basic project information"> <echo message="Hello, welcome to ${phing.project.name}!" /> <echo message="Current environment is: ${project.env}" /> </target> </project>
Существует цель по умолчанию «hello», которая отображает основные данные, такие как имя проекта и текущая среда (производство, разработка, подготовка и т. Д.).
Вместо того чтобы объявлять список свойств внутри файла сборки, Phing велит загрузить настройки проекта из внешнего файла. Путь этого файла указывается относительно атрибута basedir
определенного в теге property
. Делая это, мы можем повторно использовать один и тот же файл для нескольких проектов.
Файл project.properties
— это просто текстовый файл, в котором хранятся настройки с использованием синтаксиса ключ-значение, например:
ftp.host = host.example.com ftp.port = 21 ftp.username = пример ftp.password = 123456 app.home = $ {ftp.host} / MyApp
Вы можете использовать тот же синтаксис ‘$ {varname}’ для ссылки на значение ранее объявленных свойств.
Файлы свойств хранятся в каталоге build/config
. Также в этом каталоге находится подкаталог hosts
, который содержит конкретные настройки клиент-хост, и подкаталог scripts
котором хранится шаблон сценария установки. Файлы, оканчивающиеся на «-sample.properties», имеют версии в репозитории и используются в качестве шаблонов, в то время как файлы проекта и хоста помечаются как игнорируемые. Каталог build/export
будет содержать файлы, сгенерированные нашими целями.
Чтобы запустить любую из целей, мы должны вызвать Phing из каталога build
. Общий синтаксис:
$ phing[-D PropertyName = PropertyValue ]
Опция -D
позволяет вам переопределить значение свойства, объявленного с его атрибутом переопределения, установленным в «true».
Цель развертывания
Цель deploy
— наш инструмент непрерывной интеграции. Он синхронизирует и развертывает текущую рабочую копию с удаленного хоста с использованием определенной конфигурации (например, servername.properties
), хранящейся в build/config/hosts
. Цель должна иметь возможность найти файл .properties
для требуемого хоста, а затем подключиться к удаленному хосту с помощью этих настроек и выполнить задачу sync
.
Цель запускается с помощью следующей команды:
$ phing deploy -Dhostname = myhostname
Код цели:
<target name="deploy" description="Deploys the current working copy to a remote host using FileSync"> <!-- Default hostname is empty, must be passed from the command line --> <property name="hostname" value="false" override="true" /> <!-- Set default LISTONLY to false --> <property name="listonly" value="false" override="true" /> <property name="hostfile" value="build/config/hosts/${hostname}.properties" /> <!-- Check for specific host/env file, if not fail! --> <available file="${hostfile}" property="hostfilefound" value="true"/> <fail unless="hostfilefound" message="Missing host configuration file (${hostfile})!" /> <!-- Host file exists so loading... --> <property file="${hostfile}" /> <!-- Get timestamp --> <tstamp /> <!-- Set default VERBOSE flag to TRUE --> <if> <not> <isset property="sync.verbose" /> </not> <then> <property name="sync.verbose" value="true" override="true" /> <echo message="The value of sync.verbose has been set to true" /> </then> </if> <!-- Set default DELETE flag to FALSE --> <if> <not> <isset property="sync.delete" /> </not> <then> <property name="sync.delete" value="false" override="true" /> <echo message="The value of sync.delete has been set to false" /> </then> </if> <!-- Get auth info, password will be always required --> <property name="sync.remote.auth" value="${sync.remote.user}@${sync.remote.host}" /> <!-- Perform Sync --> <!-- See: http://fedecarg.com/wiki/filesynctask --> <taskdef name="sync" classname="phing.tasks.ext.FileSyncTask" /> <sync sourcedir="${sync.source.projectdir}" destinationdir="${sync.remote.auth}:${sync.destination.projectdir}" listonly="${listonly}" excludefile="${sync.exclude.file}" delete="${sync.delete}" verbose="${sync.verbose}" /> </target>
Сначала свойство hostname
определяется как перезаписываемое и ему присваивается произвольное значение по умолчанию (я выбрал «true»). Тогда свойство listonly
определяется со значением по умолчанию. Это свойство используется задачей sync
, и если оно установлено в значение true, то задача будет отображать только список файлов, которые должны быть обработаны, но не будет выполнять фактическую синхронизацию.
Следующие три оператора определяют путь к файлу настроек хоста и используют available
задачу, чтобы установить свойство hostfilefound
если файл присутствует. Если это не так, свойство не будет установлено, и задача fail
прервет сценарий с предоставленным сообщением об ошибке. Если файл свойств хоста присутствует, он загружается; этот файл содержит набор настроек для задачи, сгруппированных с использованием префикса «синхронизация». Все они говорят сами за себя, но стоит обратить особое внимание на sync.delete
: если установлено значение true, все удаленные файлы, которых нет в локальной копии, будут удалены. Я рекомендую всегда устанавливать его в ложь, если у вас нет веской причины, чтобы это было правдой
Далее мы можем увидеть два других примера того, насколько мощен Phing: мы используем синтаксис if
/ then
для определения значения свойств sync.delete
и sync.verbose
.
Последняя часть является актуальной задачей deploy
. Оператор taskdef
сообщает Phing, что sync
является пользовательской задачей, и предоставляет путь к файлу PHP для загрузки, а параметры для этой задачи загружаются из файла хоста. excludefile
указывает на текстовый файл, содержащий список шаблонов для исключения из синхронизации, по одному шаблону на строку, используя синтаксис rsync
.
Примечание: если ваш удаленный сервер использует файл идентификации SSH, вы должны установить атрибут identityfile
с полным путем к вашему файлу ключа.
Цель «подготовить»
Задача prepare
цель состоит в том, чтобы создать снимок текущего транка и пометить его для выпуска. Текущий транк копируется в каталог tags/ nameoftag
репозитория (например, tags/1.0.1
).
Код для prepare
выглядит так:
<target name="prepare" description="Prepares a tag in the remote repository"> <!-- Ask for a tag label to copy the current trunk --> <property name="tagLabel" value="false" override="true" /> <!-- The tag name cannot be empty! --> <if> <isfalse value="${tagLabel}"/> <then> <fail message="Invalid tag name!" /> </then> </if> <echo>Preparing tag ${tagLabel}...</echo> <!-- Copy trunk to the new tag under tags/tagLabel --> <svncopy force="true" nocache="true" repositoryurl="${svn.repository.url}/trunk" todir="${svn.repository.url}/tags/${tagLabel}" username="${svn.repository.username}" password="${svn.repository.password}" message="Tag release ${tagLabel}" /> <!-- Switch the working copy repo to the newly created tag --> <svnswitch repositoryurl="${svn.repository.url}/tags/${tagLabel}" username="${svn.repository.username}" password="${svn.repository.password}" todir="." /> <!-- Here you can perform any kind of editing: generate documentation, export SQL files, ecc --> <touch file="README.txt" /> <!-- Commit changes --> <svncommit workingcopy="." message="Finish editing tag ${tagLabel}" /> <echo message="Committed revision: ${svn.committedrevision}"/> <!-- Reset working copy repo to trunk --> <svnswitch repositoryurl="${svn.repository.url}/trunk" /> <echo msg="Tag ${tagLabel} completed!" /> </target>
В первых двух строках мы устанавливаем значение по умолчанию для метки тега, потому что мы хотим, чтобы оно передавалось из командной строки, затем мы isFalse
, чтобы это значение не было пустым, используя оператор if
/ isFalse
.
Затем мы используем задачу svncopy
чтобы скопировать наш транк в нужный каталог тегов, и задачу svnswitch
чтобы сообщить Subversion, что мы сейчас работаем над вновь созданным тегом. С этого момента мы можем вносить любые изменения в наши файлы, такие как обновление файла README
или редактирование файлов конфигурации с подходящими значениями по умолчанию. После того, как все изменения выполнены, мы используем svncommit
для сохранения изменений и другой svnswitch
для сброса нашей рабочей копии обратно в предыдущее состояние.
Цель выпуска
Цель release
— наш упаковочный инструмент. Он начинается с извлечения транка или выбранного тега из хранилища и выполняет с ним следующие действия:
- экспортировать файлы тегов куда-нибудь (по умолчанию для сборки / экспорта)
- создать пакет TAR.GZ из экспортированных файлов
- вычислить дайджест SHA1 сжатого файла
- создать сценарий установки с помощью файла
install.template.sh
- загрузить файлы на сервер релизов
Код для release
:
<target name="release" description="Exports the trunk or the given tag along with install scripts and FTP uploads"> <property name="release" value="trunk" override="true" /> <echo message="Creating package for '${release}'" /> <!-- Process repository path for trunk or tag --> <if> <equals arg1="${release}" arg2="trunk" /> <then> <property name="repo-path" value="${release}" override="true" /> </then> <else> <property name="repo-path" value="tags/${release}" override="true" /> </else> </if> <!-- Export selected branch/tag from remote repository --> <svnexport repositoryurl="${svn.repository.url}/${repo-path}" force="true" username="${svn.repository.username}" password="${svn.repository.password}" nocache="true" todir="${svn.export.basedir}/${release}" /> <!-- Do other custom editing here... --> <!-- Create TAR archive --> <tar destfile="${svn.export.basedir}/${phing.project.name}-${release}.tar.gz" compression="gzip"> <fileset dir="${svn.export.basedir}/${release}"> <include name="*" /> </fileset> </tar> <!-- Delete Temporary Export directory --> <delete dir="${svn.export.basedir}/${release}" includeemptydirs="true" verbose="false" failonerror="true" /> <!-- Compute SHA1 digest --> <property name="hash" value="empty" /> <filehash file="${svn.export.basedir}/${phing.project.name}-${release}.tar.gz" hashtype="1" propertyname="hash" /> <echo msg="SHA1 Digest = ${hash}" /> <echo msg="Files copied and compressed in build directory OK!" /> <!-- Prepare install.sh, backup.sh and update.sh scripts --> <copy todir="${svn.export.basedir}" overwrite="true"> <mapper type="glob" from="*.template.sh" to="*.sh"/> <fileset dir="./build/config/scripts"> <include name="*.sh" /> </fileset> <filterchain> <replacetokens begintoken="##" endtoken="##"> <token key="SRCURL" value="${http.srcurl}/${release}/" /> <token key="FILENAME" value="${phing.project.name}-${release}" /> <token key="FILEXT" value="tar.gz" /> <token key="HASH" value="${hash}" /> <token key="APPNAME" value="${phing.project.name}" /> <token key="APPVERSION" value="${release}" /> </replacetokens> </filterchain> </copy> <!-- Upload the generated file(s) to FTP --> <property name="upload" value="false" override="true" /> <if> <equals arg1="${upload}" arg2="true" /> <then> <echo msg="Uploading to FTP server for release..." /> <ftpdeploy host="${ftp.host}" port="${ftp.port}" username="${ftp.username}" password="${ftp.password}" dir="${ftp.dir}/${release}" passive="${ftp.passive}" mode="${ftp.mode}"> <fileset dir="${svn.export.basedir}"> <include name="${phing.project.name}-${release}.tar.gz" /> <include name="install.sh" /> </fileset> </ftpdeploy> <echo>Now you can run: wget ${http.srcurl}/${release}/install.sh && sh install.sh [stage|local|prod] 2>&1 > ./install.log</echo> </then> </if> <echo msg="Done!" /> </target>
Первые несколько строк касаются входных параметров, с помощью которых мы указываем значение по умолчанию «trunk» для выпуска для экспорта и используем оператор if
для вычисления пути к исходному репозиторию.
Задача svnexport
экспортирует данные файлы непосредственно из хранилища (не рабочей копии) в наш целевой каталог. Создается временный каталог с именем «AppName-ReleaseName».
Задача tar
генерирует файл пакета из временного каталога экспорта, используя внутренний fileset
качестве источника. Задача delete
, которая является необязательной, удаляет временные файлы.
Затем мы используем задачу filehash
для генерации дайджеста SHA1 для упакованного файла (другой вариант — MD5) и сохраняем его в свойстве hash
.
Задача copy
вызывается для копирования сценария установщика шаблона в наш каталог экспорта и использует некоторые очень мощные ресурсы Phing. mapper
— это инструмент выбора и преобразования фильтров, применяемый к filenames
. Файлы, выбранные с заданным fileset
, обрабатываются картографом перед копированием. В этом случае файлы, соответствующие «* .template.sh», переименовываются с расширением «.sh» в целевом каталоге.
Другая мощная функция, используемая здесь, это filterchain
Фильтры Phing используются для преобразования содержимого файлов во время выполнения другой задачи. В этом случае «родительской» задачей является copy
. Используемый здесь фильтр — replacetokens
, который заменяет в каждом файле набор определенных переменных шаблона динамически генерируемым значением. Он используется здесь для вставки конкретных сведений о выпуске (имя, версия, хеш, URL и т. Д.) В скрипт установки.
Последний штрих — задача ftpdeploy
, запускаемая свойством upload
. Эта задача загружает файл нашего пакета и скрипт установки на удаленный сервер и отображает URL-адрес, который будет использоваться для вашей установки. Если ошибок нет, вы можете выполнить команду:
$ wget http://www.yoursite.com/helloworld/releases/trunk/install.sh && sh install.sh
Команды загружают и проверяют файл пакета, а затем выполняют заданные вами задачи установки.
Резюме
Мы видели много полезных функций Phing здесь, но есть много других, чтобы обнаружить. Вы можете использовать файл сборки «как есть» для развертывания текущих и будущих проектов или расширить его, добавив дополнительные функции, такие как поддержка модульного тестирования, Git, обработка базы данных и даже пользовательские расширения. Как всегда, официальная документация будет вашим лучшим началом.
Изображение через 1971yes / Shutterstock