Visual Studio Extensibility. Часть первая: MSBuild

  • Tutorial
Привет Хабр, в этих статьях я попытаюсь осветить тему расширений Microsoft Visual Studio(а попутно ещё и MSBuild), т.к. эта сфера является крайне плохо документированной и вообще покрыта пеленой какой-то загадочности.



Пролог


Я являюсь профессиональным разработчиком C++ с достаточно большим опытом работы, а также большим поклонником продукции компании Microsoft и в первую очередь главного моего инструмента разработки — Visual Studio. Не так давно, в качестве хобби, я начал заниматься программированием микроконтроллеров, и микроконтроллеры я выбрал от компании Microchip. Единственное, что меня не устраивало это использование инструментов разработки, которые предоставляет сама компания Microchip. Я ничего не могу сказать плохого про эти продукты, просто я не хотел бы устанавливать несколько IDE на свой рабочий(или домашний) компьютер, поэтому родилась мысль интегрировать компилятор XC8 от компании Microchip в Microsoft Visual Studio. Позже я увидел в этой идее ещё один плюс — множество моих(и не только моих) проектов прямо или косвенно связаны с подключением их к компьютеру, поэтому приходится разрабатывать ответную программную часть — было бы здорово объединять их в одно решение(solution) с проектом микропрограммы. Проковырявшись большое количество времени, я понял, что тема интеграции чего либо в Visual Studio является эдаким белом пятном: нормального описания нет, куцые описания в блогах, и что самое плохое — практически нет примеров. Собрав кое какую информацию по крупицам, кое-что поняв из описания и опираясь на метод научного тыка, я решил поделиться полученными знаниями. Итак, поехали.

План действий


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

  1. Чтобы при просмотре свойств проекта(Project Properties) в Visual Studio — были бы наши собственные свойства.
  2. Определить свой набор расширений файлов, которые можно было бы включать в проект.
  3. Естественно определить, свою систему сборки, чтобы вызывались наши компиляторы, линковщики и т.д.
  4. Создать свой тип проекта, чтобы студия могла открывать файл с нашим собственным расширением.
  5. Создать волшебник для генерации этого типа проекта.


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

Часть 1: MSBuild


Начнем, пожалуй, с теории:
  • Microsoft Visual Studio — чуть менее чем полностью построена на технологии COM. Поэтому нужно быть готовым столкнуться с ней. Хотя мы попробуем все же этого избежать.
  • Любой проект Microsoft Visual Studio является скриптом MSBuild.


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

Сразу оговорюсь: у меня под рукой была достаточно старая VIsual Studio 2010 — поэтому все описание будет для нее, но я полагаю, что с 2012 и 2013 студиями все будет аналогично.

Итак открываем студию, создаем Blank Solution и добавляем в него Empty Project (мне ближе проект в категории С++, поэтому я выбрал его)


Я назвал и проект и решение «test» и сделал так, чтобы они лежали в одной директории(это удобно для наших экспериментов, объясню позже)- таким образом получив файлы test.sln и test.vcxproj

Теперь закрываем студию и берем какой нибудь текстовый редактор(лучше с подсветкой XML синтаксиса — в принципе подойдет та же студия, только уже другой её экземпляр) и открываем test.vcxproj как текстовый файл.

Посмотрим, что внутри у test.vcxproj:
test.vcxproj
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup Label="ProjectConfigurations">
    <ProjectConfiguration Include="Debug|Win32">
      <Configuration>Debug</Configuration>
      <Platform>Win32</Platform>
    </ProjectConfiguration>
    <ProjectConfiguration Include="Release|Win32">
      <Configuration>Release</Configuration>
      <Platform>Win32</Platform>
    </ProjectConfiguration>
  </ItemGroup>
  <PropertyGroup Label="Globals">
    <ProjectGuid>{E1064D79-B415-4EDC-9FAC-C50E4102268B}</ProjectGuid>
    <RootNamespace>test</RootNamespace>
  </PropertyGroup>
  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
    <ConfigurationType>Application</ConfigurationType>
    <UseDebugLibraries>true</UseDebugLibraries>
    <CharacterSet>MultiByte</CharacterSet>
  </PropertyGroup>
  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
    <ConfigurationType>Application</ConfigurationType>
    <UseDebugLibraries>false</UseDebugLibraries>
    <WholeProgramOptimization>true</WholeProgramOptimization>
    <CharacterSet>MultiByte</CharacterSet>
  </PropertyGroup>
  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
  <ImportGroup Label="ExtensionSettings">
  </ImportGroup>
  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
  </ImportGroup>
  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
  </ImportGroup>
  <PropertyGroup Label="UserMacros" />
  <PropertyGroup />
  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
    <ClCompile>
      <WarningLevel>Level3</WarningLevel>
      <Optimization>Disabled</Optimization>
    </ClCompile>
    <Link>
      <GenerateDebugInformation>true</GenerateDebugInformation>
    </Link>
  </ItemDefinitionGroup>
  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
    <ClCompile>
      <WarningLevel>Level3</WarningLevel>
      <Optimization>MaxSpeed</Optimization>
      <FunctionLevelLinking>true</FunctionLevelLinking>
      <IntrinsicFunctions>true</IntrinsicFunctions>
    </ClCompile>
    <Link>
      <GenerateDebugInformation>true</GenerateDebugInformation>
      <EnableCOMDATFolding>true</EnableCOMDATFolding>
      <OptimizeReferences>true</OptimizeReferences>
    </Link>
  </ItemDefinitionGroup>
  <ItemGroup>
  </ItemGroup>
  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
  <ImportGroup Label="ExtensionTargets">
  </ImportGroup>
</Project>


Самое главное, что необходимо увидеть здесь:
  • тег ProjectConfiguration.
  • тег ProjectGuid.
  • файлы, которые подключаются тегом Import.

Все остальное можно спокойно удалять. Также предлагаю удалить файл test.filters, чтобы нам не мешались виртуальные директории в проекте.
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup Label="ProjectConfigurations">
    <ProjectConfiguration Include="Debug|Win32">
      <Configuration>Debug</Configuration>
      <Platform>Win32</Platform>
    </ProjectConfiguration>
    <ProjectConfiguration Include="Release|Win32">
      <Configuration>Release</Configuration>
      <Platform>Win32</Platform>
    </ProjectConfiguration>
  </ItemGroup>
  <PropertyGroup Label="Globals">
    <ProjectGuid>{E1064D79-B415-4EDC-9FAC-C50E4102268B}</ProjectGuid>
    <RootNamespace>test</RootNamespace>
  </PropertyGroup>
  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
</Project>

Приведенный выше файл абсолютно валидный с точки зрения студии и MSBuild, его можно открыть и даже собрать.

Также предлагаю сразу разобраться со сборкой нашего проекта из командной строки:
  1. Запускаем Microsoft Visual Studio Command Prompt
  2. Переходим в директорию, где находится test.vcxproj
  3. Выполняем msbuild test.vcxproj /p:Configuration=Debug /p:Platform=Win32

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

Теперь начинаем разбираться в том, что отвечает за свойства проекта и вообще, что делает наш проект — проектом студии. А делают это все строчки:
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />

Импортируемые файлы у меня находятся с директории C:\Program Files\MSBuild\Microsoft.Cpp\v4.0

Можно конечно попробовать их посмотреть, но я боюсь человек, который никогда не разбирался с MSBuild, вряд ли что-то поймет, а желание разбираться в этом во всем мгновенно улетучится. Так что я предлагаю пока туда не смотреть, т.к. можно потратить уйму времени(как сделал автор) и все равно ничего не понять.

Важно!!: При каждой правке vcxproj и сопутствующих файлов необходимо перезапускать студию полностью! Опытным путем было установлено, что Visual Studio, что-то(скорее всего файлы *.props и *.targets) кеширует — поэтому простого Unload Project/Reload Project не достаточно! Именно поэтому я изначально создал sln файл рядом с vcxproj, чтобы было удобно перезапускать, не меняя директории.

Итак давайте просто удалим строчки с тегом Import и посмотрим, что получится.
Файл должен быть таким:
test.vcxproj
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup Label="ProjectConfigurations">
    <ProjectConfiguration Include="Debug|Win32">
      <Configuration>Debug</Configuration>
      <Platform>Win32</Platform>
    </ProjectConfiguration>
    <ProjectConfiguration Include="Release|Win32">
      <Configuration>Release</Configuration>
      <Platform>Win32</Platform>
    </ProjectConfiguration>
  </ItemGroup>
  <PropertyGroup Label="Globals">
    <ProjectGuid>{E1064D79-B415-4EDC-9FAC-C50E4102268B}</ProjectGuid>
    <RootNamespace>test</RootNamespace>
  </PropertyGroup>
</Project>


Открыв его в студии — мы с удивлением обнаружим, что файл все ещё является валидным. Но собрать его уже не получится, мы получим сообщение:
1>Error: The «ConfigurationGeneral» rule is missing from the project.
Пока мы не желаем собирать проект — поэтому не обращаем на это внимания.

Посмотрим, что у нас в свойствах проекта — и наблюдаем следующую картину:

Красота! Мы убрали все лишнее, вернее всего, убрали вообще все.

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


Лирическое отступление:
Читатель возможно задаст вопрос, а как же студия все ещё определяет, что это проект C++ — элементарно по расширению vcxproj, которое явно указывает на Visual C++. Возможно читателю этот вопрос покажется достаточно глупым, но когда долго экспериментируешь с проектом, который фактически уже не является С++ проектом, а студия все ещё пытается вести себя по правилам С++, совершенно забываешь про расширение самого файла — а оно имеет кардинальное значение. Мы будем от этого избавляться, но лишь в одной из следующих частей данного повествования.

Проект пуст. Приступим к наполнению.


Читатель наверняка уже догадался, что мы будем создавать свои собственные файлы *.props и *.targets, но в начале немного теории:
  • файлы *.props и *.targets совсем не обязательно должны иметь расширения *.props и *.targets — это фактически, что-то типа подключаемых файлов(include). Но мы не будем нарушать гегемонию студии и оставим привычные всем расширения.
  • *.props обычно отвечают за свойства проекта и переменные окружения.
  • *.targets отвечают за сборку — в них описывается, что делать с файлами(а может и не с файлами), которые добавлены к проекту, т.е. все возможные действия/задания(Tasks) и за типы файлов проекта определямые схемой(ProjectSchema) и правилами(Rules).


Файл *.targets.


Для дальнейшего повествования, я предлагаю перейти от абстракции к конкретной задаче прикручивания компилятора XC8 от компании Microchip к Visual Studio. Компилятор XC8 необходимо скачать с сайта Microchip и установить.

Отступление: Вообще файлы *.targets как и *.props находятся в определенных папках самого MSBuild, но сейчас мы так делать не будем, т.к. это уже относится к задачам распространения новоиспеченного расширения и, в целом, задачам инсталятора, а для наших экспериментов удобнее всего все хранить в одной директории рядом с проектом.

Создадим файл XC8.targets в директории рядом с test.vcxproj.

Содержание XC8.targets:
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
	<!-- Properties -->
	<ItemGroup>
		<PropertyPageSchema Include="XC8.Items.xml"/>
	</ItemGroup>
</Project>


Из файла видно, что мы пытаемся определить схему страницы свойств(PropertyPageSchema) и подключить файл XC8.Items.xml. Как догадался читатель — в файле XC8.Items.xml мы будем описывать типы файлов, которые будут участвовать в нашем проекте. Конкретно для компилятора XC8 — это файлы *.c; *.h; *.asm итд.

Создадим XC8.Items.xml поместив его в директорию с нашим проектом.
Содержание XC8.Items.xml:
<?xml version="1.0" encoding="utf-8"?>
<ProjectSchemaDefinitions
    xmlns="http://schemas.microsoft.com/build/2009/properties">

	<ContentType
		Name="CCode"
		DisplayName="C Code"
		ItemType="Compile">
	</ContentType>
	
	<ContentType
		Name="AsmCode"
		DisplayName="Asm Code"
		ItemType="Compile">
	</ContentType>
	
	<ContentType
		Name="Header"
		DisplayName="C/C++ Header"
		ItemType="Include">
	</ContentType>

	<ContentType
		Name="Text"
		DisplayName="Text file"
		ItemType="None">
	</ContentType>

	<ItemType Name="Compile" DisplayName="XC8 Compiler"/>
	<ItemType Name="Include" DisplayName="C header"/>
	<ItemType Name="None" DisplayName="Does not participate in build"/>

	<FileExtension Name=".c" ContentType="CCode"/>
	<FileExtension Name=".cc" ContentType="CCode"/>
	<FileExtension Name=".s" ContentType="AsmCode"/>
	<FileExtension Name=".asm" ContentType="AsmCode"/>
	<FileExtension Name=".h" ContentType="Header"/>
  	<FileExtension Name=".txt" ContentType="Text"/>
</ProjectSchemaDefinitions>

Как видно в этом файле мы определили те типы файлов, которые нам нужны в проекте. А с помощью тега FileExtension определили расширения этих файлов.

Теперь наконец импортируем XC8.targets в наш проект, для этого используем тег Import
Содержание test.vcxproj:
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup Label="ProjectConfigurations">
    <ProjectConfiguration Include="Debug|Win32">
      <Configuration>Debug</Configuration>
      <Platform>Win32</Platform>
    </ProjectConfiguration>
    <ProjectConfiguration Include="Release|Win32">
      <Configuration>Release</Configuration>
      <Platform>Win32</Platform>
    </ProjectConfiguration>
  </ItemGroup>
  <PropertyGroup Label="Globals">
    <ProjectGuid>{E1064D79-B415-4EDC-9FAC-C50E4102268B}</ProjectGuid>
    <RootNamespace>test</RootNamespace>
  </PropertyGroup>
  <Import Project="XC8.targets" />
</Project>


Теперь открываем студию и пробуем добавить простейший файл main.c
Содержание файла:
main.c
#include <xc.h>

int main()
{
	return 0;
}


Вуаля — теперь файл добавился. Посмотрим свойства файла:

Мы видим, что в выпадающем списке Item Type отображаются элементы которые мы указали в файле XC8.Items.xml.

Важно!!: Основным тегом в файле XC8.Items.xml, в целом, является ContentType с атрибутом ItemType. Именно значение атрибута ItemType мы будем использовать в дальнейшем как основное.

Сохраним в студии наш test.vcxproj, закроем студию, а затем посмотрим его текст:
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <ItemGroup Label="ProjectConfigurations">
    <ProjectConfiguration Include="Debug|Win32">
      <Configuration>Debug</Configuration>
      <Platform>Win32</Platform>
    </ProjectConfiguration>
    <ProjectConfiguration Include="Release|Win32">
      <Configuration>Release</Configuration>
      <Platform>Win32</Platform>
    </ProjectConfiguration>
  </ItemGroup>
  <PropertyGroup Label="Globals">
    <ProjectGuid>{E1064D79-B415-4EDC-9FAC-C50E4102268B}</ProjectGuid>
    <RootNamespace>test</RootNamespace>
  </PropertyGroup>
  <ItemGroup>
    <Compile Include="main.c" />
  </ItemGroup>
  <Import Project="XC8.targets" />
</Project>

Мы видим, что у нас добавился ещё тег ItemGroup внутри, которого находится тег Compile. Название этого тега — это и есть то, что мы указали в качестве значения атрибута ItemType тега ContentType в файле XC8.Items.xml. Как уже догадался уважаемый читатель — с помощью этого механизма в наш проект включаются новые файлы для сборки.

Теперь займемся свойствами проекта. Для этого необходимо в наш XC8.targets включить 2 файла:
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
	<!-- Properties -->
	<ItemGroup>
		<PropertyPageSchema Include="XC8.Items.xml"/>
		<PropertyPageSchema Include="XC8.General.xml">
			<Context>Project</Context>
		</PropertyPageSchema>
		<PropertyPageSchema Include="XC8.General.PS.xml">
			<Context>PropertySheet</Context>
		</PropertyPageSchema>
	</ItemGroup>
</Project>

И, как обычно, необходимо создать файлы XC8.General.xml и XC8.General.PS.xml в директории с проектом.

Сразу оговорюсь: зачем нужен второй файл XC8.General.PS.xml я, до конца, так и не выяснил, т.к. он присутствовал во всех материалах, которые я изучал — я решил его оставить, назвав в соответствии с нашим проектом. Если кто-нибудь имеет информацию по данному вопросу — прошу поделиться.
Содержимое:
XC8.General.PS.xml
<?xml version="1.0" encoding="utf-8"?>
<Rule
      Name="ConfigurationGeneral"
      DisplayName="General"
      PageTemplate="generic"
      Description="General"
      xmlns="http://schemas.microsoft.com/build/2009/properties">

  <Rule.Categories>
    <Category Name="General" DisplayName="General" Description="General" />
  </Rule.Categories>
  <Rule.DataSource>
    <DataSource Persistence="ProjectFile" Label="Configuration" />
  </Rule.DataSource>
</Rule>



Теперь обратимся к файлу XC8.General.xml в нем мы будем описывать свойства нашего проекта, а именно — страницу свойств General. Напоминаю: именно эту страницу от нас просила Visual Studio при попытке сборки проекта. Страницы свойств в проекте описываются тегом Rule. Без лишних слов я просто приведу свой файл XC8.General.xml а затем попытаюсь объяснить его структуру.
<?xml version="1.0" encoding="utf-8"?>

<Rule
      Name="ConfigurationGeneral"
      DisplayName="General"
      PageTemplate="generic"
      Description="General"
	  SwitchPrefix="-"
      xmlns="http://schemas.microsoft.com/build/2009/properties">

	<Rule.Categories>
		<Category Name="General" DisplayName="General" Description="General" />
		<Category Name="ProjectDefaults" DisplayName="Project Defaults" Description="Project Defaults" />
	</Rule.Categories>
	<Rule.DataSource>
			<DataSource Persistence="ProjectFile" Label="Configuration" />
	</Rule.DataSource>

	<!-- General -->
	<StringProperty Name="OutDir" DisplayName="Output Directory"
				  Description="Specifies a relative path to the output file directory; can include environment variables."
				  Category="General" Default="$(SolutionDir)$(Configuration)\"
				  Subtype="folder"
				  F1Keyword="VC.Project.VCConfiguration.OutputDirectory">
		<StringProperty.DataSource>
			<DataSource Persistence="ProjectFile" Label="" />
		</StringProperty.DataSource>
	</StringProperty>
	<StringProperty Name="IntDir" DisplayName="Intermediate Directory"
				  Description="Specifies a relative path to the intermediate file directory; can include environment variables."
				  Category="General" Default="$(Configuration)\"
				  Subtype="folder"
				  F1Keyword="VC.Project.VCConfiguration.IntermediateDirectory">
		<StringProperty.DataSource>
			<DataSource Persistence="ProjectFile" Label="" />
		</StringProperty.DataSource>
	</StringProperty>
	<StringProperty Name="TargetName" DisplayName="Target Name"
				  Description="Specifies a file name that this project will generate."
				  F1Keyword="VC.Project.VCConfiguration.TargetName"
				  Category="General" Default="$(ProjectName)"
				  >
		<StringProperty.DataSource>
			<DataSource Persistence="ProjectFile" Label="" />
		</StringProperty.DataSource>
	</StringProperty>
	<StringListProperty Name="ExtensionsToDeleteOnClean" DisplayName="Extensions to Delete on Clean"
						Description="Semi-colon delimited wildcard specification for which files in the intermediate directory to delete on clean or rebuild."
						Category="General" 
						Separator=";"
						F1Keyword="VC.Project.VCConfiguration.DeleteExtensionsOnClean">
		<StringListProperty.DataSource>
			<DataSource Persistence="ProjectFile" Label="" />
		</StringListProperty.DataSource>
	</StringListProperty>
	<StringProperty Name="BuildLogFile" DisplayName="Build Log File"
					Description="Specifies the build log file to write to when build logging is enabled."
					F1Keyword="VC.Project.VCConfiguration.BuildLogFile"
					Category="General" 
					Default="$(ProjectDir)\$(MSBuildProjectName).log"
					Subtype="file">
		<StringProperty.DataSource>
			<DataSource Persistence="ProjectFile" ItemType="BuildLog" PersistedName="Path" />
		</StringProperty.DataSource>
	</StringProperty>

	<StringProperty Name="Chip" DisplayName="Select Processor" 
				Description="This option selects the processor target for compilation. A list of available processors can be obtained by using the chipinfo option." 
				Category="ProjectDefaults"
				Switch="-chip="/>
	
	<BoolProperty Name="QuietMode" DisplayName="Quiet Mode" 
			Description="This option suppresses the Microchip copyright message, and any summary information usually printed by the compiler." 
			Category="ProjectDefaults" 
			Default="false" 
			Switch="Q"/>
			
	<BoolProperty Name="VerboseMode" DisplayName="Verbose Mode" 
				Description="This option is used to display the command lines used to invoke each of the compiler passes."
				Category="ProjectDefaults" 
				Default="false" 
				Switch="V"/>
</Rule>

Файл довольно объемный, но мне все же не хотелось убирать его под спойлер, т.к. он имеет важное значение.

Итак, тег Rule имеет несколько важных атрибутов:
  • Name=«ConfigurationGeneral» — студия ищет правило именно с этим значением атрибута Name. Если у Вас не будет правила с названием ConfigurationGeneral, скорее всего у Вас ничего не получится.
  • DisplayName=«General» — это название, которое будет отображаться в окне конфигурации проекта, можете назвать как угодно. Я оставил, канонически: «General».
  • Description=«General» — описание. Пояснять надеюсь не требуется.
  • SwitchPrefix="-" — этот атрибут определяет префикс ключа команд при передаче их в систему сборки. Допустим ключи вашего компилятора начинаются с "/" (пример: /Ipath определяет путь подключаемых файлов) — соответсвенно значение этого атрибута будет "/", у XC8 компилятора все ключи начинаются с "-", что собственно у меня и написано. Если у вас ключи разных форматов, то этот атрибут можно оставить пустым или вовсе не указывать.
  • xmlns — атрибут поведения документа в целом, пространства имен и т.д., соответсвенно менять его не имеет смысла.
  • PageTemplate — определяет отображение страницы свойств в настройке проекта, мы будем работать с шаблонами generiic и tool — всю разницу между ними я покажу позже на скриншотах (уже в следующей части).

Тег Rule.Categories служит для определения категорий внутри страницы свойств. Таких категорий у нас две:
  • General — общие настройки проекта — обычно всякие пути.
  • ProjectDefaults — настройки особенностей нашего XC8 проекта — я вынес сюда ключики, которые должны передаваться и компилятору и к линковщику.

Понятно, что Вы можете определить таких категорий сколько пожелаете.

Тег Rule.DataSource определяет где будут храниться значения свойств. У нас указано ProjectFile — т.е. в файле проекта. Я атрибут Persistence не менял, т.к. не представляю где ещё могут храниться настройки проекта как не в файле этого самого проекта, в нашем случае в test.vcxproj.

Теги с окончанием Property, как Вы уже догадались — это и есть свойства которые будут у нас отображаться в окне свойств нашего проекта.
  • StringProperty — строковое.
  • StringListProperty — список строк. Например перечисление Include директорий или, как здесь, перечесление файлов при очистке проекта(Clean).
  • IntProperty — числовое, но оно ведет себя почему-то как строковое, так что этот момент остается загадкой.
  • BoolProperty — флаг.
  • EnumProperty — перечисление, я расскажу о нем позже т.к. в нашем файле XC8.General.xml таких нет.


Рассмотрим основные атрибуты свойств:
  • Name — название. Имеет важное значение т.к. может участвовать в объявлении макросов студии(те которые $(BlaBlaBla)). Как это сделать я расскажу позже.
  • DisplayName — отображаемое название.
  • Description — описание. Отображается внизу — при выборе данного свойства в окне свойств проекта.
  • Category — определяет, в какой категории будет отображаться наше свойство. В нашем случае категорий 2: General и ProjectDefaults.
  • Subtype — определяет подтип свойства StringProperty. Из тех, которые я видел — это folder и file, но поведение их почти ничем не отличается.
  • F1Keyword — определяет справку по данному свойству.
  • Default — этот атрибут по идее должен определять значение свойства по умолчанию, но он этого не делает — возможно, это просто подсказка (которую я тоже нигде не нашел). Для определения значений свойств по умолчанию существует совершенно другой механизм, о котором я буду рассказывать в следующей части повествования.
  • Switch — определяет ключ данного свойства. В конечном итоге складывается с атрибутом SwitchPrefix тега Rule и передается в сборку. В случае со свойством QuietMode это будет "-Q"


Атрибутов у свойств и правил достаточно много — читатель может познакомится с ними по ссылке: Microsoft.Build.Framework.XamlTypes Namespace. Но будьте готовы не найти там никакого внятного описания, такое ощущение, что данная документация сделана автоматически каким-то генератором, без описаний. Радует только то, что назначение многих атрибутов понятно по их названиям.

Теперь открываем наш проект в студии и смотрим окно свойств проекта

Если попытаться собрать проект, то мы увидим уже другую ошибку: error MSB4057: The target «build» does not exist in the project., которая говорит нам об отсутствии Target тега с названием build.

На этом первую часть повествования я заканчиваю, мы добились определенного результата. В следующий раз я расскажу, как создавать Target, Task, определять значения по умолчанию и макросы студии.

Файлы проекта можно скачать здесь.

Материалы, которые позволили мне изучить вопрос:


MSDN: Microsoft.Build.Framework.XamlTypes Namespace
Проект: vs-android
Статья на Хабре: Минимальный проект MsBuild
MSDN: Пошаговое руководство. Создание файла проекта MSBuild с нуля
А также метод научного тыка в директории С:\Program Files\MSBuild\Microsoft.Cpp\v4.0\
Ads
AdBlock has stolen the banner, but banners are not teeth — they will be back

More

Comments 24

    0
    Вот спасибо. Всё самое интересное как всегда — to be continue… ждем.
      0
      «Любой проект Microsoft Visual Studio является скриптом MSBuild» — не совсем так. Все «коробочные» типы проектов в VS являются MSBuild-скриптами, это да. Но исторически это было не так (VC++ до VS2010 имел свой формат, и еще был Setup project), и в общем случае это некорректно, т.к. формат файла проекта отдается целиком на откуп проектной системе (project system — компоненту, отвечающему за загрузку и работу с данным конкретном типа проекта).
        0
        Вы правы. По поводу project system, то в будущих частях, я надеюсь, расскажу про создание своей системы проектов(это пункт 4й плана действий, если Вы не заметили), а пока я посчитал, что упоминание про это бессмысленно. По поводу истории — я прекрасно её знаю(сижу на студии с версии 5.0 — эта которая 97) поэтому и начал с 2010 т.к. между 2010 и 2012, 2013 — разница в проектных системах минимальна.
        +3
        Описанный вами способ добавления свойств — это не MSBuild вообще, а конкретно те виды проектов, которые построены на Common Project System (это проектная система, изначально появившаяся в VS 2010 для C++-проектов, а на сегодня используется также для WinStore JavaScript проектов, и .NET vNext / Project K / .kproj). Для обычных C#/VB проектов, например, это работать не будет. Для F#, Python, Node.js, PHP — тоже. Тут та же проблема — каждая проектная система реализует эти штуки сама и по-своему; в CPS сделали такое вот декларативное описание пропертей, в других — что-то еще.

        В C# добавить свои свойства вполне можно, но там придется еще рисовать руками property page для их редактирования, и в целом это все будет выглядеть совсем по-другому.
          +2
          $(BlaBlaBla) — это не макрос, это способ адресации Свойства (Property) в MSBuild.

          Вообще все остальное, включая ваши наборы свойств выглядит как мало относящееся к MSBuild. Props и Targets это просто «соглашения», можете именовать как угодно, но студия любит их и например может дать возможность редактировать ваши собственные props в окне свойств проекта.

          int19h А в чем отличия этой Common Project System от стандартной системы сборки проектов в MSBuild? И почему декларативное описание свойств не будет работать? Мсбилд всегда так работает, ну или я не правильно понимаю ваше высказывание.
            0
            про $(BlaBlaBla) — я написал в терминологии студии, чтобы людям было привычнее. А в студии эти штуки как раз находятся под кнопкой «Macros». Мои наборы свойств включаются в проект студии, который является скриптом MSBuild и не упоминать его неправильно, понятно, что для сборки из консоли они не нужны, но ведь статья относится к MVS, а не чисто к MSBuild. К тому же в следующих частях я уже перейду уже к сборке — а это уже MSBuild в чистом виде.
            Props и Targets это просто «соглашения» — Вы статью читали? Там про это написано.
              +1
              То, что они где-то находятся в IDE — не определяет их предназначение. Вы такими выдуманными обозначениями запутаете читателей еще больше, а потом опять будем видеть жалобы на то, что кто-то потратил уйму времени (как автор) просматривая targets мсбилда и все равно ничего не понял.

              Да, статью я читал. На соглашения я указал потому, что если им следовать — можно получить некоторые встроенные в msbuild(и поставляемые targets) ништяки. Если не следовать — можно сделать все то же самое, но руками и через «мучения».
                +2
                Нет, тут как раз все правильно. Надо четко понимать, что свойства MSBuild и свойства проекта в VS — это две совершенно разные вещи, которые иногда пересекаются как деталь реализации данного конкретного типа проектов.
                  +1
                  Ок. т.е. получается в данном случае они не только используют свойства как способ хранения, но и используют такой же синтаксис? Или все-таки они заимствуют синтаксис из мсбилда, потому что само строковое значение будет интерпретироваться\разворачиваться не студией, а мсбилдом? Кто их разворачивает, эти «макросы»? Если второе — то все-таки это сущность билда, а не макрос студии.
                    +3
                    Конкретно $(BlaBlaBla) — на практике это сущность MSBuild, да, и разворачивает их он (если попросят).

                    Под капотом там создается экземпляр класса Project, а на нем дергается метод GetEvaluatedProperty, который и возвращает развернутое значение. При записи, соответственно, зовется SetProperty. Для редактирования же значение проперти обычно читается в исходном виде непосредственно из коллекции Properties, чтобы отобразить его точно в том виде, в котором оно было в файле проекта, со всеми $(...).

                    Про Macros я просто пропустил. Это (точнее — project macros) терминология, специфичная для C++-проектов, больше её никто не использует. Если мне не изменяет память, она осталась с тех времен, когда C++-проекты были не MSBuild. По факту на сегодня она просто отдается на откуп мсбилду, но в документации, тем не менее, она зачем-то описана как отдельная сущность.

                    (гипотетически, если MSBuild завтра внезапно умрет, то макросы его переживут, поэтому какой-то смысл в том, чтобы говорить о них в отдельности, наверное есть)
                0
                и да, проект становится проектом «студии» если у него соответствующий ProjectGuid, а не невинные импорты
                <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
                <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
                
                  +1
                  Собственно я об этом упомянул — когда сказал, что если их удалить — то проект остается валидным.
                +5
                >> А в чем отличия этой Common Project System от стандартной системы сборки проектов в MSBuild? И почему декларативное описание свойств не будет работать? Мсбилд всегда так работает, ну или я не правильно понимаю ваше высказывание.

                Тут мне придется разлиться мысию по древу, немного определиться с терминологией, и углубиться в историю.

                С точки зрения MSBuild, «проект» — это просто корневой скрипт системы сборки, точка входа в неё. Никакого другого смысла в него не вкладывается, и обычные VS-термины вроде «файлов проекта» или там «выходной сборки» на этом уровне смысла не имеют. Есть просто набор properties (по сути, скалярных переменных), items (коллекций), и targets (функций), который выполняется. Все.

                С точки зрения VS, «проект» — это отнюдь не MSBuild-скрипт, и даже вообще не обязательно файл. Это просто некоторый COM-объект в памяти, реализующий интерфейсы IVsProject и IVsHierarchy, и ряд связанных с ними. Соответственно, «проектная система» (project system) — это реализация конкретной фабрики (IVsProjectFactory) для создания таких объектов, и реализации этих самых объектов для этой конкретной фабрики. Вся эта система очень абстрактна, и то, как именно реализован проект под капотом, может сильно варьироваться, в т.ч. и откуда грузится при открытии проекта, и куда пишется при сохранении. На практике постепенно пришли к единому знаменателю в виде MSBuild-скриптов, чтобы логика билда, по крайней мере, у всех проектных систем была одна.

                При этом реализация всего остального в проектной системе (т.е. собственно IVsProject и IVsHierarchy — интерфейсы, ответственные за наполнение Solution Explorer, открытие редакторов для документов etc) по прежнему различается. В частности, если взять тот же VS 2010, то в нем C++ — это одна реализация (что забавно, написанная на C#), C# и VB — совершенно другая (что еще более забавно, написанная на C++), F# — третья (форк MPF — примера по написанию проектной системы из VS SDK), Setup project — четвертая. Если ставить расширения, то, например, Python Tools и Node.js Tools — это будет пятая (тоже форк MPF, но не имеет родства с C#), PHP Tools — шестая (форк PTVS), RemObjects Oxygene — седьмая (что-то свое, но, кажется, тоже форк MPF). Плюс есть всякие мелкие вспомогательные проектные системы, не привязанные к языкам, и не имеющие смысла сами по себе — например, Cloud Service Project в Azure SDK.

                Понятно, что подобный зоопарк приводит к головной боли в виде необходимости реализовывать одни и те же фичи по десять раз. Поэтому периодически предпринимаются попытки написать одну универсальную проектную систему, которая покроет все требования, и может быть использована и кастомизирована всеми языками под свои нужды. Как правило, это работало примерно так, как описано в xkcd про стандарты (например, проектная система C#/VB была именно такой попыткой, но в итоге её никто больше использовать не стал). К VS 2010 очередной попыткой стал CPS, который вырос из переписывания проектной системы C++ (старая не использовала MSBuild, поэтому её все равно надо было переделывать). Попыткой, похоже, успешной, потому как его подхватали JS, и теперь вот .NET Core.

                Возвращаясь к терминологии — с точки зрения VS самой по себе (не включая стандартные проектные системы), понятия «свойства проекта» в общем случае не существует. Есть фиксированный набор свойств у корневой иерархии проекта (IVsHierarchy), есть понятие «конфигурация» (IVsCfg) и «платформа», и, в общем-то, все. Все остальное — это штуки, специфичные для каждой проектной системы, и какие они вообще есть (и есть ли!), и где и как хранятся — решает именно она. На практике, опять же, есть некоторые общие вещи вроде выходного файла, но даже для них в общем случае нет одного универсального интерфейса, через который их можно получить из любого произвольного проекта. Есть IVsBuildPropertyStorage (который появился одновременно с MSBuild и проектной системой для С#/VB на его основе), который поддерживают все MSBuild-based проектные системы — но, в принципе, они это делать не обязаны.

                Графический интерфейс для редактирования этих свойств тоже реализуется проектной системой. VS здесь предоставляет костыли в виде реализации COM property pages, но каждая конкретная страница целиком и полностью на откупе у проектной системы.

                Для систем, которые в качестве файлового формата для проекта используют MSBuild, наиболее очевидный маппинг свойств на файл — это MSBuild properties (а для файлов проекта — соответственно, MSBuild items). Обратное неверно, т.е. далеко не каждое свойство в MSBuild является свойством проекта в данной системе, и не каждый item является файлом проекта. Собственно, если сделать дамп свойств после чтения какого-нибудь шарпного или плюсового проекта, их там будет с пару сотен, и из них только несколько десятков видны в VS как свойства проекта. Остальное — по сути, просто временные переменные. Что есть что, решает опять же проектная система.

                Так вот, в случае с CPS, это решение не зашито в код системы, как во всех остальных случаях, а вынесено в декларативные описания в сам MSBuild-скрипт. Т.е. файл проекта, а точнее, импортируемые им .targets и .props файлы, описывают, как его надо интерпретировать, и в частности, какие именно MSBuild-свойства должны быть отображены в UI, и каким именно образом. Это как раз и есть PropertyPageSchema, и используемые им XML-файлы (которые на самом деле XAML). И вся эта машинерия — специфична именно для CPS, и работает только с его проектами. К MSBuild она прямого отношения не имеет, и последний рассматривает её просто как обычный набор свойств и items, никак ни с чем не связанный и не имеющий никакого сакрального смысла. В сборке, соответственно, она тоже не участвует.
                  +1
                  Классно, спасибо за пояснения, как работает мсбилд я знаю неплохо, а вот такие детали про проектные системы и их проекции на систему сборки — очень полезны.
                  Кстати такой технический вопрос — можно ли убедить студию что ей нужно отрисовать содержимое файла Х как набор свойств проекта в GUI (т.е. убедить ее в том, что «проектная система — C#, но вот тут отрисуй пожалуйста используя другой компонент, из CPS» ), или это все жестко связано с расширениями и внутренними Guid? А то я сейчас покопался — и обнаружил что куда-то пропал графический редактор свойств в С#, похоже раньше это мне какое-то расширение делало… Не то что бы очень надо, просто любопытно, можно ли так обмануть систему…
                    +2
                    Можно, понятие редактора не привязано к проектной системе — ассоциация идет обычно по типу файла (т.е. расширению). Проектная система может перехватить открытие файла и насильно заставить открыть какой-то редактор по умолчанию для данного типа, отличный от глобального умолчания (например, PTVS перехватывает открытие .html в Django-проектах, чтобы открыть их в своем редакторе, который понимает синтаксис Django). Но в любом случае остается вариант в виде Open With в контекстном меню файла — там будут вообще все зарегистрированные в VS редакторы, и можно выбрать любой. Вполне вероятно, что ваш редактор где-то там :)
                0
                Спасибо! А про написание аддинов к Студии не будет ничего?
                  +1
                  Возможно будет — я сейчас сам в этом во всем разбираюсь. Если забегать вперед и прикручивать XC8 полностью, то необходимо реализовать карту памяти микроконтроллера — подумываю сделать это как раз в виде добавления своего инструментального окна. Так что вполне вероятно, что напишу об этом.
                  0
                  Поправил пару опечаток.
                    –2
                    Автору конечно спасибо за проделанный труд, но тут стоило бы заметить что MSBuild так же покрыт огромным слоем пыли и пепла. Для людей, кто ищет современные средства построения (build) могу посоветовать FAKE или PSAKE, они оба в той или иной мере используют, под капотом, MSBuild, но они просто северное сияние по сравнению с бараньими тестикулами обычным MSBuild.
                      –1
                      А еще есть TFS — вообще гуй есть и можно в workflow в drag-and-drop стиле редактировать, не то что ваши тестикулярные скриптовые языки. Типа ура…

                      Обычно, необходимость в скриптовых и императивных языках возникает когда в команде многие не способны ( в силу ограниченности кругозора) понимать XML мсбилда и его декларативность. Ну это в общем проблемы команды, а не мсбилда. Хоть на batch пишите, только все эти обвязки — как забивать гвозди микроскопом — от непонимания.
                        –1
                        Так же наверное как и потребность в использовании языков высокого уровня возникает когда в команде многие не способны(в силу ограниченности кругозора) понимать всей красоты и мощи Assembler. И это тоже не проблемы Assembler.

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

                        Насчет TFS и его workflow, он обречен в его современном виде, что есть хорошо, посмотрите какова тенденция TFS 2015 и поймете о чем речь.

                        Мое мнение в том что не нужно тратить время и нервы на MSBuild, лучше брать высокоуровневые билд-системы. Как FAKE, sbt (из стана врагов), gulp (из стана неприятеля).
                          0
                          Эти системы (я про fake &psake) направлены на другое — это просто обертка, которая прячет от вас функциональную часть сборки. Мсбилд — возможность заглянуть и понять что происходит под капотом. Просто не понимая что внутри — невозможно модифицировать систему чтобы она делала что нужно вам, всегда будут ритуальные пляски — а вот такой плагин добавим, а вот такой модуль, а еще их надо вызвать в строгом порядке, а между вызовами очистить вот эту папку. Это называется культ карго.

                          Да, мсбилд использует не самый дружественный синтаксис, да, весь каскад импортов и переопределенных свойств может вызвать мигрень, да, половина таргетов для не основных продуктов написана криворукими идиотами, но понимание самой системы сборки дает вам знание о том, что происходит когда вы вызываете msbuild ./mylib.csproj. А также вы знаете как повлиять на это, в любой билд системе. И это кстати совсем не хардкор 8-)

                          PS про TFS — ппкс, и спасибо за наводку, как то пропустил что появились детали 2015…
                      +1
                      Для интересующихся ещё на тему расширений Microsoft Visual Studio: Руководство по разработке модулей расширений на C# для Visual Studio 2005-2012 и Atmel Studio.
                        0
                        Спасибо, статья действительно полезная.
                        Сам, в своё время, пытался разбираться в системе MSBuild для этого.
                        Ничего путного не нашёл в сети — делал по аналогии с примерами.

                        Only users with full accounts can post comments. Log in, please.