在 Visual Studio 構建和配置 Boost (MSBuild)
在 Visual Studio 構建和配置 Boost (MSBuild)
引言
Boost 是一個非常流行地開源C++庫。最近作為構建的一部分,我已經把它集成在Visual Studio中。僅僅使用Makefile項目和Command Line工具,要想完全正確地配置構建項目還是很困難的。Boost也一直指引著我發現所有相關的知識以及學習如何使用它。這些是分散在多個地方的文檔(這里, 這里, 以及 這里),但是并不是那么淺顯易懂。我目前所寫的文檔就是為了不讓其他人浪費很長時間在同樣的問題上。
這是有關主題的第三章。在前面兩章中,我已經講了了它的屬性和它的語法.。你可以在這里和這里閱讀。在第四章中,我會解釋如何用Visual Studio項目參考系統進行整合,目的是為了Boost也可以像其它的MSBuild 項目那樣被人使用。
目的
本章的目的是演示如何使用MSBuild為自定義庫和工具創建項目以及整合MSBuild到Visual Studio的配置環境當中。接下來的方面是很重要的。這些工具和庫建構起來又是異常復雜。我給你們舉個例子:
我們想在最小化的配置下構建Boost,把這些中間文件放到:"c:\Temp\"這個目錄并存儲這些文件。命令很簡單
b2 --stagedir="c:\Temp\" --build-type=minimal stage
執行命令的時失敗并伴有如下錯誤:
The value of the --build-type option should be either 'complete' or 'minimal'
#$%#什么?!!我們檢查命令、拼寫等。再試一次…一次又一次…又一次完整的而不是最小的。……仍不行!
失敗的原因是--stagedir在路徑的結尾有“\”字符!像“C:Temp”這樣執行才能成功地構建出庫。
b2 --stagedir="c:\Temp" --build-type=minimal stage
Visual Studio在發送工具時需要帶有斜線的目錄來控制,在這種情況下不會正常工作。
因此開發環境的整合并且提供簡單的UI能夠消除所有的這些應該被消除的無意義的問題。
背景
Boost是一個C++編程語言的庫集合。它的大多數庫被作為頭文件的實現并且不用被編譯成二進制文件。包含這些特定的頭文件,把他們放入你的工程中,對你的工程來說足夠了。但是這些模塊需要編譯和連接到這些程序才能使之成為一個整體。
Boost使用它自己的Jam build tool作為庫編譯工具。工具是C++源代碼文件的集合,那么任何模塊被處理前都是需要編譯的。
獲得庫文件
Boost庫可以通過正式的發布來下載也可從GitHub上面復制或分支出一份。
所下載的庫作為庫的存檔中被正式發布的一部分,包含了全部的資源、文檔和目錄,而且可以立即構建。
如何庫是在Git上面復制的,在構建的時候可能需要更多的步驟。Boost是結構化的主模塊,并且帶有一些子模塊。
1. 復制 Boost | 將庫文件放到本地。 |
2. 獲取子模塊 | 通過更新命令來獲取所有的子模塊。需要遞歸地執行 |
3. 構建Jam工具 | Jam解釋器“b2.exe”需要使用工具集從源文件中構建,MSVC就是這樣。 |
4. 包含dir的重構 |
當從文檔庫中復制庫文件時,‘boost’并沒有創建文件夾。所有頭文件都存儲在各自libs的文件夾目錄下。這樣做是防止‘boost’和‘libs’目錄重復。相反,‘boost’目錄是通過執行b2.exe headers命令來產生。 |
只要下載或是復制了庫文件,我們就可以開始構建了,最好是設置指向boot庫根目錄的環境變量BOOST_BUILD_PATH。
項目文件
在MSBuild中的所有項目都是一起啟動的,它總是像這樣啟動:
<?xml version="1.0" encoding="utf-8"?> <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> </Project>
有一些推薦的(但非必須的)配置項, 項目配置(ProjectConfiguration),全局配置(Global),拓展設置(ExtensionSetting), 用戶宏(UserMacros),以及拓展目標(ExtensionTarget)。關于這個部分的更多信息,請查看這個博客.
項目配置
項目配置(ProjectConfiguration)是Visual Studio配置管理器存儲可用平臺(Platform)或配置(Configuration)的選項組(ItemGroup)。這個部分由Visual Studio配置管理器創建和維護:
<ItemGroup Label="ProjectConfigurations"> <ProjectConfiguration Include="Debug|Win32"> <Configuration>Debug</Configuration> <Platform>Win32</Platform> </ProjectConfiguration> <ProjectConfiguration Include="Debug|x64"> <Configuration>Debug</Configuration> <Platform>x64</Platform> </ProjectConfiguration> <ProjectConfiguration Include="Release|Win32"> <Configuration>Release</Configuration> <Platform>Win32</Platform> </ProjectConfiguration> <ProjectConfiguration Include="Release|x64"> <Configuration>Release</Configuration> <Platform>x64</Platform> </ProjectConfiguration> </ItemGroup>
全局配置
你可能已經猜到它存儲整個項目的全局設置。這個部分由模板或者人工創建,Visula Studio并不直接修改。它通常包含以下的元素:
<PropertyGroup Label="Globals"> <ProjectGuid>{9cd23c68-ba74-4c50-924f-2a609c25b7a0}</ProjectGuid> <ProjectName>boost</ProjectName> <BoostDir>$(BOOST_BUILD_PATH)</BoostDir> </PropertyGroup>
需要重點指出的是,元素ProjectGuid在整個解決方案的所有項目中,必須是唯一的GUID。
我們將增加BoostDir變量,指向由BOOST_BUILD_PATH環境變量設置的路徑。
拓展設置
拓展設置(ExtensionSettings)和 用戶宏(UserMacro) 當前并不需要,所以我們可以設置它們為空:
<ImportGroup Label="ExtensionSettings" /> <PropertyGroup Label="UserMacros" />
現在我們需要做的就是,為整個項目設置構建目標以及配置屬性頁。保留項目文件,并在單獨的以.targets為后綴的文件中定義目標以及任務,這是一個好的編程風格。于是,我們創建boost.targets文件,并將它導入到項目之中。
導入
這是此刻我們所需的全部設置。接下來,為C++項目導入默認定義。這個導入不是必須的,屬于“錦上添花”的事情:
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
拓展目標(ExtensionTargets)是我們添加代碼到工程里面的地方。MSBuild 推薦為設置保留 .vcproj 文件,同時在 .targets 文件中定義目標(Targets)和任務(Tasks)。 我們將所有的代碼移到boost.targets文件之中,并像這樣引用它:
<ImportGroup Label="ExtensionTargets"> <Import Project="boost.targets" /> </ImportGroup>
這樣我們的項目配置就完成了!
Targets
現在我們需要定義在構建的時候做什么,并且我們在定義目標時這樣做。Visual Studio 已經建立了有一個預定義的目標集文件:Build, Rebuild, 和 Clean:
<Target Name="Build" /> <Target Name="Rebuild" /> <Target Name="Clean" />
上述定義可以分別執行構建,重建和清除操作。我們還記得以前我們構建boost,構建Jamfile工具b2.exe,如果在git庫上克隆的在包含的目錄下重新加載入'boost'。我們可以通過addind中的先決條件,用DependsOnTarget屬性來構建目標。
<Target Name="Build" DependsOnTargets="JamToolBuild;BoostHeaders;" > <Target Name="Rebuild" DependsOnTargets="JamToolBuild;BoostHeaders;" > <Target Name="Clean" DependsOnTargets="JamToolBuild;" >
要構建Jamfile工具 b2.exe,我們需要執行本地路徑"tools\build\src\engine\".下的build.bat文件。我們這樣做的目標是構建JamTool:
<Target Name="BuildJamTool" Label="Building BJAM engine" > <Exec Command="call build.bat" ... /> <ItemGroup Label="List of builtt exe files" > <BJamTools Include="*.exe" /> </ItemGroup> <Copy SourceFiles="@(BJamTools)" DestinationFolder="$(BoostRoot)\" /> </Target>
這些目標執行build.bat并且復制*.exe文件到boost的根目錄,我們可以運行build命令集。
如果從 Git 倉庫克隆 boost 項目,那么 boost 目錄所包含的所有文件都將丟失。想要填入它丟失的信息,我們需要執行像"b2.exe headers"這樣的 Jam 工具。我們將這個過程放在 BoostHeaders target 中做:
<Target Name="BoostHeaders" DependsOnTargets="JamToolBuild" > <Exec Command="b2.exe headers" /> </Target>
注意,這個過程依賴于 b2.exe 已經被創建完成。因此我們添加 DependsOnTargets="JamToolBuild"這句話來確認 b2.exe 被創建并且可以被執行。
現在我們準備創建整個庫:
<Target Name="Build" DependsOnTargets="JamToolBuild;BoostHeaders;" > <Exec Command="b2.exe" WorkingDirectory="$(BoostRoot)" /> </Target> <Target Name="Rebuild" DependsOnTargets="BuildJamTool;"> <Exec Command="b2.exe -a" WorkingDirectory="$(BoostRoot)\" /> </Target> ...
現在就要做最有趣的部分:配置 Boost 且把這些配置信息整合到 Visual Studio 屬性系統中。
配置
為了將 Boost 集成進 Visual Studio,我們需要創建允許我們設置所有構建選項和開關的屬性頁。我們可以通過創建帶 ProjectSchemaDefinition 和 Rule 的 XML 文件來做到這點。
用戶界面
我在我的其它文章里深入地闡述了這一話題:第一部分和第二部分。第一部分介紹了屬性頁的架構(schema),第二部分描述了如何將這些元素加入到一個屬性頁中。
我們先創建boost.xml并將其添加到項目中。當我們把所有屬性都添加進去后,就能看到這樣的界面:
該頁將允許我們設置在 Jamroot 中提及的構建選項。如果選項未被定義(單元格留空),構建器將使用設置在 Jamfile 中的默認值。每個配置的設定項都被分開存儲,互相完全獨立。
值得注意的是,每個設定項都有一個像下面這樣的簡短描述,用于描述它的功能。如果要查看更多信息可以按 F1 鍵,在打開的 URL 中有更深入的講解。
我個人最愛的是 Output Verbosity 設定項:
它允許你選擇在構建時顯示信息的多少和內容。這些等級在 b2 的默認幫助畫面上是隱藏的,通常看不到它們。
在Libraries分類中,我們可以指定在構建時需要包含哪些庫。這里只會列出需要編譯和鏈接的庫。對于在頭文件里已經實現而無需構建的庫,這里也是不會列出的。
在 Compression 分類中,我們可以指定 BZip2 和 Zlib 的源代碼或二進制文件的位置:
最后,你可以在Command Line視圖中檢查你所有的構建選項:
Command Line視圖還允許你在Additional Options框中指定額外的開關或選項。它會將它們自動添加到命令行中。
工具配置
如果你想調整編譯器或鏈接器的選項,或者禁用一個警告,或者傳入額外的定義,你可以通過分別向編譯器和鏈接器傳入cxxflags和linkflags參數來實現。
Visual Studio對內置的編譯器和鏈接器都有一組屬性頁。這些屬性頁具備所有已經定義好的開關。在cxxflags和linkflags的屬性頁里,我們可以看到它們并使用它們。
CXXFLAGS
編譯標志(Compiler flags)被定義在 CL.XML 中,該文件位于 C:\Program Files (x86)\MSBuild\Microsoft.Cpp\v4.0\...\1033 目錄中。有不少設定項雖然在構建 Boost 時用不到,但是不乏一些有用的。我們可以創建 cxxflags.xml 并將所有相關的屬性復制進去。當我們完成這些操作并適當地將這個文件加入到項目中時,我們就有了下面這樣的界面:
LINKFLAGS
同樣我們可以將 LINK.XML 定義的鏈接器屬性頁的內容添加到項目的 linkflags.xml 文件中:
將配置傳給構建工具
創建完 XML 屬性頁,此時只完成了整個工作的一半。現在我們需要將這些設定項傳給構建工具。我們需要從變量中獲取數據,適當地對其進行格式化并傳給構建命令。
為此我們通常在代碼里對變量進行賦值,同時對構建命令行切換配置。但其實我們有更好的辦法:格式化數據已經在 XML 文件里設置了,我們所要做的只是獲取它并應用到項目變量上。在這篇文章中描述了實現它的一條途徑,但還存在一種更好的方式。當涉及到處理 XML 數據時,沒有什么能勝過 XSLT,所以我們借助 XSLT 來完成這個任務。
選項集成
到這個地方,事情就變得簡單了。 屬性頁,開關,命令行。。。。。。現在,我們需要使用C/C++和鏈接選項創建分層結構,其中鏈接選項以下面的格式作為cxxflags和linkflags的命令行參數:
... cxxflags="/cxx1 /cxx2='Some dir here' ... " linkflags="/L1 /L2='Some dir here' ... "
我們可以通過遍歷頁面來實現,為每個頁面創建配置選項,并通過合適的前綴(cxxflags, linkflags)--需要則添加,來連接結果。我們通過配置 PrepareForBuild來處理:
<Target Name="PrepareForBuild" Returns="@(boost-options)" > ... <MSBuild Targets="ConfigHelper" BuildInParallel="true" Properties="PropertyPageSchema=%(PropertyPageSchema.Identity); Context=%(PropertyPageSchema.Context)" > <Output ItemName="boost-options" TaskParameter="TargetOutputs"/> </MSBuild> <ItemGroup Label="Build Settings"> <boost-options Condition="'$(AdditionalOptions)'!=''" Include="$(AdditionalOptions)" /> </ItemGroup>
在代碼里面,我們使用MSBuild為 選項PropertyPageSchema中的每個元素調用ConfigHelper,傳遞路徑到xml文件,同時將存儲在Metadata Context中的數據作為參數。返回值存儲在選項boost-options之中。 接下來的代碼中,增加了在其它選項(Additional Options)框中定義的所有開關,返回由空格分隔的所有開關的字符串。
ConfigHelper是一切之源。只需要幾個步驟,它就處理了所有的XML文件。首先,它獲取所有屬性的列表:
<XmlPeek XmlInputPath="$(File)" Query="/ns:Rule/*[not(contains(@IncludeInCommandLine, 'false'))]/@Name| /ns:ProjectSchemaDefinitions/ns:Rule/*[not(contains(@IncludeInCommandLine, 'false'))]/@Name"> <Output TaskParameter="Result" ItemName="Names" /> </XmlPeek>
然后,它創建包含屬性值的屬性列表:
<data-name-map Include="$(%(Names.Identity))" Condition="'%(%(Names.Identity))'!=''" > <Name>%(Names.Identity)</Name> </data-name-map>
注意這個標記: $(%(Names.Identity)) 。它告知系統,返回變量名存儲在%(Names.Identity)之中的變量的值。如果變量包含逗號分隔的一系列值,它會將變量當作一個列表,并單獨添加這些值。例如,如果我們在名為xxList的變量中包含val1;val2;val3,它將像這樣增加數據:
<data-name-map Include="val1" > <Name>xxList<Name> </data-name-map> <data-name-map Include="val2" > <Name>xxList<Name> </data-name-map> <data-name-map Include="val3" > <Name>xxList<Name> </data-name-map>
這為數據創建了有效的外部連接。 接下來,它將以 <Property Name="name" >value</Property> 的格式生成數據:
<temp-data Condition="'@(data-name-map)'!='' And '%(data-name-map.Identity)'!=''" Include="<Prop Name="%(data-name-map.Name)" >%(data-name-map.Identity)</Prop>"/>
同時,像這樣將它們添加到XSL樣式表:
<xsl:variable name="Data" >@(temp-data, '')</xsl:variable>
只要數據添加了,它就執行XSL轉換:
<XslTransformation Condition="'@(temp-data)'!=''" Parameters="@(xslParameters, '')" XmlInputPaths="$(PropertyPageSchema)" XslContent="$(raw-xsl)" OutputPaths="$(TempFile)" />
接下來就變得簡單了:它從文件讀取轉換結果,如果需要則增加前綴,然后返回選項。在所有的處理完畢后,當構建工具調用的時候,它們就會被添加到命令行。
利用代碼
被包含的歸檔中含有構建項目所需要的完整文件集合. 你可以將它復制到任何的目錄中去,指定Boost根路徑以及構建的位置.
針對 cxxflags 和 linkflags 的設置沒有通通測試過,因此請悠著點進行處理. 如果你發現有些東西不起作用了,記得告訴我. 或者也可以在 GitHub 給我發一條請求.