Сообщений 62    Оценка 881        Оценить  
Система Orphus

MSBuild

Автор: Чистяков Влад (VladD2)
The RSDN Group

Источник: RSDN Magazine #6-2004
Опубликовано: 09.07.2005
Исправлено: 15.04.2009
Версия текста: 1.0
Что за зверь?
Формат файла проекта
Элементы (Items)
Переименования элементов
Свойства
Условия (Condition)
Цели (Target) и задачи (Task)
Import
Вывод сообщений
Цель DumpSpecialMacros
Расширение функциональности стандартных проектов
Список стандартных задач
Пример использования задания
Редактирование проектов MSBuild и xsd
Интеграция с VS 2005
API MSBuild
Преобразование проектов предыдущих версий
Ключи командной строки
Расширение возможностей MSBuild
Собственные Logger-ы
Собственные задачи

MSBuilgApiTest.zip (~27 Kb) – Пример использования MSBuild API (Viewer проектов VS 2005).
Samples.zip (~35 Kb) – Остальные примеры к статье.

ПРЕДУПРЕЖДЕНИЕ

Эта статья сделана на основе предварительной сборки второй беты VS 2005. Хотя вероятность не велика, но в окончательной версии кое-что может измениться.

Что за зверь?

Для сборки маленьких проектов достаточно компилятора командой строки и пакетного файла, содержащего его вызов. Если проект растет, то программисту требуется некая программа, позволяющая отслеживать версии файлов и перекомпилировать модули, зависящие от изменившихся файлов. Издавна такой программой был make. Make появился в Unix и постепенно перекочевал на все платформы. Его идея очень проста. Он считывает файл, содержащий описание зависимостей между исходными (source) файлами и целевыми (target) файлами (получающимся в результате процесса компиляции или другой обработки), сравнивает версии исходных и целевых файлов и принимает решение, нужно ли пересобрать целевые файлы.

Несмотря на простоту идеи, формат make-файла (файла, содержащего описание зависимостей) был не очень понятен. К тому же за долгие годы накопилось много разновидностей этой утилиты, имеющих несколько различающийся синтаксис. Еще одной проблемой было то, что make был изначально рассчитан на пакетную работу в режиме командной строки. С развитием визуальных интегрированных сред разработки (IDE, Integrated Development Environment) стало понятно, что make перестал соответствовать современным потребностям. Так в VS 6.0 (точнее в VC++ 6.0) поддерживалась возможность генерации make-файла по файлу проекта, но сама среда использовала свой (с расширением .dsw) закрытый (в том смысле, что недокументированный) формат проекта. Этот формат, по сути, был аналогичен формату make, но позволял хранить в файле проекта не только зависимости между исходными и целевыми файлами, но и дополнительную информацию о файлах проекта, а также и о самом проекте. К тому же этот формат было проще считывать и записывать программно.

В VS 2002 (вышедшей вслед за VS 6.0) от генерации make-файлов отказались вовсе, тем самым делая невозможным (вернее, затруднительным) компиляцию проектов VS в режиме командной строки. Многие конкурирующие среды (например, Borland Delphi) вообще не имели никаких средств компиляции вне IDE (вернее, такими средствами была сама IDE).

Параллельно с сообществом Windows-программистов с make боролось и сообщество Java-программистов. У них, правда, были иные причины для борьбы. Основной проблемой make для Java-программистов стало то, что make сильно завязан на команды операционной системы. По сути, make ничего не делал сам. Он только проверял зависимости и вызывал внешнюю команду. Ну а команда, естественно, бралась из оболочки ОС, на которой производилась сборка. Естественно, что такой подход – не лучший выбор для кроссплатформных приложений. В пылу борьбы с make внутри проекта «Apache» (http://ant.apache.org/) был создан интересный продукт – Ant.

Основное его отличие от make было в том, что вместо команд ОС для выполнения действий использовались внутренние команды Ant, проецируемые на специально для этого созданные Java-классы (ну и еще тем, что в качестве формата описания зависимостей использовался XML). Со временем Ant стал своеобразным стандартом де-факто в области сборки кроссплатформных проектов. Учитывая то, что формат Ant-а оказался намного более простым в понимании и менее подверженным ошибкам ввода, он быстро завоевал популярность.

Но причем тут Microsoft, спросите вы? Как известно, Microsoft обожает создавать такие же продукты, как у других, но другие. MSBuild не стал исключением. Его прототипом был именно тот самый Ant. Возможно, правда, что реальным прототипом был NAnt (порт Ant-а на .NET Framework), но сути дела это не меняет, так как NAnt – это клон Ant.

Поначалу продукт имел внутреннее имя XMake, но потом оно было изменено на MSBuild, под которым, по всей видимости, и предстанет в окончательной версии.

MSBuild будет входить в поставку .NET Framework 2.0, который в свою очередь, будет входить во множество продуктов. В том числе в новую версию Windows – Longhorn.

Формат файла проекта

MSBuild использует XML в качестве основы формата файла проекта. В листинге 1 приведен пример простого файла проекта (имя проекта «rsc»).

Листинг 1. Файл C#-проекта, сгенерированный VS 2005.
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">
        Debug
    </Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    <ProductVersion>8.0.41115</ProductVersion>
    <SchemaVersion>2.0</SchemaVersion>
    <ProjectGuid>{730C240D-EF8E-4BF6-86C4-E0340AA72396}</ProjectGuid>
    <OutputType>Exe</OutputType>
    <RootNamespace>rsc</RootNamespace>
    <AssemblyName>rsc</AssemblyName>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup 
      Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>.\bin\Debug\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
  </PropertyGroup>
  <PropertyGroup 
      Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugSymbols>false</DebugSymbols>
    <Optimize>true</Optimize>
    <OutputPath>.\bin\Release\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="System" />
    <Reference Include="System.Data" />
    <Reference Include="System.Xml" />
  </ItemGroup>
  <ItemGroup>
    <Compile Include="Program.cs" />
    <Compile Include="Properties\AssemblyInfo.cs" />
    <EmbeddedResource Include="Properties\Resources.resx">
      <Generator>ResXFileCodeGenerator</Generator>
      <LastGenOutput>Resources.cs</LastGenOutput>
    </EmbeddedResource>
    <Compile Include="Properties\Resources.cs">
      <AutoGen>True</AutoGen>
      <DependentUpon>Resources.resx</DependentUpon>
      <DesignTime>True</DesignTime>
    </Compile>
    <Compile Include="Properties\Settings.cs">
      <AutoGen>True</AutoGen>
      <DependentUpon>Settings.settings</DependentUpon>
    </Compile>
    <None Include="app.config" />
    <None Include="Properties\Settings.settings">
      <Generator>SettingsSingleFileGenerator</Generator>
      <LastGenOutput>Settings.cs</LastGenOutput>
    </None>
    <AppDesigner Include="Properties\" />
  </ItemGroup>
  <ItemGroup>
    <ProjectReference Include="..\RSharp.Compiler\RSharp.Compiler.csproj">
      <Project>{153C3D32-D0AE-42F6-8E22-E6ECF5A3CA4A}</Project>
      <Package>{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}</Package>
      <Name>RSharp.Compiler</Name>
    </ProjectReference>
  </ItemGroup>
  <Import Project="$(MSBuildBinPath)\Microsoft.CSHARP.Targets" />
</Project>

Этот проект сгенерирован VS 2005, и потому содержит довольно много непонятных, на первый взгляд, деталей.

Однако если присмотреться, то он содержит только три группы элементов:

Все эти группы элементов заключены в корневой тег Project.

Свойства используются для конфигурирования проекта. Значения свойств можно переопределять различным образом (из командной строки, в импортируемом файле, программно и т.п.). Это позволяет очень гибко управлять ходом сборки проекта.

Элементы олицетворяют собой файлы, входящие в проект (или другие списки). Типов элементов может быть сколько угодно.

Принцип работы MSBuild очень прост. С помощью элементов и свойств определяются, скажем так, данные проекта. Для выполнения таких действий над проектом, как сборка (Build), очистка (Clean), пересборка (Rebuild), вызываются цель (Target). Цели производят некие действия путем вызова задач (Task). Эти действия изменяют дату или состав файлов, и порождают выходные файлы (output). Выполнение целей не обязано быть безусловным. Оно может зависеть от того, были ли изменены элементы (вернее, ассоциированные с ними файлы). Например, такая проверка производится перед сборкой проекта.

Некоторые цели, например, очистка, не требуют предварительных проверок.

Кроме всего прочего, цели могут зависеть друг от друга. При этом зависимая цель выполняется, если произошло выполнение цели, от которой она зависит.


Рисунок 1. Схема обработки файла проекта.

Чтобы лучше понять суть работы MSBuild, давайте попробуем пройти по шагам путь ручного создания простенького C#-проекта.

В качестве подопытного кролика используем классический пример кибер-творчества – приложение из разряда HelloWorld.exe. Вот его исходный код:

using System;

class Program
{
  static void Main()
  {
    Console.WriteLine("Hеllo, world!");
  }
}

Поместим этот код в файл HelloWorld.cs, а сам файл – в отдельную директорию.

Рядом создадим еще один файл с именем HelloWorld.proj, содержащий следующее:

Листинг 2. Пример простого проекта.
<Project 
  DefaultTargets="Build" 
  xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <ItemGroup>
    <Compile Include="HelloWorld.cs" />
  </ItemGroup>

  <Target
    Name="Build"
    Inputs="@(Compile)"
    Outputs="HelloWorld.exe"
  >
    <Csc
      OutputAssembly="HelloWorld.exe"
      Sources="@(Compile)"
      TargetType="exe"
    />
  </Target>
</Project>

Теперь перейдите в эту директорию, и из консоли выполните следующую команду:

%SystemRoot%\Microsoft.NET\Framework\v2.0.41115\MSBuild.exe
СОВЕТ

Для работы с MSBuild лучше не использовать разные FAR-ы и другие утилиты а-ля NC. Дело в том, что MSBuild порождает много выходной текстовой информации, которая к тому же подсвечивается разными цветами. Поэтому удобнее всего использовать MSBuild из обычной консоли, предварительно увеличив высоту виртуального буфера до 1000 строк. Это позволит прокручивать выводимый текст. Можно, конечно, выводить сообщения MSBuild в файл, но при этом вы потеряете подсветку. Можно также написать свой logger и использовать MSBuild внутри собственного приложения.

Она запустит MSBuild, который выполнит сборку проекта.

СОВЕТ

Если вы собираетесь работать с MSBuild из командной строки, то стоит или завести пакетный файл, в котором будет прописан вызов MSBuild.exe, или прописать путь к каталогу второго Framework-а в переменную окружения «PATH».

Если не указать имя конкретного файла проекта, MSBuild попытается найти файл проекта самостоятельно. Для этого он проанализирует список файлов в текущем каталоге и попытается найти файл с известным ему расширением. Это может быть файл с расширением «.*proj», например, «.proj», «.csproj» , «.vcproj», «.MyProj» и т.п., или «.sln». Если такой файл найден, то производится его сборка. Если нашлось более одного файла, выдается сообщение об ошибке.

Если используемая вами версия Framework-а не совпадает с моей (а это почти наверняка так), то следует заменить «41115» на номер имеющейся у вас сборки Framework-а. Его нетрудно определить, зайдя в каталог «%SystemRoot%\Microsoft.NET\Framework».

Если вы все сделали правильно, то должны увидеть сообщения, показанные на рисунке 2.


Рисунок 2. Сообщения MSBuild при успешной сборке проекта.

Повторный запуск выведет сообщения, показанные на рисунке 3.


Рисунок 3. Сообщения MSBuild при сборке проекта, файлы которого не были изменены.

Как видите, MSBuild определил, что дата модификации исходного файла – более ранняя, нежели дата порождаемого при компиляции исполнимого файла, и не стал производить компиляцию.

Сообщения об ошибках окрашены в красный цвет (см. рисунок 4).


Рисунок 4. Сообщения MSBuild, выдаваемые в случае ошибки при сборке.

В данном случая я неверно назвал имя файла, и MSBuild не смог его найти.

Что же происходит при вызове MSBuild? А происходит следующее. MSBuild считывает файл проекта, разбирает его и выполняет цели, указанные в атрибуте DefaultTargets тега Project:

<Project 
  DefaultTargets="Build" 

Атрибут xmlns задает пространство имен, по которому MSBuild определяет версию формата файла проекта. Если задать какое-либо другое пространство имен, или не указать сообщение вовсе, MSBuild выдаст сообщение об ошибке «Y:\RSDN\2004-6\MSBuild\Samples\HеlloWorld\HеlloWorld.proj(1,1): error MSB4041: The default XML namespace of the project must be the MSBuild XML namespace. If the project is authored in the MSBuild 2003 format, please add xmlns="http://schemas.microsoft.com/developer/msbuild/2003" to the <Project> element. If the project has been authored in the old 1.0 or 1.2 format, please convert it to MSBuild 2003 format.».

Элементы (Items)

Элементы позволяют описывать списки. Обычно это списки файлов, и именно для описания списка файлов есть больше всего возможностей, но все же с помощью элементов можно описывать списки, не имеющие отношения к файлам.

Список файлов нашего простенького проекта невелик и описывается одной строкой:

<Compile Include="HelloWorld.cs" />

Compile – это имя типа элемента. Имя типа элемента может быть каким угодно. Чтобы MSBuild мог понять, что это именно элемент теги элементов нужно помещать в тег «PropertyGroup»:

<PropertyGroup>
  <Compile Include="HelloWorld.cs" />
</PropertyGroup>

Можно задать несколько элементов одного типа:

<Compile Include="HelloWorld.cs" />
<Compile Include="file1.cs" />

Кроме того, один элемент может описывать сразу несколько файлов, разделенных точкой с запятой:

<Compile Include="HelloWorld.cs;file1.cs" />

При этом можно задавать относительный путь:

<Compile Include="MyDir\file2.cs" />

Если путь не указан, то считается, что файл находится в том же каталоге, что и проект.

Путь может указывать и на каталоги, не являющиеся подкаталогом каталога проекта:

<Compile Include="..\HelloWorld.cs" />
<Compile Include="Y:\RSDN\2004-6\MSBuild\Samples\fole2.cs" />

Особенно приятно то, что имя файла может содержать маску (wildcard):

<Compile Include="*.cs" />

Надеюсь, пояснять тут ничего не нужно.

И уж совсем радует то, что и путь тоже может содержать маски:

<Compile Include="*\*.cs" />
<Compile Include="**\*.cs" />

Первый вариант указывает MSBuild, что в состав элементов Compile должны входить все файлы с расширением «.cs» которые находятся в подкаталогах, непосредственно вложенных в каталог проекта. Файлы из любых других каталогов будут игнорироваться.

Второй вариант указывает MSBuild, что в состав Compile должны входить файлы из всех каталогов, вложенных в каталог проекта, включая сам каталог проекта. Таким образом, «**» позволяет легко включать в проект целую иерархию файлов. Это очень удобно при подключении к проекту автоматически генерируемых файлов.

Бывает ситуации, когда в проект нужно включить все файлы из некоторого каталога, кроме некоторых. Такое исключение можно задать через атрибут «Exclude». Например, следующий элемент будет включать в себя все файлы всех подкаталогов, за исключением файлов, имеющих расширение «.tmp»:

<Compile Include="**\*.*" Exclude="**\*.tmp" />

Сочетание нескольких элементов может описывать списки файлов любой сложности. При этом можно создавать очень краткие и ясные описания. Это, несомненно, большой шаг вперед по сравнению с make-файлами и форматами проектов предыдущих версий VS.

Очень приятно и то, что вся эта функциональность ничего не требует от заданий и целей. Они получают списки файлов в виде строки, где отдельные файлы разделены точкой с запятой (точнее даже будет сказать, что они получают элементы в виде массивов элементов – объектов определенного типа, но об этом я расскажу чуть позже). При этом в этих списках не присутствуют wildcard-ы. Вместо этого список содержит вхождения для всех файлов входящих в определенное wildcard-ами множество.

Чтобы превратить элементы в список файлов можно воспользоваться специальным макросом «@(...)». Этот макрос можно использовать при инициализации атрибутов разных сущностей MSBuild. Например, их можно использовать при задании атрибутов свойствам, заданиям и целям. Так, в примере я указал список входных файлов цели следующим образом:

<Target
  Inputs="@(Compile)"
  ...
>
  ...

При раскрытии макросов вместо «@(Compile)» будет подставлен список файлов, разделенных точкой с запятой (во втором, не обязательном параметре этого макроса, можно задать и другой разделитель). В случае примера «HelloWorld» – это будет всего один файл «HelloWorld.cs».

Точно такая же конструкция использовалась и при задании списка компилируемых файлов задания:

<Csc
  Sources="@(Compile)"
  ...
/>

Если в проекте присутствует несколько типов элементов, то их можно перечислять, разделяя точкой с запятой:

Sources="@(Compile);@(OtherElementType)"

В принципе, макросы можно комбинировать со строками, заданными явно и/или со значениями свойств. Например, если в проекте обязательно есть файл EntryPoint.cs и неизвестный заранее список файлов, определяемых элементами типа «Compile», то можно написать следующее:

Sources="@(Compile);EntryPoint.cs"
ПРЕДУПРЕЖДЕНИЕ

Следует четко понимать, что список, описываемый элементами, вычисляется в момент считывания их описаний и в дальнейшем не изменяется. Даже если список элементов задан маской, этот список вычисляется и превращается в список полностью детерминированных путей к файлам. Таким образом, если в процессе сборки проекта возникнут новые файлы, то они не будут учитываться, даже если они удовлетворяют маске описывающей список элементов. Любая задача может порождать элементы. Для этого используется вложенный в тег задачи тег <Output>, который позволяет указать значение каких свойств должно породить новые элементы. Для создания новых элементов или модификации списка старых можно также воспользоваться специальной задачей CreateItem.

Переименования элементов

Может случиться так, что вы захотите сформировать список на базе уже имеющегося списка элементов. Для этого макрос «@(...)» поддерживает специальный синтаксис, похожий на синтаксис prinf() в C:

@(ИмяТипаЭлементов->'Строка формата')

Здесь строка формата определяет алгоритм преобразования имен файлов, входящих в список элементов. Поддерживаются следующие форматирующие символы:

<Message Importance="High" 
  Text="@(Compile->'Filename=%(Filename);
    Extension=%(Extension); 
    RelativeDir=%(RelativeDir);
    FullPath=%(FullPath);')" />

Выведет:

Filename=HelloWorld;
Extension=.cs;
RelativeDir=;
FullPath=Y:\MSBuild\Samples\HelloWorld\HelloWorld.cs;

Если тип элемента описывает более одного файла, то преобразование будет применено к каждому имени файла.

Свойства

Зачастую списки файлов нужно задавать не в одном месте, а сразу в нескольких. Если определение списка файлов сложное, то лучше описать его через свойство:

<PropertyGroup>
  <InputFiles>@(Compile);@(AutoGenerated);EntryPoint.cs</InputFiles>
</PropertyGroup>
.
<Target
  Name="Build"
  Inputs="$(InputFiles)"
  Outputs="HelloWorld.exe"
>
  <Csc
    OutputAssembly="HelloWorld.exe"
    Sources="$(InputFiles)"
    TargetType="exe"
  />
</Target>

Обратите внимание, что для раскрытия значения свойства тоже используется макрос, но уже макрос «$(...)». Не знаю, зачем разработчики MSBuild ввели два макроса. Возможно, у них были на то весомые причины, но грабли они этим самым приготовили отменные. Перепутав в большом проекте @ и $, можно часами искать ошибку. Диагностические сообщения при этом выдаются совсем невразумительные. Дело в том, что при применении неверного макроса сообщение об ошибке не выдается. Вместо этого результат раскрытия макроса становится пустой строкой. Так, например, если в строке:

      Sources="$(InputFiles)"

Ошибиться и написать:

      Sources="@(InputFiles)"

то MSBuild передаст компилятору пустую строку и сообщение будет выглядеть так (см. рисунок 5).


Рисунок 5. Вот такое невразумительное сообщение можно получить, если перепутать «$» и «@».

Моногие скажут «А что? Очень даже ничего себе сообщеньице...». Но это сообщение компилятора C#. А ведь на его месте может оказаться другой! И этот другой может оказаться не столь интеллектуальным товарищем. В общем, будьте осторожны... двери открываются автоматически... случайным образом. :)

Свойств, как и элементов, может быть сколько угодно. Но, в отличие от элементов, каждое свойство уникально. Если объявить свойство несколько раз, то последнее определение затрет предыдущие. Это удобно, так как позволяет задать значения по умолчанию для свойств, а потом, при необходимости, изменять их. Такие значения удобно задавать во включаемых файлах, речь о которых пойдет ниже.

Изменить значение свойств можно также с помощью параметра командой строки MSBuild «/property:ИмяСвойства=Значение». Такое изменение действует только на время запуска MSBuild и не сохраняется в файле проекта. Это позволяет сделать процесс сборки более гибким. Ведь заданное из командной строки значение свойства переопределяет значение уже имеющихся свойств.

Свойства, как и элементы вычисляются в момент считывания их описания. Если нужно задать значение свойства динамически, то можно воспользоваться задачей CreateProperty.

MSBuild проецирует переменные среды окружения на свойства. Так что с помощью макроса $(...) можно узнать значение любой переменной среды окружения:

<Target Name="Test"><Message Text="$(TEMP)" /></Target>

Эта строчка выведет путь к описанном в переменной среды окружения «TEMP» каталогу, предназначенному для хранения временных файлов.

Приятно, что значения таких свойств также можно переопределять.

Кроме свойств, определенных в проекте, переменных среды окружения и свойств, заданных через параметр командной строки, еще существуют предопределенные свойства. Список предпределенных свойств приведен в таблице 1.

Имя свойстваОписаниеПример
MSBuildProjectDirectoryПолный путь к каталогу проекта.C:\HelloWorld
MSBuildProjectFileИмя файла проекта.HelloWorld.MyProj
MSBuildProjectExtensionРасширение имени файла проекта..MyProj
MSBuildProjectFullPathПолный путь к файлу проекта (квалифицированное имя файла проекта).C:\HelloWorld\HelloWorld.proj
MSBuildProjectNameИмя файла проекта (без учета расширения и пути).HelloWorld
MSBuildBinPathПуть к msbuild.exe. На сегодня совпадает с путем к каталогу .NET Framework 2.0.D:\WINDOWS\Microsoft.NET\Framework\v2.0.41115
Таблица 1. Предопределенные свойства.

Условия (Condition)

Большинство тегов в MSBuild-проекте могут содержать условия. Условия задаются атрибутом «Condition». Например, в листинге, приведенном в начале статьи, есть два определения свойств, отвечающих за наличие отладочной информации и оптимизацию проекта. Одно для отладочной версии проекта:

<PropertyGroup 
     Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
  <DebugSymbols>true</DebugSymbols>
  <DebugType>full</DebugType>
  <Optimize>false</Optimize>
  <OutputPath>.\bin\Debug\</OutputPath>
  <DefineConstants>DEBUG;TRACE</DefineConstants>
</PropertyGroup>

Другое для Release-версии:

<PropertyGroup 
     Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
  <DebugSymbols>false</DebugSymbols>
  <Optimize>true</Optimize>
  <OutputPath>.\bin\Release\</OutputPath>
  <DefineConstants>TRACE</DefineConstants>
</PropertyGroup>

Если свойству «Configuration» будет присвоено значение «Release», проект будет собран в Release-версии. Если «Debug», то в отладочной. Ну, а если свойство не будет определено вовсе, то, значит, не судьба :). Чтобы судьба сложилась как надо, в самом начале файла проекта присутствует вот такая строчка:

<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>

Эта строка, если свойство «Configuration» не установлено, инициализирует его значением «Debug». Собственно, не обязательно «Debug». Это значение устанавливается VS при переключении пользователем конфигурации проекта.

Как я уже говорил выше, при сборке проекта можно задать нужное значение свойств через параметры командной строки MSBuild. Так что, изменив одно свойство, можно коренным образом повлиять на ход сборки проекта.

Цели (Target) и задачи (Task)

Свойства и элементы являются строительными блоками, описывающими структуру проекта. В старых версиях VS, где формат файла проектов был закрытым, и где сборка проектов осуществлялась средствами самой VS, описание проекта на этом и оканчивалось. Но MSBuild, кроме описательной части, содержит также часть функциональную. Функциональная часть описывает, что и как нужно делать. Это описание выражается в виде целей (Target) и задач (Task). Поскольку любой программист может описать любое количество собственных задач и целей, то потенциально их может быть неисчислимое множество. Есть часто встречающиеся цели и задачи, которые уже встретились программистам из Microsoft. :) Эти цели уже описаны в стандартных включаемых файлах. Наиболее часто встречаются цели Build, Clean и Rebuild. Есть еще целая куча стандартных целей, вроде сборки ресурсных файлов или генерации подписей для сборок, но они все же встречаются реже, так что я в качестве примеров буду приводить именно Build, Clean и Rebuild.

Задачи можно рассматривать как некие исполняемые модули, способные выполнять те или иные действия. Например, в листинге 2 используется задача Csc. Это ни что иное, как компилятор C#. Отличие задачи от обычного исполняемого модуля ОС заключается в том, что задача является специальным образом зарегистрированным managed-компонентом. Для унификации взаимодействия с задачами был введен интерфейс Microsoft.Build.Framework.ITask, о котором я расскажу чуть позже. Пока что достаточно знать, что задача – это компонент, общение с которым происходит через свойства и специальные интерфейсы, а также то, что задачи используются для выполнения тех или иных действий над файлами проекта. В поставке .NET Framework имеется множество задач, например: Csc, Vbc, FxCop, Copy, Exec, MakeDir, MSBuild, ResGen.

Итак, задачи – это действия, но выполнять их напрямую нельзя. Выполняются в MSBuild цели (Targets). Именно цели и содержат вызовы задач. Цель описывается тегом Target:

  <Target
    Name="Build"
    Inputs="@(Compile)"
    Outputs="HelloWorld.exe"
  >
    <!-- Список конкретных действий. -->
  </Target>

Как видите, у цели есть имя. Имя должно быть уникальным в рамках проекта. Если встречается вторая цель с таким же именем, то она «затирает» предыдущее описание. Эту особенность можно использовать для изменения поведения уже имеющихся проектных файлов.

MSBuild «выполняет» цели. Конкретную цель можно задать в качестве параметра ключом «/target:ИмяЦели» или программно. Если при сборке не задано конкретной цели, то выполняется сборка цели, указанной в атрибуте DefaultTargets тега Project. Если этот атрибут не указан, то вызовется первая цель, встретившаяся в проекте.

Цель может вызываться без оглядки на состояние файлов проекта, например, довольно бессмысленно ставить условия для выполнения очистки (Clean), так как если даже очищать нечего, то ничего страшного не произойдет. Но многие цели все же должны вызываться только если были изменены файлы проекта. Например, сборка проекта (Build) может занимать много времени, и ее лучше делать, только если с момента прошлой компиляции были изменены те или иные файлы проекта (их дата новее, чем дата выходных файлов).

Для отслеживания зависимостей между файлами, получаемыми в процессе сборки (в дальнейшем я буду называть их «целевыми файлами»), и исходными файлами служат атрибуты Inputs и Outputs. В Inputs задается список исходных файлов, а в Outputs – соответственно, целевых. Перед вызовом цели MSBuild проверяет версии обеих групп файлов, и если исходные файлы новее, чем целевые, вызывает цель. В обратном случае MSBuild пишет, что «Skipping target "Build" because its outputs are up-to-date.» (см. рисунок 3), что означает – «Пропускаю выполнение цели "Build", так как целевые файлы для данной задачи свежи, как белье в рекламе Ленора». Так, в приведенном выше примере цель Build вызывается, только если дата хотя бы одного файла, входящего в список Compile, больше, чем дата HelloWorld.exe.

Часто бывает так, что цель зависит от других целей, и если она вызывается, то перед ней нужно вызывать те цели, от которых она зависит. Для описания подобных зависимостей тег Target поддерживает атрибут DependsOnTargets. В нем можно перечислить цели, от которых зависит выполнение данной цели. Например, следующее объявление приведет к тому, что при вызове цели Build будут последовательно вызваны цели Test и MyBuild:

<Target
  Name="Build"
  DependsOnTargets="Test;MyBuild"
/>

<Target
  Name="MyBuild"
  Inputs="$(InputFiles)"
  Outputs="HelloWorld.exe"
>
  <Csc
    OutputAssembly="HelloWorld.exe"
    Sources="$(InputFiles)"
    TargetType="exe"
  />
</Target>

<Target
  Name="Test"
>
  <Message Text="Test!!!" />
</Target>

Интересно, что на этом основаны средства расширения MSBuild. В стандартных файлах, включаемых в проекты, цель Build описана, как зависящая от списка целей, описанных в свойстве BuildDependsOn:

<Target
  Name="Build"
  Condition=" '$(InvalidConfigurationWarning)' != 'true' "
  Outputs="$(TargetPath)"
  DependsOnTargets="$(BuildDependsOn)"
  />

Чтобы переопределить список зависимостей цели Build, достаточно переопределить свойство BuildDependsOn. Таким образом, приведенный выше пример можно переписать так:

<Import Project="$(MSBuildBinPath)\Microsoft.CSHARP.Targets" />

<PropertyGroup>
  <BuildDependsOn>
    Test;
    MyBuild
  </BuildDependsOn>
</PropertyGroup>

<Target
  Name="MyBuild"
  Inputs="$(InputFiles)"
  Outputs="HelloWorld.exe"
>
  <Csc
    OutputAssembly="HelloWorld.exe"
    Sources="$(InputFiles)"
    TargetType="exe"
  />
</Target>

<Target Name="Test"><Message Text="Test!!!" /></Target>

Import

Думаю, из предыдущего примера вы уже поняли, зачем нужен этот тег, и как его применять. Кроме атрибута Project, позволяющего задать путь к проекту, этот тег поддерживает только атрибут Condition, о котором я уже тоже рассказывал. Так что добавить тут нечего.

Думаю, ясно, как божий день, что этот тег нужен, чтобы вынести часто встречаемые описания в отдельные файлы. Использование тега Import совместно с переопределением других тегов и условиями, задаваемыми атрибутами Condition, позволяют создавать очень гибкие схемы проектов. По сути, в проектах, которыми пользуются конечные пользователи, т.е. мы с вами, остается только декларативное описание свойств и элементов. Все, что связано с целями и задачами, обычно выносится во включаемые файлы.

Включаемым файлам, содержащим описание целей и другую служебную информацию, обычно дают расширение «.targets». Хотя особого смысла это и не имеет, но подчеркивает предназначение этих файлов.

Стандартные включаемые файлы, поставляемые Microsoft, находятся в каталоге .NET Framework «%SystemRoot%\Microsoft.NET\Framework\v2.0.XXXXX». Вот список .targets-файлов, доступных в установленной у меня версии .NET Framework:

Вывод сообщений

Для вывода сообщений в MSBuild имеется три тега: Message, Warning и Error. Два последних, как следует из названия, позволяют инспирировать предупреждение и сообщение об ошибке соответственно. А первый выдать информационное сообщение. Ошибки останавливают выполнение сборки целей и высвечиваются красным цветом. Предупреждения не прерывают сборку и подсвечиваются желтым. Ну а сообщения выводятся градациями серого и никак не влияют на ход сборки проекта. У сообщений имеется атрибут Importance. Доподлинно известно, что этот атрибут может иметь значения «Low» и «High», которые заставляют подавлять, или, соответственно, выводить сообщения при разных уровнях многословности (verbosity), задающихся при помощи параметра командной строки «/verbosity:Уровень»). Если атрибут Importance вовсе опустить, то будет считаться, что сообщение имеет нормальный приоритет. В зависимости от важности сообщение выводится разными градациями серого. Чем менее важно сообщение, тем более темный оттенок оно имеет.

ПРЕДУПРЕЖДЕНИЕ

Атрибут Importance появился только во второй бета-версии VS 2005. Так что если вы используете более старую версию VS 2005, то этот атрибут у вас может не работать.

Цель DumpSpecialMacros

В Microsoft.Common.targets описано много разнообразных целей. Все они в основном относятся к разным стадиям сборки стандартных проектов VS 2005. Однако есть одна цель, которая не относится к сборке проекта, и может быть интересна нам с вами. Эта цель – DumpSpecialMacros. Она вызывается последней в процессе сборки проекта и выводит содержимое наиболее значимых свойств, определенных в Microsoft.Common.targets. Ее приоритет задан как низкий (значение «Low» в атрибуте Importance тега Message, выводящего сообщение). Это приводит к тому, что текст не выводится. Чтобы его увидеть, нужно либо повысить приоритет выводимой информации, либо изменить значение «Low» на «High» непосредственно в файле Microsoft.Common.targets. Вот вывод этой цели на моей машине:

ConfigurationName  =Debug
DevEnvDir          =*Undefined*
OutDir             =bin\Debug\
PlatformName       =.NET
ProjectExt         =.MyProj
ProjectFileName    =HelloWorld2.MyProj
ProjectName        =HelloWorld2
SolutionDir        =*Undefined*
SolutionExt        =*Undefined*
SolutionFileName   =*Undefined*
SolutionName       =*Undefined*
SolutionPath       =*Undefined*
TargetExt          =.exe
TargetFileName     =HelloWorld2.exe
TargetName         =HelloWorld2
MSBuildAllProjects =Y:\RSDN\2004-6\MSBuild\Samples\HelloWorld\HelloWorld2.MyProj;
                    I:\WINDOWS\Microsoft.NET\Framework\v2.0.41115\
                    Microsoft .Common.targets; I:\WINDOWS\Microsoft.NET\Framework\
                    v2.0.41115\Microsoft.CSharp.targets
ProjectDir         =Y:\RSDN\2004-6\MSBuild\Samples\HelloWorld\
ProjectPath        =Y:\RSDN\2004-6\MSBuild\Samples\HelloWorld\HelloWorld2.MyProj
TargetDir          =Y:\RSDN\2004-6\MSBuild\Samples\HelloWorld\bin\Debug\
TargetPath         =Y:\RSDN\2004-6\MSBuild\Samples\HelloWorld
                      \bin\Debug\HelloWorld2.exe

Значения «*Undefined*» появляются потому, что сборка проекта осуществлялась из командной строки. Эти свойства определяются VS 2005, когда сборка производится под ее управлением. В принципе, ничто не мешает задать эти свойства самостоятельно через параметры командной строки.

Думаю вы понимаете, что цель DumpSpecialMacros можно переопределять в своих проектах для отладочных целей.

Расширение функциональности стандартных проектов

Как уже говорилось, стандартные проекты VS 2005 включают в себя стандартные .targets-файлы. В начале каждого такого файла содержится строчка, включающая Microsoft.Common.targets. Этот файл содержит следующую строчку:

<Import Project="$(MSBuildProjectFullPath).user" Condition="Exists('$(MSBuildProjectFullPath).user')"/>

Думаю, вы догадались, что это, и зачем нужно. Это включение некого файла, который должен находиться в каталоге проекта и иметь расширение «.user». Хотя это нигде не документировано, но расширение файла и местоположение данной директивы импорта не оставляют сомнения, что это сделано, чтобы обеспечить возможность расширения возможностей проектов VS без изменения содержимого файлов проекта. VS 2005 использует эту лазейку для задания необходимой информации. Если создать с помощью VS 2005 проект и перейти в каталог этого проекта, то можно обнаружить соответствующий файл. Обычно он содержит следующую информацию:

<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <LastOpenVersion>8.0.41115</LastOpenVersion>
    <ProjectView>ProjectFiles</ProjectView>
    <ProjectTrust>0</ProjectTrust>
  </PropertyGroup>
</Project>

Список стандартных задач

На сегодня Microsoft документировал некоторые поставляемые им задачи. Их список приведен в таблице 2.

ЗадачаОписание
AL Обертка над AL.exe (линковщиком сборок).
CopyПозволяет скопировать файл.
CreateItem Позволяет динамически создать элемент.
CreateProperty Позволяет динамически создать свойство.
Csc Компилятор C#. Обертка над csc.exe.
Delete Позволяет удалить файл.
Exec Позволяет выполнить команду ОС или внешнюю утилиту.
GenerateApplicationManifest Позволяет сгенерировать манифест сборки.
GenerateDeployManifest Позволяет сгенерировать манифест распространения для ClickOnce.
LC Обертка над LC.exe. Позволяет сгенерировать .license-фйла из .licx.
MakeDir Позволяет создать каталог.
MSBuild Позволяет вызвать сборку MSBuild-проекта, т.е. произвести вложенную сборку проекта.
RegisterAssembly Позволяет зарегистрировать сборку в качестве COM-объекта.
RemoveDir Позволяет удалить каталог.
ResGen Обертка над resgen.exe. Компилятор managed-ресурсов.
ResolveAssemblyReference Позволяет найти путь к сборкам. На вход задачи подается имя сборки и список путей, где нужно производить поиск. На выходе задача возвращает полностью квалифицированный путь к файлу сборки.
ResolveCOMReference Позволяет найти путь к библиотеке типов.
Touch Позволяет установить дату модификации файлов.
UnregisterAssembly Производит дерегистрацию сборок зарегистрированных с помощью задачи RegisterAssembly.
Vbc Компилятор VS.NET.
VCBuild Обертка над vcbuild.exe. Позволяет скомпилировать проект VC.NET. VC как всегда идет свои путем... пока что формат файла проекта VC несовместим с MSBuild. Однако наличие задачи VCBuild позволяет собирать под MSBuild и VC-проекты.
Таблица 2. Список документированных задач, поставляемых Microsoft.

Описание этих задач можно будет найти в MSDN, а пока что оно доступно по адресу http://winfx.msdn.microsoft.com. К сожалению, пока оно может порадовать только лаконичностью, но, в общем-то, в основном его достаточно, чтобы понять, что делает та или иная задача.

Пример использования задания

В качестве примера приведу использование задания «Exec». Оно позволяет вызвать утилиту командной строки ОС, на которой производится сборка проекта.

ПРЕДУПРЕЖДЕНИЕ

По возможности нужно избегать использования задачи Exec, так как ее использование приводит к зависимости от ОС.

<Exec 
  WorkingDirectory = "D:\"
  Command = "format C: /X"
/>

В атрибуте WorkingDirectory задачи Exec задается рабочая директория, а в Command – команда, подлежащая выполнению, и ее параметры. Так же можно задать атрибуты ContinueOnError, указывающий, нужно ли продолжать выполнение сборки проекта, если произошла ошибка, IgnoreExitCode, позволяющая вообще проигнорировать код возврата внешней команды, Outputs – список элементов, порождаемый заданием (Exec не использует этот список сам, но это позволяет заставить MSBuild «увидеть» порожденные элементы).

СОВЕТ

Думаю, этот пример может заодно послужить причиной для проверки каждого скачанного с Интернета проекта. :)

Редактирование проектов MSBuild и xsd

С MSBuild Microsoft поставляет XML-схемы, описывающие допустимые форматы файлов проектов MSBuild. Их можно найти в каталоге .NET Framework:

VS использует их для контроля синтаксиса и автоматического завершения ввода. Так что ручное редактирование проектов MSBuild довольно удобно. Есть только одна досада. Дело в том, что VS не позволяет открывать C#-проекты. Но VS можно обмануть, если переименовать файл, дав ему, например, расширение «.MyProj».

Если у вас есть другие редакторы XML, позволяющие использовать XML-схемы, то вам не составит труда подключить указанные схемы к ним.

На рисунке 6 изображен процесс редактирования MSBuild-проекта.


Рисунок 6. Процесс редактирования проекта MSBuild в VS 2005.

Интеграция с VS 2005

Как я уже говорил ранее, VS 2005 умеет создавать и считывать проекты MSBuild. Однако не каждый проект может быть считан VS. Для нормальной работы VS-проект должен содержать ряд стандартных свойств и включать импорт .Targets-файла, содержащего описание требуемых целей и заданий.

VS недостаточно метаинформации, предоставляемой MSBuild. В некоторых случаях VS необходимо, чтобы файл проекта содержал некоторую дополнительную информацию. Для задания дополнительной информации в путь тегов элементов (Items) можно добавлять собственные теги, содержащие метаинформацию. MSBuild считывает содержимое этих тегов, но никак его не интерпретирует. Такие теги в терминологии MSBuild называются атрибутами (довольно неудачное название, так как оно пересекается с атрибутами XML). Их значения можно получить с помощью API MSBuild, что собственно и делает VS. Речь об этом API пойдет чуть ниже.

Для чего VS использует дополнительные атрибуты? Например, если подключить к проекту файл, находящийся за пределами каталога проекта, VS будет отображать его в Solution Explorer так, будто он находится непосредственно в каталоге проекта. На то, что это ссылка на файл, а не сам файл, будет указывать только дополнительный значок внизу иконки файла. Полный путь к файлу можно будет увидеть только в свойствах файла, а реальный – только если посмотреть содержимое файла проекта в текстовом редакторе. Например, если в проекте создать каталог и сделать в нем ссылку на файл «..\..\Shared\PerfCounter.cs», VS создаст описание элемента следующего вида:

<Compile Include="..\..\Shared\PerfCounter.cs">
  <Link>FolderTest\PerfCounter.cs</Link>
</Compile>

Если затем установить для этого файла значение свойства «Copy to Output Directory» в True, то описание элемента станет таким:

<Compile Include="..\..\AlfaBlandCs1\AlfaBlandCs\PerfCounter.cs">
  <Link>FolderTest\PerfCounter.cs</Link>
  <CopyToOutputDirectory>True</CopyToOutputDirectory>
</Compile>

Аналогично можно добавить собственные теги. MSBuild прекрасно переживает это. Однако есть две трудности. Во-первых, считать информацию из них можно только программно (с помощью API MSBuild), а во-вторых, xsd-описание формата файла MSBuild не предусматривает подобных расширений, и при открытии файла проекта, содержащего нестандартные расширения, в редакторе, поддерживающем контроль документов по xsd-схеме, будут выдаваться сообщения об ошибке. Например, при попытке открыть на редактирование файл проекта, содержащий следующее описание:

<Compile Include="..\..\AlfaBlandCs1\AlfaBlandCs\PerfCounter.cs">
  <Link>FolderTest\PerfCounter.cs</Link>
  <CopyToOutputDirectory>True</CopyToOutputDirectory>
  <MyMetadata>Test</MyMetadata>
</Compile>

Будет выдано вот такое сообщение об ошибке:

The element 'Compile' in namespace 'http://schemas.microsoft.com/developer/msbuild/2003' has invalid child element 'MyMetadata' in namespace 'http://schemas.microsoft.com/developer/msbuild/2003'. Expected 'SubType, DependentUpon, AutoGen, DesignTime, Link, DesignTimeSharedInput' in namespace 'http://schemas.microsoft.com/developer/msbuild/2003'.

В общем-то, такое поведение никак не мешает работе, и подобные сообщения в бете 2 (доступной мне на момент написания статьи) можно получить и для некоторых стандартных расширений от Microsoft.

Есть кое-какие проблемы и с поддержкой возможностей MSBuild со стороны VS 2005. Так, при попытке подключить к проекту иерархию файлов, хранящуюся за пределами каталога проекта, VS 2005 свалила все найденные файлы в корне проекта. Если подключать файлы по одному, то проблем не возникает. Но это неудобно, разве что делать это из самой VS.

API MSBuild

MSBuild.exe является всего лишь оболочкой. По сути, он разбирает ключи командной строки, создает объекты Microsoft.Build.BuildEngine.Engine и Microsoft.Build.BuildEngine.Project из Microsoft.Build.BuildEngine.dll, передает им параметры и управление. Вся реальная работа по сборке проекта осуществляется уже ими.

VS 2005 не использует MSBuild.exe. Она использует MSBuild API напрямую. Это позволяет упростить код обработки файлов проекта и увеличить гибкость процесса сборки.

Естественно, что этим API можем воспользоваться и мы с вами. Для упрощения работы с проектами MSBuild я создал небольшой оберточный класс. Его код приведен в листинге 3.

Листинг 3. Оберточный класс MsBuildProjectHelper, упрощающий работу с MSBuild.
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Build.BuildEngine;
using System.IO;
using System.Reflection;

namespace RSharp.Compiler
{
  /// <summary>
  /// Класс предназначен для упрощения работы с MSBuild (утилитой сборки
  /// проектов).
  /// </summary>
  /// <remarks>
  /// <para>Для загрузки проекта воспользуйтесь специальным конструктором или
  /// методом LoadProjectFromFile.</para>
  /// <para>
  /// Далее можно воспользоваться свойством Project (для доступа к 
  /// свойствам и содержимому проекта) или получить список файлов
  /// входящих в проект с помощью метода GetProjectFiles.</para>
  /// <para>Чтобы получить нужный список файлов и правильные значения
  /// свойств, нужно сначала задать значения глобальных свойств проекта.
  /// Это можно сделать с помощью метода Project.SetProperty.</para>
  /// <para>Узнать список и значения доступных глобальных свойств можно 
  /// с помощью Project.GlobalProperties (перебором или по имени свойства).
  /// </para>
  /// <para>Например, чтобы задать Release-конфигурацию, нужно 
  /// выполнить следующий код:
  /// <code>
  /// MsBuildProjectHelper msbuild = new MsBuildProjectHelper(prjPath);
  /// msbuild.Project.SetProperty("Configuration", "Release", "");
  /// </code>
  /// </para>
  /// <para>Чтобы узнать значения свойств, зависящих от конфигурации можно
  /// воспользоваться свойством Project.EvaluatedProperties:
  /// <code>
  /// string defineConstants = 
  ///    msbuild.Project.EvaluatedProperties["DefineConstants"].Value;
  /// </code></para>
  /// </remarks>
  public class MsBuildProjectHelper
  {
    /// <param name="prjPath">
    /// Путь к проекту или Solution MSBuild.
    /// </param>
    public MsBuildProjectHelper(string prjPath)
    {
      LoadProjectFromFile(prjPath);
    }

    /// <summary>
    /// Позволяет получить путь к сборке.
    /// </summary>
    /// <param name="assembly">
    /// Сборка, путь к которой требуется получить.
    /// </param>
    /// <returns>Путь к сборке.</returns>
    public static string GetPath(Assembly assembly)
    {
      string result = new Uri(assembly.CodeBase).LocalPath;
      return result;
    }

    /// <summary>
    /// Позволяет получить путь к каталогу .Net Framework.
    /// </summary>
    public static string GetDotNetRoot()
    {
      return Path.GetDirectoryName(GetPath(typeof(int).Assembly));
    }

    /// <summary>
    /// Загружает проект или Solutiоn MSBuild.
    /// </summary>
    /// <param name="prjPath">Путь к проекту</param>
    public void LoadProjectFromFile(string prjPath)
    {
      if (prjPath == null)
        throw new ArgumentNullException("prjPath");

      // Получаем путь к каталогу, где лежат dll с "целями" компиляции.
      string msBuildPath = GetDotNetRoot();
      // Инициализируем Build Engine
      Engine engine = new Engine(msBuildPath);
      // Создаем хелпер, позволяющий считывать проекты.
      _project = new Project(engine);
      // Грузим проект.
      _project.LoadFromFile(prjPath);
    }

    private Project _project;

    /// <summary>
    /// Возвращает ссылку на загруженный проект.
    /// </summary>
    public Project Project
    {
      get { return _project; }
    }

    /// <summary>
    /// Возвращает список путей к файлам проекта заданного типа.
    /// Можно задать ноль, один или несколько типов.
    /// Если не задать тип вообще, то будут возвращены все входящие
    /// в проект элементы.
    /// При выборке осуществляются все проверки условий и преобразований.
    /// </summary>
    /// <param name="itemTypes">Список типов</param>
    /// <returns>Список путей к файлам.</returns>
    public IEnumerable<string> GetProjectFiles(params string[] itemTypes)
    {
      // Копируем элементы из перечисленных в itemTypes групп в массив.
      List<string> files = new List<string>(200);

      if (itemTypes == null || itemTypes.Length == 0)
        foreach (Item item in Project.EvaluatedItems)
          yield return item.FinalItemSpec;
      else
        foreach (string itemType in itemTypes)
          foreach (Item item in Project.GetEvaluatedItemsByType(itemType))
            yield return item.FinalItemSpec;
    }

    /// <summary>
    /// Возвращает список элементов проекта заданного типа.
    /// Можно задать ноль, один или несколько типов.
    /// Если не задать тип вообще, то будут возвращены все входящие
    /// в проект элементы.
    /// При выборке осущуществляются все проверки условий и преобразований.
    /// <param name="itemTypes"></param>
    /// <returns></returns>
    public IEnumerable<Item> GetProjectItems(params string[] itemTypes)
    {
      // Копируем элементы из перечисленных в itemTypes групп в массив.
      List<string> files = new List<string>(200);

      if (itemTypes == null || itemTypes.Length == 0)
        foreach (Item item in Project.EvaluatedItems)
          yield return item;
      else
        foreach (string itemType in itemTypes)
          foreach (Item item in Project.GetEvaluatedItemsByType(itemType))
            yield return item;
    }
  }
}

С использованием этого класса получение доступа к содержимому проекта не составляет труда. Для получения списка файлов или элементов в этой обертке есть специальные методы GetProjectItems и GetProjectFiles. Остальную информацию можно получить через свойства объекта Project, доступного через одноименное свойство.

Всего в API MSBuild входит несколько основных типов и целое море вспомогательных. В таблице 3 приведено описание основных классов API MSBuild.

КлассОписание
EngineДвижок MSBuild. Откровенно говоря, натуральный рудимент. Ну да, может, я просто не могу оценить всей глубины мысли разработчиков MSBuild.
ProjectПозволяет считывать содержимое MSBuild-проектов и манипулировать их содержимым.
ItemGroupГруппа элементов. Позволяет более тонко манипулировать данными проекта (вплоть до позиции элементов).
ItemОлицетворяет элемент MSBuild.
PropertyОлицетворяет свойство MSBuild.
PropertyGroupГруппа свойств. Позволяет более тонко манипулировать данными проекта (в плоть до позиции отдельных свойств).
TargetОлицетворяет цель MSBuild.
TaskElementОлицетворяет задачу MSBuild.
Таблица 3. Описание основных классов составляющих MSBuild API.

В таблице 4 приведено описание свойств класса Project.

setТипНазвание свойстваОписание
-stringCurrentEncodingКодировка файла проекта.
+stringDefaultTargetsСписок подлежащих сборке целей, если таковые не заданы явно.
-ItemGroupEvaluatedItemsСписок элементов проекта. При его получении вычисляются все макросы (вместо них подставляются актуальные значения) и проверяются все условия.
-ItemGroupEvaluatedItemsIgnoringConditionТо же, что и предыдущее свойство, но при формирования списка не производится проверка условий.
-PropertyGroupEvaluatedPropertiesСписок свойств проекта. При его получении вычисляются все макросы (вместо них подставляются актуальные значения) и проверяются все условия.
+stringFullFileNameПолностью квалифицированный путь к файлу проекта, открытому в данный момент.
+PropertyGroupGlobalPropertiesСписок глобальных свойств. Его можно задать, чтобы изменить поведение MSBuild.
-boolIsDirtyУказывает, был ли проект модифицирован (через данный Project-объект).
+boolIsValidatedУказывает, производить ли проверки.
-ItemGroupCollectionItemGroupsСписок групп элементов.
-EngineParentEngineСсылка на Engine-объект, к которому подключен данный Project-объект.
-PropertyGroupCollectionPropertyGroupsСписок групп свойств.
+stringSchemaFileФайл схемы.
-TargetCollectionTargetsСписок целей.
-DateTimeTimeOfLastDirtyВремя последнего изменения проекта.
-XmlDocumentXmlXML проекта.
Таблица 4. Описание свойств класса Project.

Методы класса Project я приводить полностью не стану. Отчасти потому, что это займет слишком много времени, а отчасти потому, что еще сам в них до конца не разобрался. :) Но отдельные методы привести все-таки стоит.

Метод Build позволяет программно собрать одну или несколько целей проекта:

bool Build(string[] targetNamesToBuild, IDictionary targetOutputs);

Методы BuildTarget позволяет собрать заданную цель:

bool BuildTarget(string targetName, IDictionary targetOutputs);

Метод GetConditionedPropertyValues позволяет получить список условий, использованных в проекте:

string[] GetConditionedPropertyValues(string propertyName);

Это довольно интересный и сложный метод. Дело в том, что нет единого места, где были бы описаны условия, входящие в проект. Но разным программным продуктам такой список может потребоваться. Например, такой список требуется VS для отображения списка конфигураций проекта (не путать со списком конфигураций Solution-а). Так вот этот метод и позволяет получить такой список. Так как для его получения Project производит полный разбор и анализ содержимого проекта, этот список получается полным и непротиворечивым.

Функция GetEvaluatedItemsByType позволяет получить вычисленный список элементов заданного типа, удовлетворяющих текущим условиям.

ItemGroup GetEvaluatedItemsByType(string itemType);

LoadFromFile загружает содержимое проекта из файла:

void LoadFromFile(string projectFileName);

Функция LoadFromXml позволяет загрузить содержимое проекта из XmlDocument:

void LoadFromXml(XmlDocument projectXml);

SaveToFile позволяет записать состояние проекта в файл:

void SaveToFile(string projectFileName);

Функция SaveToFileWithEncoding делает то же самое, что предыдущая, но позволяет указать кодировку, в которой будет записан файл:

void SaveToFileWithEncoding(string projectFileName, ProjectFileEncoding encoding);

SaveToTextWriter позволяет получить XML-представление проекта:

void SaveToTextWriter(TextWriter outTextWriter);

SetProperty позволяет установить значение свойства:

void SetProperty(string propertyName, string propertyValue, string condition);

Следует обратить особое внимание на свойства и методы, содержащие в своем названии слово «Evaluated». Их значения вычисляются. При этом подставляются все макросы и производится вся необходимая фильтрация. В результате получаются конечные списки, которые учитывают все настройки проекта. Если перед обращением к этим свойствам/функциям изменить те или иные свойства проекта, то их новые значения отразятся на результате, выдаваемом этими свойствами/методами. Таким образом, для создания приложений вроде VS нужно просто считать содержимое проекта LoadXxx, настроить (если это необходимо) нужные свойства проекта и изучать те или иные свойства и методы с именем Evaluated.

Чтобы продемонстрировать использование MSBuild API, я создал тестовый проект MSBuilgApiTest. В нем можно найти примеры использования многих описанных здесь функций и свойств. Это приложение позволяет загрузить реальные проекты и изучить их содержимое. На рисунках 7 и 8 показаны снимки экранов этого приложения.


Рисунок 7. MSBuilgApiTest – список элементов в конфигурации «Release».


Рисунок 8. MSBuilgApiTest – список свойств в конфигурации «Release».

MSBuilgApiTest позволяет переключить конфигурацию текущего проекта. В зависимости от выбранной конфигурации, он меняет выводимую информацию. Естественно, что MSBuilgApiTest не делает сам каких бы то ни было проверок. Вместо этого он вовсю использует MSBuild API. Несмотря на это, объем кода в данном проекте довольно велик, так что я не буду приводить его в статье. Кстати, этот пример сделан на базе новой версии библиотеки Windows.Forms, так что MSBuilgApiTest может быть интересен и как пример использования новых элементов управления.

В общем и целом, при наличии такого мощного API создать IDE наподобие VS становится уже не такой неподъемной задачей. А что? Рецепт прост и изящен. Берем MSBuild API для работы с файлами проекта, конфигурациями, сборкой и п.р. Для реализации IntelliSense берем немного доработанный парсер R#. Редактирование кода возлагаем на Scintilla. Склеиваем все это собственным кодом и получаем очень недурственную IDE собственного изготовления. Просто-таки компонентная мечта :). Единственное, что останавливает – это то, что в скором времени будет доступен практически бесплатный C# Express, и то, что неохота тратить на это время.

В любом случае, наличие такого API – это приятный бонус, который способен значительно упростить создание разнообразных продуктов.

К тому же, это своего рода API автоматизации (automation API). Только без COM и очень быстрый (ведь все вызовы делаются в том же процессе, и нет непроизводительных затрат на общение через COM).

Преобразование проектов предыдущих версий

Преобразование проектов предыдущих версий VS можно осуществить с помощью класса Microsoft.Build.Conversion.ProjectFileConverter. Он находится в сборке Microsoft.Build.Conversion.dll. Создать с его помощью конвертер проще простого:

using System;
using System.Text;
using Microsoft.Build.Conversion;

class ConvertProject
{
  static void Main(string[] args)
  {
    ProjectFileConverter c = new ProjectFileConverter();
    c.OldProjectFile = args[0];
    c.NewProjectFile = args[1];
    c.Convert();
  }
}

Ключи командной строки

Как почти у любой утилиты под Windows (и не только) ключи: «-?», «/?», «-h», «-help» и т.п. выводит список ключей командной строки. Так что на этом можно было и закончить, но, наверное, для полноты описания будет неплохо перевести это описание с пристрастием. :)

Итак, общий синтаксис MSBuild таков:

MSBuild.exe [options] [project file]
КлючОписание
/nologo Подавляет вывод стартового сообщения и копирайта.
/version (/ver)Выводит версию MSBuild.
@<file>Заставляет MSBuild считывать ключи командной строки из файла «<file>».
/noautoresponse (/noautorsp)Подавляет считывание файла MSBuild.rsp. Этот файл, как и MSBuild.exe, находится в корне .NET Framework и по умолчанию пуст. Он может включать опции командной строки, которые необходимо задавать по умолчанию. Этот файл считывается в самом начале работы, так что если MSBuild явно заданы некоторые ключи, также имеющиеся в MSBuild.rsp, то они переопределяют значения из этого файла.
/target:<targets> (/t)Позволяет задать список целей, которые требуется собрать. Для разделения списка используется точка с запятой.
/property:<n>=<v> Задает или переопределяет свойства на уровне проекта. <n> - имя свойства, <v> - значение. Для разделения свойств используется точка с запятой. Короткая форма - /p. Пример: /property:WarningLevel=2;OutputDir=bin\DebugSet or override these project-level properties. <n> is the property name, and <v> is the property value. Use a semicolon or a comma to separate multiple properties, or specify each property separately. (Short form: /p) Example: /property:WarningLevel=2;OutputDir=bin\Debug
/logger:<logger> (/l)Позволяет задать собственный (внешний) logger. Logger – это компонент, позволяющий выводить информацию о ходе сборки. Одновременно можно задавать несколько logger-ов.Синтаксис: [<logger class>,]<logger assembly>[;<параметры logger-а> Синтаксис <logger class>: [<partial or full namespace>.]<logger class name>Синтаксис <logger assembly>:{<assembly name>[,<strong name>] | <assembly file>}<параметры logger-а> является необязательными и передаются как есть.Примеры:/logger:XMLLogger,MyLogger,Version=1.0.2,Culture=neutral /logger:XMLLogger,C:\Loggers\MyLogger.dll;OutputAsHTML
/verbosity:<уровень> (/v)Указывает уровень информации выводимый в лог.Почти ничего – q[uiet], мало – m[inimal], обычный объем – n[ormal], детализированная информация – d[etailed], и расширенная диагностика – diag[nostic].
/noconsolelogger (/noconlog)Подавляет вывод лога на консоль.
/validate (/val)Проверяет проект в отношении к схеме принятой по умочанию.
/validate:<схема> (/val:<схема>)Проверяет проект по отношению к схеме заданной параметром «схема».Пример: /validate:MyExtendedBuildSchema.xsd
Таблица 5. Список ключей MSBuild (в скобках приводится сокращенная форма).

Расширение возможностей MSBuild

В MSBuild можно расширять список заданий и создавать дополнительные logger-ы.

Собственные Logger-ы

Logger – это класс, реализующий интерфейс ILogger:

public interface ILogger
{
  void Initialize(IEventSource eventSource);
  void Shutdown();

  string Parameters { get; set; }
  LoggerVerbosity Verbosity { get; set; }
}

Для упрощения реализации logger-ов в Microsoft.Build.Utilities.dll, пространство имен Microsoft.Build.Utilities был добавлен класс Logger:

public abstract class Logger : ILogger
{
  protected Logger();
  public abstract void Initialize(IEventSource eventSource);
  public virtual void Shutdown();

  public string Parameters { get; set; }
  public LoggerVerbosity Verbosity { get; set; }
}

Собственно, единственный метод, который обязательно нужно переопределять – это Initialize. Казалось бы почти все сделано за нас, но как говорится на всякую бочку меда найдется ложка с... :) Единственный параметр этого метода является довольно пушистым интерфейсом, сплошь состоящим из событий:

public interface IEventSource
{
  event AnyEventHandler AnyEventRaised;
  event BuildFinishedEventHandler BuildFinished;
  event BuildStartedEventHandler BuildStarted;
  event CustomBuildEventHandler CustomEventRaised;
  event BuildErrorEventHandler ErrorRaised;
  event BuildMessageEventHandler MessageRaised;
  event ProjectFinishedEventHandler ProjectFinished;
  event ProjectStartedEventHandler ProjectStarted;
  event BuildStatusEventHandler StatusEventRaised;
  event TargetFinishedEventHandler TargetFinished;
  event TargetStartedEventHandler TargetStarted;
  event TaskFinishedEventHandler TaskFinished;
  event TaskStartedEventHandler TaskStarted;
  event BuildWarningEventHandler WarningRaised;
}

Событий не просто много, а очень много. В общем-то все очень просто. Нужно подключиться к этим событиям и вывести поступающую в них информацию.

Думаю, не стоит останавливаться создании собственного logger-а, так как это займет слишком много места, а задача эта довольно рутинная. Если вам все же придется реализовать свой logger, то примеры этого можно найти в Интернете по имени приведенных мной интерфейсов. Ну, а самым лучшим примером, наверно, будет изучение встроенного в MSBuild консольного logger-а Microsoft.Build.BuildEngine.ConsoleLogger находящегося в Microsoft.Build.Engine.dll. Как говорится – «Рефлектор вам в руки». :)

Собственные задачи

Как я уже упоминал, возможности MSBuild можно расширять созданием собственных задач. Создать собственную задачу очень просто. Нужно создать библиотечный проект, к которому подключить ссылки на microsoft.build.framework.dll и (необязательно, но желательно) microsoft.build.utilities.dll. В этой библиотеке нужно создать класс, в котором реализовать интерфейс Microsoft.Build.Framework.ITask. Вот его описание:

public interface ITask
{
  public IBuildEngine BuildEngine { get; set; }
  public object HostObject { get; set; }

  public bool Execute();
}

Реальный интерес в этом интерфейсе представляет только метод Execute, который будет вызываться, когда MSBuild активирует задание. Остальные свойства служат для связи с движком MSBuild или для обратной связи. Чтобы избавить программиста от рутины, связанной с необходимостью каждый раз реализовывать эти свойства, в microsoft.build.utilities.dll был добавлен класс Task:

public abstract class Task : ITask
{
  protected Task();
  protected Task(ResourceManager taskResources);
  protected Task(ResourceManager taskResources, string helpKeywordPrefix);

  public IBuildEngine BuildEngine { get; set; }
  protected string HelpKeywordPrefix { get; set; }
  public object HostObject { get; set; }
  public TaskLoggingHelper Log { get; }
  protected ResourceManager TaskResources { get; set; }

  public abstract bool Execute();
}

Наследование свого класса от класса Task упрощает жизнь разработчика. Как видите, класс содержит только один абстрактный метод Execute. Его и нужно реализовать.

СОВЕТ

Реализацию методов можно упростить до одного нажатия мышью, если воспользоваться smart-тегом, появляющимся при подведении мыши к имени класса, содержащего абстрактные методы.

Остальные свойства упрощают доступ к возможностям MSBuild. Например, свойство Log позволяет вывести сообщения (в том числе об ошибке) или предупреждения, причем это свойство возвращает ссылку на объект-помощник TaskLoggingHelper, который предоставляет просто огромный список методов. Тут тебе и вывод сообщения из ресурса, и преобразование исключения в сообщение. В общем, вагон и маленькая тележка.

Остальные свойства менее интересны, поэтому я не буду на них останавливаться.

Простейшее задание будет выглядеть так:

using System;
using Microsoft.Build.Utilities;
using Microsoft.Build.Framework;

namespace Rsdn.Build.Tasks
{
  public class RsdnTask : Task
  {
    public override bool Execute()
    {
      this.Log.LogMessage(
        MessageImportance.Normal,
        "Hello! :)");
      return true;
    }
  }
}

Это задание всего лишь выводит сообщение.

Чтобы вызвать такое задание, его нужно описать с помощью тега UsingTask:

<Project 
  DefaultTargets="Build" 
  xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

    <UsingTask 
      TaskName="Rsdn.Build.Tasks.RsdnTask" 
      AssemblyFile="<path>\MSBuildTaskTest.dll"/>

  <Target Name="Build">
    <RsdnTask/>
  </Target>

</Project>

Как видите, все довольно просто. Вместо <path> должен быть прямой или относительный путь к каталогу, содержащему сборку. В пути могут использоваться макросы, раскрывающие значения свойств.

Чтобы передать информацию задаче или, наоборот, получить информацию от задачи, нужно описать публичные свойства задачи. Свойства могут быть простых встроенных типов (например, int или string), типа ITaskItem или массивом перечисленных типов. ITaskItem обычно используется именно как тип элемента массива. Этот тип предоставляет интерфейс к элементу проекта. С помощью его свойства AttributeNames можно получить список доступных атрибутов. В их число входят предопределенные атрибуты, список которых приведен в таблице 6, и атрибуты, объявленные в виде тегов внутри тегов элементов (я уже говорил об этой возможности выше).

Имя свойстваОписаниеПример
FullPathПолный путь к файлу.Y:\RSDN\2004-6\MSBuild\Samples\HelloWorld\HelloWorld.cs
RootDirИмя корневого каталога.Y:\
FilenameИмя файла без расширения.HelloWorld
ExtensionРасширение, включая точку.".cs"
RelativeDirОтносительный путь к каталогу.""
DirectoryПуть к файлу за вычетом корневого каталога."RSDN\2004-6\MSBuild\Samples\TaskTest\"
RecursiveDirСудя по названию, должен быть связан с рекурсивными каталогами, но я так и не смог получить в нем значение, отличное от пустой строки.""
IdentityИмя файла с расширением."HelloWorld.cs"
ModifiedTimeВремя модификации файла."2005-02-16 01:54:59.8560160"
CreatedTimeВремя создания файла."2005-03-01 06:21:53.9211632"
AccessedTimeВремя последнего обращения к файлу."2005-03-01 06:23:54.2041216"
Таблица 6. Список предопределенных атрибутов элемента, доступных через интерфейс ITaskItem.

Свойства заданий можно помечать атрибутами Output и Required, объявленными в пространстве имен Microsoft.Build.Framework. Первый говорит, что свойство может использоваться как выходное (в теге <Output> находящемся внутри тега задания), а второе – что свойство обязательно к заполнению.

В листинге 4 содержится код задания, у которого имеется свойство – список элементов «Items». Подробная информация о каждом элементе списка выводится в виде сообщения, после чего список модифицируется. Свойство Items помечено атрибутами Required и Output, что, с одной стороны, заставляет задать его значение, а с другой – позволяет использовать измененное значение для задания элементов проекта. Проект, вызывающий данное задание, приведен на листинге 5. Он передает заданию список элементов с именем «MyItems», и по завершении задания инициализирует полученным списком список элементов «ResultingItems». После этого созданный таким образом список элементов выводится на консоль тегом Message. Результат сего захватывающего действа можно лицезреть на рисунке 9. :)

Листинг 4. Задание RsdnTask, выводящее описание и модифицирующее список элементов (свойства Items).
using System;
using Microsoft.Build.Utilities;
using Microsoft.Build.Framework;

namespace Rsdn.Build.Tasks
{
  // Класс RsdnTask описывает соответствующее задание.
  public class RsdnTask : Task
  {
    private ITaskItem[] _items;

    // Это свойство будет доступно в качестве атрибута задания
    [Required, Output]
    public ITaskItem[] Items
    {
      get { return _items; }
      set { _items = value; }
    }

    public override bool Execute()
    {
      if (Items != null)
      {
        // Выводим приветственное сообщение.
        this.Log.LogMessage(
          MessageImportance.Normal,
          "RsdnTask!\n  Items:");

        // Выводим список элементов «Items»...
        foreach (ITaskItem item in Items)
        {
          this.Log.LogMessage(
            MessageImportance.Normal,
            "    " + item.ItemSpec);

          // И список их атрибутов в формате:
          // «имя-атрибута="значение"»
          foreach (string name in item.AttributeNames)
          {
            this.Log.LogMessage(
              MessageImportance.Normal,
              "      " + name + "=\"" + item.GetAttribute(name) + "\"");
          }
        }
      }
      else
        this.Log.LogWarning("RsdnTask!\n  No Items!");

      // Изменяем состав элементов
      Items = new ITaskItem[] { new TaskItem("test.cs"), Items[0] };
      // Сообщаем, что задание отработало успешно.
      return true;
    }
  }
}
Листинг 5. Проект TaskTest.MyProj, вызывающий задание из листинга 4.
<Project 
  DefaultTargets="Build" 
  xmlns="http://schemas.microsoft.com/developer/msbuild/2003">

  <ItemGroup>
    <MyItems Include="**\*.cs;..\HelloWorld\*.cs">
      <Test1>Test Data 1</Test1>
      <Test2>Test Data 2</Test2>
    </MyItems>
  </ItemGroup>
  
    <UsingTask 
      TaskName="Rsdn.Build.Tasks.RsdnTask" 
      AssemblyFile="MSBuildTaskTest.dll"/>

  <Target Name="Build">
    <RsdnTask Items="@(MyItems)">
      <Output 
          ItemName="ResultingItems"
          TaskParameter="Items"
      />
    </RsdnTask>

    <Message Importance="High" Text="@(ResultingItems)" />
  </Target>
</Project>


Рисунок 9. Результат сборки проекта из листинга 5.

Откровенно говоря, я подозреваю, что порядком утомил вас, дорогие редакторы (надеюсь, они не проспят эти строки и хотя бы заменят редакторов на читателей) (они заметили, но заменять не будут – прим.ред.). Но все же надеюсь, что данная статья поможет вам, если вы возьметесь за тонкую настройку конфигурации своих проектов, или просто захотите сделать навороченный вариант ночной сборки ваших, скорее всего немаленьких, проектов.

Понятно, что скорее всего я что-то упустил, но, к сожалению, на момент написания статьи документация по MSBuild в основном состояла из строк: «Note: This documentation is preliminary and is subject to change.» (с) Microsoft Corporation. :) Так что пришлось действовать с помощью Гугля, декомпилятора и какой-то…


Эта статья опубликована в журнале RSDN Magazine #6-2004. Информацию о журнале можно найти здесь
    Сообщений 62    Оценка 881        Оценить