Configurar uma montagem conveniente de projetos no Visual Studio

Este artigo é um guia para personalizar o assembly de projetos do C ++ Visual Studio. Parcialmente, foi reduzido a partir de materiais de artigos dispersos sobre este tópico, em parte é o resultado da engenharia reversa de arquivos de configuração padrão do Studio. Escrevi principalmente porque a utilidade da documentação da própria Microsoft sobre esse assunto tende a zero e eu queria ter uma referência conveniente disponível que pudesse ser acessada e enviada posteriormente a outros desenvolvedores. O Visual Studio possui opções convenientes e abrangentes para configurar um trabalho verdadeiramente conveniente com projetos complexos, e fico aborrecido ao ver que, devido à documentação nojenta, esses recursos são muito raramente usados ​​agora.

Como exemplo, vamos tentar tornar possível adicionar o esquema flatbuffer ao Studio, e o Studio chama automaticamente flatc quando necessário (e não o chamava quando não havia alterações) e permite definir as configurações diretamente nas Propriedades do arquivo



Sumário


* Nível 1: suba nos arquivos .vcxproj
Fale sobre arquivos .props
Mas por que separar .vcxproj e .props?
Tornando as configurações do projeto mais legíveis
Facilitamos a conexão de bibliotecas de terceiros
Modelos de projeto - automatize a criação de projetos
* Nível 2: compilação personalizada personalizada
Abordagem tradicional
Atingir as metas do MSBuild
Vamos tentar criar um destino para criar arquivos .proto
Lembramos nosso exemplo de modelo
Arquivos U2DCheck e tlog
Finalize nosso .target personalizado
E o CustomBuildStep?
Cópia de arquivo adequada
* Nível 3: integre-se à GUI do Visual Studio
Retiramos as configurações das entranhas de .vcxproj nas Propriedades de configuração
Explique o Studios sobre novos tipos de arquivo
Associar configurações a arquivos individuais
* Nível 4: expandindo a funcionalidade do MSBuild

NOTA: todos os exemplos deste artigo foram testados no VS 2017. Como parte do meu entendimento, eles devem funcionar em versões anteriores do estúdio, começando pelo menos com o VS 2012, mas não posso prometer isso.

Nível 1: escalar dentro dos arquivos .vcxproj


Vamos dar uma olhada em um Visual Studio típico gerado .vcxproj automaticamente.

Será algo parecido com isto
<?xml version="1.0" encoding="utf-8"?> <Project DefaultTargets="Build" ToolsVersion="15.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> <ProjectConfiguration Include="Debug|x64"> <Configuration>Debug</Configuration> <Platform>x64</Platform> </ProjectConfiguration> <ProjectConfiguration Include="Release|x64"> <Configuration>Release</Configuration> <Platform>x64</Platform> </ProjectConfiguration> </ItemGroup> <PropertyGroup Label="Globals"> <VCProjectVersion>15.0</VCProjectVersion> <ProjectGuid>{0D35456E-42DA-418B-87D4-55E32B8E1373}</ProjectGuid> <Keyword>Win32Proj</Keyword> <RootNamespace>protobuftest</RootNamespace> <WindowsTargetPlatformVersion>10.0.17134.0</WindowsTargetPlatformVersion> </PropertyGroup> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" /> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration"> <ConfigurationType>Application</ConfigurationType> <UseDebugLibraries>true</UseDebugLibraries> <PlatformToolset>v141</PlatformToolset> <CharacterSet>Unicode</CharacterSet> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration"> <ConfigurationType>Application</ConfigurationType> <UseDebugLibraries>false</UseDebugLibraries> <PlatformToolset>v141</PlatformToolset> <WholeProgramOptimization>true</WholeProgramOptimization> <CharacterSet>Unicode</CharacterSet> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration"> <ConfigurationType>Application</ConfigurationType> <UseDebugLibraries>true</UseDebugLibraries> <PlatformToolset>v141</PlatformToolset> <CharacterSet>Unicode</CharacterSet> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration"> <ConfigurationType>Application</ConfigurationType> <UseDebugLibraries>false</UseDebugLibraries> <PlatformToolset>v141</PlatformToolset> <WholeProgramOptimization>true</WholeProgramOptimization> <CharacterSet>Unicode</CharacterSet> </PropertyGroup> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" /> <ImportGroup Label="ExtensionSettings"> </ImportGroup> <ImportGroup Label="Shared"> </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> <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> <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|x64'"> <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" /> </ImportGroup> <PropertyGroup Label="UserMacros" /> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'"> <LinkIncremental>true</LinkIncremental> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> <LinkIncremental>true</LinkIncremental> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'"> <LinkIncremental>false</LinkIncremental> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> <LinkIncremental>false</LinkIncremental> </PropertyGroup> <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'"> <ClCompile> <PrecompiledHeader>Use</PrecompiledHeader> <WarningLevel>Level3</WarningLevel> <Optimization>Disabled</Optimization> <SDLCheck>true</SDLCheck> <PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions> <ConformanceMode>true</ConformanceMode> <PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile> </ClCompile> <Link> <SubSystem>Console</SubSystem> <GenerateDebugInformation>true</GenerateDebugInformation> </Link> </ItemDefinitionGroup> <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> <ClCompile> <PrecompiledHeader>Use</PrecompiledHeader> <WarningLevel>Level3</WarningLevel> <Optimization>Disabled</Optimization> <SDLCheck>true</SDLCheck> <PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions> <ConformanceMode>true</ConformanceMode> <PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile> </ClCompile> <Link> <SubSystem>Console</SubSystem> <GenerateDebugInformation>true</GenerateDebugInformation> </Link> </ItemDefinitionGroup> <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'"> <ClCompile> <PrecompiledHeader>Use</PrecompiledHeader> <WarningLevel>Level3</WarningLevel> <Optimization>MaxSpeed</Optimization> <FunctionLevelLinking>true</FunctionLevelLinking> <IntrinsicFunctions>true</IntrinsicFunctions> <SDLCheck>true</SDLCheck> <PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions> <ConformanceMode>true</ConformanceMode> <PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile> </ClCompile> <Link> <SubSystem>Console</SubSystem> <EnableCOMDATFolding>true</EnableCOMDATFolding> <OptimizeReferences>true</OptimizeReferences> <GenerateDebugInformation>true</GenerateDebugInformation> </Link> </ItemDefinitionGroup> <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> <ClCompile> <PrecompiledHeader>Use</PrecompiledHeader> <WarningLevel>Level3</WarningLevel> <Optimization>MaxSpeed</Optimization> <FunctionLevelLinking>true</FunctionLevelLinking> <IntrinsicFunctions>true</IntrinsicFunctions> <SDLCheck>true</SDLCheck> <PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions> <ConformanceMode>true</ConformanceMode> <PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile> </ClCompile> <Link> <SubSystem>Console</SubSystem> <EnableCOMDATFolding>true</EnableCOMDATFolding> <OptimizeReferences>true</OptimizeReferences> <GenerateDebugInformation>true</GenerateDebugInformation> </Link> </ItemDefinitionGroup> <ItemGroup> <ClInclude Include="pch.h" /> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader> </ClCompile> <ClCompile Include="protobuf_test.cpp" /> </ItemGroup> <ItemGroup> <Text Include="test.proto" /> </ItemGroup> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" /> <ImportGroup Label="ExtensionTargets"> </ImportGroup> </Project> 

Bagunça bastante ilegível, não é? E esse ainda é um arquivo muito pequeno e quase trivial. Vamos tentar transformá-lo em algo mais legível e confortável para a percepção.

Fale sobre arquivos .props


Para fazer isso, vamos prestar atenção ao fato de que o arquivo que pegamos é um documento XML comum e pode ser logicamente dividido em duas partes, a primeira das quais lista as configurações do projeto e a segunda contém os arquivos incluídos. Vamos separar essas metades fisicamente. Para fazer isso, precisamos da tag Import já encontrada no código, que é um análogo da #include e permite incluir um arquivo em outro. Copiamos nosso arquivo .vcxproj para outro arquivo e removemos dele todas as declarações relacionadas aos arquivos incluídos no projeto e do .vcxproj, por sua vez, removemos tudo, exceto as declarações relacionadas aos arquivos realmente incluídos no projeto. O arquivo resultante com as configurações do projeto, mas sem arquivos no Visual Studio, geralmente é chamado de Folhas de propriedades e salvo com a extensão .props. Por sua vez, em .vcxproj, forneceremos a importação correspondente

Agora .vcxproj descreve apenas os arquivos incluídos no projeto e é lido muito mais facilmente
 <?xml version="1.0" encoding="utf-8"?> <Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="settings.props" /> <PropertyGroup Label="Globals"> <ProjectGuid>{0D35456E-42DA-418B-87D4-55E32B8E1373}</ProjectGuid> </PropertyGroup> <ItemGroup> <ClInclude Include="pch.h" /> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">Create</PrecompiledHeader> <PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Release|x64'">Create</PrecompiledHeader> </ClCompile> <ClCompile Include="protobuf_test.cpp" /> </ItemGroup> <ItemGroup> <Text Include="test.proto" /> </ItemGroup> </Project> 

Isso pode ser simplificado ainda mais removendo elementos XML desnecessários. Por exemplo, a propriedade "PrecompiledHeader" agora é declarada 4 vezes para diferentes opções de configuração (liberação / depuração) e plataforma (win32 / x64), mas cada vez que este anúncio é o mesmo. Além disso, vários grupos de itens diferentes são usados ​​aqui, enquanto na realidade um elemento é suficiente. Como resultado, chegamos a um .vcxproj compacto e compreensível que simplesmente lista 1) os arquivos incluídos no projeto, 2) o que cada um deles é (mais configurações específicas para arquivos individuais específicos) e 3) contém um link para as configurações do projeto armazenadas separadamente.

 <?xml version="1.0" encoding="utf-8"?><Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="settings.props" /> <PropertyGroup Label="Globals"> <ProjectGuid>{0D35456E-42DA-418B-87D4-55E32B8E1373}</ProjectGuid> </PropertyGroup> <ItemGroup> <ClInclude Include="pch.h" /> <ClCompile Include="pch.cpp"> <PrecompiledHeader>Create</PrecompiledHeader> </ClCompile> <ClCompile Include="protobuf_test.cpp" /> <Text Include="test.proto" /> </ItemGroup> </Project> 

Recarregamos o projeto no estúdio, verificamos a montagem - tudo funciona.

Mas por que separar .vcxproj e .props?


Como nada mudou na montagem, à primeira vista, pode parecer que alteramos o furador para sabão, tornando a "refatoração" inútil do arquivo no qual realmente não precisamos olhar. No entanto, digamos por um momento que nossa solução inclui mais de um projeto. Então, como você pode ver, vários arquivos .vcxproj diferentes de projetos diferentes podem usar o mesmo arquivo .props com as configurações. Separamos as regras de montagem usadas na solução do código-fonte e agora podemos alterar as configurações de montagem para todos os projetos do mesmo tipo em um único local. Na grande maioria dos casos, essa unificação da assembléia é uma boa idéia. Por exemplo, adicionando um novo projeto a uma solução, em uma ação, transferimos trivialmente para ele dessa maneira todas as configurações dos projetos existentes na solução.

Mas e se ainda precisarmos de configurações diferentes para projetos diferentes? Nesse caso, podemos simplesmente criar vários arquivos .props diferentes para diferentes tipos de projetos. Como os arquivos .props podem importar outros arquivos .props exatamente da mesma maneira, é bastante fácil e natural criar uma "hierarquia" de vários arquivos .props, desde arquivos que descrevem configurações gerais de todos os projetos em uma solução até versões altamente especializadas, especificando regras para apenas um ou dois projetos em uma solução. Existe uma regra no MSBuild de que, se a mesma configuração for declarada duas vezes no arquivo de entrada (por exemplo, ela será importada primeiro para base.props e depois declarada novamente em derivado.props, importado no início de base.props), a declaração posterior substituirá a anterior . Isso permite definir hierarquias arbitrárias e fáceis de configurações, substituindo simplesmente em cada arquivo .props todas as configurações necessárias para um determinado arquivo .props sem se preocupar com o fato de que elas já poderiam ter sido anunciadas em outro lugar. Entre outras coisas, em algum lugar em .props, é aconselhável importar as configurações padrão do ambiente do Studio que, para um projeto C ++, terão a seguinte aparência:

 <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" /> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" /> 

Observo que, na prática, é muito conveniente colocar seus próprios arquivos .props na mesma pasta que o arquivo .sln

Porque permite importar convenientemente .props, independentemente do local .vcxproj
 <?xml version="1.0" encoding="utf-8"?> <Project DefaultTargets="Build" ...> <Import Project="$(SolutionDir)\settings.props" /> ... </Project> 

Tornando as configurações do projeto mais legíveis


Agora que não precisamos mais nos preocupar com cada projeto individualmente, podemos prestar mais atenção na personalização do processo de compilação. E, para começar, recomendo dar nomes sãos aos objetos mais interessantes no sistema de arquivos relacionados à solução usando arquivos .props. Para fazer isso, devemos criar uma tag PropertyGroup marcada UserMacros:

 <PropertyGroup Label="UserMacros"> <RepositoryRoot>$(SolutionDir)\..</RepositoryRoot> <ProjectsDir>$(RepositoryRoot)\projects</ProjectsDir> <ThirdPartyDir>$(RepositoryRoot)\..\ThirdParty</ThirdPartyDir> <ProtoBufRoot>$(ThirdPartyDir)\protobuf\src</ProtoBufRoot> </PropertyGroup> 

Em seguida, nas configurações do projeto, em vez de construções do formato ".. \ .. \ .. \ ThirdParty \ protobuf \ src \ protoc.exe", podemos simplesmente escrever "$ (ProtoBufRoot) \ protoc.exe". Além da maior legibilidade, isso torna o código muito mais móvel - podemos mover livremente o .vcxproj sem medo de que suas configurações desapareçam e podemos mover (ou atualizar) o Protobuf alterando apenas uma linha em um dos arquivos .props.

Quando vários grupos de propriedades são declarados sequencialmente, seu conteúdo será mesclado - somente as macros cujos nomes coincidem com os declarados anteriormente serão substituídas. Isso permite que você complemente facilmente as declarações nos arquivos .props anexados sem medo de perder macros já anunciadas anteriormente.

Facilitamos a conexão de bibliotecas de terceiros


O processo usual de inclusão de dependências na biblioteca de terceiros no Visual Studio geralmente se parece com isso:



O processo de configurações correspondentes inclui a edição de vários parâmetros de uma só vez em diferentes guias das configurações do projeto e, portanto, bastante entediante. Além disso, geralmente isso deve ser feito várias vezes para cada configuração individual no projeto; com frequência, como resultado de tais manipulações, o projeto é montado no assembly Release, mas não no assembly Debug. Portanto, essa é uma abordagem desconfortável e não confiável. Mas como você provavelmente já adivinhou, as mesmas configurações podem ser “empacotadas” em um arquivo de acessórios. Por exemplo, para a biblioteca ZeroMQ, um arquivo semelhante pode ser algo como isto:

 <?xml version="1.0" encoding="utf-8"?> <Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <ItemDefinitionGroup> <ClCompile> <AdditionalIncludeDirectories>$(ThirdPartyDir)\libzmq\include;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories> <PreprocessorDefinitions>ZMQ_STATIC;%(PreprocessorDefinitions)</PreprocessorDefinitions> </ClCompile> <Link> <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">libzmq-v120-mt-sgd-4_3_1.lib;Ws2_32.Lib;%(AdditionalDependencies)</AdditionalDependencies> <AdditionalDependencies Condition="'$(Configuration)|$(Platform)'=='Release|x64'">libzmq-v120-mt-s-4_3_1.lib;Ws2_32.Lib;%(AdditionalDependencies)</AdditionalDependencies> <AdditionalLibraryDirectories>$(ThirdPartyDir)\libzmq\lib\x64\$(Configuration);%(AdditionalLibraryDirectories)</AdditionalLibraryDirectories> </Link> </ItemDefinitionGroup> </Project> 

Observe que, se simplesmente definirmos uma tag do tipo AdditionalLibraryDirectories no arquivo props, ela substituirá todas as definições anteriores. Portanto, uma construção um pouco mais complicada é usada na qual a tag termina com uma sequência de caracteres;% (AdditionalLibraryDirectories) formando o link da tag para si mesma. Na semântica do MSBuild, essa macro é expandida para o valor da marca anterior, portanto, uma construção semelhante anexa os parâmetros ao início da linha armazenada no parâmetro AdditionalLibraryDirectories.

Para conectar o ZeroMQ agora, basta importar o arquivo .props fornecido.

 <?xml version="1.0" encoding="utf-8"?> <Project DefaultTargets="Build" ...> <Import Project="$(SolutionDir)\settings.props" /> <Import Project="$(SolutionDir)\zeromq.props" /> ... </Project> 

E é aí que as manipulações com o final do projeto - o MSBuild incluirão automaticamente os arquivos e bibliotecas de cabeçalho necessários nos assemblies de Liberação e Depuração. Assim, gastando um pouco de tempo escrevendo zeromq.props, temos a oportunidade de conectar o ZeroMQ de maneira confiável e precisa a qualquer projeto em apenas uma linha. Os criadores do Studio ainda forneceram para isso uma GUI especial chamada Property Manager, para que os amantes do mouse possam fazer a mesma operação em apenas alguns cliques.



É verdade que, como as outras ferramentas do Studio, essa GUI adiciona algo como o código .vcxproj em vez de uma linha única legível

esse código
  <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'"> <Import Project="..\..\..\..\projects\BuildSystem\thirdparty_libraries\PropertySheets\zeromq.props" /> </ImportGroup> <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> <Import Project="..\..\..\..\projects\BuildSystem\thirdparty_libraries\PropertySheets\zeromq.props" /> </ImportGroup> <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'"> <Import Project="..\..\..\..\projects\BuildSystem\thirdparty_libraries\PropertySheets\zeromq.props" /> </ImportGroup> <ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'"> <Import Project="..\..\..\..\projects\BuildSystem\thirdparty_libraries\PropertySheets\zeromq.props" /> </ImportGroup> 

Portanto, prefiro adicionar links a bibliotecas de terceiros aos arquivos .vcxproj manualmente.

Semelhante ao discutido anteriormente, trabalhar com componentes de Terceiros através de arquivos .props facilita igualmente a atualização de bibliotecas usadas no futuro. É suficiente editar um único arquivo zeromq.props e o conjunto de toda a solução muda de forma síncrona para a nova versão. Por exemplo, em nossos projetos, a criação do projeto por esse mecanismo está vinculada ao gerenciador de dependências da Conan, que coleta o conjunto necessário de bibliotecas de terceiros do manifesto de dependência e gera automaticamente os arquivos .props correspondentes.

Modelos de projeto - automatize a criação de projetos


A edição manual de arquivos .vcxproj criados pelo Studio é certamente muito chata (embora você tenha a habilidade e não por muito tempo). Portanto, o Studio oferece uma oportunidade conveniente para criar seus próprios modelos para novos projetos que permitem configurar manualmente .vcxproj apenas uma vez e depois reutilizá-lo com um clique em qualquer novo projeto. No caso mais simples, você nem precisa editar nada manualmente - basta abrir o projeto que você precisa transformar em modelo e selecionar Projeto \ Exportar modelo no menu. Na caixa de diálogo exibida, você pode especificar vários parâmetros triviais, como o nome do modelo ou linha que será exibido em sua descrição, além de selecionar se o modelo recém-criado será imediatamente adicionado à caixa de diálogo Novo Projeto. O modelo criado dessa maneira cria uma cópia do projeto usado para criá-lo (incluindo todos os arquivos incluídos no projeto), substituindo apenas o nome do projeto e sua GUID nele. Em uma porcentagem bastante grande de casos, isso é mais do que suficiente.

Com um exame mais detalhado do modelo gerado pelo Studio, você pode facilmente garantir que seja apenas um arquivo zip no qual estão localizados todos os arquivos usados ​​no modelo e um arquivo de configuração adicional com a extensão .vstemplate. Este arquivo contém uma lista de metadados do projeto (como o ícone ou a linha de descrição usada) e uma lista de arquivos que devem ser criados ao criar um novo projeto. Por exemplo

 <VSTemplate Version="3.0.0" xmlns="http://schemas.microsoft.com/developer/vstemplate/2005" Type="Project"> <TemplateData> <Name>C++ console application</Name> <Description>C++ console application for our project</Description> <ProjectType>VC</ProjectType> <ProjectSubType> </ProjectSubType> <SortOrder>1000</SortOrder> <CreateNewFolder>true</CreateNewFolder> ` <DefaultName>OurCppConsoleApp</DefaultName> <ProvideDefaultName>true</ProvideDefaultName> <LocationField>Enabled</LocationField> <EnableLocationBrowseButton>true</EnableLocationBrowseButton> <Icon>ng.ico</Icon> </TemplateData> <TemplateContent> <Project TargetFileName="$projectname$.vcxproj" File="console_app.vcxproj" ReplaceParameters="true"> <ProjectItem ReplaceParameters="false" TargetFileName="$projectname$.vcxproj.filters">console_app.vcxproj.filters</ProjectItem> <ProjectItem ReplaceParameters="false" TargetFileName="main.cpp">main.cpp</ProjectItem> <ProjectItem ReplaceParameters="false" TargetFileName="stdafx.cpp">stdafx.cpp</ProjectItem> <ProjectItem ReplaceParameters="false" TargetFileName="stdafx.h">stdafx.h</ProjectItem> </Project> </TemplateContent> </VSTemplate> 

Preste atenção ao parâmetro ReplaceParameters = "true". Nesse caso, ele se aplica apenas ao arquivo vcxproj, que se parece com isso:

 <?xml version="1.0" encoding="utf-8"?> <Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <Import Project="$(SolutionDir)\console_app.props" /> <PropertyGroup Label="Globals"> <ProjectGuid>{$guid1$}</ProjectGuid> <RootNamespace>$safeprojectname$</RootNamespace> </PropertyGroup> <ItemGroup> <ClCompile Include="main.cpp" /> <ClCompile Include="stdafx.cpp"> <PrecompiledHeader>Create</PrecompiledHeader> </ClCompile> </ItemGroup> <ItemGroup> <ClInclude Include="stdafx.h" /> </ItemGroup> </Project> 

No lugar do GUID e RootNamespace, como você pode ver, não há valores específicos, mas os stubs $ guid1 $ e $ safeprojectname $. Ao usar o modelo, o Studio examina os arquivos ReplaceParamters = "true", procura por stubs do formulário $ name $ e os substitui por valores calculados usando um dicionário especial. Por padrão, o Studio não suporta muitos parâmetros , mas ao escrever as Extensões do Visual Studio (sobre as quais falaremos mais adiante), é fácil adicionar quantos dos seus próprios parâmetros calculados (ou inseridos pelo usuário) quando você inicia a caixa de diálogo para criar um novo projeto a partir do modelo. Como você pode ver no arquivo .vstemplate, o mesmo dicionário também pode ser usado para gerar um nome de arquivo, o que permite, em particular, gerar nomes exclusivos para arquivos .vcxproj para diferentes projetos. Ao definir ReplaceParameters = false, o arquivo especificado no modelo será simplesmente copiado sem processamento adicional.

O arquivo ZIP resultante com o modelo pode ser adicionado à lista de modelos conhecidos pelo Studio de várias maneiras. A maneira mais fácil é simplesmente copiar esse arquivo para a pasta % USERPROFILE% \ Documents \ Visual Studio XX \ Templates \ ProjectTemplates . É importante notar que, apesar de nesta pasta você encontrar muitas subpastas diferentes que correspondem aos nomes das pastas na janela para criar um novo projeto, na verdade o modelo deve ser colocado simplesmente na pasta raiz, já que a posição do modelo na árvore de novos projetos é determinada pelo Studio pelas tags ProjectType e ProjectSubType no arquivo .vstemplate. Esse método é mais adequado para criar modelos "pessoais" exclusivos apenas para você e se você marcar a caixa de seleção "Importar modelo automaticamente para o Visual Studio" na caixa de diálogo Exportar modelo, é exatamente isso que o Studio fará colocando o arquivo zip criado durante a exportação nesta pasta com padrões. No entanto, compartilhar esses modelos com os colegas copiando-os manualmente certamente não é muito conveniente. Então, vamos nos familiarizar com uma opção um pouco mais avançada - crie uma Extensão do Visual Studio (.vsix)

Para criar o VSIX, precisamos instalar o componente opcional do Studio, chamado de ferramenta de desenvolvimento de extensões do Visual Studio:


Depois disso, a opção "projeto VSIX" aparecerá na seção Visual C # \ Extensibility. Observe que, apesar de sua localização (C #), ele é usado para criar quaisquer extensões, incluindo conjuntos de modelos de projeto em C ++.


No projeto criado pelo VSIX, você pode fazer muitas coisas diferentes - por exemplo, crie sua própria caixa de diálogo que será usada para configurar os projetos criados pelo modelo. Mas este é um tópico enorme para discussão, que não abordarei neste artigo. Para criar modelos no VSIX, tudo é muito simples: crie um projeto VSIX vazio, abra o arquivo .vsixmanifest e configure todos os dados para o projeto diretamente na GUI. Digite os metadados (nome da extensão, descrição, licença) na guia Metadados. Preste atenção ao campo "Versão" localizado no canto superior direito - é desejável especificá-lo corretamente, pois o Studio posteriormente o utiliza para determinar qual versão da extensão está instalada no computador. Em seguida, vamos para a guia Ativos e selecione “Adicionar novo ativo”, com Tipo: Microsoft.VisualStudio.ProjectTemplate, Fonte: Arquivo no sistema de arquivos, Caminho: (nome do arquivo zip com o modelo). Clique em OK, repita o processo até adicionarmos todos os modelos desejados ao VSIX.


Depois disso, resta escolher Configuração: Liberar e comandar a Solução de Compilação. Você não precisa escrever código, mas também editar manualmente os arquivos de configuração. A saída é um arquivo portátil com a extensão .vsix, que é, de fato, um instalador para a extensão que criamos. O arquivo criado será "iniciado" em qualquer computador com o Studio instalado, mostrará uma caixa de diálogo com a descrição da extensão e da licença e oferecerá a instalação de seu conteúdo. Permitindo a instalação - adicionamos nossos modelos na caixa de diálogo "Criar um novo projeto"


Essa abordagem facilita a unificação do trabalho de um grande número de pessoas em um projeto. Para instalar e usar os modelos, o usuário não precisa de qualificações, exceto alguns cliques do mouse. A extensão instalada pode ser visualizada (e removida) na caixa de diálogo Ferramentas \ Extensões e atualizações


Nível 2: compilação personalizada personalizada


OK, nesta fase, descobrimos como os arquivos vcxproj e props são organizados e aprendemos como organizá-los. Vamos agora assumir que queremos adicionar ao nosso projeto alguns esquemas .proto para serializar objetos com base na maravilhosa biblioteca de buffers de protocolo do Google. Deixe-me lembrá-lo da idéia principal desta biblioteca: você escreve uma descrição do objeto ("esquema") em uma meta-linguagem independente de plataforma especial (arquivo .proto) que é compilada por um compilador especial (protoc.exe) no arquivo .cpp / .cs / .py / .java / etc. arquivos que implementam serialização / desserialização de objetos de acordo com este esquema na linguagem de programação desejada e que você pode usar em seu projeto. Portanto, ao compilar o projeto, primeiro precisamos chamar protoc, que criará para nós um conjunto de arquivos .cpp que usaremos no futuro.

Abordagem tradicional


A implementação clássica da testa é direta e consiste simplesmente em adicionar uma chamada protoc à etapa de pré-construção de um projeto que precisa de arquivos .proto. Algo assim:



Mas isso não é muito conveniente:
  • É necessário especificar explicitamente a lista de arquivos processados ​​no comando
  • Se você alterar esses arquivos, a compilação NÃO será reconstruída automaticamente
  • Ao alterar OUTROS arquivos em um projeto que o Studio reconhece como códigos-fonte, pelo contrário, uma etapa de pré-construção será executada desnecessariamente
  • Os arquivos gerados não são incluídos na montagem do projeto por padrão
  • Se incluirmos manualmente os arquivos gerados no projeto, o projeto gerará um erro quando o abrirmos pela primeira vez (já que os arquivos ainda não foram gerados pela primeira montagem).

Em vez disso, tentamos "explicar" ao próprio Visual Studio (ou melhor, ao sistema de compilação MSBuild usado) como lidar com esses arquivos .proto.

Atingir as metas do MSBuild


Do ponto de vista do MSBuild, a montagem de qualquer projeto consiste em uma sequência de montagem de entidades chamadas destinos de criação, destinos abreviados. Por exemplo, a criação de um projeto pode incluir a execução do destino Limpo, que removerá os arquivos temporários restantes das compilações anteriores, a execução do destino Compilar, que compilará o projeto, o destino do Link e, finalmente, o destino de Implantação. Todos esses destinos, juntamente com as regras para sua montagem, não são corrigidos com antecedência, mas são definidos no próprio arquivo .vcxproj. Se você está familiarizado com o utilitário nix make e a palavra "makefile" vem à mente naquele momento, então você está absolutamente certo: .vcxproj é uma variação XML no tópico makefile.

Mas pare-pare-pare será dito por um leitor confuso. Como é isso? Antes disso, analisamos o .vcxproj em um projeto simples e não havia metas ou semelhanças com o makefile clássico. Que tipo de alvo pode ser discutido então? Acontece que eles estão simplesmente "ocultos" nesta linha, que inclui em .vcxproj um conjunto de destinos padrão para a criação de código C ++.

 <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" /> 

«» - C++- «» Build, Clean Rebuild «» . toolset toolset , , Clang. toolset- toolset . , .

target-. target MSBuild
  • Lista de entradas
  • Lista de saídas
  • Dependências em outros destinos (dependências)
  • Configurações de destino
  • A sequência das etapas reais executadas pelo destino (tarefas)

Por exemplo, o destino ClCompile recebe uma lista de arquivos .cpp no ​​projeto como uma entrada e gera um conjunto de arquivos .obj a partir deles, arrastando o compilador cl.exe. As configurações de destino do ClCompile se transformam em sinalizadores de compilação passados ​​para o cl.exe. Quando escrevemos a linha no arquivo .vcxproj

 <ClCompile Include="protobuf_test.cpp" /> 

adicionamos o arquivo Incluir protobuf_tests.cpp à lista de entradas desse destino e, quando escrevemos

 <ItemDefinitionGroup> <ClCompile> <PrecompiledHeader>Use</PrecompiledHeader> </ClCompile> </ItemDefinitionGroup> 

em seguida, atribuímos o valor "Use" à configuração ClCompile.PrecompiledHeader cujo destino será o sinalizador / Yu passado para cl.exe para o compilador.

Vamos tentar criar um destino para criar arquivos .proto


A adição de um novo destino é implementada usando a tag target:

 <Target Name="GenerateProtobuf"> ...steps to take... </Target> 

Tradicionalmente, os destinos são colocados em um arquivo com a extensão .targets. Não que isso fosse estritamente necessário (tanto o vcxproj quanto os arquivos de destinos e props são XML equivalentes), mas esse é um esquema de nomenclatura padrão e nós vamos nos ater a ele. Para que, no código do arquivo .vcxproj, você possa escrever algo como

 <ItemGroup> <ClInclude Include="cpp.h"/> <ProtobufFile Include="my.proto" /> <ItemGroup> 

o destino que criamos deve ser adicionado à lista AvailableItemName

 <ItemGroup> <AvailableItemName Include="ProtobufFile"> <Targets>GenerateProtobuf</Targets> </AvailableItemName> </ItemGroup> 

Também precisaremos descrever o que exatamente queremos fazer com nossos arquivos de entrada e o que deve acontecer na saída. Para fazer isso, o MSBuild usa uma entidade chamada "tarefa". Tarefa - esta é uma ação simples que precisa ser realizada durante a montagem do projeto. Por exemplo, “crie um diretório”, “compile um arquivo”, “execute um comando”, “copie algo”. No nosso caso, usaremos o Exec oculto para executar protoc.exe e a Mensagem oculta para exibir esta etapa no log de compilação. Também indicamos que o lançamento desse destino deve ser realizado imediatamente após o destino padrão PrepareForBuild. Como resultado, obtemos algo parecido com este arquivo protobuf.targets

 <Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <ItemGroup> <AvailableItemName Include="ProtobufSchema"> <Targets>GenerateProtobuf</Targets> </AvailableItemName> </ItemGroup> <Target Name="GenerateProtobuf" Inputs="%(ProtobufSchema.FullPath)" Outputs=".\generated\%(ProtobufSchema.Filename).pb.cc" AfterTargets="PrepareForBuild"> <Message Importance="High" Text="Compiling schema %(ProtobufSchema.Identity)" /> <Exec Command="$(Protoc) --cpp_out=.\generated %(ProtobufSchema.Identity)" /> </Target> </Project> 

Usamos aqui um operador não trivial "%" ( operador de lote ), que significa "para cada item da lista" e adicionamos automaticamente metadados . A idéia aqui é a seguinte: quando escrevemos código do formulário

 <ItemGroup> <ProtobufSchema Include="test.proto"> <AdditionalData>Test</AdditionalData> </ProtobufSchema> </ItemGroup> 

esse registro é adicionado à lista com o nome "ProtobufSchema", um elemento filho "test.proto", que possui um elemento filho (metadados) AdditionalData que contém a cadeia "Test". Se escrevermos "ProtobufSchema.AdditionalData", teremos acesso ao registro "Test". Além dos metadados AdditionalData que declaramos explicitamente, para nossa conveniência, o MSBuild astucioso adiciona automaticamente ao registro outra dúzia de elementos filho frequentemente usados, descritos aqui, entre os quais usamos Identity (a linha de origem), Filename (nome do arquivo sem extensão) e FullPath ( caminho completo para o arquivo). Um registro com o sinal de% força o MSBuild a aplicar a operação descrita por nós a cada elemento da lista - ou seja, para cada arquivo .proto individualmente.

Adicione agora

  <Import Project="protobuf.targets" Label="ExtensionTargets"/> 

em protobuf.props, reescrevemos nossos arquivos de proto em .vcxproj-e na tag ProtobufSchema

  <ItemGroup> ... <ProtobufSchema Include="test.proto" /> <ProtobufSchema Include="test2.proto" /> </ItemGroup> 

e verifique a montagem

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 ==========


Viva!Ganhou! É verdade que agora nossos arquivos .proto não estão mais visíveis no projeto. Subimos para .vcxproj.filters e entramos por analogia

 ... <ItemGroup> <ProtobufSchema Include="test.proto"> <Filter>Resource Files</Filter> </ProtobufSchema> <ProtobufSchema Include="test2.proto"> <Filter>Resource Files</Filter> </ProtobufSchema> </ItemGroup> ... 

Recarregamos o projeto - os arquivos são visíveis novamente.

Lembramos nosso exemplo de modelo


No entanto, na verdade, eu trapacei um pouco. Se você não criar manualmente a pasta gerada antes do início da compilação, ela será travada.Para

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.

corrigir isso, adicione um destino auxiliar que criará a pasta necessária

 ... <Target Name="PrepareToGenerateProtobuf" Inputs="@(ProtobufSchema)" Outputs=".\generated"> <MakeDir Directories=".\generated"/> </Target> <Target Name="GenerateProtobuf" DependsOnTargets="PrepareToGenerateProtobuf" ... 

Usando a propriedade DependsOnTargets, indicamos que, antes de iniciar qualquer uma das tarefas GenerateProtobuf, você deve executar o PrepareToGenerateProtobuf, e a entrada @ (ProtobufSchema) se refere à lista inteira do ProtobufSchema, como uma entidade única usada como entrada para esta tarefa, para que seja iniciada apenas uma vez.

Reiniciando a montagem - funciona! Vamos tentar agora reconstruir novamente, dessa vez, com certeza, para ter certeza de tudo o que Em, mas para onde foram nossas novas tarefas? Um pouco de depuração - e vemos que as tarefas são realmente iniciadas pelo MSBuild, mas não são executadas, pois a pasta gerada já está na pasta de saída especificada. Basta colocar, em Reconstruir, Limpar para. \ Arquivos gerados não funciona para nós. Corrija isso adicionando outro destino

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 ==========




 <Target Name="CleanProtobuf" AfterTargets="Clean"> <RemoveDir Directories=".\generated"/> </Target> 

Verifique - funciona. O Clean limpa os arquivos que criamos, o Rebuild os recria novamente, chamando Build novamente não inicia a reconstrução desnecessariamente. Fazemos uma edição em um dos arquivos C ++, tentamos fazer com que o arquivo novamente .proto do Build não tenha sido alterado, portanto o protoc não foi reiniciado, tudo é esperado. Agora tentamos alterar o arquivo .proto. É interessante que, se você iniciar a montagem do MSBuild pela linha de comando manualmente, e não pela interface do usuário do Studio, não haverá esse problema - o MSBuild recompilará corretamente os arquivos .pp.cc necessários. Se alterarmos qualquer arquivo .cpp, o MSBuild, que foi iniciado no estúdio, recompilará não apenas o arquivo, mas também o arquivo .props que alteramos anteriormente .

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



1>------ Build started: Project: protobuf_test, Configuration: Debug x64 ------
1>protobuf_test.cpp
1>protobuf_test.vcxproj -> S:\Temp\msbuild\protobuf_msbuild_integration\x64\Debug\protobuf_test.exe
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========




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



1>------ Build started: Project: protobuf_test, Configuration: Debug x64 ------
1>Compiling schema test.proto
1>protobuf_test.cpp
1>protobuf_test.vcxproj -> S:\Temp\msbuild\protobuf_msbuild_integration\x64\Debug\protobuf_test.exe
========== Build: 1 succeeded, 0 failed, 0 up-to-date, 0 skipped ==========




Arquivos U2DCheck e tlog


Acontece que os criadores do Visual Studio acharam que chamar o MSBuild para todos era muito caro e ... implementaram sua própria "verificação rápida" sobre a criação ou não de um projeto. Ele se chama U2DCheck e, se, na sua opinião, o projeto não foi alterado, o Studio simplesmente não iniciará o MSBuild para este projeto. Normalmente, o U2DCheck funciona tão "silenciosamente" que poucas pessoas pensam sobre sua existência, mas você pode ativar um sinalizador útil no registro que forçará o U2DCheck a exibir relatórios mais detalhados.

Em seu trabalho, o U2DCheck conta com arquivos .tlog especiais. Eles podem ser facilmente encontrados na pasta de saída intermediária (nome_do_projeto) .tlog e para o U2DCheck responder corretamente às alterações nos arquivos de origem, precisamos gravar em um dos arquivos tlog lidos nessa pasta e para o U2DCheck responder corretamente à remoção dos arquivos de saída - gravar em um dos arquivos de gravação tlog.

Amaldiçoando, retornamos à edição correspondente de nossa meta

 ... <Exec Command="$(Protoc) --cpp_out=.\generated %(ProtobufSchema.Identity)" /> <WriteLinesToFile File="$(TLogLocation)\protobuf.read.1.tlog" Lines="^%(ProtobufSchema.FullPath)" /> 

Verificamos - funciona: a edição do arquivo .props aciona a reconstrução necessária; a montagem, na ausência de edição, mostra que o projeto está atualizado. Neste exemplo, por simplicidade, não escrevi write tlog para rastrear a exclusão de arquivos criados durante a compilação, mas ele foi adicionado ao destino da mesma maneira.

A partir da atualização 15.8 do Visual Studio 2017, uma nova tarefa padrão GetOutOfDateItems foi adicionada ao MSBuild que automatiza essa magia negra, mas desde que isso aconteceu recentemente, quase todos os .target-s personalizados continuam trabalhando com arquivos .tlog manualmente.

Se desejar, você também pode desativar completamente o U2DCheck para qualquer projeto adicionando uma linha ao campo ProjectCapability

 <ItemGroup> <ProjectCapability Include="NoVCDefaultBuildUpToDateCheckProvider" /> </ItemGroup> 

No entanto, nesse caso, o Studio conduzirá o MSBuild para este projeto e todos os que dependem dele para cada compilação. Sim, o U2DCheck foi adicionado por um motivo - ele não funciona tão rápido quanto eu gostaria.

Finalize nosso .target personalizado


O resultado obtido é bastante funcional, mas ainda há espaço para melhorias. Por exemplo, no MSBuild, existe um modo de "montagem seletiva" quando a linha de comando indica que não é necessário montar o projeto inteiro como um todo, mas apenas arquivos individuais especificamente selecionados nele. O suporte para este modo requer que o destino verifique o conteúdo da lista @ (SelectedFiles).

Além disso, em nosso código-fonte existem muitas linhas repetindo umas às outras que devem coincidir. As boas maneiras recomendam dar nomes legíveis a todas essas entidades e abordá-las por esses nomes. Para isso, muitas vezes é criado um destino especial separado que cria e preenche a lista auxiliar com todos os nomes que serão necessários no futuro.

Por fim, ainda não percebemos a ideia prometida no início - a inclusão automática dos arquivos gerados no projeto. Já podemos # incluir os arquivos de cabeçalho gerados pelo protobuf, sabendo que eles serão criados automaticamente antes da compilação, mas esse número não funciona com o vinculador :). Portanto, simplesmente adicionamos os arquivos gerados à lista ClCompile.

Um exemplo de uma implementação penteada semelhante de protobuf.targets
 <?xml version="1.0" encoding="utf-8"?> <Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <ItemGroup> <AvailableItemName Include="ProtobufSchema"> <Targets>GenerateProtobuf</Targets> </AvailableItemName> </ItemGroup> <PropertyGroup> <ProtobufOutputFolder>.\generated</ProtobufOutputFolder> </PropertyGroup> <Target Name="ComputeProtobufInput"> <ItemGroup> <ProtobufCompilerData Include="@(ProtobufSchema)"> <OutputCppFile>$(ProtobufOutputFolder)\%(ProtobufSchema.Filename).pb.cc</OutputCppFile> <OutputPythonFile>$(ProtobufOutputFolder)\%(ProtobufSchema.Filename)_pb2.py</OutputPythonFile> <OutputFiles>%(ProtobufCompilerData.OutputCppFile);%(ProtobufCompilerData.OutputPythonFile)</OutputFiles> </ProtobufCompilerData> <ClCompile Include="%(ProtobufCompilerData.OutputCppFile)"> <PrecompiledHeader>NotUsing</PrecompiledHeader> </ClCompile> </ItemGroup> </Target> <Target Name="PrepareToGenerateProtobuf" Condition="'@(ProtobufSchema)'!=''" Inputs="@(ProtobufSchema)" Outputs="$(ProtobufOutputFolder)"> <MakeDir Directories="$(ProtobufOutputFolder)"/> </Target> <Target Name="GenerateProtobuf" DependsOnTargets="PrepareToGenerateProtobuf;ComputeProtobufInput" Inputs="%(ProtobufCompilerData.FullPath)" Outputs="%(ProtobufCompilerData.OutputFiles)" AfterTargets="PrepareForBuild" BeforeTargets="Compile"> <Message Importance="High" Text="Compiling schema %(ProtobufCompilerData.Identity)" /> <Exec Command="$(Protoc) --cpp_out=$(ProtobufOutputFolder) --python_out=$(ProtobufOutputFolder) %(ProtobufCompilerData.Identity)"> <Output ItemName="GeneratedFiles" TaskParameter="Outputs"/> </Exec> <WriteLinesToFile File="$(TLogLocation)\protobuf.read.1.tlog" Lines="^%(ProtobufCompilerData.FullPath)" /> </Target> <Target Name="CleanProtobuf" AfterTargets="Clean"> <RemoveDir Directories="$(ProtobufOutputFolder)"/> </Target> </Project> 

As configurações gerais aqui foram feitas no PropertyGroup e o novo destino ComputeProtobufInput preenche as listas de arquivos de entrada e saída. Ao longo do caminho (para demonstrar o trabalho com listas de arquivos de saída), foi adicionada a geração de código a partir do esquema de integração com o python. Começamos e verificamos que tudo funciona corretamente

 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>test.pb.cc 1>test2.pb.cc 1>Generating Code... 1>protobuf_test.vcxproj -> S:\Temp\msbuild\protobuf_msbuild_integration\x64\Debug\protobuf_test.exe 1>Done building project "protobuf_test.vcxproj". ========== Rebuild All: 1 succeeded, 0 failed, 0 skipped ========== 

E o CustomBuildStep?


Devo dizer que os desenvolvedores da Microsoft estimaram sensatamente que tudo o que foi mencionado acima, hmm, é um tanto trivial e pouco documentado e tentou facilitar a vida dos programadores ao introduzir um CustomBuildStep personalizado. Como parte desse conceito, teríamos que observar nas configurações do arquivo que nossos arquivos .props são do tipo Etapa de compilação personalizada



. Devemos indicar as etapas necessárias de montagem na guia Etapa de compilação personalizada.



Em .vcxproj, é algo como isto

  <ItemDefinitionGroup> <CustomBuildStep> <Command>$(Protoc) --cpp_out=.\generated\%(FileName).pb.cc %(FullPath)</Command> <Message>Generate protobuf files</Message> <Outputs>.\generated\%(FileName).pb.cc</Outputs> </CustomBuildStep> </ItemDefinitionGroup> <ItemGroup> ... <CustomBuild Include="test.proto"/> <CustomBuild Include="test2.proto"/> ... </ItemGroup> 

Essa construção funciona devido ao fato de os dados inseridos dessa maneira serem substituídos nas entranhas do Microsoft.CppCommon.targets em um destino especial CustomBuildStep, que geralmente faz o mesmo que eu descrevi acima. Mas tudo funciona através da GUI e não há necessidade de pensar em implementar clean e tlog-ah :). Se desejado, esse mecanismo pode ser usado, mas eu não recomendaria fazer isso devido às seguintes considerações:

  • CustomBuildStep pode ser apenas um para todo o projeto
    • Assim, apenas 1 tipo de arquivo pode ser processado por projeto
    • Não é prático incluir essa etapa no arquivo .props usado para conectar a biblioteca ThirdParty, porque bibliotecas diferentes podem se sobrepor
  • Se algo ocorrer no CustomBuildStep, entender o que aconteceu será ainda mais difícil do que escrever um destino do zero.

Cópia de arquivo adequada


Um tipo muito comum de destino de construção é copiar alguns arquivos de um lugar para outro. Por exemplo, copiar arquivos de recursos para uma pasta com um projeto compilado ou copiar uma DLL de terceiros para um binário compilado. E muitas vezes essa operação é implementada "de frente" através do lançamento do utilitário do console xcopy nos destinos pós-compilação. Por exemplo,



você não precisa fazer isso pelos mesmos motivos que não precisa tentar inserir outros destinos de criação nas etapas pós-criação. Em vez disso, podemos dizer diretamente ao Studio que ele precisa copiar um arquivo específico. Por exemplo, se o arquivo for incluído diretamente no projeto, será suficiente especificar ItemType = Copy



Depois de clicar no botão aplicar, aparecerá uma guia adicional na qual você poderá configurar onde e como copiar o arquivo selecionado. No código do arquivo .vcxproj, será algo parecido com isto:

  <ItemGroup> ... <ProtobufSchema Include="test2.proto" /> <CopyFileToFolders Include="resource.txt"> <DestinationFolders>$(OutDir)</DestinationFolders> </CopyFileToFolders> </ItemGroup> 

Tudo funcionará imediatamente, incluindo o suporte correto para arquivos tlog. Por dentro, é implementado de acordo com o mesmo princípio de “tarefa padrão especial para copiar arquivos” que a Etapa de Construção Personalizada, que eu critiquei literalmente na seção anterior, mas como copiar arquivos é uma operação bastante trivial e não redefinimos a operação (cópia), mas apenas alteramos a lista arquivos de entrada e saída para ela, então funciona bem.

Observo que, ao criar listas de arquivos CopyFilesToFolder, você pode usar curingas. Por exemplo

 <CopyFileToFolders Include="$(LibFolder)\*.dll"> <DestinationFolders>$(OutDir)</DestinationFolders> </CopyFileToFolders> 

Adicionar arquivos à lista CopyFileToFolders é talvez a maneira mais fácil de implementar a cópia ao criar um projeto, incluindo arquivos .props que incluem bibliotecas de terceiros. No entanto, se você deseja obter mais controle sobre o que está acontecendo, outra opção é adicionar a tarefa de cópia especializada ao seu destino de construção . Por exemplo

 <Target Name="_CopyLog4cppDll" Inputs="$(Log4cppDll)" Outputs="$(Log4cppDllTarget)" AfterTargets="PrepareForBuild"> <Message Text="Copying log4cpp.dll..." importance="high"/> <Copy SourceFiles="$(Log4cppDll)" DestinationFiles="$(Log4cppDllTarget)" SkipUnchangedFiles="true" Retries="10" RetryDelayMilliseconds="500" /> </Target> 

Uma ligeira digressão
Em geral, o conjunto de várias tarefas padrão para o MS é muito extenso e inclui tarefas como DownloadFile, VerifyFileHash, Unzip e muitas outras primitivas úteis. E a tarefa de cópia padrão pode tentar novamente, ignorar arquivos inalterados e criar links físicos em vez de cópias estúpidas, se suportada pelo sistema de arquivos.

Infelizmente, a tarefa de cópia não suporta caracteres curinga e não preenche arquivos .tlog. Se desejado, isso pode ser implementado manualmente,

por exemplo assim
  <Target Name="_PrepareToCopy"> <ItemGroup> <MyFilesToCopy Include="$(LibFolder)\*.dll"/> <MyFilesToCopy> <DestinationFile>$(TargetFolder)\%(MyFilesToCopy.Filename)%(MyFilesToCopy.Extension)</DestinationFile> </MyFilesToCopy> </ItemGroup> </Target> <Target Name="_Copy" Inputs="@(MyFilesToCopy)" Outputs="%(MyFilesToCopy.DestinationFile)" DependsOnTargets="_PrepareToCopy" AfterTargets="PrepareForBuild"> <Message Text="Copying %(MyFilesToCopy.Filename)..." importance="high" /> <Copy SourceFiles="@(MyFilesToCopy)" DestinationFolder="$(TargetFolder)" SkipUnchangedFiles="true" Retries="10" RetryDelayMilliseconds="500" /> <WriteLinesToFile File="$(TLogLocation)\mycopy.read.1.tlog" Lines="^%(MyFilesToCopy.Identity)" /> <WriteLinesToFile File="$(TLogLocation)\mycopy.write.1.tlog" Lines="^%(MyFilesToCopy.Identity);%(MyFilesToCopy.DestinationFile)" /> </Target> 

No entanto, trabalhar com CopyFileToFolders padrão geralmente será muito mais fácil.

Nível 3: integração com a GUI do Visual Studio


Tudo o que temos feito até agora pelo lado pode parecer uma tentativa bastante monótona de implementar a funcionalidade do make normal em uma ferramenta que não é muito adequada para isso. Edição manual de arquivos XML, construções não óbvias para solucionar tarefas simples, arquivos tlog de muleta ... No entanto, o sistema de criação do Studio tem suas vantagens - por exemplo, após a configuração inicial, ele fornece ao plano de criação resultante uma boa interface gráfica. Para sua implementação, a tag PropertyPageSchema é usada, sobre a qual falaremos agora.

Retiramos as configurações das entranhas de .vcxproj nas Propriedades de configuração


Vamos tentar fazer com que possamos editar a propriedade $ (ProtobufOutputFolder) da "implementação combinada de protobuf.targets" não manualmente no arquivo, mas com conforto diretamente do IDE. Para fazer isso, precisamos escrever um arquivo XAML especial com uma descrição das configurações. Abra um editor de texto e crie um arquivo com o nome, por exemplo, custom_settings.xml

 <?xml version="1.0" encoding="utf-8"?> <ProjectSchemaDefinitions xmlns="clr-namespace:Microsoft.Build.Framework.XamlTypes;assembly=Microsoft.Build.Framework"> <Rule Name="CustomProperties" PageTemplate="generic" DisplayName="My own properties"> <Rule.DataSource> <DataSource Persistence="ProjectFile"/> </Rule.DataSource> <StringProperty Name="ProtobufOutputFolder" DisplayName="Protobuf Output Directory" Description="Directory where Protobuf generated files are created." Subtype="folder"> </StringProperty> </Rule> </ProjectSchemaDefinitions> 

Além da própria tag StringProperty, que indica ao Studio a existência da configuração "ProtobufOutputFolder" com os tipos String e Subtype = Folder e explica como deve ser exibida na GUI, esse XML-nick indica que essas informações devem ser armazenadas no arquivo do projeto. Além do ProjectFile, você também pode usar o UserFile - os dados serão gravados em um arquivo .vcxproj.user separado que, conforme concebido pelos criadores do Studio, é destinado a configurações privadas (não salvas no VCS). Conectamos o esquema descrito por nós ao projeto, adicionando a tag PropertyPageSchema ao nosso protobuf.targets

 <ItemGroup> <AvailableItemName Include="ProtobufSchema"> <Targets>GenerateProtobuf</Targets> </AvailableItemName> <PropertyPageSchema Include="custom_settings.xml"/> </ItemGroup> 

Para que nossas edições entrem em vigor, reinicie o Studio, carregue nosso projeto, abra as propriedades do projeto e veja ...



SimNossa página com a nossa configuração apareceu e seu valor padrão foi lido corretamente pelo Studio. Tentamos alterá-lo, salvar o projeto, ver .vcxproj ...

  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> <ProtobufOutputFolder>.\generated_custom</ProtobufOutputFolder> </PropertyGroup> 

Como você pode ver pela condição Condição tradicional, as configurações padrão estão associadas a uma configuração de compilação específica. Mas, se desejado, isso pode ser bloqueado definindo o sinalizador DataSource HasConfigurationCondition = "false". É verdade que no estúdio de 2017 há um erro devido ao qual as configurações do projeto podem não ser mostradas se não houver pelo menos uma configuração associada a alguma configuração entre elas. Felizmente, essa configuração pode não estar visível.

Opção sem ligação à configuração
<?xml version="1.0" encoding="utf-8"?>
<Rule.DataSource>
/>
</Rule.DataSource>
<StringProperty Name="ProtobufOutputFolder"
DisplayName="Protobuf Output Directory"
Description="Directory where Protobuf generated files are created."
Subtype="folder">
<StringProperty.DataSource>
/>
</StringProperty.DataSource>

/>



Você pode adicionar qualquer número de configurações. Os tipos possíveis incluem BoolProperty, StringProperty (com os subtipos opcionais "pasta" e "arquivo"), StringListProperty, IntProperty, EnumProperty e DynamicEnumProperty, sendo o último preenchido dinamicamente em qualquer lista disponível em .vcxproj. Leia mais sobre isso aqui . Você também pode agrupar as configurações em seções. Por exemplo, vamos tentar adicionar mais uma configuração como Bool

Código
 <?xml version="1.0" encoding="utf-8"?> <ProjectSchemaDefinitions xmlns="clr-namespace:Microsoft.Build.Framework.XamlTypes;assembly=Microsoft.Build.Framework"> <Rule Name="CustomProperties" PageTemplate="generic" DisplayName="My own properties"> <Rule.DataSource> <DataSource Persistence="ProjectFile"/> </Rule.DataSource> <Rule.Categories> <Category Name="General" DisplayName="General"/> </Rule.Categories> <BoolProperty Name="EnableCommonPCH" Category="General" DisplayName="Enable common precompiled headers" Description="Should we use solution-wide precompiled headers instead of project-specific?"> <BoolProperty.DataSource> <DataSource HasConfigurationCondition="false" /> </BoolProperty.DataSource> </BoolProperty> <StringProperty Name="ProtobufOutputFolder" DisplayName="Protobuf Output Directory" Description="Directory where Protobuf generated files are created." Subtype="folder" Category="General"> <StringProperty.DataSource> <DataSource HasConfigurationCondition="false" /> </StringProperty.DataSource> </StringProperty> <StringProperty Name="Dummy" Visible="false" /> </Rule> </ProjectSchemaDefinitions> 

Reiniciamos o Studio.



Edite a configuração, salve o projeto - tudo funciona como esperado.

 <PropertyGroup> <EnableCommonPCH>true</EnableCommonPCH> </PropertyGroup> <PropertyGroup> <ProtobufOutputFolder>.\generated_ustom</ProtobufOutputFolder> </PropertyGroup> 

Explique o Studios sobre novos tipos de arquivo


Até agora, para adicionar um arquivo protobuf ao projeto, tivemos que registrar manualmente o que ele estava no arquivo .vcxproj. Isso pode ser facilmente corrigido adicionando três tags ao .xml mencionado acima.

  <ContentType Name="Protobuf" DisplayName="Google Protobuf Schema" ItemType="ProtobufSchema" /> <ItemType Name="ProtobufSchema" DisplayName="Google Protobuf Schema" /> <FileExtension Name="*.proto" ContentType="Protobuf" /> 

Reiniciamos o estúdio, examinamos as propriedades dos nossos arquivos .proto.É



fácil ver os arquivos que agora são reconhecidos corretamente como "Esquema do Google Protobuf". Infelizmente, o item correspondente não é adicionado automaticamente à caixa de diálogo "Adicionar novo item", mas se adicionarmos um arquivo .proto existente (menu de contexto do projeto \ Adicionar \ item existente ...) ao projeto, ele será reconhecido e adicionado corretamente. Além disso, nosso novo "tipo de arquivo" pode ser selecionado na lista suspensa Tipo de item:



Associar configurações a arquivos individuais


Além das configurações “para o projeto como um todo”, de maneira completamente semelhante, você pode fazer “configurações para um arquivo separado”. Basta especificar o atributo ItemType na marca DataSource.

  <Rule Name="ProtobufProperties" PageTemplate="generic" DisplayName="Protobuf properties"> <Rule.DataSource> <DataSource Persistence="ProjectFile" ItemType="ProtobufSchema" /> </Rule.DataSource> <Rule.Categories> <Category Name="General" DisplayName="General"/> </Rule.Categories> <StringProperty Name="dllexport_decl" DisplayName="dllexport macro" Description="Add dllexport / dllimport statements controlled by #define with this name." Category="General"> </StringProperty> </Rule> 

Marque



Salvar, veja o conteúdo de .vcxproj

  <ProtobufSchema Include="test2.proto"> <dllexport_decl Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">MYLIB_EXPORT</dllexport_decl> </ProtobufSchema> 

Tudo funciona como esperado.

Nível 4: expandindo a funcionalidade do MSBuild


Eu nunca tive a necessidade de entrar no processo de montagem tão profundamente, mas como o artigo acabou sendo bastante grande, mencionarei brevemente a última possibilidade de personalização: uma extensão do próprio MSBuild. Além de uma coleção bastante extensa de tarefas "padrão", no MSBuild, as tarefas podem ser "importadas" de diferentes fontes usando a tag UsingTask. Por exemplo, podemos escrever nossa extensão para o MSBuild , compilá-la em uma biblioteca DLL e importar algo como isto:

 <UsingTask TaskName="CL" AssemblyFile="$(MSBuildThisFileDirectory)Microsoft.Build.CppTasks.Common.dll" /> 

É assim que a maioria das tarefas "padrão" fornecidas pelo Studio são implementadas. Mas transportar uma DLL personalizada para montagem por razões óbvias geralmente é inconveniente. Portanto, uma marca chamada TaskFactory é suportada na marca UsingTask. TaskFactory pode ser considerado um "compilador de tarefas" - passamos a ele uma entrada de algum "meta-código" inicial e gera um objeto do tipo Task que o implementa. Por exemplo, usando o CodeTaskFactory, você pode colar o código escrito nas tarefas de C # dentro do arquivo .props.

Uma abordagem semelhante é usada, por exemplo, pelo Qt VS Tools
 <UsingTask TaskName="GetItemHash" TaskFactory="CodeTaskFactory" AssemblyFile="$(MSBuildToolsPath)\Microsoft.Build.Tasks.v4.0.dll"> <ParameterGroup> <Item ParameterType="Microsoft.Build.Framework.ITaskItem" Required="true" /> <Keys ParameterType="System.String[]" Required="true" /> <Hash Output="true" ParameterType="System.String" /> </ParameterGroup> <Task> <Using Namespace="System"/> <Using Namespace="System.Text"/> <Using Namespace="System.IO"/> <Using Namespace="System.IO.Compression"/> <Code Type="Fragment" Language="cs"> <![CDATA[ var data = Encoding.UTF8.GetBytes(string.Concat(Keys.OrderBy(x => x) .Select(x => string.Format("[{0}={1}]", x, Item.GetMetadata(x)))) .ToUpper()); using (var dataZipped = new MemoryStream()) { using (var zip = new DeflateStream(dataZipped, CompressionLevel.Fastest)) zip.Write(data, 0, data.Length); Hash = Convert.ToBase64String(dataZipped.ToArray()); } ]]> </Code> </Task> </UsingTask> 

Se alguém usou funcionalidade semelhante - escreva sobre casos de uso interessantes nos comentários.

Isso é tudo. Espero poder mostrar como, ao configurar o MSBuild, o trabalho com um grande projeto no Visual Studio pode ser simplificado e conveniente. Se você planeja implementar alguma das opções acima, darei um pequeno conselho: para depuração .props, .targets e .vcxproj, é conveniente definir o MSBuild para um nível de "depuração" de log, no qual descreve com muito cuidado suas ações com arquivos de entrada e saída



Obrigado a todos que leram até o final, espero que tenha sido interessante :).

Compartilhe suas receitas para o msbuild nos comentários - tentarei atualizar a postagem para que ela sirva como um guia completo para configurar a solução no Studio.

Source: https://habr.com/ru/post/pt448216/


All Articles