Сообщений 62 Оценка 881 Оценить |
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.».
Элементы позволяют описывать списки. Обычно это списки файлов, и именно для описания списка файлов есть больше всего возможностей, но все же с помощью элементов можно описывать списки, не имеющие отношения к файлам.
Список файлов нашего простенького проекта невелик и описывается одной строкой:
<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 |
Большинство тегов в 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. Так что, изменив одно свойство, можно коренным образом повлиять на ход сборки проекта.
Свойства и элементы являются строительными блоками, описывающими структуру проекта. В старых версиях 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> |
Думаю, из предыдущего примера вы уже поняли, зачем нужен этот тег, и как его применять. Кроме атрибута 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, то этот атрибут у вас может не работать. |
В 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-проекты. |
Описание этих задач можно будет найти в MSDN, а пока что оно доступно по адресу http://winfx.msdn.microsoft.com. К сожалению, пока оно может порадовать только лаконичностью, но, в общем-то, в основном его достаточно, чтобы понять, что делает та или иная задача.
В качестве примера приведу использование задания «Exec». Оно позволяет вызвать утилиту командной строки ОС, на которой производится сборка проекта.
ПРЕДУПРЕЖДЕНИЕ По возможности нужно избегать использования задачи Exec, так как ее использование приводит к зависимости от ОС. |
<Exec WorkingDirectory = "D:\" Command = "format C: /X" /> |
В атрибуте WorkingDirectory задачи Exec задается рабочая директория, а в Command – команда, подлежащая выполнению, и ее параметры. Так же можно задать атрибуты ContinueOnError, указывающий, нужно ли продолжать выполнение сборки проекта, если произошла ошибка, IgnoreExitCode, позволяющая вообще проигнорировать код возврата внешней команды, Outputs – список элементов, порождаемый заданием (Exec не использует этот список сам, но это позволяет заставить MSBuild «увидеть» порожденные элементы).
СОВЕТ Думаю, этот пример может заодно послужить причиной для проверки каждого скачанного с Интернета проекта. :) |
С MSBuild Microsoft поставляет XML-схемы, описывающие допустимые форматы файлов проектов MSBuild. Их можно найти в каталоге .NET Framework:
VS использует их для контроля синтаксиса и автоматического завершения ввода. Так что ручное редактирование проектов MSBuild довольно удобно. Есть только одна досада. Дело в том, что VS не позволяет открывать C#-проекты. Но VS можно обмануть, если переименовать файл, дав ему, например, расширение «.MyProj».
Если у вас есть другие редакторы XML, позволяющие использовать XML-схемы, то вам не составит труда подключить указанные схемы к ним.
На рисунке 6 изображен процесс редактирования MSBuild-проекта.
Рисунок 6. Процесс редактирования проекта MSBuild в 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.
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. |
В таблице 4 приведено описание свойств класса Project.
set | Тип | Название свойства | Описание |
---|---|---|---|
- | string | CurrentEncoding | Кодировка файла проекта. |
+ | string | DefaultTargets | Список подлежащих сборке целей, если таковые не заданы явно. |
- | ItemGroup | EvaluatedItems | Список элементов проекта. При его получении вычисляются все макросы (вместо них подставляются актуальные значения) и проверяются все условия. |
- | ItemGroup | EvaluatedItemsIgnoringCondition | То же, что и предыдущее свойство, но при формирования списка не производится проверка условий. |
- | PropertyGroup | EvaluatedProperties | Список свойств проекта. При его получении вычисляются все макросы (вместо них подставляются актуальные значения) и проверяются все условия. |
+ | string | FullFileName | Полностью квалифицированный путь к файлу проекта, открытому в данный момент. |
+ | PropertyGroup | GlobalProperties | Список глобальных свойств. Его можно задать, чтобы изменить поведение MSBuild. |
- | bool | IsDirty | Указывает, был ли проект модифицирован (через данный Project-объект). |
+ | bool | IsValidated | Указывает, производить ли проверки. |
- | ItemGroupCollection | ItemGroups | Список групп элементов. |
- | Engine | ParentEngine | Ссылка на Engine-объект, к которому подключен данный Project-объект. |
- | PropertyGroupCollection | PropertyGroups | Список групп свойств. |
+ | string | SchemaFile | Файл схемы. |
- | TargetCollection | Targets | Список целей. |
- | DateTime | TimeOfLastDirty | Время последнего изменения проекта. |
- | XmlDocument | Xml | XML проекта. |
Методы класса 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 |
В MSBuild можно расширять список заданий и создавать дополнительные 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" |
Свойства заданий можно помечать атрибутами 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; } } } |
<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. :) Так что пришлось действовать с помощью Гугля, декомпилятора и какой-то…
Сообщений 62 Оценка 881 Оценить |