пятница, 30 сентября 2011 г.

Установка и настройка сервера непрерывной интеграции phpUnderControl

Описание


Сервер непрерывной интеграции (CI - Continuous Integration) предназначен для автоматической периодической сборки проектов, запуска модульных тестов, проверки кода на наличие распространенных ошибок и формирования ряда других отчетов. В качестве CI-сервера будет использоваться CruiseСontrol (CC) с дополнением phpUnderControl. CС запускается в качестве демона, периодически опрашивает SVN-репозиторий и, в случае наличия изменений, создает локальную копию, над которой в дальнейшем производится ряд определенных действий. Для выполнения этих действий в нужной последовательности используется утилита Ant (также поддерживается работа с его php-клоном phing), которая поставляется в комплекте с CC.


Дополнение phpUnderControl предоставляет консольную утилиту phpuc которая дополняет CC рядом шаблонов, а также позволяет быстро добавлять/удалять проекты в CC. В phpUnderControl встроена поддержка проверки синтаксиса php-кода, а также следующих утилит:


phpunit

автоматизированное тестирование приложения

phpdoc

PhpDocumentor - автоматическая генерация документации

phpcb

PHP_CodeBrowser - просмотр кода онлайн (включая подсветку и описание проблемных мест, таких как: нарушение стандартов кодирования, дублирование кода, высокая сложность кода)

phpcs

PHP_CodeSniffer - обнаружение нарушений стандартов кодирования

ezcGraph

простроение графиков изменения контролируемых параметров с течением времени


Кроме того есть возможность добавить поддержку других утилит, таких как:

phpmd

анализ сложности исходного кода

phpcpd

поиск дублирующегося кода


Скриншоты


Главная страница: список проектов, ручной запуск сборок:

Страница с результатами выполнения тестов phpunit:

Страница метрик, изменение контролируемых параметров от сборки к сборке:

Страница, отображающая покрытие кода тестами:

Онлайн-просмотр кода с подсветкой и описанием проблемных участков (оформление, сложность, дублирование и пр.):

Онлайн документация:

Страница отчета о нарушениях стандартов кодирования:

Страница отчета сложности кода, дублирования кода и других проблем:

Просмотр продублированного кода:


Установка CruiseСontrol


Для работы сервера CC нужна java-машина, устанавливаем, если еще не установлена:

sudo apt-get install sun-java6-bin

Затем следует скачать архив с последней версией CC с сайта http://cruisecontrol.sourceforge.net/download.html (на момент написания последней была версия 2.8.4) и распаковать в локальный каталог (в данном примере /opt):

sudo unzip cruisecontrol-bin-2.8.4.zip -d /opt

Для удобства обновлений программы можно создать символическую ссылку cruisecontrol, которая будет указывать на используемую версию данного ПО:

cd /opt
sudo ln -s cruisecontrol-bin-2.8.4 cruisecontrol

Теперь, когда CC-сервер установлен, можно настроить его автоматический запуск при старте системы. Для этого следует добавить нового пользователя, от имени которого будет запускаться процесс CC, разместить стартовый скрипт cruisecontrol (приложение А) в каталоге /etc/init.d/ (для debian-подобных систем), установить необходимые для его запуска права и, собственно, настроить автозапуск:

sudo adduser --system --group --home /opt/cruisecontrol cruisecontrol
sudo chown -R cruisecontrol:cruisecontrol /opt/cruisecontrol-bin-2.8.4 /opt/cruisecontrol
sudo cp cruisecontrol /etc/init.d/cruisecontrol
sudo chmod 755 /etc/init.d/cruisecontrol
sudo update-rc.d cruisecontrol defaults

Теперь вручную сервер CC можно запустить командой

sudo /etc/init.d/cruisecontrol start

Остановить:

sudo /etc/init.d/cruisecontrol stop

Для отладочных целей запуск сервера CC можно выполнить командой (из каталога /opt/cruisecontrol/):

sudo ./cruisecontrol.sh -configfile /opt/cruisecontrol/config.xml -webport 8080 -jmxport 8082 -rmiport

После успешного запуска CC-сервера должен быть доступен адрес http://localhost:8080.


Возможна проблема при запуске CC сервера, когда в файл /opt/cruisecontrol/cruisecontrol.sh из стартового скрипта не передается переменная окружения JAVA_HOME. Одним из вариантов решения данной проблемы может быть запись строки JAVA_HOME=/usr/lib/jvm/java-6-sun в общесистемный файл /etc/environment. Также, в качестве временного решения, можно указать значение этой переменной непосредственно в файле /opt/cruisecontrol/cruisecontrol.sh. Однако при использовании в стартовом скрипте команды su cruisecontrol -p -c ... этой проблемы возникать не должно.


Установка phpUnderControl и необходимых модулей


Прежде чем переходить к установке phpuc следует проверить наличие требуемых модулей, и установить/обновить недостающие (может потребоваться обновление xdebug и pear installer):

sudo apt-get install php5-xdebug
sudo pear install PhpDocumentor
sudo pear install PHP_CodeSniffer
sudo pecl install pecl/xdebug
pear channel-discover pear.phpunit.de
pear install phpunit/PHPUnit
sudo pear install phpunit/phpcpd
sudo pear upgrade pear
sudo pear install phpunit/PHP_CodeBrowser

Далее можно перейти к установке phpuc (на момент написания использовалась версия 0.6.1beta1):

sudo pear channel-discover components.ez.no
sudo pear channel-discover pear.phpundercontrol.org
sudo pear install --alldeps phpuc/phpUnderControl-beta

А также установить дополнительные модули, поддержка которых в phpUnderControl на данный момент реализована лишь частично (требуется ручная правка конфигов):

sudo pear channel-discover pear.phpmd.org
sudo pear channel-discover pear.pdepend.org
sudo pear install --alldeps phpmd/PHP_PMD
sudo pear install --alldeps phpunit/PHPCPD

phpuc представляет собой консольную утилиту для взаимодействия с установленной версией CC. Следующая команда скопирует набор дополнительных файлов и шаблонов для веб-интерфейса в каталоги с установленной копией CC:

sudo phpuc install /opt/cruisecontrol


Добавление проектов


Утилита phpuc также предоставляет команды для быстрого добавления проектов под управление ci-сервера (следующая команда - это одна строка, отформатированная для удобства чтения):


sudo phpuc project
--ant-script /opt/cruisecontrol/apache-ant-1.7.0/bin/ant
--version-control svn
--version-control-url file://localhost/home/user/svn/myProjectName/trunk
--coding-guideline Zend
--source-dir application
--test-dir tests
--project-name myProjectName
/opt/cruisecontrol


В результате работы данной команды


  1. в каталоге projects CC-сервера будут созданы подкаталоги для указанного проекта

  2. в конфигурационный файл СС будет добавлена новая секция <project/> (см. приложение Б). Стоит отметить что нода <shedule/> отвечает за выполнение запланированных для проекта действий (в данном примере каждые 300 сек. будет запускаться указанный ant-скрипт)

  3. в каталоге проекта будет создан файл build.xml (см. приложение В), содержащий ряд заданий для последующих регулярных сборок проекта

  4. выполнена команда svn checkout

Для удаления проекта следует использовать команду

sudo phpuc delete myProjectName


Настройка проектов


На данном этапе мы должны получить готовую автоматическую систему сборки и тестирования проектов, однако она может нуждаться в дополнительных доработках. В частности для цели lint (файл build.xml проекта) можно исключить из списка проверяемых путей некоторые каталоги (например каталог с дистрибутивом Zend) - это уменьшит время сборки:


<target name="lint">
<apply executable="php" dir="${basedir}/source" failonerror="on" logerror="on">
<arg line="-l"/>
<fileset dir="${basedir}/source">
<include name="**/*.php"/>
<exclude name="library/Zend/**/*.php"/>
</fileset>
</apply>
</target>


Для публикации отчетов о проблемах кода (phpmd) следует добавить новую цель:


...
<target name="phpmd">
<exec executable="phpmd" dir="${basedir}/source">
<arg line="application
xml
codesize,unusedcode,naming
--reportfile ${basedir}/build/logs/phpmd.xml"/>
</exec>
</target>
...

А также не забыть указать её в зависимостях цели сборки по умолчанию:

<target name="build" depends="checkout,lint,php-documentor,php-codesniffer,phpunit,phpcpd,phpmd,pdepend"/>


Для публикации отчетов о дублировании кода (phpcpd) нужно также добавить вручную новую цель в файл build.xml:


...
<target name="phpcpd" >
<exec executable="phpcpd" failonerror="false">
<arg line="--log-pmd ${basedir}/build/logs/pmd-cpd.xml
${basedir}/source/application" />
</exec>
</target>
...

А также не забыть указать её в зависимостях цели сборки по умолчанию:

<target name="build" depends="checkout,lint,php-documentor,php-codesniffer,phpunit,phpcpd,phpmd,pdepend"/>


Для построении графиков внутренних зависимостей кода (pdepend) нужно добавить следующую цель в build.xml:


...
<target name="pdepend" depends="lint">
<exec executable="pdepend" dir="${basedir}/source" logerror="on">
<arg line="--summary-xml=${basedir}/build/logs/pdepend.xml
--jdepend-chart=${basedir}/build/graph/jdepend.svg
--overview-pyramid=${basedir}/build/graph/overview-pyramid.svg
--coderank-mode=inheritance,property,method
application" />
</exec>
</target>
...

А также не забыть указать её в зависимостях цели сборки по умолчанию:

<target name="build" depends="checkout,lint,php-documentor,php-codesniffer,phpunit,phpcpd,phpmd,pdepend"/>

Кроме того, для публикации этих графиков на странице метрик, следует добавить в ноду <publishers/> соответствующего проекта в файле config.xml следующую строку:

<publishers>
...
<artifactspublisher subdirectory="graph" dest="artifacts/${project.name}" dir="projects/${project.name}/build/graph"/>
...
</publishers>


Можно добавить и другие цели, которые могут потребоваться для сборки проекта, например создание необходимых каталогов, установка прав на файлы и каталоги, подготовка БД, выполнение других скриптов, минимизация css, js и др.


Наконец, есть возможность рассылать уведомления об удачных и неудачных сборках на email, jabber и др. Ниже пример для рассылки e-mail уведомлений:


<publishers>
...
<email mailhost="localhost"
returnaddress="cruisecontrol@my.marketgid.net"
buildresultsurl="http://localhost:8080/cruisecontrol/buildresults/mgtest"
skipusers="true" spamwhilebroken="true">
<map alias="management" address="root@localhost" />
<map alias="qa" address="user@localhost" />
<map alias="developer" address="user@my.marketgid.net" />
<always address="management" />
<success address="qa" />
<failure address="developer" reportWhenFixed="true" />
</email>
...
</publishers>

В данном случае определены 3 группы получателей; первая группа management получает уведомления о всех сборках, вторая группа qa только об удачных сборках, и третья группа developer о неудачных.


Примеры окончательного варианта файлов настроек config.xml и build.xml в приложении Г и приложении Д.




http://habrahabr.ru/blogs/php/68571/

на русском, есть неточности

http://nohn.net/blog/view/id/cruisecontrol_ant_and_phpunit

настройка phpunit+ant, рассылка email-ов

http://www.phpunit.de/manual/3.2/en/continuous-integration.html

пример cli для phpuc (создание проекта в сс)

http://recursive-design.com/blog/2011/05/13/continuous-integration-for-php-with-php-under-control/

подробное руководство

http://techportal.ibuildings.com/2009/03/03/getting-started-with-phpundercontrol/

неполный мануал и пример выполнения sql-кода в ant

http://topecoders.blogspot.com/2010/05/how-to-configure-phpundercontrol.html

пример cli phpuc для zend, добавление нового таба

http://criticallog.thornet.net/2010/03/02/integrate-php_depend-with-phpundercontrol/

настройка pdepend (+см. комментарии)

http://zendframework.ru/articles/continuous-integration-and-cruisecontrol

CC с учетом специфики zf


Приложение А - скрипт для автоматического запуска CC при загрузке ОС


Скрипт отличается от источника установленными значениями переменных, а также опцией команды su -p, что решает проблему передачи переменной окружения JAVA_HOME серверу CC. Стоить отметить что инициализацию переменных можно вынести в отдельный файл /etc/default/cruisecontrol. Значения, указанные в этом файле, будут иметь приоритет (имеет смысл воспользоваться этой возможностью, если принято решение собрать deb-пакет для установки сервера CC).


#!/bin/sh
#content of /opt/cruisecontrol/init script
# chkconfig: 345 99 05
# description: CruiseControl build loop (see /home/tools)

# CruiseControl Unix Startup Script Version 2.1
#
# based on http://confluence.public.thoughtworks.org/display/CC/UnixStartupScriptVersion1.x
# adapted for multiple projects
# also based on the file attached to the above page created by Jerome Lacoste

#
# CruiseControl startup: Startup and kill script for Cruise Control
#


###################################################################################################
# USER CONFIGURATION
#
# Fill in these values for your Cruise Control setup

# What user will Cruise Control run as? The user will need permission to write and modify files
# in the next entries.
CC_USER=cruisecontrol


# Where is the CC startup script located?
CC_INSTALL_DIR=/opt/cruisecontrol

# In what directory is the config.xml file located for CC?
# default: CC_WORK_DIR=$CC_INSTALL_DIR
CC_WORK_DIR=$CC_INSTALL_DIR

# Where will the cruisecontrol.log file be located?
# default: CC_LOGFILE_DIR=$CC_INSTALL_DIR
CC_LOGFILE_DIR=$CC_INSTALL_DIR


#######################
# ENVIRONMENT ADDITIONS

# Add environement variables here that are needed by your build.
# example:
# export JAVA_HOME=/usr/local/java
#
# or like this for local variables ONLY used in this file:
# JAVA_HOME=/usr/local/java
JAVA_HOME=/usr/lib/jvm/java-6-sun
export JAVA_HOME

# Add path to additional executables needed for project build. See PATH entry below for base config.
# No additional action taken when blank.
PATH_ADDITIONS=



##############################
# CRUISE CONTROL PORT SETTINGS

# Port for Jetty reporting application. You can access it by going to http://localhost:8080
# default CC_WEBPORT=8080
CC_WEBPORT=8080

# JMX port for webapp and Java Management eXtensions (JMX). You can access it by going to http://localhost:8080
# Change only if this port is in use as the webapp will also need modification.
# default CC_JMXPORT=8082
CC_JMXPORT=8082

# RMI port for control via Java's Remote Management Interface (RMI)
# Leave blank to disable.
CC_RMIPORT=



###################################################################################################
# DO NOT MODIFY ENTRIES BELOW THIS LINE

NAME=cruisecontrol
DESC="CruiseControl - continuous integration build loop"


PATH=/sbin:/usr/sbin:/usr/bin:/bin
# add additions if variable has text defined
if [ -n "$PATH_ADDITIONS" ]; then
PATH=$PATH_ADDITIONS:$PATH
fi
export PATH


CC_DAEMON=$CC_INSTALL_DIR/cruisecontrol.sh

CC_CONFIG_FILE=$CC_WORK_DIR/config.xml

CC_LOG_FILE=$CC_LOGFILE_DIR/cruisecontrol.log

CC_COMMAND="$CC_DAEMON -configfile $CC_CONFIG_FILE -webport $CC_WEBPORT -jmxport $CC_JMXPORT -rmiport $CC_RMIPORT"

# overwrite settings from default file
if [ -f /etc/default/cruisecontrol ]; then
. /etc/default/cruisecontrol
fi

# does the executable exist?
test -f $CC_DAEMON || (echo "The executable $CC_DAEMON does not exist!" && exit 0)

if [ `id -u` -ne 0 ]; then
echo "Not starting/stopping $DESC, you are not root."
exit 4
fi

# Get the PID output from the startup script
if [ -f $CC_INSTALL_DIR/cc.pid ]; then
CC_PID=`cat $CC_INSTALL_DIR/cc.pid`
else
echo "No cc.pid file found. CC process may not be controllable from this script!"
fi


case "$1" in

'start')
cd $CC_INSTALL_DIR
# echo "CC environtment at startup" &gt; cc.startup.env
# env &gt;&gt; cc.startup.env
su $CC_USER -p -c "/bin/sh -c \"$CC_COMMAND &gt;&gt; $CC_LOG_FILE 2&gt;&1\"" & RETVAL=$?
echo "$NAME started with jmx on port ${CC_JMXPORT}"
;;

'stop')
if [ -n "$CC_PID" ] && ps -p ${CC_PID} &gt; /dev/null ; then
kill -9 ${CC_PID}
$0 status
RETVAL=$?
else
echo "$NAME is not running"
RETVAL=1
fi
;;

'status')
if [ -n "$CC_PID" ] && ps -p ${CC_PID} &gt; /dev/null ; then
echo $NAME \(pids $CC_PID\) is running
RETVAL=0
else
echo "$NAME is stopped"
RETVAL=1
fi
;;

'restart')
$0 stop && $0 start
RETVAL=$?
;;

*)
echo "Usage: $0 { start | stop | status | restart }"
exit 1
;;
esac
#echo ending $0 $$....
exit 0;


Приложение Б - project-секция config.xml для проекта myProjectName



<cruisecontrol>
...
<project name="myProjectName" buildafterfailed="false">
<listeners>
<currentbuildstatuslistener file="logs/${project.name}/status.txt"/>
</listeners>
<modificationset>
<svn localWorkingCopy="/opt/cruisecontrol/projects/myProjectName/source"/>
</modificationset>
<bootstrappers>
<svnbootstrapper localWorkingCopy="/opt/cruisecontrol/projects/myProjectName/source"/>
</bootstrappers>
<schedule interval="300">
<ant buildfile="projects/${project.name}/build.xml" antscript="/opt/cruisecontrol/apache-ant-1.7.0/bin/ant"/>
</schedule>
<log dir="logs/${project.name}">
<merge dir="projects/${project.name}/build/logs/"/>
</log>
<publishers>
<artifactspublisher dir="projects/${project.name}/build/api" dest="artifacts/${project.name}" subdirectory="api"/>
<artifactspublisher dir="projects/${project.name}/build/coverage" dest="artifacts/${project.name}" subdirectory="coverage"/>
<execute command="phpcb --log projects/${project.name}/build/logs --source projects/${project.name}/source/application --output projects/${project.name}/build/php-code-browser"/>
<artifactspublisher dir="projects/${project.name}/build/php-code-browser" dest="artifacts/${project.name}" subdirectory="php-code-browser"/>
<execute command="/usr/bin/phpuc graph logs/${project.name} artifacts/${project.name}"/>
</publishers>
</project>
...
</cruisecontrol>


Приложение В - сгенерированный конфигурационный файл build.xml для проекта myProjectName



<?xml version="1.0" encoding="UTF-8"?>
<project name="myProjectName" default="build" basedir=".">
<target name="build" depends="checkout,lint,php-documentor,php-codesniffer,phpunit"/>
<target name="checkout">
<exec executable="svn" dir="${basedir}/source" failonerror="on">
<arg line="up"/>
</exec>
</target>
<target name="lint">
<apply executable="php" dir="${basedir}/source" failonerror="on" logerror="on">
<arg line="-l"/>
<fileset dir="${basedir}/source">
<include name="**/*.php"/>
</fileset>
</apply>
</target>
<target name="php-documentor" depends="lint">
<exec executable="phpdoc" dir="${basedir}/source" logerror="on">
<arg line="--title '${ant.project.name}' -ue on -t ${basedir}/build/api -d application -tb '/usr/share/php/data/phpUnderControl/data/phpdoc' -o HTML:Phpuc:phpuc"/>
</exec>
</target>
<target name="php-codesniffer" depends="lint">
<exec executable="phpcs" dir="${basedir}/source" output="${basedir}/build/logs/checkstyle.xml" error="/tmp/checkstyle.error.log">
<arg line="--report=checkstyle --standard=Zend application"/>
</exec>
</target>
<target name="phpunit" depends="lint">
<exec executable="phpunit" dir="${basedir}/source" failonerror="on">
<arg line=" --log-junit ${basedir}/build/logs/phpunit.xml --coverage-clover ${basedir}/build/logs/phpunit.coverage.xml --coverage-html ${basedir}/build/coverage tests"/>
</exec>
</target>
</project>


Приложение Г - пример окончательного варианта файла настройки проекта в config.xml



<?xml version="1.0"?>
<cruisecontrol>
...
<project name="myProjectName" buildafterfailed="false">
<listeners>
<currentbuildstatuslistener file="logs/${project.name}/status.txt"/>
</listeners>
<modificationset>
<svn localWorkingCopy="/opt/cruisecontrol/projects/myProjectName/source"/>
</modificationset>
<bootstrappers>
<svnbootstrapper localWorkingCopy="/opt/cruisecontrol/projects/myProjectName/source"/>
</bootstrappers>
<schedule interval="300">
<ant buildfile="projects/${project.name}/build.xml" antscript="/opt/cruisecontrol/apache-ant-1.7.0/bin/ant"/>
</schedule>
<log dir="logs/${project.name}">
<merge dir="projects/${project.name}/build/logs/"/>
</log>
<publishers>
<artifactspublisher dir="projects/${project.name}/build/api" dest="artifacts/${project.name}" subdirectory="api"/>
<artifactspublisher dir="projects/${project.name}/build/coverage" dest="artifacts/${project.name}" subdirectory="coverage"/>
<execute command="phpcb --log projects/${project.name}/build/logs --source projects/${project.name}/source --output projects/${project.name}/build/php-code-browser"/>
<artifactspublisher dir="projects/${project.name}/build/php-code-browser" dest="artifacts/${project.name}" subdirectory="php-code-browser"/>
<execute command="/usr/bin/phpuc graph logs/${project.name} artifacts/${project.name}"/>
<artifactspublisher subdirectory="graph" dest="artifacts/${project.name}" dir="projects/${project.name}/build/graph"/>
<email mailhost="localhost"
returnaddress="cruisecontrol@my.marketgid.net"
buildresultsurl="http://localhost:8080/cruisecontrol/buildresults/myProjectName"
skipusers="true" spamwhilebroken="true">
<map alias="management" address="root@localhost" />
<map alias="qa" address="user@localhost" />
<map alias="developer" address="user@my.marketgid.net" />
<always address="management" />
<success address="qa" />
<failure address="developer" reportWhenFixed="true" />
</email>
</publishers>
</project>
...
</cruisecontrol>


Приложение Д - пример окончательного варианта файла build.xml



<?xml version="1.0" encoding="UTF-8"?>
<project name="mgtest" default="build" basedir=".">
<target name="build" depends="checkout,mkdirs,lint,php-documentor,php-codesniffer,phpunit,phpcpd,phpmd,pdepend"/>
<target name="checkout">
<exec executable="svn" dir="${basedir}/source" failonerror="on">
<arg line="up"/>
</exec>
</target>
<target name="lint">
<apply executable="php" dir="${basedir}/source" failonerror="on" logerror="on">
<arg line="-l"/>
<fileset dir="${basedir}/source">
<include name="**/*.php"/>
<exclude name="library/Zend/**/*.php"/>
</fileset>
</apply>
</target>
<target name="mkdirs">
<mkdir dir="${basedir}/source/cache/models"/>
</target>
<target name="php-documentor" depends="lint">
<exec executable="phpdoc" dir="${basedir}/source" logerror="on">
<arg line="--title '${ant.project.name}'
-ue on
-t ${basedir}/build/api
-d application
-tb '/usr/share/php/data/phpUnderControl/data/phpdoc'
-o HTML:Phpuc:phpuc"/>
</exec>
</target>
<target name="php-codesniffer" depends="lint">
<exec executable="phpcs" dir="${basedir}/source" output="${basedir}/build/logs/checkstyle.xml" error="/tmp/checkstyle.error.log">
<arg line="--report=checkstyle
--standard=Zend
application"/>
</exec>
</target>
<target name="phpmd">
<exec executable="phpmd" dir="${basedir}/source">
<arg line="application
xml
codesize,unusedcode,naming
--reportfile ${basedir}/build/logs/phpmd.xml"/>
</exec>
</target>
<target name="phpunit" depends="lint">
<exec executable="phpunit" dir="${basedir}/source/tests" failonerror="false">
<arg line="--bootstrap ${basedir}/source/tests/bootstrap.php
--log-junit ${basedir}/build/logs/phpunit.xml
--coverage-clover ${basedir}/build/logs/phpunit.coverage.xml
--coverage-html ${basedir}/build/coverage
."/>
</exec>
</target>
<target name="phpcpd" >
<exec executable="phpcpd" failonerror="false">
<arg line="--log-pmd ${basedir}/build/logs/pmd-cpd.xml
${basedir}/source/application" />
</exec>
</target>
<target name="pdepend" depends="lint">
<exec executable="pdepend" dir="${basedir}/source" logerror="on">
<arg line="--summary-xml=${basedir}/build/logs/pdepend.xml
--jdepend-chart=${basedir}/build/graph/jdepend.svg
--overview-pyramid=${basedir}/build/graph/overview-pyramid.svg
--coderank-mode=inheritance,property,method
application" />
</exec>
</target>
</project>

1 комментарий:

NickSun комментирует...
Этот комментарий был удален автором.