Не много любви к 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>