
Так получилось, что год назад, мне пришлось написать билд-план с использованием 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>
В данной задаче производится обработка файлов проекта, с расширением csproj. Из файла, выделяются данные тегов: 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, предназначенные для установки пути, до папки с правилами
/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. В параметрах к subant, указанием путь, до динамически сформированного файла билд-плана, из которого и будет выполнена run-fx-cop-report-creation задача.Заключение
Надеюсь что данный материал был интересен :) Спасибо за внимание!
Полный код xml билд-файла для FxCop
<?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>