Configure un conjunto conveniente de proyectos en Visual Studio

Este artículo es una guía para personalizar el ensamblaje de proyectos de C ++ Visual Studio. En parte, se redujo de materiales de artículos dispersos sobre este tema, en parte es el resultado de la ingeniería inversa de los archivos de configuración estándar de Studio. Lo escribí principalmente porque la utilidad de la documentación de Microsoft sobre este tema tiende a cero, y quería tener a mano una referencia conveniente a la que luego se pudiera acceder y enviar a otros desarrolladores. Visual Studio tiene opciones convenientes y extensas para configurar un trabajo realmente conveniente con proyectos complejos, y me molesta ver que debido a la documentación repugnante, estas características se usan muy raramente ahora.

Como ejemplo, intentemos que sea posible agregar el esquema de búfer plano al Studio, y el Studio llama automáticamente a flatc cuando es necesario (y no lo llamó cuando no hubo cambios) y le permite establecer la configuración directamente a través de Propiedades del archivo



Tabla de contenidos


* Nivel 1: subir dentro de archivos .vcxproj
Habla sobre archivos .props
Pero, ¿por qué incluso separar .vcxproj y .props?
Hacer que la configuración del proyecto sea más legible
Hacemos que sea fácil conectar bibliotecas de terceros
Plantillas de proyectos: automatice la creación de proyectos
* Nivel 2: compilación personalizada personalizada
Enfoque tradicional
Cumplir objetivos de MSBuild
Intentemos crear un objetivo para construir archivos .proto
Traemos nuestro ejemplo modelo a la mente
U2DCheck y archivos tlog
Finalice nuestro objetivo personalizado.
¿Qué pasa con CustomBuildStep?
Copia adecuada de archivos
* Nivel 3: integrar con la GUI de Visual Studio
Extraemos la configuración de las entrañas de .vcxproj en Propiedades de configuración
Explicar a Studios sobre los nuevos tipos de archivos.
Asociar configuraciones con archivos individuales
* Nivel 4: ampliando la funcionalidad de MSBuild

NOTA: todos los ejemplos en este artículo se probaron en VS 2017. Como parte de mi entendimiento, deberían funcionar en versiones anteriores del estudio comenzando al menos con VS 2012, pero no puedo prometer esto.

Nivel 1: subir dentro de archivos .vcxproj


Echemos un vistazo dentro de un típico .vcxproj Visual Studio generado automáticamente.

Se verá algo así
<?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> 

Bastante lío ilegible, ¿no? Y este sigue siendo un archivo muy pequeño y casi trivial. Tratemos de convertirlo en algo más legible y cómodo para la percepción.

Habla sobre archivos .props


Para hacer esto, prestemos atención al hecho de que el archivo que hemos tomado es un documento XML ordinario y se puede dividir lógicamente en dos partes, la primera de las cuales enumera la configuración del proyecto y la segunda contiene los archivos incluidos en él. Separemos estas mitades físicamente. Para hacer esto, necesitamos la etiqueta Importar que ya se encuentra en el código, que es un análogo de #include y le permite incluir un archivo en otro. Copiamos nuestro .vcxproj en otro archivo y eliminamos de él todos los anuncios relacionados con los archivos incluidos en el proyecto, y del .vcxproj, a su vez, eliminamos todo excepto los anuncios relacionados con los archivos realmente incluidos en el proyecto. El archivo resultante con la configuración del proyecto pero sin archivos en Visual Studio generalmente se denomina Hojas de propiedades y se guarda con la extensión .props. A su vez, en .vcxproj suministraremos la importación correspondiente

Ahora .vcxproj describe solo los archivos incluidos en el proyecto y se lee mucho más fácilmente
 <?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> 

Se puede simplificar aún más eliminando elementos XML innecesarios. Por ejemplo, la propiedad "PrecompiledHeader" ahora se declara 4 veces para diferentes opciones de configuración (lanzamiento / depuración) y plataforma (win32 / x64), pero cada vez este anuncio es el mismo. Además, aquí se usan varios ItemGroups diferentes, mientras que en realidad un elemento es suficiente. Como resultado, llegamos a un .vcxproj compacto y comprensible que simplemente enumera 1) los archivos incluidos en el proyecto, 2) qué es cada uno de ellos (más configuraciones específicas para archivos individuales específicos) y 3) contiene un enlace a las configuraciones del proyecto almacenadas por separado.

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

Recargamos el proyecto en el estudio, verificamos el ensamblaje, todo funciona.

Pero, ¿por qué incluso separar .vcxproj y .props?


Como nada ha cambiado en el ensamblaje, a primera vista puede parecer que cambiamos el punzón por jabón, haciendo una "refactorización" inútil del archivo en el que realmente no teníamos que mirar. Sin embargo, digamos por un momento que nuestra solución incluye más de un proyecto. Luego, como puede ver, varios archivos .vcxproj diferentes de diferentes proyectos pueden usar el mismo archivo .props con la configuración. Hemos separado las reglas de ensamblaje utilizadas en la solución del código fuente y ahora podemos cambiar la configuración de ensamblaje para todos los proyectos del mismo tipo en un solo lugar. En la gran mayoría de los casos, tal unificación de la asamblea es una buena idea. Por ejemplo, al agregar un nuevo proyecto a una solución, en una acción transferimos trivialmente de esta manera todas las configuraciones de los proyectos existentes en la solución.

Pero, ¿qué pasa si todavía necesitamos diferentes configuraciones para diferentes proyectos? En este caso, simplemente podemos crear varios archivos .props diferentes para diferentes tipos de proyectos. Dado que los archivos .props pueden importar otros archivos .props exactamente de la misma manera, es bastante fácil y natural construir una "jerarquía" de varios archivos .props, desde archivos que describen configuraciones generales para todos los proyectos en una solución hasta versiones altamente especializadas que especifican reglas para solo uno o dos proyectos en una solución. Hay una regla en MSBuild que si la misma configuración se declara dos veces en el archivo de entrada (por ejemplo, primero se importa en base.props y luego se declara nuevamente en deriva.props que se importa al comienzo de base.props), entonces la declaración posterior anula a la anterior . Esto le permite establecer jerarquías arbitrarias de configuraciones de manera fácil y conveniente simplemente anulando en cada archivo .props todas las configuraciones necesarias para un archivo .props dado sin preocuparse por el hecho de que ya podrían haberse anunciado en otro lugar. Entre otras cosas, en algún lugar de .props es aconsejable importar la configuración estándar del entorno de Studio que para un proyecto C ++ se verá así:

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

Observo que en la práctica es muy conveniente poner sus propios archivos .props en la misma carpeta que el archivo .sln

Porque le permite importar convenientemente .props independientemente de la ubicación de .vcxproj
 <?xml version="1.0" encoding="utf-8"?> <Project DefaultTargets="Build" ...> <Import Project="$(SolutionDir)\settings.props" /> ... </Project> 

Hacer que la configuración del proyecto sea más legible


Ahora que ya no necesitamos molestarnos con cada proyecto individualmente, podemos prestar más atención a la personalización del proceso de construcción. Y para empezar, recomiendo dar nombres sanos a los objetos más interesantes en el sistema de archivos relacionados con la solución usando archivos .props. Para hacer esto, debemos crear una etiqueta PropertyGroup marcada UserMacros:

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

Luego, en la configuración del proyecto, en lugar de construcciones de la forma ".. \ .. \ .. \ ThirdParty \ protobuf \ src \ protocol.exe" simplemente podemos escribir "$ (ProtoBufRoot) \ protocol.exe". Además de una mayor legibilidad, esto hace que el código sea mucho más móvil: podemos mover libremente .vcxproj sin temor a que su configuración se salga volando y podemos mover (o actualizar) Protobuf cambiando solo una línea en uno de los archivos .props.

Cuando varios PropertyGroups se declaran secuencialmente, sus contenidos se fusionarán, solo se sobrescribirán las macros cuyos nombres coincidan con los declarados previamente. Esto le permite complementar fácilmente las declaraciones en archivos .props adjuntos sin temor a perder las macros ya anunciadas anteriormente.

Hacemos que sea fácil conectar bibliotecas de terceros


El proceso habitual de incluir dependencias en la biblioteca de terceros en Visual Studio a menudo se parece a esto:



El proceso de configuración correspondiente incluye la edición de varios parámetros a la vez en diferentes pestañas de la configuración del proyecto y, por lo tanto, bastante aburrido. Además, generalmente se debe hacer varias veces para cada configuración individual en el proyecto, por lo que a menudo como resultado de tales manipulaciones resulta que el proyecto se ensambla en el ensamblaje Release, pero no en el ensamblaje Debug. Por lo tanto, este es un enfoque incómodo y poco confiable. Pero como probablemente ya haya adivinado, la misma configuración se puede "empaquetar" en un archivo de accesorios. Por ejemplo, para la biblioteca ZeroMQ, un archivo similar podría verse así:

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

Tenga en cuenta que si simplemente definimos una etiqueta de tipo AdditionalLibraryDirectories en el archivo de accesorios, anulará todas las definiciones anteriores. Por lo tanto, se utiliza una construcción un poco más complicada en la que la etiqueta termina con una secuencia de caracteres;% (AdditionalLibraryDirectories) formando el enlace de la etiqueta a sí mismo. En la semántica de MSBuild, esta macro se expande al valor de etiqueta anterior, por lo que una construcción similar agrega los parámetros al comienzo de la línea almacenada en el parámetro AdditionalLibraryDirectories.

Para conectar ZeroMQ ahora es suficiente simplemente importar el archivo .props dado.

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

Y aquí es donde terminan las manipulaciones con el proyecto: MSBuild incluirá automáticamente los archivos de encabezado y las bibliotecas necesarios en los ensamblajes Release y Debug. Por lo tanto, al pasar un poco de tiempo escribiendo zeromq.props, tenemos la oportunidad de conectar ZeroMQ de manera confiable y precisa a cualquier proyecto en una sola línea. Los creadores de Studio incluso proporcionaron para esto una GUI especial llamada Property Manager, para que los amantes del mouse puedan hacer la misma operación con unos pocos clics.



Es cierto que, al igual que las otras herramientas de Studio, esta GUI agregará algo parecido al código .vcxproj en lugar de una sola línea legible

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

Por lo tanto, prefiero agregar enlaces a bibliotecas de terceros a archivos .vcxproj manualmente.

Similar a lo que se discutió anteriormente, trabajar con componentes de ThirdParty a través de archivos .props hace que sea igualmente fácil actualizar las bibliotecas usadas en el futuro. Es suficiente editar un solo archivo zeromq.props y el ensamblaje de toda la solución cambiará sincrónicamente a la nueva versión. Por ejemplo, en nuestros proyectos, la construcción del proyecto a través de este mecanismo está vinculada al administrador de dependencias de Conan, que recopila el conjunto necesario de bibliotecas de terceros del manifiesto de dependencia y genera automáticamente los archivos .props correspondientes.

Plantillas de proyectos: automatice la creación de proyectos


Editar manualmente los archivos .vcxproj creados por Studio es ciertamente bastante aburrido (aunque si tienes la habilidad y no por mucho tiempo). Por lo tanto, Studio ofrece una oportunidad conveniente para crear sus propias plantillas para nuevos proyectos que le permiten configurar manualmente .vcxproj solo una vez, y luego reutilizarlo con un clic en cualquier proyecto nuevo. En el caso más simple, ni siquiera necesita editar nada manualmente, solo abra el proyecto que necesita convertir en una plantilla y seleccione Proyecto \ Exportar plantilla en el menú. En el cuadro de diálogo que se abre, puede especificar varios parámetros triviales, como el nombre de la plantilla o línea que se mostrará en su descripción, así como seleccionar si la plantilla recién creada se agregará inmediatamente al cuadro de diálogo Nuevo proyecto. La plantilla creada de esta manera crea una copia del proyecto utilizado para crearlo (incluidos todos los archivos incluidos en el proyecto), reemplazando solo el nombre del proyecto y su GUID. En un porcentaje bastante grande de casos, esto es más que suficiente.

Con un examen más detallado de la plantilla generada por Studio, puede asegurarse fácilmente de que es solo un archivo zip en el que se encuentran todos los archivos utilizados en la plantilla y un archivo de configuración adicional con la extensión .vstemplate. Este archivo contiene una lista de metadatos del proyecto (como el icono utilizado o la línea descriptiva) y una lista de archivos que deben crearse al crear un nuevo proyecto. Por ejemplo

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

Presta atención al parámetro ReplaceParameters = "true". En este caso, se aplica solo al archivo vcxproj, que se ve así:

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

En lugar del GUID y RootNamespace, como puede ver, no hay valores específicos, sino los stubs $ guid1 $ y $ safeprojectname $. Cuando se usa la plantilla, el Studio revisa los archivos marcados como ReemplazarParamters = "true", busca trozos de la forma $ name $ en ellos y los reemplaza con valores calculados usando un diccionario especial. De forma predeterminada, Studio no admite muchos parámetros , pero al escribir Extensiones de Visual Studio (de las que hablaremos más adelante), es fácil agregar tantos de sus propios parámetros calculados (o ingresados ​​por el usuario) al iniciar el diálogo para crear un nuevo proyecto a partir de la plantilla. Como puede ver en el archivo .vstemplate, el mismo diccionario también se puede utilizar para generar un nombre de archivo, que permite, en particular, generar nombres únicos para archivos .vcxproj para diferentes proyectos. Al configurar Reemplazar parámetros = falso, el archivo especificado en la plantilla simplemente se copiará sin procesamiento adicional.

El archivo ZIP resultante con la plantilla se puede agregar a la lista de plantillas conocidas por Studio de varias maneras. La forma más fácil es simplemente copiar este archivo a la carpeta % USERPROFILE% \ Documents \ Visual Studio XX \ Templates \ ProjectTemplates . Vale la pena señalar que, a pesar del hecho de que en esta carpeta encontrará muchas subcarpetas diferentes que coinciden con los nombres de las carpetas en la ventana para crear un nuevo proyecto, de hecho, la plantilla debe colocarse simplemente en la carpeta raíz ya que la posición de la plantilla en el árbol de nuevos proyectos está determinada por el Studio a partir de las etiquetas ProjectType y ProjectSubType en el archivo .vstemplate. Este método es más adecuado para crear plantillas "personales" exclusivas para usted, y si selecciona la casilla de verificación "Importar plantilla automáticamente en Visual Studio" en el cuadro de diálogo Exportar plantilla, esto es exactamente lo que hará Studio al colocar el archivo zip creado durante la exportación en esta carpeta con patrones Sin embargo, compartir dichas plantillas con colegas copiándolas manualmente ciertamente no es muy conveniente. Así que vamos a familiarizarnos con una opción un poco más avanzada: crear una extensión de Visual Studio (.vsix)

Para crear VSIX, necesitamos instalar el componente opcional de Studio, que se llama la herramienta de desarrollo de Extensiones de Visual Studio:


Después de eso, la opción "Proyecto VSIX" aparecerá en la sección Visual C # \ Extensibility. Tenga en cuenta que, a pesar de su ubicación (C #), se utiliza para crear cualquier extensión, incluidos conjuntos de plantillas de proyecto de C ++.


En el proyecto creado por VSIX, puede hacer muchas cosas diferentes, por ejemplo, crear su propio cuadro de diálogo que se usará para configurar proyectos creados por la plantilla. Pero este es un gran tema de discusión por separado, que no abordaré en este artículo. Para crear plantillas en VSIX, todo es muy simple: cree un proyecto VSIX vacío, abra el archivo .vsixmanifest y configure todos los datos para el proyecto directamente en la GUI. Ingrese los metadatos (nombre de la extensión, descripción, licencia) en la pestaña Metadatos. Preste atención al campo "Versión" ubicado en la esquina superior derecha; es conveniente especificarlo correctamente, ya que Studio lo utiliza posteriormente para determinar qué versión de la extensión está instalada en la computadora. Luego vamos a la pestaña Activos y seleccionamos "Agregar nuevo activo", con Tipo: Microsoft.VisualStudio.ProjectTemplate, Fuente: Archivo en el sistema de archivos, Ruta: (nombre del archivo zip con la plantilla). Haga clic en Aceptar, repita el proceso hasta que agreguemos todas las plantillas deseadas a VSIX.


Después de eso, queda por elegir Configuración: liberar y ordenar la solución de compilación. No necesita escribir código; edite manualmente los archivos de configuración también. La salida es un archivo portátil con la extensión .vsix, que es, de hecho, un instalador para la extensión que creamos. El archivo creado se "iniciará" en cualquier computadora con Studio instalado, mostrará un cuadro de diálogo con la descripción de la extensión y la licencia y ofrecerá instalar su contenido. Permitir la instalación: obtenemos la adición de nuestras plantillas en el cuadro de diálogo "Crear un nuevo proyecto"


Este enfoque facilita la unificación del trabajo de un gran número de personas en un proyecto. Para instalar y usar las plantillas, el usuario no necesita ninguna calificación, excepto un par de clics del mouse. La extensión instalada se puede ver (y eliminar) en el cuadro de diálogo Herramientas \ Extensiones y actualizaciones


Nivel 2: compilación personalizada personalizada


OK, en esta etapa descubrimos cómo se organizan los archivos vcxproj y props y aprendimos cómo organizarlos. Supongamos ahora que queremos agregar a nuestro proyecto un par de esquemas .proto para serializar objetos basados ​​en la maravillosa biblioteca Google Protocol Buffers. Permítame recordarle la idea principal de esta biblioteca: escribe una descripción del objeto ("esquema") en un metalenguaje especial independiente de la plataforma (archivo .proto) que es compilado por un compilador especial (protocol.exe) en el .cpp / .cs / .py / .java / etc. archivos que implementan serialización / deserialización de objetos de acuerdo con este esquema en el lenguaje de programación deseado y que puede usar en su proyecto. Por lo tanto, al compilar el proyecto, primero debemos llamar a protocol, que nos creará un conjunto de archivos .cpp que utilizaremos en el futuro.

Enfoque tradicional


La implementación clásica de la frente es sencilla y consiste simplemente en agregar una llamada de protocolo al paso previo a la compilación para un proyecto que necesita archivos .proto. Algo como esto:



Pero esto no es muy conveniente:
  • Se requiere especificar explícitamente la lista de archivos procesados ​​en el comando
  • Si cambia estos archivos, la compilación NO se reconstruirá automáticamente
  • Al cambiar OTROS archivos en un proyecto que Studio reconoce como códigos fuente, por el contrario, se realizará un paso previo a la compilación innecesariamente
  • Los archivos generados no se incluyen en el ensamblaje del proyecto de forma predeterminada
  • Si incluimos manualmente los archivos generados en el proyecto, el proyecto generará un error cuando lo abramos por primera vez (ya que los archivos aún no han sido generados por el primer ensamblaje).

En cambio, tratamos de "explicar" a Visual Studio (o más bien, el sistema de compilación MSBuild que usa) cómo manejar dichos archivos .proto.

Cumplir objetivos de MSBuild


Desde el punto de vista de MSBuild, el ensamblaje de cualquier proyecto consiste en una secuencia de ensamblaje de entidades llamadas objetivos de construcción, objetivos abreviados. Por ejemplo, la construcción de un proyecto puede incluir la ejecución del objetivo Clean, que eliminará los archivos temporales restantes de compilaciones anteriores, luego ejecutar el objetivo Compile, que compilará el proyecto, luego el objetivo Link y finalmente el objetivo Deploy. Todos estos objetivos junto con las reglas para su ensamblaje no se corrigen de antemano sino que se definen en el archivo .vcxproj. Si está familiarizado con la utilidad nix make y la palabra "makefile" le viene a la mente en ese momento, tiene toda la razón: .vcxproj es una variación XML sobre el tema de makefile.

Pero stop-stop-stop será dicho por un lector confundido. Como es eso Antes de eso, vimos .vcxproj en un proyecto simple y no había objetivos ni similitudes con el clásico makefile. ¿Qué tipo de objetivo se puede discutir entonces? Resulta que simplemente están "ocultos" en esta línea, que incluye en .vcxproj un conjunto de objetivos estándar para construir 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 salidas
  • Dependencias de otros objetivos (dependencias)
  • Configuraciones de destino
  • La secuencia de pasos reales realizados por el objetivo (tareas)

Por ejemplo, el destino ClCompile recibe una lista de archivos .cpp en el proyecto y genera un conjunto de archivos .obj arrastrando el compilador cl.exe. La configuración de destino de ClCompile se convierte en indicadores de compilación pasados ​​a cl.exe. Cuando escribimos la línea en el archivo .vcxproj

 <ClCompile Include="protobuf_test.cpp" /> 

luego agregamos el archivo Incluir protobuf_tests.cpp a la lista de entradas de este objetivo, y cuando escribimos

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

luego asignamos el valor "Usar" a la configuración de ClCompile.PrecompiledHeader cuyo objetivo se convertirá en el indicador / Yu pasado a cl.exe al compilador.

Intentemos crear un objetivo para construir archivos .proto


Agregar un nuevo objetivo se implementa utilizando la etiqueta de destino:

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

Tradicionalmente, los objetivos se colocan en un archivo con la extensión .targets. No es que fuera estrictamente necesario (tanto vcxproj como los archivos de objetivos y accesorios en el interior son XML equivalentes), pero este es un esquema de nomenclatura estándar y lo mantendremos. Para que en el código del archivo .vcxproj ahora pueda escribir algo como

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

el objetivo que creamos debe agregarse a la lista AvailableItemName

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

También necesitaremos describir qué es exactamente lo que queremos hacer con nuestros archivos de entrada y qué debería suceder en la salida. Para hacer esto, MSBuild utiliza una entidad llamada "tarea". Tarea: esta es una acción simple que debe realizarse durante el montaje del proyecto. Por ejemplo, "crear un directorio", "compilar un archivo", "ejecutar un comando", "copiar algo". En nuestro caso, utilizaremos el Exec al acecho para ejecutar el protocolo.exe y el Mensaje al acecho para mostrar este paso en el registro de compilación. También indicamos que el lanzamiento de este objetivo debe llevarse a cabo inmediatamente después del objetivo estándar PrepareForBuild. Como resultado, obtenemos algo como este archivo 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> 

Aquí utilizamos un operador bastante no trivial "%" ( operador de procesamiento por lotes ) que significa "para cada elemento de la lista" y agregamos metadatos automáticamente . La idea aquí es esta: cuando escribimos el código del formulario

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

entonces este registro lo agregamos a la lista con el nombre "ProtobufSchema" un elemento hijo "test.proto" que tiene un elemento hijo (metadatos) AdditionalData que contiene la cadena "Test". Si escribimos "ProtobufSchema.AdditionalData", obtendremos acceso al registro de "Prueba". Además de los metadatos AdditionalData que declaramos explícitamente, por nuestra conveniencia, el astuto MSBuild agrega automáticamente al registro otra docena de elementos secundarios útiles de uso frecuente descritos aquí, entre los cuales utilizamos Identity (la línea de origen), Filename (nombre de archivo sin extensión) y FullPath ( ruta completa al archivo). Un registro con el signo% obliga a MSBuild a aplicar la operación descrita por nosotros a cada elemento de la lista, es decir a cada archivo .proto individualmente.

Añadir ahora

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

en protobuf.props, reescribimos nuestros archivos de proto en .vcxproj-e en la etiqueta ProtobufSchema

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

y verifique el montaje

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


¡Hurra!Ganado! Es cierto que nuestros archivos .proto ya no son visibles en el proyecto. Subimos a .vcxproj.filters y entramos allí por analogía.

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

Volvemos a cargar el proyecto: los archivos vuelven a estar visibles.

Traemos nuestro ejemplo modelo a la mente


Sin embargo, de hecho, hice trampa un poco. Si no crea manualmente la carpeta generada antes del inicio de la compilación, entonces la compilación realmente falla.

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.

Para solucionar esto, agregue un objetivo auxiliar que creará la carpeta necesaria

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

Usando la propiedad DependsOnTargets, indicamos que antes de comenzar cualquiera de las tareas GenerateProtobuf, debe ejecutar PrepareToGenerateProtobuf, y la entrada @ (ProtobufSchema) se refiere a toda la lista ProtobufSchema, como una sola entidad utilizada como entrada para esta tarea, por lo que se iniciará solo una vez.

Reiniciar el ensamblaje: ¡funciona! Intentemos ahora reconstruir nuevamente, así que esta vez seguro de estar seguro de todo Em, pero ¿a dónde fueron nuestras nuevas tareas? Un poco de depuración, y vemos que MSBuild realmente inicia las tareas, pero no se ejecutan ya que la carpeta generada ya está en la carpeta de salida especificada. En pocas palabras, en Reconstruir, Limpiar para. \ Los archivos generados no funcionan para nosotros. Solucione esto agregando otro objetivo

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> 

Comprueba, funciona. Clean limpia los archivos que creamos, Reconstruir los recrea nuevamente, llamando a Build otra vez no comienza a reconstruir innecesariamente nuevamente. Realizamos una edición en uno de los archivos C ++, intentamos hacer que Build de nuevo .proto-file no cambie, por lo que el protocolo no se reinició, todo se espera. Ahora intentamos cambiar el archivo .proto. Es interesante que si inicia el ensamblaje de MSBuild a través de la línea de comandos manualmente, y no a través de la interfaz de usuario de Studio, entonces no habrá tal problema: MSBuild recompilará correctamente los archivos .pp.cc necesarios. Si cambiamos cualquier .cpp, entonces MSBuild, que comenzó en el estudio, lo recompilará no solo, sino también el archivo .props que cambiamos 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 ==========




U2DCheck y archivos tlog


Resulta que los creadores de Visual Studio sintieron que llamar a MSBuild para todos era demasiado costoso y ... implementaron su propia "verificación rápida" sobre si construir un proyecto o no. Se llama U2DCheck y si en su opinión el proyecto no ha cambiado, entonces Studio simplemente no iniciará MSBuild para este proyecto. Por lo general, U2DCheck funciona de manera tan "silenciosa" que pocas personas adivinan sobre su existencia, pero puede habilitar un indicador útil en el registro que obligará a U2DCheck a mostrar informes más detallados.

En su trabajo, U2DCheck se basa en archivos especiales .tlog. Se pueden encontrar fácilmente en la carpeta de salida intermedia (nombre_proyecto) .tlog y para que U2DCheck responda correctamente a los cambios en los archivos de origen, necesitamos escribir en uno de los archivos de lectura de tlog en esta carpeta, y para que U2DCheck responda correctamente a la eliminación de los archivos de salida - escribir en uno de los archivos de escritura tlog.

Maldiciendo, volvemos a la edición correspondiente de nuestro objetivo

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

Verificamos que funciona: la edición del archivo .props desencadena la reconstrucción necesaria, el ensamblaje en ausencia de edición muestra que el proyecto está actualizado. En este ejemplo, por simplicidad, no escribí escribir tlog para rastrear la eliminación de archivos creados durante la compilación, pero se agrega al destino de la misma manera.

Comenzando con la actualización 15.8 de Visual Studio 2017, se agregó una nueva tarea estándar GetOutOfDateItems a MSBuild que automatiza esta magia negra, pero como esto ha sucedido recientemente, casi todos los .target-s personalizados continúan trabajando con archivos .tlog manualmente.

Si lo desea, también puede deshabilitar completamente U2DCheck para cualquier proyecto agregando una línea al campo ProjectCapability

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

Sin embargo, en este caso, Studio conducirá MSBuild para este proyecto y todos los que dependen de él para cada compilación, y sí, U2DCheck se agregó por una razón: no funciona tan rápido como me gustaría.

Finalice nuestro objetivo personalizado.


El resultado que hemos obtenido es bastante funcional, pero todavía hay margen de mejora. Por ejemplo, en MSBuild hay un modo de "ensamblaje selectivo" cuando la línea de comando indica que no es necesario ensamblar todo el proyecto como un todo, sino solo archivos individuales específicamente seleccionados en él. La compatibilidad con este modo requiere que el objetivo verifique el contenido de la lista @ (SelectedFiles).

Además de esto, en nuestro código fuente hay muchas líneas que se repiten entre sí que deben coincidir. Los buenos modales recomiendan dar a todas estas entidades nombres legibles y abordarlos con estos nombres. Para esto, a menudo se crea un objetivo especial separado que crea y llena la lista auxiliar con todos los nombres que se necesitarán en el futuro.

Finalmente, todavía no nos dimos cuenta de la idea prometida desde el principio: la inclusión automática de los archivos generados en el proyecto. Ya podemos # incluir los archivos de encabezado generados por el protobuf sabiendo que se crearán automáticamente antes de la compilación, pero este número no funciona con el enlazador :). Por lo tanto, simplemente agregamos los archivos generados a la lista ClCompile.

Un ejemplo de una implementación combinada similar 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> 

La configuración general aquí se realizó en el PropertyGroup, y el nuevo ComputeProtobufInput de destino llena las listas de archivos de entrada y salida. En el camino (para demostrar el trabajo con listas de archivos de salida), se agregó la generación de código del esquema para la integración con python. Comenzamos y comprobamos que todo funciona correctamente

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

¿Qué pasa con CustomBuildStep?


Debo decir que los desarrolladores de Microsoft estimaron con bastante sensatez que todo lo anterior, hmm, es algo no trivial y pobremente documentado e intentaron facilitar la vida de los programadores mediante la introducción de un CustomBuildStep personalizado. Como parte de este concepto, tendríamos que tener en cuenta en la configuración del archivo que nuestros archivos .props son del tipo Custom Build Step.



Luego, deberíamos indicar los pasos de ensamblaje necesarios en la pestaña Custom Build Step.



En .vcxproj esto se parece a esto

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

Esta construcción funciona debido al hecho de que los datos ingresados ​​de esta manera se sustituyen en las entrañas de Microsoft.CppCommon.targets en un destino especial CustomBuildStep que hace, en general, todo lo mismo que describí anteriormente. Pero todo funciona a través de la GUI y no es necesario pensar en implementar clean y tlog-ah :). Si lo desea, puede usar este mecanismo, pero no recomendaría hacerlo debido a las siguientes consideraciones:

  • CustomBuildStep solo puede ser uno para todo el proyecto
    • En consecuencia, solo se puede procesar 1 tipo de archivo por proyecto
    • No es práctico incluir dicho paso en el archivo .props utilizado para conectar la biblioteca ThirdParty, porque diferentes bibliotecas pueden superponerse entre sí
  • Si algo se rompe en CustomBuildStep, comprender lo que sucedió será aún más difícil que escribir un objetivo desde cero.

Copia adecuada de archivos


Un tipo muy común de destino de compilación es copiar algunos archivos de un lugar a otro. Por ejemplo, copiar archivos de recursos a una carpeta con un proyecto compilado o copiar una DLL de terceros en un binario compilado. Y muy a menudo esta operación se implementa "de frente" a través del lanzamiento de la utilidad de consola xcopy en los objetivos posteriores a la compilación. Por ejemplo,



por lo que no tiene que , por las mismas razones, que no trate de meter en el posterior a la generación fases de los otros tipos de generación. En cambio, podemos decirle directamente al Studio que necesita copiar un archivo en particular. Por ejemplo, si el archivo se incluye directamente en el proyecto, es suficiente especificar ItemType = Copy



Después de hacer clic en el botón Aplicar, aparecerá una pestaña adicional en la que puede configurar dónde y cómo copiar el archivo seleccionado. En el código del archivo .vcxproj, se verá más o menos así:

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

Todo saldrá de la caja, incluido el soporte correcto para los archivos tlog. En el interior, se implementa de acuerdo con el mismo principio de "tarea estándar especial para copiar archivos" como el paso de compilación personalizada que critiqué literalmente en la sección anterior, pero dado que copiar archivos es una operación bastante trivial y no redefinimos la operación en sí (copiando) sino que solo cambiamos la lista archivos de entrada y salida para ella, entonces funciona bien.

Observo que al crear listas de archivos CopyFilesToFolder, puede usar comodines. Por ejemplo

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

Agregar archivos a la lista CopyFileToFolders es quizás la forma más fácil de implementar la copia al crear un proyecto, incluso en archivos .props que incluyen bibliotecas de terceros. Sin embargo, si desea tener más control sobre lo que está sucediendo, entonces otra opción es agregar la tarea de copia especializada a su destino de compilación . Por ejemplo

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

Una ligera digresión
task- MS DownloadFile, VerifyFileHash, Unzip . Copy Retry, hard-link .

R desafortunadamente, la tarea Copiar no admite comodines y no llena archivos .tlog. Si lo desea, esto puede implementarse manualmente,

por ejemplo asi
  <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> 

Sin embargo, trabajar con CopyFileToFolders estándar generalmente será mucho más fácil.

Nivel 3: integración con la GUI de Visual Studio


Todo lo que hemos estado haciendo hasta ahora puede parecer un intento bastante aburrido de implementar la funcionalidad de la marca normal en una herramienta que no es muy adecuada para esto. Edición manual de archivos XML, construcciones no obvias para resolver tareas simples, archivos de muleta tlog ... Sin embargo, el sistema de compilación de Studio tiene sus ventajas: por ejemplo, después de la configuración inicial, proporciona el plan de compilación resultante con una buena interfaz gráfica. Para su implementación, se utiliza la etiqueta PropertyPageSchema, de la que hablaremos ahora.

Extraemos la configuración de las entrañas de .vcxproj en Propiedades de configuración


Intentemos hacerlo para que podamos editar la propiedad $ (ProtobufOutputFolder) de la "implementación combinada de protobuf.targets" no manualmente en el archivo, sino con comodidad directamente desde el IDE. Para hacer esto, necesitamos escribir un archivo XAML especial con una descripción de la configuración. Abra un editor de texto y cree un archivo con el nombre, por ejemplo, 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> 

Además de la etiqueta StringProperty en sí, que indica al Studio la existencia de la configuración "ProtobufOutputFolder" con los tipos String y Subtype = Folder y explica cómo debe mostrarse en la GUI, este nick XML indica que esta información debe almacenarse en el archivo del proyecto. Además de ProjectFile, también puede usar UserFile; luego, los datos se registrarán en un archivo .vcxproj.user separado que, tal como lo concibieron los creadores de Studio, está destinado a configuraciones privadas (no guardadas en VCS). Conectamos el esquema descrito por nosotros al proyecto, agregando la etiqueta PropertyPageSchema a nuestro protobuf.targets

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

Para que nuestras ediciones surtan efecto, reinicie Studio, cargue nuestro proyecto, abra las propiedades del proyecto y vea ...



Si!Nuestra página con nuestra configuración apareció y su valor predeterminado fue leído correctamente por Studio. Intentamos cambiarlo, guardar el proyecto, ver .vcxproj ...

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

Como puede ver en la condición de condición tradicional, la configuración predeterminada está asociada con una configuración de compilación específica. Pero si lo desea, esto puede bloquearse configurando el indicador DataSource HasConfigurationCondition = "false". Es cierto que en el estudio de 2017 hay un error debido al cual la configuración del proyecto puede no mostrarse si no hay al menos una configuración asociada con una configuración entre ellas. Afortunadamente, esta configuración puede no ser visible.

Opción sin vinculación a la configuración
<?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>

/>



Puede agregar cualquier cantidad de configuraciones. Los tipos posibles incluyen BoolProperty, StringProperty (con los subtipos opcionales "carpeta" y "archivo"), StringListProperty, IntProperty, EnumProperty y DynamicEnumProperty, este último se completa sobre la marcha desde cualquier lista disponible en .vcxproj. Lea más sobre esto aquí . También puede agrupar configuraciones en secciones. Por ejemplo, intentemos agregar una configuración más 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 el Studio.



Edite la configuración, guarde el proyecto, todo funciona como se esperaba.

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

Explicar a Studios sobre nuevos tipos de archivos


Hasta ahora, para agregar un archivo protobuf al proyecto, teníamos que registrar manualmente lo que está en .vcxproj. Esto se puede solucionar fácilmente agregando tres etiquetas al .xml mencionado anteriormente.

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

Reiniciamos el estudio, miramos las propiedades de nuestros archivos .proto.



Es fácil ver los archivos que ahora se reconocen correctamente como "Google Protobuf Schema". Desafortunadamente, el elemento correspondiente no se agrega automáticamente al cuadro de diálogo "Agregar nuevo elemento", pero si agregamos un archivo .proto existente (menú contextual del proyecto \ Agregar \ Elemento existente ...) al proyecto, se reconocerá y se agregará correctamente. Además, nuestro nuevo "tipo de archivo" se puede seleccionar en la lista desplegable Tipo de elemento:



Asociar configuraciones con archivos individuales


Además de la configuración "para el proyecto en su conjunto", de manera completamente similar, puede realizar "configuraciones para un archivo separado". Es suficiente especificar el atributo ItemType en la etiqueta 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



Guardar, mire el contenido de .vcxproj

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

Todo funciona como se esperaba.

Nivel 4: expandiendo la funcionalidad de MSBuild


Nunca tuve la necesidad de entrar en el proceso de ensamblaje tan profundamente, pero como el artículo resultó ser bastante grande de todos modos, mencionaré brevemente la última oportunidad de personalización: una extensión de MSBuild. Además de una colección bastante extensa de tareas "estándar", en MSBuild, las tareas se pueden "importar" desde diferentes fuentes utilizando la etiqueta UsingTask. Por ejemplo, podemos escribir nuestra extensión para MSBuild , compilarla en una biblioteca DLL e importar algo como esto:

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

Así es como se implementan la mayoría de las tareas "estándar" proporcionadas por Studio. Pero llevar una DLL personalizada para el ensamblaje por razones obvias a menudo es inconveniente. Por lo tanto, una etiqueta llamada TaskFactory es compatible con la etiqueta UsingTask. TaskFactory puede considerarse un "compilador para tareas": le pasamos una entrada de algún "metacódigo" inicial y genera un objeto de tipo Task que lo implementa. Por ejemplo, usando CodeTaskFactory, puede pegar el código escrito en tareas de C # directamente dentro del archivo .props.

Qt VS Tools utiliza un enfoque similar, por ejemplo
 <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> 

Si alguien usó una funcionalidad similar, escriba sobre casos de uso interesantes en los comentarios.

Eso es todo Espero haber podido mostrar cómo, al configurar MSBuild, trabajar con un gran proyecto en Visual Studio puede ser simple y conveniente. Si está planeando implementar cualquiera de los anteriores, le daré un pequeño consejo: para depurar .props, .targets y .vcxproj es conveniente configurar MSBuild en un nivel de "depuración" de registro en el que describe muy cuidadosamente sus acciones con archivos de entrada y salida



Gracias a todos los que leyeron hasta el final, espero que haya resultado interesante :).

Comparta sus recetas para msbuild en los comentarios. Intentaré actualizar la publicación para que sirva como una guía exhaustiva para configurar la solución en Studio.

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


All Articles