Предварительная обработка файла C ++ с использованием MSBuild и Visual Studio 2012+ - PullRequest
0 голосов
/ 25 апреля 2019

У меня есть серия стандартных файлов cpp, и каждый из этих файлов содержит специфический для файла оператор #include. Однако содержимое этих включенных файлов должно быть заполнено инструментом предварительной обработки перед вызовом стандартного компилятора C ++.

Сложность в том, что я хочу, чтобы это было полностью интегрировано в Visual Studio с использованием MSBuild. Поэтому, когда я открываю окно свойств Visual Studio для файла cpp, я хочу видеть все стандартные параметры компилятора C ++ и, в идеале, некоторые пользовательские свойства, управляющие инструментом препроцессора. Как аналогия с ООП, я хочу, чтобы мой инструмент сборки унаследовал все от стандартного правила CL MSBuild и добавил к нему некоторые пользовательские свойства и шаги сборки.

Я успешно выполнил это с помощью чрезвычайно трудоемкого процесса, заключающегося в создании собственного пользовательского правила MSBuild и копировании / вставке большинства параметров C ++ в мое пользовательское правило. Наконец, я передаю миллион опций C ++ стандартному компилятору C ++ через запись CommandLineTemplate в моем файле MSBuild .props . Это невероятно сложно, и параметры C ++ не обновляются автоматически при обновлении Visual Studio.

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

1 Ответ

1 голос
/ 29 апреля 2019

Не много любви к MSBuild, я так понимаю ...

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

Обычно, когда вы создаете настройку сборки в VS, вы получаете 3 файла:

MyCustomBuild.xml:
Содержит свойства и переключатели, как показано в листе свойств VS.

MyCustomBuild.props:
Содержит значения по умолчанию для этих свойств. Их можно сделать условными с помощью атрибута Condition.

MyCustomBuild.targers:
Содержит строку для загрузки вашего xml и записи Target / Task.

Итак, первая часть заключалась в расширении существующих свойств C / C ++, как показано в Visual Studio. Я нашел эту ссылку, которая, наконец, дала мне кое-что для работы: https://github.com/Microsoft/VSProjectSystem/blob/master/doc/extensibility/extending_rules.md

Вот бит xml.

<Rule
  Name="RuleToExend"
  DisplayName="File Properties"
  PageTemplate="generic"
  Description="File Properties"
  OverrideMode="Extend"
  xmlns="http://schemas.microsoft.com/build/2009/properties">
  <!-- Add new properties, data source, categories, etc -->
</Rule>

Атрибут имени:
Атрибут Name должен соответствовать расширяемому правилу . В этом случае я хотел расширить правило CL, поэтому я установил этот атрибут = "CL".

Атрибут DisplayName:
Это необязательно. Когда предоставлено, это перезапишет имя инструмента, замеченное на Листе свойств. В этом случае указывается имя инструмента "C / C ++". Я могу изменить его, чтобы показать «Мой C / C ++», установив этот атрибут.

Атрибут PageTemplate:
Если указан этот атрибут, он должен соответствовать значению перезаписанного правила . В этом случае это будет «инструмент». Просто не обращая внимания, кажется, работает нормально. Я подозреваю, что может быть необходимым, если 2 правила имеют одинаковое имя, но другой шаблон. Вы можете использовать это, чтобы уточнить, какой из них вы хотите расширить.

Атрибут описания:
Необязательный. Я не знаю, где это отображается в графическом интерфейсе VS. Может быть, это просто для документирования XML-файла.

Атрибут OverrideMode:
Это важный параметр! Можно установить значение «Расширить» или «Заменить». В моем случае я выбрал «Расширить».

атрибут xmlns:
Необходимые. Не работает должным образом, если не присутствует.

Как следует из ссылки, вы можете указать свойства, источник данных и категории. Имейте в виду, что категории обычно отображаются в порядке их появления в файле XML. Поскольку я расширял существующее правило, все мои пользовательские категории отображались бы после стандартных категорий C / C ++. Учитывая, что мой инструмент предназначен для предварительной обработки файлов, я бы предпочел, чтобы мои собственные параметры находились в верхней части листа свойств. Но я не мог найти способ обойти это.

Обратите внимание, что вам НЕ нужны свойства ItemType / FileExtension или ContenType, обычно используемые для пользовательских правил .

Так что, как только я ввел все это, мои пользовательские параметры предварительной обработки появились вместе со стандартными свойствами C / C ++ в Списке свойств. Обратите внимание, что все эти новые свойства будут присоединены к списку элементов "ClCompile" со всеми другими свойствами C / C ++.

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

Последний шаг - заставить файл .targets делать то, что я хотел.

Первая часть заключалась в том, чтобы «импортировать» (на самом деле не запись импорта) пользовательское правило через типичную запись:

<ItemGroup>
    <PropertyPageSchema Include="$(MSBuildThisFileDirectory)MyCustomBuild.xml" />
</ItemGroup>

Тогда мне нужно было предварительно обработать каждый исходный файл. В идеале было бы лучше предварительно обработать файл и затем скомпилировать его - один файл за раз. Я мог бы сделать это, переписав цель «ClCompile» в моем собственном файле .targets. Эта цель определяется в файле «Microsoft.CppCommon.targets» (расположение в папке «C: \ Program Files (x86)» зависит от версии VS). По сути, я мог бы вырезать и вставить целевую цель в свой файл, а затем добавить код задачи предварительной обработки перед задачей «CL». Мне также нужно было бы преобразовать цель в пакет цели , добавив атрибут «Outputs =% (ClCompile.Identity)» в цель «ClCompile». Без этого моя задача предварительной обработки запустилась бы на всех файлах, прежде чем перейти к задаче «CL», что вернуло бы меня к исходной точке. Наконец, мне нужно было бы иметь дело с файлами предварительно скомпилированных заголовков, так как их нужно сначала скомпилировать.

Все это было слишком много боли. Поэтому я выбрал более простой вариант определения цели, который выглядит следующим образом:

<Target Name="MyPreProcessingTarget"
    Condition="'@(ClCompile)' != ''"
    Outputs ="%(ClCompile.Identity)"
    DependsOnTargets="_SelectedFiles"
    BeforeTargets="ClCompile">

Определено несколько атрибутов, но наиболее важным является атрибут BeforeTargets = "ClCompile". Это то, что заставляет эту цель выполнить до компиляции файлов cpp.

Я также решил выполнить целевую пакетную обработку здесь [Outputs = "% (ClCompile.Identity)"], потому что было проще сделать то, что я хотел сделать, если предполагать, что одновременно обрабатывается 1 файл в моей цели.

Атрибут DependsOnTargets = "_ SelectedFiles" используется, чтобы узнать, есть ли у пользователя графического интерфейса какой-либо выбранный файл в VS Solution Explorer. В этом случае файлы будут сохранены в списке элементов @ (SelectedFiles) (созданном объектом «_SelectedFiles»). Как правило, при выборе определенных файлов в обозревателе решений и выборе их компиляции VS принудительно компилирует их, даже если они актуальны. Я хотел сохранить эту функциональность для автоматически сгенерированных предварительно обработанных включаемых файлов и принудительно регенерировать их, а также для выбранных файлов. Поэтому я добавил этот блок:

<ItemGroup Condition="'@(SelectedFiles)' != ''">
  <IncFilesToDelete Include="%(ClCompile.Filename)_pp.h"/>
</ItemGroup>
<Delete 
  Condition="'@(IncFilesToDelete)' != ''"
  Files="%(IncFilesToDelete.FullPath)" />

Обратите внимание, что автоматически создаваемые включаемые файлы называются SourceFileName_pp.h. Удалив эти файлы, моя Задача предварительной обработки принудительно сгенерирует их заново.

Затем я создаю новый список элементов из списка элементов "ClCompile", но с версиями файлов "_pp.h". Я делаю это с помощью следующего кода:

<ItemGroup>
  <PPIncFiles
    Condition="'@(ClCompile)' != '' and '%(ClCompile.ExcludedFromBuild)' != 'true'"
    Include="%(ClCompile.Filename)_pp.h" />
</ItemGroup>

Финальная часть немного страшнее.

Для запуска моей предварительной обработки exe я использую стандартную задачу «Exec». Но я, очевидно, хочу запустить его, только если исходный файл новее сгенерированного файла. Я делаю это, сохраняя известные метаданные «ModifiedTime» исходного файла и сгенерированный файл в несколько динамических свойств. Но я не могу использовать метаданные ModifiedTime напрямую, так как это несопоставимое значение. Поэтому я использовал следующий код, который я нашел здесь в StackOverflow: Сравнение меток DateTime в Msbuild

<PropertyGroup>
  <SourceFileDate>$([System.DateTime]::Parse('%(ClCompile.ModifiedTime)').Ticks)</SourceFileDate>
  <PPIncFileDate Condition="!Exists(%(PPIncFiles.Identity))">0</PPIncFileDate>  
  <PPIncFileDate Condition="Exists(%PPIncFiles.Identity))">$([System.DateTime]::Parse('%(PPIncFiles.ModifiedTime)').Ticks)</PPIncFileDate>  
</PropertyGroup>

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

Наконец, я могу вызвать свой препроцессор, используя задачу «Exec», следующим образом:

<Exec 
  Condition="'@(PPIncFiles)' != '' and $(SourceFileDate) > $(PPIncFileDate)"  
  Command="pptool.exe [options] %(ClCompile.Identity)" />

Предоставление опций было еще одной головной болью.

Как правило, параметры, определенные в файле xml, просто передаются метаданным «CommandLineTemplate» в файле .props с помощью [OptionName]. Это передаст атрибут «Switch» свойства, определенного в XML-файле. Но это подразумевает определение вашего собственного элемента TaskName, созданного из TaskFactory, в файле .targets. Но в моем случае я просто использовал существующую задачу «Exec», которая ничего не знает о моих пользовательских свойствах. Я не знал, как извлечь атрибут «Switch» в этом случае, и то, что кажется доступным, - это то, что содержится в атрибуте «Name». К счастью, свойство имеет как имя, так и отображаемое имя. DisplayName - это то, что видит пользователь GUI. Поэтому я просто скопировал значение «Switch» в значение «Name» при определении свойств в файле XML. Затем я мог бы передать опцию в Exec Task, используя что-то вроде:

<Exec 
  Condition="'@(PPIncFiles)' != '' and $(SourceFileDate) > $(PPIncFileDate)"      
  Command="pptool.exe %(ClCompile.Option1) %(ClCompile.Option2)... %(ClCompile.Identity)" />

Где я определил все свои свойства как «EnumProperty», с «EnumValue», имеющим Name = «» для отключенных опций, и другим EnumValue, имеющим Name = «switch» для других. Не очень элегантно, но я не знал, как с этим справиться.

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

<PropertyGroup>
    <CleanDependsOn>$(CleanDependsOn);PPIncCleanTarget</CleanDependsOn>
</PropertyGroup>

<Target Name="PPIncCleanTarget"  Condition ="'@(ClCompile)' != ''">
  <ItemGroup>
     <PPIncFilesToDelete Include="%(ClCompile.Filename)_pp.h" />
  </ItemGroup>
  <Delete Files="%(PPIncFilesToDelete.FullPath)" Condition="'@(PPIncFilesToDelete)' != ''"/>
</Target>
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...