Continuous Integration: Процесс сборки проекта и трансформации Config-файлов

6 мая 2013 г.

Скачать исходный код

Мы рассмотрели основы трансформации Web.config. Теперь стоит углубится в тему сборки проекта и рассмотреть детально, когда и как делается трансформация файлов конфигурации.

Я считаю, что без детального понимания процесса сборки и трансформации, мы будем использовать эти инструменты не достаточно эффективно и можем не видеть причин возникающих ошибок.

Как происходит сборка проекта

Файл проекта представляет XML-код для утилиты MSBuild. Предназначение у этой утилиты схоже с NAnt и Psake, но в отличие от них MSBuild используется в по-умолчанию в Visual Studio. Когда в VS мы нажимаем Build, фактически в MSBuild запускается наш файл проекта.

Для лучшего понимания стоит изучить основы работы с MSBuild в книге Inside the Microsoft® Build Engine: Using MSBuild and Team Foundation Build.

Из чего состоит файл проекта в Visual Studio

Рассмотрим основные части файла проекта. Из кода убраны повторяющиеся и неважные части.

MvcApplication.csproj:

<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
  <PropertyGroup>
    <Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
    <Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
    
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    <DebugType>full</DebugType>
    <Optimize>false</Optimize>
    <OutputPath>bin\</OutputPath>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    <OutputPath>bin\</OutputPath>
    <DefineConstants>TRACE</DefineConstants>
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="Microsoft.CSharp" />
    <Reference Include="System" />
    <Reference Include="System.Data" />
    
  </ItemGroup>
  <ItemGroup>
    <Compile Include="App_Start\AuthConfig.cs" />
    <Compile Include="App_Start\BundleConfig.cs" />
    
  </ItemGroup>
  <ItemGroup>
    <Content Include="Content\themes\base\images\ui-bg_flat_0_aaaaaa_40x100.png" />
    <Content Include="Content\themes\base\images\ui-bg_flat_75_ffffff_40x100.png" />
    
  
  
  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
  <Import Project="$(VSToolsPath)\WebApplications\Microsoft.WebApplication.targets" Condition="'$(VSToolsPath)' != ''" />
  <Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" Condition="false" />
  <Target Name="MvcBuildViews" AfterTargets="AfterBuild" Condition="'$(MvcBuildViews)'=='true'">
    <AspNetCompiler VirtualPath="temp" PhysicalPath="$(WebProjectOutputDir)" />
  </Target>
  <ProjectExtensions>
    
  </ProjectExtensions>
  <Target Name="AfterBuild">
    <TransformXml Source="Web.config" Transform="Web.$(Configuration).config" Destination="Web.config" StackTrace="true" />
  </Target>
  <Import Project="$(SolutionDir)\.nuget\NuGet.targets" Condition="Exists('$(SolutionDir)\.nuget\NuGet.targets')" />
</Project>

В начале XML-кода мы видим, что первым будет запущен DefaultTargets, который называется Build:

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

Этот target определен в Microsoft.Common.targets, как:

  <PropertyGroup>
    <BuildDependsOn>
      BeforeBuild;
      CoreBuild;
      AfterBuild
    </BuildDependsOn>
  </PropertyGroup>
  <Target
      Name="Build"
      Condition=" '$(_InvalidConfigurationWarning)' != 'true' "
      DependsOnTargets="$(BuildDependsOn)"
      Returns="$(TargetPath)" />

В свойстве BuildDependsOn видно основную последовательность сборки проекта. BeforeBuild и AfterBuild мы будем определять сами, как, например, в прошлый раз в AfterBuild мы добавляли TransformXml. Стоит посмотреть на последовательность выполнения CoreBuild, которые определены в том же файле:

  <PropertyGroup>
    <CoreBuildDependsOn>
      BuildOnlySettings;
      PrepareForBuild;
      PreBuildEvent;
      ResolveReferences;
      PrepareResources;
      ResolveKeySource;
      Compile;
      ExportWindowsMDFile;
      UnmanagedUnregistration;
      GenerateSerializationAssemblies;
      CreateSatelliteAssemblies;
      GenerateManifests;
      GetTargetPath;
      PrepareForRun;
      UnmanagedRegistration;
      IncrementalClean;
      PostBuildEvent
    </CoreBuildDependsOn>
  </PropertyGroup>
  <Target
      Name="CoreBuild"
      DependsOnTargets="$(CoreBuildDependsOn)">

Здесь мы видим более детальную последовательность вызовов всех этапов сборки нашего проекта. Каждый из этих этапов можно посмотреть в файлах *.targets.

Дальше в файле проекта идет определение настроек для каждой конфигурации сборки. В нашем случае есть только две конфигурации:

  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
    <DebugSymbols>true</DebugSymbols>
    
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
    <DebugType>pdbonly</DebugType>
    <Optimize>true</Optimize>
    

После этого определяются ItemGroup'ы для ссылок на сторонние сборки, файлы для компиляции и контент проекта.

  <ItemGroup>
    <Compile Include="App_Start\AuthConfig.cs" />
    <Compile Include="App_Start\BundleConfig.cs" />
    

После этого идет основная часть, в которой содержатся все определения для нашего проекта. Подключаются Microsoft.CSharp.targets (внутри него подключается знакомый нам Microsoft.Common.targets) и Microsoft.WebApplication.targets:

  <Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
  <Import Project="$(VSToolsPath)\WebApplications\Microsoft.WebApplication.targets" Condition="'$(VSToolsPath)' != ''" />

MSBuild, как основной инструмент сборки

Мы увидели, что наши проекты это просто код, который выполняется утилитой MSBuild. Любой Target можно переопределить или дополнить своим кодом. В основном изменениям подвергаются BeforeBuild и AfterBuild.

Что такое Build, Rebuild, Clean?

Мы все пользуемся этими тремя функциями в Visual Studio:

Что значит Build мы только что подробно рассмотрели. Теперь остановимся на Rebuild и Clean. Как можно было догадаться это обычные target'ы, которые определены в Microsoft.Common.targets. Когда мы выбираем в меню команду Rebuild, то MSBuild запускает не DefaultTargets, а Rebuild. Аналогично происходит с Clean.

Clean определен следующим образом:

  <PropertyGroup>
    <CleanDependsOn>
      BeforeClean;
      UnmanagedUnregistration;
      CoreClean;
      CleanReferencedProjects;
      CleanPublishFolder;
      AfterClean
    </CleanDependsOn>
  </PropertyGroup>
  <Target
      Name="Clean"
      Condition=" '$(_InvalidConfigurationWarning)' != 'true' "
      DependsOnTargets="$(CleanDependsOn)" />

Если посмотреть по реализации каждого из target'ов, то мы увидим, что отчищаются все промежуточные файлы и результаты сборок проекта.

Rebuild определен в том же файле:

  <PropertyGroup>
    <_ProjectDefaultTargets Condition="'$(MSBuildProjectDefaultTargets)' != ''">$(MSBuildProjectDefaultTargets)</_ProjectDefaultTargets>
    <_ProjectDefaultTargets Condition="'$(MSBuildProjectDefaultTargets)' == ''">Build</_ProjectDefaultTargets>

    <RebuildDependsOn>
      BeforeRebuild;
      Clean;
      $(_ProjectDefaultTargets);
      AfterRebuild;
    </RebuildDependsOn>
  </PropertyGroup>

  <Target
      Name="Rebuild"
      Condition=" '$(_InvalidConfigurationWarning)' != 'true' "
      DependsOnTargets="$(RebuildDependsOn)"
      Returns="$(TargetPath)"/>

Фактически Rebuild является комбинацией из Clean и Build.

Основные директории *.targets

Вы можете сами увидеть и изучить все основные target'ы, которые уже определены и реализованы в папках:

  • %programfiles%\MSBuild
  • %windir%\Microsoft.NET\Framework\версия

Процесс трансформации Web.config

В примере с трансформацией Web.config мы добавили следующий код в AfterBuild:

MvcApplication.csproj:

...
  <Target Name="AfterBuild">
    <TransformXml Source="Web.config" Transform="Web.$(Configuration).config" Destination="Web.config" StackTrace="true" />
  </Target>
...

Выше мы рассмотрели, что AfterBuild вызывается в самом конце сборки проекта.

Задача TransformXml

Рассмотрим где и как определена задача TransformXml. Мы импортировали файл Microsoft.WebApplication.targets, который в свою очередь импортирует Microsoft.Web.Publishing.targets. В последнем и определена наша задача:

  <UsingTask TaskName="TransformXml" AssemblyFile="Microsoft.Web.Publishing.Tasks.dll"/>

Посмотрим внутренности сборки Microsoft.Web.Publishing.Tasks.dll через dotPeek. У нас есть стандартный класс, который унаследован от Task. В методе Execute видим:

namespace Microsoft.Web.Publishing.Tasks
{
  public class TransformXml : Task
  {
    // ...
    public override bool Execute()
    {
      // ...
      document = this.OpenSourceFile(this.Source);
      xmlTransformation = this.OpenTransformFile(this.Transform, logger);
      flag = xmlTransformation.Apply((XmlDocument) document);
      // ...
  }
}

Идем дальше и смотрим реализацию метода Apply и доходим до TransformLoop и дальше корни уходят в сборку Microsoft.Web.XmlTransform.dll:

namespace Microsoft.Web.XmlTransform
{
  public class XmlTransformation : IServiceProvider, IDisposable
  {
    private void TransformLoop(XmlNodeContext parentContext)
    {
      foreach (XmlNode xmlNode in parentContext.Node.ChildNodes)
      {
        XmlElement element = xmlNode as XmlElement;
        if (element != null)
        {
          XmlElementContext elementContext = this.CreateElementContext(parentContext as XmlElementContext, element);
          // ...
          this.HandleElement(elementContext);
          // ...
        }
      }
    }
  }
}

Получается, что команда TransformXml по определенным XPath выполняет нужные нам трансформации.

Вызов TransformXml в файле проекта

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

MvcApplication.csproj:

  <Target Name="AfterBuild">
    <TransformXml Source="Web.config" Transform="Web.$(Configuration).config" Destination="Web.config" StackTrace="true" />
  </Target>

Исходный файл Source, который мы будем изменять, выставлен в значение Web.config. MSBuild будет ссылаться на директорию проекта, когда начнет искать этот файл.

Описание трансформации Transform выставлено в Web.$(Configuration).config. Переменная $(Configuration) выставляется в зависимости от типа конфигурации, выбранной в Visual Studio, либо в Microsoft.Common.targets:

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

Получается, что если в VS мы выбрали конфигурацию Debug для сборки проекта, то значение Transform будет равно Web.Debug.config.

Преобразованный файл мы записываем в Destination, в нашем случае это Web.config.

Создание своих Task для MSBuild

Вы можете создавать свои задачи по примеру TransformXml. Для этого достаточно создать класс, унаследованные от Task и подключить задачу через UsingTask.

Для многих задач уже есть готовые наборы Task'ов. Кроме них, я рекомендую использовать наборы от сообщества разработчиков с открытым исходным кодом:

Всё в целом

Мы подробно рассмотрели процесс сборки проекта, чтобы он не был черным ящиком с магической кнопкой Build на панеле задач. Исходя из процесса, рассмотрели когда и как срабатывает функция TransformXml, которая трансформирует Web.config.

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


Статья из серии Continuous Integration: Работа с Config-файлами:

  1. Continuous Integration: Трансформация Web.config
  2. Continuous Integration: Процесс сборки проекта и трансформации Config-файлов
  3. Continuous Integration: Создание собственной конфигурации
  4. Continuous Integration: Трансформация App.config
  5. Continuous Integration: Рефакторинг файлов конфигурации

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

  1. Александр Евсеев11 мая 2013 г. в 13:57

    Спасибо. Оказывается всё просто :-)

    ОтветитьУдалить

Моя книга «Антихрупкость в IT»

Как достигать результатов в IT-проектах в условиях неопределённости. Подробнее...