Настраиваем удобную сборку проектов в Visual Studio

Эта статья является руководством по настройке сборки C++ проектов Visual Studio. Частично она сводилась из материалов разрозненных статей на эту тему, частично является результатом реверс-инжениринга стандартных конфигурационных файлов Студии. Я написал ее в основном потому что полезность документации от самой Microsoft на эту тему стремится к нулю и мне хотелось иметь под рукой удобный референс к которому в дальнейшем можно будет обращаться и отсылать других разработчиков. Visual Studio имеет удобные и широкие возможности для настройки по-настоящему удобной работы со сложными проектами и мне досадно видеть что из-за отвратительной документации эти возможности очень редко сейчас используются.

В качестве примера попробуем сделать так чтобы в Студию можно было добавлять flatbuffer schema, а Студия автоматически вызывала flatc в тех случаях когда это нужно (и не вызывала — когда изменений не было) и позволяла задавать настройки напрямую через File Properties

6ijeutripbsheofekqliayn0wqa.png

Оглавление


* Level 1: лезем внутрь .vcxproj файлов
     Поговорим о .props файлах
     Но зачем вообще разделять .vcxproj и .props?
     Делаем настройку проекта читабельнее
     Делаем удобным подключение сторонних библиотек
     Project Templates — автоматизируем создание проектов
* Level 2: настраиваем кастомную компиляцию
     Традиционный подход
     Знакомимся с MSBuild targets
     Попробуем создать target для сборки .proto файлов
     Доводим наш модельный пример до ума
     U2DCheck и tlog файлы
     Финализуем наш кастомный .target
     А что насчет CustomBuildStep?
     Правильное копирование файлов
* Level 3: интегрируемся с GUI от Visual Studio
     Вытаскиваем настройки из недр .vcxproj в Configuration Properties
     Объясняем Студии про новые типы файлов
     Ассоциируем настройки с индивидуальными файлами
* Level 4: расширяем функциональность MSBuild

ЗАМЕЧАНИЕ: все приведенные в статье примеры проверялись в VS 2017. В рамках моего понимания они должны работать и в более ранних версиях студии начиная по крайней мере с VS 2012, но обещать я этого не могу.

Level 1: лезем внутрь .vcxproj файлов


Давайте взглянем внутрь типичного .vcxproj автоматически сгенеренного Visual Studio.

Он будет выглядеть как-то примерно так


  
    
      Debug
      Win32
    
    
      Release
      Win32
    
    
      Debug
      x64
    
    
      Release
      x64
    
  
  
    15.0
    {0D35456E-42DA-418B-87D4-55E32B8E1373}
    Win32Proj
    protobuftest
    10.0.17134.0
  
  
  
    Application
    true
    v141
    Unicode
  
  
    Application
    false
    v141
    true
    Unicode
  
  
    Application
    true
    v141
    Unicode
  
  
    Application
    false
    v141
    true
    Unicode
  
  
  
  
  
  
  
    
  
  
    
  
  
    
  
  
    
  
  
  
    true
  
  
    true
  
  
    false
  
  
    false
  
  
    
      Use
      Level3
      Disabled
      true
      WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)
      true
      pch.h
    
    
      Console
      true
    
  
  
    
      Use
      Level3
      Disabled
      true
      _DEBUG;_CONSOLE;%(PreprocessorDefinitions)
      true
      pch.h
    
    
      Console
      true
    
  
  
    
      Use
      Level3
      MaxSpeed
      true
      true
      true
      WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)
      true
      pch.h
    
    
      Console
      true
      true
      true
    
  
  
    
      Use
      Level3
      MaxSpeed
      true
      true
      true
      NDEBUG;_CONSOLE;%(PreprocessorDefinitions)
      true
      pch.h
    
    
      Console
      true
      true
      true
    
  
  
    
  
  
    
      Create
      Create
      Create
      Create
    
    
  
  
    
  
  
  
  


Довольно нечитаемое месиво, не правда ли? И это ведь еще очень небольшой и практически тривиальный файл. Попробуем превратить его во что-то более читабельное и удобное для восприятия.

Поговорим о .props файлах


Для этого обратим пока внимание на то что взятый нами файл — это обычный XML-документ и его можно логически разделить на две части, в первой из которых перечисляются настройки проекта, а во второй — входящие в него файлы. Давайте эти половинки разделим физически. Для этого нам понадобится уже встречающийся в коде тэг Import который является аналогом сишного #include и позволяет включить один файл в другой. Скопируем наш .vcxproj в какой-нибудь другой файл и уберем из него все объявления относящиеся к файлам входящим в проект, а из .vcxproj-а в свою очередь наоборот уберем все кроме объявлений относящихся к файлам собственно входящим в проект. Получившийся у нас файл с настройками проекта, но без файлов в Visual Studio принято называть Property Sheets и сохранять с расширением .props. В свою очередь в .vcxproj мы поставим соответствующий Import

Теперь .vcxproj описывает только файлы входящие в проект и читается намного легче


  
  
    {0D35456E-42DA-418B-87D4-55E32B8E1373}
  
  
    
  
  
    
      Create
      Create
      Create
      Create
    
    
  
  
    
  


Его можно упростить еще больше, убрав лишние XML-элементы. К примеру свойство «PrecompiledHeader» объявляется сейчас 4 раза для разных вариантов конфигурации (release / debug) и платформы (win32 / x64), но каждый раз это объявление одно и то же. Кроме того у нас здесь используется несколько разных ItemGroup тогда как в реальности вполне достаточно одного элемента. В результате приходим к компактному и понятному .vcxproj который просто перечисляет 1) входящие в проект файлы, 2) то чем является каждый из них (плюс настройки специфичные для конкретных отдельных файлов) и 3) содержит в себе ссылку на хранящиеся отдельно настройки проекта.


  
  
    {0D35456E-42DA-418B-87D4-55E32B8E1373}
  
  
    
    
      Create
    
    
    
  


Перезагружаем проект в студии, проверяем сборку — все работает.

Но зачем вообще разделять .vcxproj и .props?


Поскольку в сборке ничего не поменялось, то на первый взгляд может показаться что мы поменяли шило на мыло, сделав бессмысленный «рефакторинг» файла в который нам до этого собственно и не было никакой нужды заглядывать. Однако допустим на минутку что в наш solution входит более одного проекта. Тогда, как несложно заметить, несколько разных .vcxproj-файлов от разных проектов могут использовать один и тот же .props файл с настройками. Мы отделили правила сборки используемые в solution от исходного кода и можем теперь менять настройки сборки для всех однотипных проектов в одном месте. В подавляющем большинстве случаев подобная унификация сборки — это хорошая идея. К примеру добавляя в solution новый проект мы в одно действие тривиально перенесем в него подобным образом все настройки из уже существующих в solution проектов.

Но что если нам все же нужны разные настройки для разных проектов? В этом случае мы можем просто создать несколько разных .props-файлов для разных типов проектов. Поскольку .props-файлы могут совершенно аналогичным образом Import-ить другие .props-файлы, то довольно легко и естественно можно выстроить «иерархию» из нескольких .props-файлов, от файлов описывающих общие настройки для всех проектов в solution до узкоспециализированных версий задающих специальные правила для всего одного-двух проектов в solution. В MSBuild действует правило что если одна и та же настройка объявляется во входном файле дважды (скажем вначале импортится в base.props, а затем объявляется повторно в derived.props который import-ит в своем начале base.props) то более позднее объявление перекрывает более раннее. Это позволяет легко и удобно задавать произвольные иерархии настроек просто перекрывая в каждом .props файле все необходимые для данного .props-а настройки не заботясь о том что они могли быть где-то уже объявлены ранее. В числе прочего где-нибудь в .props-ах разумно импортировать стандартные настройки окружения Студии которые для C++-проекта будут выгледеть вот так:



Отмечу что на практике весьма удобно класть собственные .props файлы в ту же папку что и .sln файл

Поскольку это позволяет удобно импортировать .props независимо от местоположения .vcxproj


  
   ...


Делаем настройку проекта читабельнее


Теперь когда нам больше не требуется возиться с каждым проектом по отдельности мы можем уделить больше внимания настройке процесса сборки. И для начала я рекомендую дать с помощью .props-файлов вменяемые имена большинству интересных объектов в файловой системе относящихся к solution. Для этого нам следует создать тэг PropertyGroup с пометкой UserMacros:


    $(SolutionDir)\..
    $(RepositoryRoot)\projects
    $(RepositoryRoot)\..\ThirdParty
    $(ThirdPartyDir)\protobuf\src
  


Тогда в настройках проектов вместо конструкций вида »…\…\…\ThirdParty\protobuf\src\protoc.exe» мы сможем написать просто »$(ProtoBufRoot)\protoc.exe». Помимо большей читабельности это делает код намного мобильнее — мы можем свободно перемещать .vcxproj не боясь что у него слетят настройки и можем перемещать (или обновлять) Protobuf изменив всего одну строчку в одном из .props файлов.

При последовательном объявлении нескольких PropertyGroups их содержимое будет объединено — перезапишутся только макросы имена которых совпадают с ранее объявлявшимися. Это позволяет легко дополнять объявления во вложенных .props файлах не боясь потерять макросы уже объявленные ранее.

Делаем удобным подключение сторонних библиотек


Обычный процесс включения зависимости от thirdparty-библиотеки в Visual Studio частенько выглядит примерно вот так:

trpe2tayormaecnx1e5gslbufyo.png

Процесс соответствующей настройки включает в себя редактирование сразу нескольких параметров находящихся на разных вкладках настроек проекта и потому довольно зануден. Вдобавок его обычно приходится проделывать по нескольку раз для каждой отдельно взятой конфигурации в проекте, так что нередко в результате подобных манипуляций оказывается что проект в Release-сборке собирается, а в Debug-сборке — нет. Так что это неудобный и ненадежный подход. Но как Вы наверное уже догадываетесь, те же самые настройки можно «упаковать» в props-файл. К примеру для библиотеки ZeroMQ подобный файл может выглядеть примерно так:



  
    
      $(ThirdPartyDir)\libzmq\include;%(AdditionalIncludeDirectories)
      ZMQ_STATIC;%(PreprocessorDefinitions)
    
    
      libzmq-v120-mt-sgd-4_3_1.lib;Ws2_32.Lib;%(AdditionalDependencies)
      libzmq-v120-mt-s-4_3_1.lib;Ws2_32.Lib;%(AdditionalDependencies)
      $(ThirdPartyDir)\libzmq\lib\x64\$(Configuration);%(AdditionalLibraryDirectories)
    
  


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

Для подключения ZeroMQ теперь достаточно просто импортировать данный .props файл.



  
  
   ...


И на этом манипуляции с проектом заканчиваются — MSBuild автоматически подключит необходимые заголовочные файлы и библиотеки и в Release и в Debug сборках. Таким образом потратив немного времени на написание zeromq.props мы получаем возможность надежно и безошибочно подключать ZeroMQ к любому проекту всего в одну строчку. Создатели Студии даже предусмотрели для этого специальный GUI который называется Property Manager, так что любители мышки могут проделать ту же операцию в несколько кликов.

iq9ulkjib5xs3y4dsez_kwylr2w.png

Правда как и остальные инструменты Студии этот GUI вместо читабельного однострочника добавит в код .vcxproj что-то вроде

вот такого кода
  
    
  
  
    
  
  
    
  
  
    
  


Так что я предпочитаю добавлять ссылки на сторонние библиотеки в .vcxproj файлы вручную.

Аналогично тому что уже обсуждалось ранее, работа с ThirdParty-компонентами через .props файлы позволяет так же легко в дальнейшем обновлять используемые библиотеки. Достаточно отредактировать единственный файл zeromq.props — и сборка всего solution синхронно переключится на новую версию. К примеру в наших проектах сборка проекта через этот механизм увязана с менеджером зависимостей Conan который собирает необходимый набор thirdparty-библиотек по манифесту зависимостей и автоматически генерирует соответствующие .props-файлы.

Project Templates — автоматизируем создание проектов


Править вручную .vcxproj-файлы созданные Студией конечно довольно скучно (хотя при наличии навыка и недолго). Поэтому в Студии предусмотрена удобная возможность по созданию собственных шаблонов для новых проектов, которые позволяют провести ручную работу по настройке .vcxproj лишь один раз, после чего повторно использовать ее одним кликом в любом новом проекте. В простейшем случае для этого даже не надо ничего править вручную — достаточно открыть проект который нужно превратить в шаблон и выбрать в меню Project \ Export Template. В открывшемся диалоговом окне можно задать несколько тривиальных параметров вроде имени для шаблона или строки которая будет показываться в его описании, а так же выбрать, будет ли вновь созданный шаблон сразу добавлен в диалоговое окно «New Project». Созданный таким способом шаблон создает копию использованного для его создания проекта (включая все файлы входящие в проект), заменяя в нем только имя проекта и его GUID. В довольно большом проценте случаев этого более чем достаточно.

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


  
    C++ console application
    C++ console application for our project
    VC
    
    
    1000
    true                  `
    OurCppConsoleApp
    true
    Enabled
    true
    ng.ico
  
  
    
      console_app.vcxproj.filters
      main.cpp
      stdafx.cpp
      stdafx.h
    
  


Обратите внимание на параметр ReplaceParameters=«true». В данном случае он применяется только к vcxproj-файлу который выглядит следующим образом:



  
  
    {$guid1$}
    $safeprojectname$
  
  
    
    
      Create
    
  
  
    
  


На месте GUID и RootNamespace как видите стоят не конкретные значения, а «заглушки» $guid1$ и $safeprojectname$. При использовании шаблона, Студия проходит по файлам помеченным ReplaceParamters=«true», ищет в них заглушки вида $name$ и заменяет их на вычисляемые значения по специальному словарю. По умолчанию Студия поддерживает не очень много параметров, но при написании Visual Studio Extensions (о чем мы поговорим чуть позже) туда нетрудно добавить сколько угодно своих собственные параметров вычисляемых (или вводимых пользователем) при запуске диалога по созданию нового проекта из шаблона. Как можно увидеть в файле .vstemplate, тот же словарь может использоваться и для формирования имени файла что позволяет, в частности, сформировать шаблону уникальные имена .vcxproj-файлов для разных проектов. При задании ReplaceParameters=false файл указанный в шаблоне будет просто скопирован без дополнительной обработки.

Полученный ZIP-архив с шаблоном можно добавить в список шаблонов известных Студии одним из нескольких способов. Проще всего просто скопировать этот файл в папку %USERPROFILE%\Documents\Visual Studio XX\Templates\ProjectTemplates. Стоит заметить, что несмотря на то что в этой папке Вы найдете множество разных подпапок совпадающих по названиям с папками в окне создания нового проекта, по факту шаблон следует положить просто в корневую папку поскольку положение шаблона в дереве новых проектов определяется Студией из тэгов ProjectType и ProjectSubType в .vstemplate-файле. Этот способ удобнее всего подходит для создания «персональных» шаблонов уникальных только для Вас и если Вы выберете в диалоге Export Template галочку «Automatically import template into Visual Studio» то Студия именно это и сделает, поместив созданный при экспорте zip-архив в эту папку с шаблонами. Однако делиться такими шаблонами с коллегами путем их ручного копирования конечно не очень удобно. Поэтому давайте познакомимся с чуть более продвинутым вариантом — создадим Visual Studio Extension (.vsix)

Для создания VSIX нам понадобится установить опциональный компонент Студии который так и называется — средства для разработки Visual Studio Extensions:
snqlumiv8ugnupw0fqo15ptydhw.png

После этого в разделе Visual C# \ Extensibility появится вариант «VSIX project». Обратите внимание, что несмотря на свое расположение (C#), он используется для создания любых расширений, в том числе и наборов шаблонов проектов на C++.
1oqqgl_9ds8e7u1qo5_1arwpqhq.png

В созданном VSIX проекте можно делать массу самых разных вещей — к примеру, создать свое собственное диалоговое окно которое будет использоваться для настройки создаваемых по шаблону проектов. Но это отдельная огромная тема для обсуждения которую я не буду в этой статье затрагивать. Для создания же шаблонов в VSIX все устроено предельно просто: создаем пустой VSIX проект, открываем файл .vsixmanifest и прямо в GUI задаем все данные для проекта. Вписываем метаданные (название расширения, описание, лицензия) на вкладке Metadata. Обратите внимание на расположенное в правом верхнем углу поле «Version» — его желательно указать правильно, поскольку Студия впоследствии использует именно его для определения того какая версия расширения установлена на компьютере. Затем идем на вкладку Assets и выбираем «Add new Asset», с Type: Microsoft.VisualStudio.ProjectTemplate, Source: File on filesystem, Path: (имя к zip-архиву с шаблоном). Нажимаем OK, повторяем процесс пока не добавим в VSIX все желаемые шаблоны.
sz7m3ro0bll9zw1zn88_kis3afo.png

После этого остается выбрать Configuration: Release и скомандовать Build Solution. Код писать не требуется, править конфигурационные файлы вручную — тоже. На выходе получается переносимый файл с расширением .vsix, который является, по сути, инсталлятором для созданного нами расширения. Созданный файл будет «запускаться» на любом компьютере с установленной Студией, показывать диалог с описанием расширения и лицензией и предлагать установить его содержимое. Разрешив установку — получаем добавление наших шаблонов в диалоговое окно «Создать новый проект»
ehz6htgeb-yvsmgbsao9hb0p7ag.png

Подобный подход позволяет легко унифицировать работу большого количества человек над проектом. Для установки и использования шаблонов от пользователя не требуется никакой квалификации кроме пары кликов мышкой. Установленное расширение можно посмотреть (и удалить) в диалоге Tools \ Extensions and Updates
6v1cuek5yuf6lbbnweb4ndpsqvc.png

Level 2: настраиваем кастомную компиляцию


ОК, на этом этапе мы разобрались как организованы vcxproj и props файлы и научились их организовывать. Давайте теперь предположим что мы хотим добавить в наш проект парочку .proto схем для сериализации объектов на основе замечательной библиотеки Google Protocol Buffers. Напомню основную идею этой библиотеки: Вы пишите описание объекта («схему») на специальном платформонезависимом мета-языке (.proto-файл) которая компилируется специальным компилятором (protoc.exe) в набор .cpp / .cs / .py / .java / etc. файлов которые реализуют сериализацию / десериализацию объектов по этой схеме в нужном языке программирования и которые Вы можете использовать в своём проекте. Таким образом при компиляции проекта нам нужно первым делом позвать protoc который создаст для нас набор .cpp файлов которые мы в дальнейшем будем использовать.

Традиционный подход


Классическая реализация «в лоб» прямолинейна и состоит в том чтобы просто добавить вызов protoc в pre-build step для проекта которому нужны .proto-файлы. Примерно вот так:

uowg985t9ighok2hqe3kk3simfu.png

Но это не очень удобно:

  • Требуется явно указывать список обрабатываемых файлов в команде
  • При изменении этих файлов билд НЕ будет пересобран автоматически
  • При изменении ДРУГИХ файлов в проекте которые Студия распознает как исходные коды, напротив, без нужды будет выполнен pre-build step
  • Сгенерированные файлы не входят по умолчанию в сборку проекта
  • Если мы включим сгенерированные файлы в проект вручную, то проект будет выдавать ошибку когда мы его будем открывать в первый раз (поскольку файлы еще не сгенерированы первой сборкой).


Вместо этого мы попробуем «объяснить» самой Visual Studio (а точнее используемой ею системе сборки MSBuild) то как следует обрабатывать подобные .proto-файлы.

Знакомимся с MSBuild targets


С точки зрения MSBuild, сборка любого проекта состоит из последовательности сборки сущностей которые называются build targets, сокращенно targets. К примеру сборка проекта может включать в себя выполнение таргета Clean который удалит оставшиеся от предыдущих билдов временные файлы, затем выполнение таргета Compile который скомпилирует проект, затем таргета Link и наконец таргета Deploy. Все эти таргеты вместе с правилами по их сборке не фиксированы заранее, а определяются в самом .vcxproj файле. Если Вы знакомы с nix-овой утилитой make и Вам на ум в этот момент приходит слово «makefile», то Вы совершенно правы: .vcxproj является XML-вариацией на тему makefile.

Но стоп-стоп-стоп скажет тут сбитый с толку читатель. Как это так? Мы просмотрели до этого .vcxproj в простом проекте и там не было ни target-ов ни какого-либо сходства с классическим makefile. О каких target-ах тогда может идти речь? Оказывается что они просто «спрятаны» вот в этой строчке включающей в .vcxproj набор стандартных target-ов для сборки C++ — кода.


«Стандартный» билд-план предлагаемый Студией довольно обширен и предлагает большой набор правил для компиляции C++-кода и «стандартных» таргетов типа Build, Clean и Rebuild к которым умеет «цепляться» Студия. Этот набор часто известен под собирательным названием toolset и заменяя в импорте toolset можно заставить Студию компилировать один и тот же проект с помощью другой версии Студии или, к примеру, интеловским компилятором или Clang. Кроме того при желании от стандартного toolset-а можно вообще отказаться и написать свой собственный toolset с нуля. Но мы будем рассматривать в этой статье более простой вариант в котором мы ничего не будем заменять, а лишь дополним стандартные правила необходимыми нам дополнениями.

Но вернемся обратно к target-ам. Любой target в MSBuild определяется через

  • Список входов (inputs)
  • Список выходов (outputs)
  • Зависимости от других targets (dependencies)
  • Настройки target-а
  • Последовательность фактических шагов выполняемых target-ом (tasks)


Например таргет ClCompile получает на вход список .cpp файлов в проекте и генерирует из них путем таски вызывающей компилятор cl.exe набор .obj файлов. Настройки таргета ClCompile при этом превращаются в флаги компиляции передаваемые cl.exe. Когда мы пишем в .vcxproj файле строчку


то мы добавляем (Include) файл protobuf_tests.cpp в список входов (inputs) данного таргета, а когда пишем


    
      Use
    


то присваем значение «Use» настройке ClCompile.PrecompiledHeader которую target затем превратит в флаг /Yu переданный компилятору cl.exe.

Попробуем создать target для сборки .proto файлов


Добавление нового target-а реализуется с помощью тэга target:


...steps to take...


Традиционно target-ы выносят в подключаемый файл с расширением .targets. Не то чтобы это было строго необходимо (и vcxproj и targets и props файлы внутри являются равнозначным XML-ем), но это стандартная схема именования и мы будем ее придерживаться. Чтобы в коде .vcxproj файла теперь можно было писать что-то вроде


    
    


созданный нами target необходимо добавить в список AvailableItemName


    
      GenerateProtobuf
    


Нам также понадобится описать что же конкретно мы хотим сделать с нашими входными файлами и что должно получиться на выходе. Для этого в MSBuild используется сущность которая называется «task». Таска — это какое-то простое действие которое нужно сделать в ходе сборки проекта. К примеру «создать директорию», «скомпилировать файл», «запустить команду», «скопировать что-то». В нашем случае мы воспользуемся таской Exec чтобы запустить protoc.exe и таской Message чтобы отобразить этот шаг в логе компиляции. Укажем так же что запуск данного target-а следует провести сразу после стандартного таргета PrepareForBuild. В результате у нас получится примерно вот такой файлик protobuf.targets


    
    
      GenerateProtobuf
    
  
  
     
     
  


Мы использовали здесь довольно нетривиальный оператор »%» (batching operator) который означает «для каждого элемента из списка» и автоматически добавляемые метаданные. Идея тут в следующем: когда мы записываем код вида


  
    Test
  


то мы этой записью добавляем в список с названием «ProtobufSchema» дочерний элемент «test.proto» у которого есть дочерний элемент (метадата) AdditionalData содержащая строку «Test». Если мы напишем «ProtobufSchema.AdditionalData» то мы получим доступ к записи «Test». При этом помимо явно объявленных нами метаданных AdditionalData, хитрый MSBuild ради нашего удобства автоматически добавляет к записи еще добрый десяток полезных часто используемых дочерних элементов описанных вот здесь из числа которых мы использовали Identity (исходная строка), Filename (имя файла без расширения) и FullPath (полный путь к файлу). Запись же со знаком % заставляет MSBuild применить описанную нами операцию к каждому элементу из списка — т.е. к каждому .proto файлу по отдельности.

Добавляем теперь

  


в protobuf.props, переписываем наши proto-файлы в .vcxproj-е на тэг ProtobufSchema

  
    ...
    
    
  


и проверяем сборку

1>------ Rebuild All started: Project: protobuf_test, Configuration: Debug x64 ------
1>Compiling schema test.proto
1>Compiling schema test2.proto
1>pch.cpp
1>protobuf_test.cpp
1>protobuf_test.vcxproj -> S:\Temp\msbuild\protobuf_msbuild_integration\x64\Debug\protobuf_test.exe
========== Rebuild All: 1 succeeded, 0 failed, 0 skipped ==========

Ура! Заработало! Правда наши .proto файлы теперь стали не видны в проекте. Лезем в .vcxproj.filters и вписываем там по аналогии

...

    
      Resource Files
    
    
      Resource Files
    
  
...


Перезагружаем проект — файлы снова видны.

Доводим наш модельный пример до ума


Впрочем на самом деле я немного схитрил. Если не создать вручную перед началом билда папку generated, то билд на самом деле падает

1>...\protobuf_test\protobuf.targets(13,6): error MSB3073: The command "...\ThirdParty\protobuf\bin\protoc.exe --cpp_out=.\generated test.proto" exited with code 1.

Чтобы это исправить добавим вспомогательный target который создаст необходимую папку

...

    


С помощью свойства DependsOnTargets мы указываем что перед тем как запускать любую из задач GenerateProtobuf следует запустить PrepareToGenerateProtobuf, а запись @(ProtobufSchema) ссылается на список ProtobufSchema целиком, как единую сущность используемую как вход для этой задачи, так что запущена она будет лишь один раз.

Перезапускам сборку — работает! Давайте попробуем сделать теперь еще раз Rebuild, чтобы уж на этот раз точно во всем убедиться

1>------ Rebuild All started: Project: protobuf_test, Configuration: Debug x64 ------
1>pch.cpp
1>protobuf_test.cpp
1>protobuf_test.vcxproj -> S:\Temp\msbuild\protobuf_msbuild_integration\x64\Debug\protobuf_test.exe
========== Rebuild All: 1 succeeded, 0 failed, 0 skipped ==========

Эм, а куда же пропали наши новые таски? Небольшая отладка — и мы видим что таски на самом деле запускаются MSBuild, но не выполняются поскольку в указанной нами выходной папке уже есть сгенерированные файлы. Проще говоря в Rebuild у нас не работает Clean для .\generated файлов. Исправим это, добавив еще один таргет


    
  


Проверяем — работает. Clean очищает созданные нами файлы, Rebuild пересоздает их заново, повторный вызов Build не запускает без нужды пересборку еще раз.

========== Build: 0 succeeded, 0 failed, 1 up-to-date, 0 skipped ==========

Вносим правку в один из C++ файлов, пробуем сделать Build еще раз

1>------ Buil

© Habrahabr.ru