Так получилось, что год назад, мне пришлось написать билд-план с использованием ant. Он предназначался для нашего небольшого веб-проекта, исполнялся на Hudson и должен был производить: компиляцию, прогон NUnit тестов, подсчет % покрытия кода тестам, поиск дублирующегося кода и выявление основных стилистических несоответствий в коде. Но это вступление, а далее поговорим, о написании билд-плана для анализа файлов проекта, посредством FxCop.
И так! Поехали!
Вводная
Как водится, я разбил билд-план на несколько составных частей:
- dbdeploy.build.xml — отвечает за создание тестовой базы данных и накат появляющихся скриптов
- fxcop.build.xml — отвечает за запуск анализа и обработки FxCop'ом файлов проекта и построения отчета о найденных проблемах
- main.build.xml — тут производятся основные действия по заполнению конфигов, автоматическому поиску sln файлов для их сборки
- ncover.build.xml — в этой части производится построение отчета, о покрытии кода тестами
- simian.build.xml — а тут производится построение отчета, о дублировании в коде
- tests.build.xml — ну а тут производится поиск всех NUnit тестов в папке проекта и их запуск
Такое модульное построение позволяет легко исключать отдельные части, разделенные по конкретной ответственности. Нам же с Вами, предстоит рассмотреть именно устройство fxcop.build.xml файла.
Приступим
Сначала я пробовал передавать, список подготовленных путей до анализируемых файлов, посредством командной строки, но как показала практика, это занятие муторное и долгое. И не очень надежное, так как при расширении проекта, нужно будет обновлять и список файлов для анализа. Тогда я стал искать способы динамического формирования списка файлов и передачи FxCop посредством Ant. Так как анализируемых файлов было не мало, нужна была именно автоматическая система поиска нужных файлов и передача их FxCop. Покопавшись в интернете и почитав мануал по командам Ant-Contrib и Ant, нашел, то что надо. Именно команда subant позволила достичь поставленной цели. Но об этом ниже!
Реализация
Рассмотрим устройство файла. В нем присутствуют несколько задач:
- clean-fxcop-result-folder — очищает папку отчетов FxCop и удаляет динамически сформированных файл параметров для FxCop
- run-fxcop — главная задача, которая производит запуск анализа файлов FxCop'ом
- create-arguments — задача, которая обрабатывает пути к файлам, пригодным для анализа и записывает построчно в динамически формируемый файл суб билд-плана
- write-head-part — производит запись заголовка в динамически формируемый файл суб билд-плана
- write-footer-part — производит запись команд FxCop, завершающей список путей до анализируемых файлов
Далее рассмотрим основные команды Apache Ant для решения задачи.
basename — позволяет получить имя файла с расширением из полного пути.
loadfile — позволяет загрузить определенные данных из файла. В данном случае, таска используется для разбора proj файла .NET проекта.
subant — позволяет выполнить таску из другого билд файла, в данном случае из динамически сформированного для FxCop
propertyregex — позволяет выполнить выборку данных посредством заданного регулярного выражения на входной строке.
if — позволяет добавить в билд файл логику выполнения зависящую от логических выражений.
Теперь можно перейти к рассмотрению каждой из задач отдельно.
write-head-part
<target name="write-head-part">
<echo file="${dynafile.path}${dynafilename}"><?xml version="1.0" encoding="UTF-8"?>
<project name="dyna-fxcop-build" default="run-fx-cop-report-creation" basedir=".">
<target name="run-fx-cop-report-creation">
<exec executable="${fxcop.path}FxCopCmd.exe" failonerror="false"></echo>
</target>
В данной задаче, производится запись стандартного заголовка билд-плана в файл с использованием < >
для экранирования символов < >
, в файл находящийся по пути ${dynafile.path}${dynafilename}
. А так же производится запись задачи exec
для передачи параметров приложению FxCop, необходимых параметров. Именно таким образом передавая параметры посредством arg
, можно решить проблему передачи длинного списка путей до анализируемых файлов.
create-arguments
<target name="create-arguments">
//Вывод пути до файла на консоль для информирования
<echo message="${item.file}"/>
//Получение имени файла с расширением, которое записывается в свойство filename
<basename property="filename" file="${item.file}"/>
//Загрузка в свойство output.path строк содержащих <OutputPath> из файла csproj
<loadfile srcfile="${item.file}" property="output.path">
<filterchain>
<linecontains>
<contains value="<OutputPath>"/>
</linecontains>
</filterchain>
</loadfile>
//Загрузка строк содержащих <OutputType> из файла csproj в свойство output.type
<loadfile srcfile="${item.file}" property="output.type">
<filterchain>
<linecontains>
<contains value="<OutputType>"/>
</linecontains>
</filterchain>
</loadfile>
//Загрузка строк содержащих <AssemblyName> из файла csproj в свойство assembly.name
<loadfile srcfile="${item.file}" property="assembly.name">
<filterchain>
<linecontains>
<contains value="<AssemblyName>"/>
</linecontains>
</filterchain>
</loadfile>
//Выделение значения между открывающим и закрывающим тегом OutputPath и запись в output.path.info
<propertyregex property="output.path.info" input="${output.path}"
regexp="<OutputPath>(.*?)</OutputPath>" select="1" />
//Выделение значения между открывающим и закрывающим тегом OutputType и запись в output.type.info
<propertyregex property="output.type.info" input="${output.type}"
regexp="<OutputType>(.*?)</OutputType>" select="1" />
//Выделение значения между открывающим и закрывающим тегом AssemblyName и запись в assembly.name.info
<propertyregex property="assembly.name.info" input="${assembly.name}"
regexp="<AssemblyName>(.*?)</AssemblyName>" select="1" />
//Получение пути до файла без имени файла
<propertyregex property="item.path" input="${item.file}"
regexp="(.*)\" select="1" />
<echo message="output.type.info = ${output.type.info}"/>
<echo message="output.path = ${output.path}"/>
//Формирование расширения файла в зависимости от значения в свойстве output.type.info
<if>
<contains string="WinExe" substring="${output.type.info}"/>
<then>
<property name="file.name.ext" value="${assembly.name.info}.exe"/>
</then>
<elseif>
<contains string="Exe" substring="${output.type.info}"/>
<then>
<property name="file.name.ext" value="${assembly.name.info}.exe"/>
</then>
</elseif>
<else>
<property name="file.name.ext" value="${assembly.name.info}.dll"/>
</else>
</if>
//Запись параметра <arg value=""/> с заполненным параметром value и записью данного значения в файл.
<echo file="${dynafile.path}${dynafilename}" append="true"> <arg value="/f:${item.path}${output.path.info}${file.name.ext}"/>
</echo>
</target>
В данной задаче производится обработка файлов проекта с расширением proj. Из файла выделяются данные тегов OutputPath, OutputType и AssemblyName для того, чтобы можно было не ориентироваться на название файла проекта (так как попадались такие файлы проектов в которых было изменено имя сборки). И в динамически создаваемый файл билд-плана записываются строки arg для задачи exec с указанием флага /f:.
write-footer-part
<target name="write-footer-part">
<echo file="${dynafile.path}${dynafilename}" append="true"> <arg value="/r:${fxcop.path}Rules"/>
<arg value="/o:${fxcop.report.full.path}"/>
</exec>
</target>
</project></echo>
</target>
Эта задача производит запись заключительной части динамически формируемого билд-плана для FxCop, дописывая директивы FxCop, которые предназначены для указания пути папки с правилами /r:${fxcop.path}Rules
и папки вывода отчета /o:${fxcop.report.full.path}
. Так же производится запись закрывающих тегов для exec
, traget
и project
.
run-fxcop
<target name="run-fxcop">
//Запись заголовка динамического билд-плана
<antcall target="write-head-part"/>
//Перевод файла в режим добавления данных в конец
<echo file="${dynafile.path}${dynafilename}" append="true">
</echo>
<var name="dll.names" value=""/>
//Перебор всех csproj файлов в папке проекта, с передачей пути до файла, записанного в переменной item.file,
//в задачу create-arguments
<foreach target="create-arguments" param="item.file" inheritall="true">
<fileset dir="${basedir}" casesensitive="no">
<include name="**/*.csproj"/>
//Исключаем все что находится в папке /obj/Debug/
<exclude name="**/obj/Debug/**.*"/>
</fileset>
</foreach>
//Запись заключительной части динамического билд-плана
<antcall target="write-footer-part"/>
//Выполнение задачи из динамически созданного билд-плана.
<subant target="run-fx-cop-report-creation">
<fileset dir="${dynafile.path}" includes="${dynafilename}"/>
</subant>
</target>
Ну и самая главная задача билд-плана для FxCop, которая и производит динамическое создание так сказать суб билд-плана для выполнения анализа файлов проекта .NET. В данной задаче посредством write-head-part записывается заголовок в файл, который создается по пути ${dynafile.path}${dynafilename}
. Далее производится перевод файла в режим добавления данных в конец, посредством команды echo с параметром append="true"
.
После этих действий производится перебор файлов с расширением csproj
при помощи foreach
. При этом путь до файла записывается в переменную item.file, которая определена, посредством param="item.file"
. Ну и для того чтобы ant не просматривал содержимое obj/Debug, при помощи инструкции /> эта папка заносится в игнор.
Далее при помощи write-footer-part
записывается заключительная часть динамически формируемого файла билда.
И теперь самое интересное! Так как мы, в динамическом билд-плане, создали задачу с именем run-fx-cop-report-creation
, то теперь мы можем ее исполнить, посредством таски subant, с указанием пути до динамически сформированного файла билд-плана из которого собственно и будет выполнена указанная задача.
Заключение
Надеюсь что данный материал был интересен :) Спасибо за внимание!
<?xml version="1.0" encoding="UTF-8"?>
<project name="fxcop-xxx-project" default="run-fxcop" basedir=".">
<property name="dynafile.path" value="${basedir}"/>
<property name="dynafilename" value="dynabuild.xml"/>
<property name="fxcop.report.dir" value="${basedir}FxCopReports"/>
<property name="fxcop.report.full.path" value="${fxcop.report.dir}fxcop.report.xml"/>
<target name="clean-fxcop-result-folder">
<echo message="Cleaning FxCop result report dir, and dynamic xml"/>
<delete>
<fileset dir="${fxcop.report.dir}" includes="**/*.*"/>
</delete>
<delete file="${dynafile.path}${dynafilename}" failonerror="false"/>
</target>
<target name="run-fxcop">
<antcall target="write-head-part"/>
<echo file="${dynafile.path}${dynafilename}" append="true">
</echo>
<var name="dll.names" value=""/>
<foreach target="create-arguments" param="item.file" inheritall="true">
<fileset dir="${basedir}" casesensitive="no">
<include name="**/*.csproj"/>
<exclude name="**/obj/Debug/**.*"/>
</fileset>
</foreach>
<antcall target="write-footer-part"/>
<subant target="run-fx-cop-report-creation">
<fileset dir="${dynafile.path}" includes="${dynafilename}"/>
</subant>
</target>
<target name="create-arguments">
<echo message="${item.file}"/>
<basename property="filename" file="${item.file}"/>
<loadfile srcfile="${item.file}" property="output.path">
<filterchain>
<linecontains>
<contains value="<OutputPath>"/>
</linecontains>
</filterchain>
</loadfile>
<loadfile srcfile="${item.file}" property="output.type">
<filterchain>
<linecontains>
<contains value="<OutputType>"/>
</linecontains>
</filterchain>
</loadfile>
<loadfile srcfile="${item.file}" property="assembly.name">
<filterchain>
<linecontains>
<contains value="<AssemblyName>"/>
</linecontains>
</filterchain>
</loadfile>
<propertyregex property="output.path.info" input="${output.path}"
regexp="<OutputPath>(.*?)</OutputPath>" select="1" />
<propertyregex property="output.type.info" input="${output.type}"
regexp="<OutputType>(.*?)</OutputType>" select="1" />
<propertyregex property="assembly.name.info" input="${assembly.name}"
regexp="<AssemblyName>(.*?)</AssemblyName>" select="1" />
<propertyregex property="item.path" input="${item.file}"
regexp="(.*)\" select="1" />
<echo message="output.type.info = ${output.type.info}"/>
<echo message="output.path = ${output.path}"/>
<if>
<contains string="WinExe" substring="${output.type.info}"/>
<then>
<property name="file.name.ext" value="${assembly.name.info}.exe"/>
</then>
<elseif>
<contains string="Exe" substring="${output.type.info}"/>
<then>
<property name="file.name.ext" value="${assembly.name.info}.exe"/>
</then>
</elseif>
<else>
<property name="file.name.ext" value="${assembly.name.info}.dll"/>
</else>
</if>
<echo file="${dynafile.path}${dynafilename}" append="true"> <arg value="/f:${item.path}${output.path.info}${file.name.ext}"/>
</echo>
</target>
<target name="write-head-part">
<echo file="${dynafile.path}${dynafilename}"><?xml version="1.0" encoding="UTF-8"?>
<project name="dyna-fxcop-build" default="run-fx-cop-report-creation" basedir=".">
<target name="run-fx-cop-report-creation">
<exec executable="${fxcop.path}FxCopCmd.exe" failonerror="false"></echo>
</target>
<target name="write-footer-part">
<echo file="${dynafile.path}${dynafilename}" append="true"> <arg value="/r:${fxcop.path}Rules"/>
<arg value="/o:${fxcop.report.full.path}"/>
</exec>
</target>
</project></echo>
</target>
</project>
Автор: CyberLight