Cet article est un guide pour personnaliser l'assemblage des projets C ++ Visual Studio. En partie, il a été réduit à partir de matériaux d'articles dispersés sur ce sujet, en partie est le résultat de la rétro-ingénierie des fichiers de configuration standard du Studio. Je l'ai écrit principalement parce que l'utilité de la documentation de Microsoft elle-même sur ce sujet tend à zéro, et je voulais avoir une référence pratique à portée de main qui pourrait ensuite être consultée et envoyée à d'autres développeurs. Visual Studio a des options pratiques et étendues pour configurer un travail vraiment pratique avec des projets complexes, et je suis ennuyé de voir qu'en raison d'une documentation dégoûtante, ces fonctionnalités sont très rarement utilisées maintenant.
Par exemple, essayons de permettre d'ajouter un schéma flatbuffer au Studio, et le Studio appelle automatiquement flatc lorsque cela est nécessaire (et ne l'a pas appelé lorsqu'il n'y a eu aucun changement) et vous permet de définir les paramètres directement via les propriétés du fichier

Table des matières
* Niveau 1: grimpez à l'intérieur des fichiers .vcxprojParlez des fichiers .propsMais pourquoi même séparer .vcxproj et .props?Rendre les paramètres du projet plus lisiblesNous facilitons la connexion de bibliothèques tiercesModèles de projets - automatisez la création de projets* Niveau 2: compilation personnalisée personnaliséeApproche traditionnelleAtteignez les objectifs MSBuildEssayons de créer une cible pour construire des fichiers .protoNous pensons à notre exemple de modèleFichiers U2DCheck et tlogFinalisez notre .target personnaliséQu'en est-il de CustomBuildStep?Copie de fichiers appropriée* Niveau 3: intégration avec l'interface graphique de Visual StudioNous retirons les paramètres des entrailles de .vcxproj dans les propriétés de configurationExpliquer aux studios les nouveaux types de fichiersAssocier des paramètres à des fichiers individuels* Niveau 4: étendre les fonctionnalités de MSBuildREMARQUE: tous les exemples de cet article ont été testés dans VS 2017. D'après ma compréhension, ils devraient fonctionner dans les versions antérieures du studio à partir d'au moins VS 2012, mais je ne peux pas le promettre.
Niveau 1: grimpez à l'intérieur des fichiers .vcxproj
Jetons un coup d'œil à l'intérieur d'un Visual Studio typique généré automatiquement par .vcxproj.
Cela ressemblera à quelque chose comme ça<?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>
Un désordre assez illisible, n'est-ce pas? Et c'est toujours un fichier très petit et presque trivial. Essayons de le transformer en quelque chose de plus lisible et confortable pour la perception.
Parlez des fichiers .props
Pour ce faire, faisons attention au fait que le fichier que nous avons pris est un document XML ordinaire et il peut être logiquement divisé en deux parties, la première énumérant les paramètres du projet et la seconde contenant les fichiers qui y sont inclus. Séparons ces moitiés physiquement. Pour ce faire, nous avons besoin de la balise d'importation déjà rencontrée dans le code, qui est un analogue du #include et vous permet d'inclure un fichier dans un autre. Nous copions notre .vcxproj dans un autre fichier et en supprimons toutes les déclarations relatives aux fichiers inclus dans le projet, et du .vcxproj, à leur tour, supprimons tout
sauf les déclarations relatives aux fichiers réellement inclus dans le projet. Le fichier résultant avec les paramètres du projet mais sans fichiers dans Visual Studio est généralement appelé feuilles de propriétés et enregistré avec l'extension .props. À son tour, dans .vcxproj, nous fournirons l'importation correspondante
Maintenant .vcxproj ne décrit que les fichiers inclus dans le projet et est beaucoup plus facile à lire <?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>
Il peut être encore simplifié en supprimant les éléments XML inutiles. Par exemple, la propriété "PrecompiledHeader" est désormais déclarée 4 fois pour différentes options de configuration (release / debug) et plate-forme (win32 / x64), mais à chaque fois cette annonce est la même. De plus, plusieurs ItemGroups différents sont utilisés ici, alors qu'en réalité un élément suffit. En conséquence, nous arrivons à un .vcxproj compact et compréhensible qui répertorie simplement 1) les fichiers inclus dans le projet, 2) ce que chacun d'eux (plus les paramètres spécifiques à des fichiers individuels spécifiques) et 3) contient un lien vers les paramètres du projet stockés séparément.
<?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>
Nous rechargeons le projet dans le studio, vérifions l'assemblage - tout fonctionne.
Mais pourquoi même séparer .vcxproj et .props?
Étant donné que rien n'a changé dans l'assemblage, à première vue, il peut sembler que nous avons changé l'alêne pour le savon, ce qui rend inutile le "refactoring" du fichier dans lequel nous n'avons en fait pas besoin de regarder. Cependant, disons un instant que notre solution comprend plusieurs projets. Ensuite, comme vous pouvez le voir, plusieurs fichiers .vcxproj différents de différents projets peuvent utiliser
le même fichier .props avec les paramètres. Nous avons séparé
les règles d'assemblage utilisées dans la solution du code source et nous pouvons maintenant modifier les paramètres d'assemblage pour
tous les projets du même type en un seul endroit. Dans la grande majorité des cas, une telle unification de l'assemblée est une bonne idée. Par exemple, en ajoutant un nouveau projet à une solution, en une seule action, nous lui transférons trivialement de cette manière tous les paramètres des projets existants dans la solution.
Mais que se passe-t-il si nous avons encore besoin de paramètres différents pour différents projets? Dans ce cas, nous pouvons simplement créer plusieurs fichiers .props différents pour différents types de projets. Étant donné que les fichiers .props peuvent importer d'autres fichiers .props exactement de la même manière, il est assez facile et naturel de créer une «hiérarchie» de plusieurs fichiers .props, des fichiers décrivant les paramètres généraux de tous les projets dans une solution aux versions hautement spécialisées spécifiant spécial règles pour un ou deux projets dans une solution. Il existe une règle dans MSBuild selon laquelle si le même paramètre est déclaré deux fois dans le fichier d'entrée (par exemple, il est d'abord importé dans base.props puis déclaré à nouveau dans dérivé.props qui est importé au début de base.props), la déclaration ultérieure remplace la précédente . Cela vous permet de définir facilement et commodément des hiérarchies arbitraires de paramètres en remplaçant simplement dans chaque fichier .props tous les paramètres nécessaires pour un fichier .props donné sans vous soucier du fait qu'ils auraient déjà pu être annoncés ailleurs. Entre autres choses, quelque part dans .props, il est judicieux d'importer les paramètres d'environnement Studio standard qui, pour un projet C ++, ressembleront à ceci:
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" /> <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
Je note qu'en pratique, il est très pratique de placer vos propres fichiers .props dans le même dossier que le fichier .sln
Parce qu'il vous permet d'importer facilement des .props quel que soit l'emplacement de .vcxproj <?xml version="1.0" encoding="utf-8"?> <Project DefaultTargets="Build" ...> <Import Project="$(SolutionDir)\settings.props" /> ... </Project>
Rendre les paramètres du projet plus lisibles
Maintenant que nous n'avons plus besoin de nous préoccuper de chaque projet individuellement, nous pouvons accorder plus d'attention à la personnalisation du processus de construction. Et pour commencer, je recommande de donner des noms sains aux objets les plus intéressants dans le système de fichiers liés à la solution utilisant des fichiers .props. Pour ce faire, nous devons créer une balise PropertyGroup marquée UserMacros:
<PropertyGroup Label="UserMacros"> <RepositoryRoot>$(SolutionDir)\..</RepositoryRoot> <ProjectsDir>$(RepositoryRoot)\projects</ProjectsDir> <ThirdPartyDir>$(RepositoryRoot)\..\ThirdParty</ThirdPartyDir> <ProtoBufRoot>$(ThirdPartyDir)\protobuf\src</ProtoBufRoot> </PropertyGroup>
Ensuite, dans les paramètres du projet, au lieu des constructions de la forme ".. \ .. \ .. \ ThirdParty \ protobuf \ src \ protocole.exe", nous pouvons simplement écrire "$ (ProtoBufRoot) \ protocol.exe". En plus d'une meilleure lisibilité, cela rend le code beaucoup plus mobile - nous pouvons déplacer librement .vcxproj sans craindre que ses paramètres s'envolent et nous pouvons déplacer (ou mettre à jour) Protobuf en changeant une seule ligne dans l'un des fichiers .props.
Lorsque plusieurs PropertyGroups sont déclarés séquentiellement, leur contenu est fusionné - seules les macros dont les noms coïncident avec ceux précédemment déclarés seront écrasées. Cela vous permet de compléter facilement les déclarations dans les fichiers .props joints sans craindre de perdre les macros déjà annoncées précédemment.
Nous facilitons la connexion de bibliothèques tierces
Le processus habituel d'inclusion de dépendances sur la bibliothèque tierce dans Visual Studio ressemble souvent à ceci:

Le processus de réglages correspondants comprend la modification de plusieurs paramètres à la fois sur différents onglets des paramètres du projet et donc plutôt ennuyeux. En outre, cela doit généralement être effectué plusieurs fois pour chaque configuration individuelle du projet, si souvent à la suite de telles manipulations, il s'avère que le projet est assemblé dans l'assembly Release, mais pas dans l'assembly Debug. Il s'agit donc d'une approche inconfortable et peu fiable. Mais comme vous l'avez probablement déjà deviné, les mêmes paramètres peuvent être «packagés» dans un fichier d'accessoires. Par exemple, pour la bibliothèque ZeroMQ, un fichier similaire pourrait ressembler à ceci:
<?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>
Notez que si nous définissons simplement une balise de type AdditionalLibraryDirectories dans le fichier d'accessoires, elle remplacera toutes les définitions antérieures. Par conséquent, une construction légèrement plus compliquée est utilisée dans laquelle la balise se termine par une séquence de caractères;% (AdditionalLibraryDirectories) formant le lien de la balise avec elle-même. Dans la sémantique de MSBuild, cette macro est développée dans la valeur de balise précédente, donc une construction similaire
ajoute les paramètres au début de la ligne stockée dans le paramètre AdditionalLibraryDirectories.
Pour connecter ZeroMQ maintenant, il suffit d'importer simplement le fichier .props donné.
<?xml version="1.0" encoding="utf-8"?> <Project DefaultTargets="Build" ...> <Import Project="$(SolutionDir)\settings.props" /> <Import Project="$(SolutionDir)\zeromq.props" /> ... </Project>
Et c'est là que les manipulations avec la fin du projet - MSBuild inclura automatiquement les fichiers d'en-tête et les bibliothèques nécessaires dans les assemblys Release et Debug. Ainsi, en passant un peu de temps à écrire zeromq.props, nous avons la possibilité de connecter ZeroMQ de manière fiable et précise à n'importe quel projet en une seule ligne. Les créateurs du Studio ont même prévu pour cela une interface graphique spéciale appelée Property Manager, afin que les amateurs de souris puissent faire la même opération en quelques clics.

Certes, comme les autres outils Studio, cette interface graphique ajoutera quelque chose comme au code .vcxproj au lieu d'une seule ligne lisible
un tel code <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>
Je préfère donc ajouter manuellement des liens vers des bibliothèques tierces vers des fichiers .vcxproj.
Semblable à ce qui a été discuté précédemment, l'utilisation des composants ThirdParty via des fichiers .props facilite également la mise à jour des bibliothèques utilisées à l'avenir. Il suffit de modifier un seul fichier zeromq.props et l'assemblage de la solution entière passera de manière synchrone à la nouvelle version. Par exemple, dans nos projets, la construction du projet via ce mécanisme est liée au gestionnaire de dépendances Conan, qui collecte l'ensemble nécessaire de bibliothèques tierces à partir du manifeste de dépendance et génère automatiquement les fichiers .props correspondants.
Modèles de projets - automatisez la création de projets
Éditer manuellement des fichiers .vcxproj créés par le Studio est certainement assez ennuyeux (même si vous avez les compétences et pas pour longtemps). Par conséquent, le Studio offre une opportunité pratique de créer vos propres modèles pour de nouveaux projets qui vous permettent de configurer manuellement .vcxproj une seule fois, puis de le réutiliser en un clic dans tout nouveau projet. Dans le cas le plus simple, vous n’avez même pas besoin de modifier quoi que ce soit manuellement. Il vous suffit d’ouvrir le projet que vous devez transformer en modèle et de sélectionner Projet \ Exporter le modèle dans le menu. Dans la boîte de dialogue qui s'ouvre, vous pouvez spécifier plusieurs paramètres triviaux, tels que le nom du modèle ou de la ligne qui sera affiché dans sa description, ainsi que sélectionner si le modèle nouvellement créé sera immédiatement ajouté à la boîte de dialogue Nouveau projet. Le modèle ainsi créé crée une copie du projet utilisé pour le créer (y compris tous les fichiers inclus dans le projet), en remplaçant uniquement le nom du projet et son GUID. Dans un pourcentage assez élevé de cas, c'est plus que suffisant.
Avec un examen plus détaillé du modèle généré par le Studio, vous pouvez facilement vous assurer qu'il ne s'agit que d'une archive zip dans laquelle se trouvent tous les fichiers utilisés dans le modèle et un fichier de configuration supplémentaire avec l'extension .vstemplate. Ce fichier contient une liste de métadonnées de projet (comme l'icône ou la ligne de description utilisée) et une liste de fichiers qui doivent être créés lors de la création d'un nouveau projet. Par exemple
<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>
Faites attention au paramètre ReplaceParameters = "true". Dans ce cas, il s'applique uniquement au fichier vcxproj, qui ressemble à ceci:
<?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>
À la place du GUID et du RootNamespace, comme vous pouvez le voir, il n'y a pas de valeurs spécifiques, mais les talons $ guid1 $ et $ safeprojectname $. Lors de l'utilisation du modèle, le Studio parcourt les fichiers marqués ReplaceParamters = "true", y recherche les talons de la forme $ name $ et les remplace par des valeurs calculées à l'aide d'un dictionnaire spécial. Par défaut, le Studio
ne prend pas en charge de nombreux paramètres , mais lors de l'écriture des extensions Visual Studio (dont nous parlerons un peu plus tard), il est facile d'ajouter autant de vos propres paramètres calculés (ou entrés par l'utilisateur) lorsque vous démarrez la boîte de dialogue pour créer un nouveau projet à partir du modèle. Comme vous pouvez le voir dans le fichier .vstemplate, le même dictionnaire peut également être utilisé pour générer un nom de fichier, ce qui permet notamment de générer des noms uniques pour les fichiers .vcxproj pour différents projets. Lorsque vous définissez ReplaceParameters = false, le fichier spécifié dans le modèle sera simplement copié sans traitement supplémentaire.
L'archive ZIP résultante avec le modèle peut être ajoutée à la liste des modèles connus par le Studio de plusieurs manières. Le moyen le plus simple consiste à copier simplement ce fichier dans le dossier
% USERPROFILE% \ Documents \ Visual Studio XX \ Templates \ ProjectTemplates . Il convient de noter que malgré le fait que dans ce dossier, vous trouverez de nombreux sous-dossiers différents correspondant aux noms des dossiers dans la fenêtre de création d'un nouveau projet, en fait, le modèle doit être placé simplement dans le dossier racine car la position du modèle dans l'arborescence des nouveaux projets est déterminée par le Studio à partir des balises ProjectType et ProjectSubType dans le fichier .vstemplate. Cette méthode est la plus appropriée pour créer des modèles «personnels» uniques uniquement pour vous, et si vous cochez la case «Importer automatiquement le modèle dans Visual Studio» dans la boîte de dialogue Exporter le modèle, c'est exactement ce que fera le Studio en plaçant l'archive zip créée lors de l'exportation dans ce dossier avec modèles. Cependant, partager de tels modèles avec des collègues en les copiant manuellement n'est certainement pas très pratique. Alors familiarisons-nous avec une option légèrement plus avancée - créer une extension Visual Studio (.vsix)
Pour créer VSIX, nous devons installer le composant Studio facultatif, qui est appelé l'outil de développement des extensions Visual Studio:

Après cela, l'option «Projet VSIX» apparaîtra dans la section Visual C # \ Extensibility. Veuillez noter que malgré son emplacement (C #), il est utilisé pour créer des extensions, y compris des ensembles de modèles de projet C ++.

Dans le projet créé par VSIX, vous pouvez faire beaucoup de choses différentes - par exemple, créer votre propre boîte de dialogue qui sera utilisée pour configurer les projets créés par le modèle. Mais c'est un énorme sujet de discussion distinct, que je n'aborderai pas dans cet article. Pour créer des modèles dans VSIX, tout est très simple: créez un projet VSIX vide, ouvrez le fichier .vsixmanifest et définissez toutes les données du projet directement dans l'interface graphique. Saisissez les métadonnées (nom de l'extension, description, licence) dans l'onglet Métadonnées. Faites attention au champ «Version» situé dans le coin supérieur droit - il est souhaitable de le spécifier correctement, car le Studio l'utilise ensuite pour déterminer quelle version de l'extension est installée sur l'ordinateur. Ensuite, nous allons dans l'onglet Actifs et sélectionnez "Ajouter un nouvel actif", avec Type: Microsoft.VisualStudio.ProjectTemplate, Source: Fichier sur le système de fichiers, Chemin: (nom de l'archive zip avec le modèle). Cliquez sur OK, répétez le processus jusqu'à ce que nous ajoutions tous les modèles souhaités à VSIX.

Après cela, il reste à choisir Configuration: Release et à commander la Build Solution. Vous n'avez pas besoin d'écrire de code; modifiez également les fichiers de configuration manuellement. La sortie est un fichier portable avec l'extension .vsix, qui est, en fait, un installateur pour l'extension que nous avons créée. Le fichier créé sera «lancé» sur n'importe quel ordinateur sur lequel Studio est installé, affichera une boîte de dialogue avec la description de l'extension et de la licence et proposera d'installer son contenu. Permettre l'installation - nous obtenons l'ajout de nos modèles dans la boîte de dialogue "Créer un nouveau projet"

Une telle approche facilite l'unification du travail d'un grand nombre de personnes sur un projet. Pour installer et utiliser les modèles, l'utilisateur n'a besoin d'aucune qualification à l'exception de quelques clics de souris. L'extension installée peut être affichée (et supprimée) dans la boîte de dialogue Outils \ Extensions et mises à jour

Niveau 2: compilation personnalisée personnalisée
OK, à ce stade, nous avons compris comment les fichiers vcxproj et props sont organisés et avons appris à les organiser. Supposons maintenant que nous voulons ajouter à notre projet quelques schémas .proto pour sérialiser des objets basés sur la merveilleuse bibliothèque Google Protocol Buffers. Permettez-moi de vous rappeler l'idée principale de cette bibliothèque: vous écrivez une description de l'objet («schéma») dans un méta-langage spécial indépendant de la plate-forme (fichier .proto) qui est compilé par un compilateur spécial (protocole.exe) dans le .cpp / .cs / .py / .java / etc. des fichiers qui implémentent la sérialisation / désérialisation des objets selon ce schéma dans le langage de programmation souhaité et que vous pouvez utiliser dans votre projet. Ainsi, lors de la compilation du projet, nous devons d'abord appeler le protocole qui va créer pour nous un ensemble de fichiers .cpp que nous utiliserons à l'avenir.
Approche traditionnelle
L'implémentation frontale classique est simple et consiste simplement à ajouter un appel de protocole à l'étape de pré-génération pour un projet qui nécessite des fichiers .proto. Quelque chose comme ça:
Mais ce n'est pas très pratique:- Il est nécessaire de spécifier explicitement la liste des fichiers traités dans la commande
- Si vous modifiez ces fichiers, la version ne sera PAS reconstruite automatiquement
- Lors de la modification d'AUTRES fichiers dans un projet que le Studio reconnaît comme codes source, au contraire, une étape de pré-construction sera effectuée inutilement
- Les fichiers générés ne sont pas inclus par défaut dans l'assemblage du projet
- Si nous incluons manuellement les fichiers générés dans le projet, le projet générera une erreur lorsque nous l'ouvrirons pour la première fois (car les fichiers n'ont pas encore été générés par le premier assemblage).
Au lieu de cela, nous essayons d '«expliquer» à Visual Studio lui-même (ou plutôt, au système de génération MSBuild qu'il utilise) comment gérer ces fichiers .proto.Atteignez les objectifs MSBuild
Du point de vue de MSBuild, l'assemblage de tout projet consiste en une séquence d'assemblage d'entités appelées cibles de construction, cibles abrégées. Par exemple, la construction d'un projet peut inclure l'exécution de la cible Clean, qui supprimera les fichiers temporaires restants des générations précédentes, puis l'exécution de la cible Compile, qui compilera le projet, puis la cible Link et enfin la cible Deploy. Toutes ces cibles ainsi que les règles de leur assemblage ne sont pas fixées à l'avance mais sont définies dans le fichier .vcxproj lui-même. Si vous êtes familier avec l'utilitaire nix make et que le mot «makefile» vous vient à l'esprit à ce moment-là, vous avez tout à fait raison: .vcxproj est une variante XML sur le sujet du makefile.Mais stop-stop-stop sera dit par un lecteur confus. Comment est-ce? Avant cela, nous avons examiné .vcxproj dans un projet simple et il n'y avait pas de cibles ou de similitudes avec le makefile classique. Quel type d'objectif peut-on alors discuter? Il s'avère qu'ils sont simplement «cachés» dans cette ligne, qui inclut dans .vcxproj un ensemble de cibles standard pour la construction de code C ++. <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
«» - C++- «» Build, Clean Rebuild «» . toolset toolset , , Clang.
toolset- toolset . ,
.
target-. target MSBuild
- Liste des entrées
- Liste des sorties
- Dépendances par rapport à d'autres cibles (dépendances)
- Paramètres cibles
- La séquence des étapes réelles effectuées par la cible (tâches)
Par exemple, la cible ClCompile reçoit une liste de fichiers .cpp dans le projet en entrée et génère un ensemble de fichiers .obj à partir d'eux en faisant glisser le compilateur cl.exe. Les paramètres cibles ClCompile se transforment en indicateurs de compilation passés à cl.exe. Lorsque nous écrivons la ligne dans le fichier .vcxproj <ClCompile Include="protobuf_test.cpp" />
puis nous ajoutons le fichier Inclure protobuf_tests.cpp à la liste des entrées de cette cible, et quand nous écrivons <ItemDefinitionGroup> <ClCompile> <PrecompiledHeader>Use</PrecompiledHeader> </ClCompile> </ItemDefinitionGroup>
nous attribuons ensuite la valeur «Use» au paramètre ClCompile.PrecompiledHeader, laquelle cible se transforme ensuite en indicateur / Yu transmis à cl.exe au compilateur.Essayons de créer une cible pour construire des fichiers .proto
L'ajout d'une nouvelle cible est implémenté à l'aide de la balise target: <Target Name="GenerateProtobuf"> ...steps to take... </Target>
Traditionnellement, les cibles sont placées dans un fichier avec l'extension .targets. Non pas que c'était strictement nécessaire (vcxproj et les fichiers cibles et accessoires à l'intérieur sont des XML équivalents), mais c'est un schéma de nommage standard et nous nous y tiendrons. Ainsi, dans le code du fichier .vcxproj, vous pouvez maintenant écrire quelque chose comme <ItemGroup> <ClInclude Include="cpp.h"/> <ProtobufFile Include="my.proto" /> <ItemGroup>
la cible que nous avons créée doit être ajoutée à la liste AvailableItemName <ItemGroup> <AvailableItemName Include="ProtobufFile"> <Targets>GenerateProtobuf</Targets> </AvailableItemName> </ItemGroup>
Nous devrons également décrire exactement ce que nous voulons faire avec nos fichiers d'entrée et ce qui doit se produire sur la sortie. Pour ce faire, MSBuild utilise une entité appelée «tâche». Tâche - il s'agit d'une action simple qui doit être effectuée lors de l'assemblage du projet. Par exemple, «créer un répertoire», «compiler un fichier», «exécuter une commande», «copier quelque chose». Dans notre cas, nous utiliserons l'Exec lurking pour exécuter le protocole.exe et le message lurking pour afficher cette étape dans le journal de compilation. Nous indiquons également que le lancement de cette cible doit être effectué immédiatement après la cible standard PrepareForBuild. En conséquence, nous obtenons quelque chose comme ce fichier 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>
Nous avons utilisé ici un opérateur plutôt non trivial "%" ( opérateur de traitement par lots ) qui signifie "pour chaque élément de la liste" et ajouté automatiquement des métadonnées . L'idée est la suivante: lorsque nous écrivons le code du formulaire <ItemGroup> <ProtobufSchema Include="test.proto"> <AdditionalData>Test</AdditionalData> </ProtobufSchema> </ItemGroup>
puis cet enregistrement nous ajoutons à la liste avec le nom "ProtobufSchema" un élément enfant "test.proto" qui a un élément enfant (métadonnées) AdditionalData contenant la chaîne "Test". Si nous écrivons «ProtobufSchema.AdditionalData», nous aurons accès à l'enregistrement «Test». En plus des métadonnées AdditionalData que nous avons explicitement déclarées, par souci de commodité, le rusé MSBuild ajoute automatiquement à l'enregistrement une douzaine d'éléments enfants utiles fréquemment utilisés décrits ici, parmi lesquels nous avons utilisé Identity (la ligne source), Filename (nom de fichier sans extension) et FullPath ( chemin d'accès complet au fichier). Un enregistrement avec le signe% force MSBuild à appliquer l'opération décrite par nous à chaque élément de la liste, c'est-à-dire à chaque fichier .proto individuellement.Ajouter maintenant <Import Project="protobuf.targets" Label="ExtensionTargets"/>
dans protobuf.props, nous réécrivons nos proto-fichiers en .vcxproj-e sur la balise ProtobufSchema <ItemGroup> ... <ProtobufSchema Include="test.proto" /> <ProtobufSchema Include="test2.proto" /> </ItemGroup>
et vérifier l'assemblage1>------ 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 ==========
Hourra!
Gagné! Certes, nos fichiers .proto ne sont plus visibles dans le projet. Nous montons dans .vcxproj.filters et y entrons par analogie ... <ItemGroup> <ProtobufSchema Include="test.proto"> <Filter>Resource Files</Filter> </ProtobufSchema> <ProtobufSchema Include="test2.proto"> <Filter>Resource Files</Filter> </ProtobufSchema> </ItemGroup> ...
Nous rechargeons le projet - les fichiers sont à nouveau visibles.Nous pensons à notre exemple de modèle
Cependant, en fait, j'ai un peu triché. Si vous ne créez pas manuellement le dossier généré avant le début de la génération, la génération se bloque réellement.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.
Pour résoudre ce problème, ajoutez une cible auxiliaire qui créera le dossier nécessaire ... <Target Name="PrepareToGenerateProtobuf" Inputs="@(ProtobufSchema)" Outputs=".\generated"> <MakeDir Directories=".\generated"/> </Target> <Target Name="GenerateProtobuf" DependsOnTargets="PrepareToGenerateProtobuf" ...
En utilisant la propriété DependsOnTargets, nous indiquons qu'avant de démarrer l'une des tâches GenerateProtobuf, vous devez exécuter PrepareToGenerateProtobuf, et l'entrée @ (ProtobufSchema) fait référence à l'ensemble de la liste ProtobufSchema, en tant qu'entité unique utilisée comme entrée pour cette tâche, elle ne sera donc lancée qu'une seule fois.Redémarrer l'assemblage - ça marche! Essayons maintenant de refaire la reconstruction, donc cette fois pour être sûr de tout Em, mais où sont passées nos nouvelles tâches? Un petit débogage - et nous voyons que les tâches sont réellement lancées par MSBuild, mais elles ne sont pas exécutées car le dossier généré est déjà dans le dossier de sortie spécifié. Autrement dit, dans Rebuild, Clean for. \ Les fichiers générés ne fonctionnent pas pour nous. Corrigez cela en ajoutant une autre cible1>------ 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>
Vérifiez - cela fonctionne. Clean nettoie les fichiers que nous avons créés, Rebuild les recrée à nouveau, appeler Build à nouveau ne recommence pas inutilement la reconstruction. Nous apportons une modification à l'un des fichiers C ++, essayons de refaire la construction . Le fichier photo n'a pas changé, donc le protocole n'a pas redémarré, tout est attendu. Maintenant, nous essayons de modifier le fichier .proto. Il est intéressant de noter que si vous démarrez l'assemblage de MSBuild via la ligne de commande manuellement, et non via l'interface utilisateur de Studio, il n'y aura pas un tel problème - MSBuild recompilera correctement les fichiers .pp.cc nécessaires. Si nous modifions un fichier .cpp, MSBuild, qui a démarré dans le studio, le recompilera non seulement, mais également le fichier .props que nous avons modifié précédemment . Quel est le problème?========== 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 ==========
Fichiers U2DCheck et tlog
Il s'avère que les créateurs de Visual Studio ont décidé qu'appeler MSBuild pour tout le monde était trop cher et ... ont mis en place leur propre «vérification rapide» pour savoir si construire un projet ou non. Il s'appelle U2DCheck et si, à son avis, le projet n'a pas changé, le Studio ne démarrera tout simplement pas MSBuild pour ce projet. Habituellement, U2DCheck fonctionne si "silencieusement" que peu de gens pensent de son existence, mais vous pouvez activer un indicateur utile dans le registre qui forcera U2DCheck à afficher des rapports plus détaillés.Dans son travail, U2DCheck s'appuie sur des fichiers spéciaux .tlog. Ils peuvent être facilement trouvés dans le dossier de sortie intermédiaire (nom_projet) .tlog et pour que U2DCheck réponde correctement aux changements dans les fichiers source, nous devons écrire dans l'un des fichiers tlog de lecture de ce dossier, et pour que U2DCheck réponde correctement à la suppression des fichiers de sortie - écrire dans l'un des fichiers d'écriture tlog.Maudissant, nous revenons à l'édition correspondante de notre cible ... <Exec Command="$(Protoc) --cpp_out=.\generated %(ProtobufSchema.Identity)" /> <WriteLinesToFile File="$(TLogLocation)\protobuf.read.1.tlog" Lines="^%(ProtobufSchema.FullPath)" />
On vérifie - ça marche: l'édition du fichier .props déclenche la reconstruction nécessaire, l'assemblage en l'absence d'édition montre que le projet est à jour. Dans cet exemple, par souci de simplicité, je n'ai pas écrit de tlog d'écriture pour suivre la suppression des fichiers créés lors de la compilation, mais il est ajouté au ciblage de la même manière.À partir de la mise à jour 15.8 de Visual Studio 2017, une nouvelle tâche GetOutOfDateItems standard a été ajoutée à MSBuild qui automatise cette magie noire, mais comme cela s'est produit récemment, presque tous les .target-s personnalisés continuent de fonctionner manuellement avec les fichiers .tlog.Si vous le souhaitez, vous pouvez également désactiver complètement U2DCheck pour tout projet en ajoutant une ligne au champ ProjectCapability <ItemGroup> <ProjectCapability Include="NoVCDefaultBuildUpToDateCheckProvider" /> </ItemGroup>
Cependant, dans ce cas, le Studio pilotera MSBuild pour ce projet et tous ceux qui en dépendent pour chaque build, et oui, U2DCheck a été ajouté pour une raison - cela ne fonctionne pas aussi vite que je le souhaiterais.Finalisez notre .target personnalisé
Le résultat que nous avons obtenu est assez fonctionnel, mais il y a encore place à amélioration. Par exemple, dans MSBuild, il existe un mode «d'assemblage sélectif» lorsque la ligne de commande indique qu'il n'est pas nécessaire d'assembler l'ensemble du projet dans son ensemble, mais uniquement des fichiers individuels spécifiquement sélectionnés dans celui-ci. La prise en charge de ce mode nécessite que la cible vérifie le contenu de la liste @ (SelectedFiles).En plus de cela, dans notre code source, il y a beaucoup de lignes se répétant qui doivent coïncider. Les bonnes manières recommandent de donner à toutes ces entités des noms lisibles et de les adresser par ces noms. Pour cela, une cible spéciale distincte est souvent créée qui crée et remplit la liste auxiliaire avec tous les noms qui seront nécessaires à l'avenir.Enfin, nous n'avions toujours pas réalisé l'idée promise au tout début - l'inclusion automatique des fichiers générés dans le projet. Nous pouvons déjà # inclure les fichiers d'en-tête générés par le protobuf sachant qu'ils seront automatiquement créés avant la compilation, mais ce nombre ne fonctionne pas avec l'éditeur de liens :). Par conséquent, nous ajoutons simplement les fichiers générés à la liste ClCompile.Un exemple d'implémentation combinée similaire 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>
Les paramètres généraux ici ont été définis dans le PropertyGroup et le nouveau ComputeProtobufInput cible remplit les listes de fichiers d'entrée et de sortie. En cours de route (pour montrer comment travailler avec des listes de fichiers de sortie), la génération de code à partir du schéma d'intégration avec python a été ajoutée. Nous commençons et vérifions que tout fonctionne correctement 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'en est-il de CustomBuildStep?
Je dois dire que les développeurs de Microsoft ont estimé assez raisonnablement que tout ce qui précède, hmm, est quelque peu banal et mal documenté et ont essayé de faciliter la vie des programmeurs en introduisant un CustomBuildStep personnalisé. Dans le cadre de ce concept, nous devrions noter dans les paramètres de fichier que nos fichiers .props sont du type Étape de construction personnalisée
Ensuite, nous devons indiquer les étapes d'assemblage nécessaires dans l'onglet Étape de construction personnalisée.
Dans .vcxproj, cela ressemble à ceci <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>
Cette conception fonctionne du fait que les données entrées de cette manière sont insérées dans les entrailles de Microsoft.CppCommon.targets dans une cible spéciale CustomBuildStep qui fait, en général, tout comme je l'ai décrit ci-dessus. Mais tout fonctionne via l'interface graphique et pas besoin de penser à l'implémentation de clean et tlog-ah :). Si vous le souhaitez, ce mécanisme peut être utilisé, mais je ne recommanderais pas de le faire en raison des considérations suivantes:- CustomBuildStep ne peut en être qu'un pour l'ensemble du projet
- En conséquence, un seul type de fichier peut être traité par projet
- Il n'est pas pratique d'inclure une telle étape dans le fichier .props utilisé pour connecter la bibliothèque ThirdParty, car différentes bibliothèques peuvent se chevaucher
- Si quelque chose se casse dans CustomBuildStep, comprendre ce qui s'est passé sera encore plus difficile que d'écrire une cible à partir de zéro.
Copie de fichiers appropriée
Un type de cible de génération très courant consiste à copier certains fichiers d'un endroit à un autre. Par exemple, copier des fichiers de ressources dans un dossier avec un projet compilé ou copier une DLL tierce dans un binaire compilé. Et très souvent, cette opération est implémentée de front par le lancement de l'utilitaire de console xcopy dans les cibles post-build. Par exemple,
vous n'avez pas besoin de le faire pour les mêmes raisons que vous n'avez pas besoin de pousser d'autres cibles de build dans les étapes de post-build. Au lieu de cela, nous pouvons dire directement au Studio qu'il doit copier un fichier particulier. Par exemple, si le fichier est directement inclus dans le projet, il suffit de spécifier ItemType = Copy
Après avoir cliqué sur le bouton Appliquer, un onglet supplémentaire apparaît sur lequel vous pouvez configurer où et comment copier le fichier sélectionné. Dans le code du fichier .vcxproj, il ressemblera à ceci: <ItemGroup> ... <ProtobufSchema Include="test2.proto" /> <CopyFileToFolders Include="resource.txt"> <DestinationFolders>$(OutDir)</DestinationFolders> </CopyFileToFolders> </ItemGroup>
Tout fonctionnera hors de la boîte, y compris la prise en charge correcte des fichiers tlog. À l'intérieur, il est implémenté selon le même principe de «tâche standard spéciale pour la copie de fichiers» que l'étape de construction personnalisée que j'ai critiquée littéralement dans la section précédente, mais puisque la copie de fichiers est une opération plutôt banale et nous ne redéfinissons pas l'opération elle-même (copie) mais modifions uniquement la liste fichiers d'entrée et de sortie pour elle, alors cela fonctionne bien.Je note que lors de la création de listes de fichiers CopyFilesToFolder, vous pouvez utiliser des caractères génériques. Par exemple <CopyFileToFolders Include="$(LibFolder)\*.dll"> <DestinationFolders>$(OutDir)</DestinationFolders> </CopyFileToFolders>
L'ajout de fichiers à la liste CopyFileToFolders est peut-être le moyen le plus simple d'implémenter la copie lors de la création d'un projet, y compris dans les fichiers .props qui incluent des bibliothèques tierces. Cependant, si vous souhaitez avoir plus de contrôle sur ce qui se passe, une autre option consiste à ajouter la tâche de copie spécialisée à votre cible de génération . Par exemple <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>
Une légère digressiontask- MS DownloadFile, VerifyFileHash, Unzip . Copy Retry, hard-link .
Malheureusement, la tâche de copie ne prend pas en charge les caractères génériques et ne remplit pas les fichiers .tlog. Si vous le souhaitez, cela peut être mis en œuvre manuellement,par exemple comme ça <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>
Cependant, travailler avec CopyFileToFolders standard sera généralement beaucoup plus facile.Niveau 3: intégration avec l'interface graphique de Visual Studio
Tout ce que nous avons fait jusqu'ici de côté peut sembler une tentative plutôt ennuyeuse d'implémenter la fonctionnalité de make normal dans un outil qui n'est pas très approprié pour cela. Édition manuelle de fichiers XML, constructions peu évidentes pour résoudre des tâches simples, fichiers tlog de béquille ... Cependant, le système de construction Studio a ses avantages - par exemple, après la configuration initiale, il fournit le plan de construction résultant avec une bonne interface graphique. Pour son implémentation, la balise PropertyPageSchema est utilisée, dont nous parlerons maintenant.Nous retirons les paramètres des entrailles de .vcxproj dans les propriétés de configuration
Essayons de faire en sorte que nous puissions éditer la propriété $ (ProtobufOutputFolder) à partir de "l'implémentation combinée de protobuf.targets" non manuellement dans le fichier, mais avec confort directement à partir de l'EDI. Pour ce faire, nous devons écrire un fichier XAML spécial avec une description des paramètres. Ouvrez un éditeur de texte et créez un fichier avec le nom, par exemple, 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>
En plus de la balise StringProperty elle-même, qui indique au Studio l'existence du paramètre "ProtobufOutputFolder" avec le type String et Subtype = Folder et explique comment il doit être affiché dans l'interface graphique, ce pseudo XML indique que ces informations doivent être stockées dans le fichier de projet. En plus de ProjectFile, vous pouvez également utiliser UserFile - puis les données seront enregistrées dans un fichier .vcxproj.user séparé qui, tel que conçu par les créateurs du Studio, est destiné à des paramètres privés (non enregistrés dans VCS). Nous connectons le schéma que nous avons décrit au projet, en ajoutant la balise PropertyPageSchema à notre protobuf.targets <ItemGroup> <AvailableItemName Include="ProtobufSchema"> <Targets>GenerateProtobuf</Targets> </AvailableItemName> <PropertyPageSchema Include="custom_settings.xml"/> </ItemGroup>
Pour que nos modifications prennent effet, redémarrez le Studio, chargez notre projet, ouvrez les propriétés du projet et voyez ...
Oui!
Notre page avec notre paramètre est apparue et sa valeur par défaut a été correctement lue par le Studio. Nous essayons de le changer, enregistrer le projet, voir .vcxproj ... <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'"> <ProtobufOutputFolder>.\generated_custom</ProtobufOutputFolder> </PropertyGroup>
Comme vous pouvez le voir par la condition Condition traditionnelle, les paramètres par défaut sont associés à une configuration de build spécifique. Mais si vous le souhaitez, cela peut être bloqué en définissant l'indicateur DataSource HasConfigurationCondition = "false". Certes, dans le studio 2017, il y a un bug à cause duquel les paramètres du projet peuvent ne pas être affichés s'il n'y a pas au moins un paramètre associé à une configuration parmi eux. Heureusement, ce paramètre peut ne pas être visible.Option sans liaison à la configuration<?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>
/>
Vous pouvez ajouter un nombre illimité de paramètres. Les types possibles incluent BoolProperty, StringProperty (avec les sous-types facultatifs "dossier" et "fichier"), StringListProperty, IntProperty, EnumProperty et DynamicEnumProperty, ce dernier étant rempli à la volée à partir de n'importe quelle liste disponible dans .vcxproj. En savoir plus à ce sujet ici . Vous pouvez également regrouper les paramètres en sections. Par exemple, essayons d'ajouter un paramètre supplémentaire comme BoolCode <?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>
Nous redémarrons le Studio,
modifions les paramètres, sauvegardons le projet - tout fonctionne comme prévu. <PropertyGroup> <EnableCommonPCH>true</EnableCommonPCH> </PropertyGroup> <PropertyGroup> <ProtobufOutputFolder>.\generated_ustom</ProtobufOutputFolder> </PropertyGroup>
Expliquer aux studios les nouveaux types de fichiers
Jusqu'à présent, pour ajouter un fichier protobuf au projet, nous devions enregistrer manuellement ce qu'il était dans .vcxproj. Cela peut être facilement résolu en ajoutant trois balises au .xml mentionné ci-dessus. <ContentType Name="Protobuf" DisplayName="Google Protobuf Schema" ItemType="ProtobufSchema" /> <ItemType Name="ProtobufSchema" DisplayName="Google Protobuf Schema" /> <FileExtension Name="*.proto" ContentType="Protobuf" />
Nous redémarrons le studio, regardons les propriétés de nos fichiers .proto
Il est facile de voir les fichiers qui sont maintenant correctement reconnus comme «Schéma Google Protobuf». Malheureusement, l'élément correspondant n'est pas automatiquement ajouté à la boîte de dialogue "Ajouter un nouvel élément", mais si nous ajoutons un fichier .proto existant (menu contextuel du projet \ Ajouter \ Élément existant ...) au projet, il sera reconnu et ajouté correctement. De plus, notre nouveau «type de fichier» peut être sélectionné dans la liste déroulante Type d'élément:
Associer des paramètres à des fichiers individuels
En plus des paramètres «pour le projet dans son ensemble», de manière complètement similaire, vous pouvez définir des «paramètres pour un fichier séparé». Il suffit de spécifier l'attribut ItemType dans la balise 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>
Cochez
Enregistrer, regardez le contenu de .vcxproj <ProtobufSchema Include="test2.proto"> <dllexport_decl Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">MYLIB_EXPORT</dllexport_decl> </ProtobufSchema>
Tout fonctionne comme prévu.Niveau 4: étendre les fonctionnalités de MSBuild
Je n'ai jamais eu besoin d'entrer aussi profondément dans le processus d'assemblage, mais comme l'article s'est avéré assez gros de toute façon, je mentionnerai brièvement la dernière possibilité de personnalisation: une extension de MSBuild elle-même. En plus d'une collection assez étendue de tâches «standard», dans MSBuild, les tâches peuvent être «importées» de différentes sources à l'aide de la balise UsingTask. Par exemple, nous pouvons écrire notre extension pour MSBuild , la compiler dans une bibliothèque DLL et importer quelque chose comme ceci: <UsingTask TaskName="CL" AssemblyFile="$(MSBuildThisFileDirectory)Microsoft.Build.CppTasks.Common.dll" />
C'est ainsi que la plupart des tâches "standard" fournies par le Studio sont implémentées. Mais transporter une DLL personnalisée pour l'assemblage pour des raisons évidentes est souvent gênant. Par conséquent, une balise appelée TaskFactory est prise en charge dans la balise UsingTask. TaskFactory peut être considéré comme un «compilateur de tâches» - nous lui transmettons un «méta-code» d'entrée à son entrée, et il génère un objet implémentant le type de tâche. Par exemple, en utilisant CodeTaskFactory, vous pouvez coller du code écrit en tâches C # directement dans le fichier .props.Une approche similaire est utilisée, par exemple, par 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>
Si quelqu'un a utilisé des fonctionnalités similaires - écrivez un cas d'utilisation intéressant dans les commentaires.C’est tout. J'espère que j'ai pu montrer comment, lors de la configuration de MSBuild, travailler avec un grand projet dans Visual Studio peut être rendu simple et pratique. Si vous prévoyez d'implémenter l'un des éléments ci-dessus, je vous donnerai un petit conseil: pour déboguer .props, .targets et .vcxproj, il est pratique de définir MSBuild sur un niveau de journalisation de «débogage» dans lequel il décrit très soigneusement ses actions avec les fichiers d'entrée et de sortie
Merci à tous ceux qui ont lu jusqu'à la fin, j'espère que cela s'est avéré intéressant :).Partagez vos recettes pour msbuild dans les commentaires - je vais essayer de mettre à jour le message afin qu'il serve de guide exhaustif pour configurer la solution dans Studio.