目錄
一,、概覽 二,、跨平臺(tái)代碼編輯器 三、GN入門 四,、示范工程 五,、關(guān)鍵細(xì)節(jié) 六、結(jié)語(yǔ) [編譯器選項(xiàng)] 其中前兩部分是前綴部分,,原本沒(méi)有跨平臺(tái)構(gòu)建經(jīng)驗(yàn)和知識(shí)的同學(xué)可以借助來(lái)幫助理解,,后四部分則是講述GN工程的基本結(jié)構(gòu)、如何搭建一個(gè)GN構(gòu)建的工程,、以及關(guān)鍵的一些GN知識(shí)
一,、概覽
如何開(kāi)始這個(gè)話題是我比較在意的,因?yàn)閷?duì)于部分人而言,,真正從思維和理解上切入這篇文章真正要闡述的點(diǎn)是有困難的,。這在于跨平臺(tái)編譯和開(kāi)發(fā)這塊,如果沒(méi)有一定的經(jīng)歷,,可能會(huì)很難理解這些跨平臺(tái)工具出現(xiàn)的真正意義,,它們所要解決的問(wèn)題是什么,所以這里我需要做一點(diǎn)前綴工作,,如果對(duì)GN/GYP/CMake這類話題已經(jīng)有上下文的同學(xué)可以直接跳過(guò)這部分贅述內(nèi)容,,同時(shí)也希望借助對(duì)這篇文章的理解,大家往后對(duì)于跨平臺(tái)工具的理解不局限于GN,,而是有自己的認(rèn)知,。在不斷發(fā)展和變化的大潮中,工具始終是在演變,,能夠始終抓住緊要的精髓,,見(jiàn)到陌生的工具時(shí)對(duì)它從根上有淡定從容的認(rèn)知,以最小的精力消耗掌握它,,最快發(fā)揮它的價(jià)值服務(wù)于我們,,才是我們的取勝之道。
集成編譯開(kāi)發(fā)環(huán)境
以實(shí)際工作為例,,我們?cè)陂_(kāi)發(fā)過(guò)程中需要接觸到的有源代碼工程管理,、代碼編輯、代碼編譯,,而統(tǒng)一囊括了這幾部分的開(kāi)發(fā)環(huán)境就是集成開(kāi)發(fā)環(huán)境,,由于集成開(kāi)發(fā)環(huán)境的存在,,大部分人其實(shí)只需要重點(diǎn)關(guān)注代碼編輯這塊,工程管理和編譯集成開(kāi)發(fā)環(huán)境已經(jīng)內(nèi)置做了絕大部分工作,,所以理所當(dāng)然也就容易忽略它們,,我們也有必要重新認(rèn)識(shí)下這幾部分工作。
工程管理:源代碼包含,、模塊輸出設(shè)置(動(dòng)態(tài)庫(kù)/靜態(tài)庫(kù)/可執(zhí)行文件,、模塊依賴關(guān)系配置、編譯選項(xiàng)設(shè)置,、鏈接選項(xiàng)設(shè)置,、生成后執(zhí)行事件……等非常多的設(shè)置。 代碼編輯:自動(dòng)補(bǔ)齊,、格式對(duì)齊,、定義跳轉(zhuǎn)、關(guān)鍵字高亮,、代碼提示……等我們編寫(xiě)代碼時(shí)用著很人性化的功能,。 代碼編譯:語(yǔ)法語(yǔ)音分析、代碼優(yōu)化,、錯(cuò)誤提示,、警告提示、最終生成二進(jìn)制代碼和符號(hào)表,。
各操作系統(tǒng)應(yīng)用開(kāi)發(fā)的集成開(kāi)發(fā)環(huán)境
Windows應(yīng)用開(kāi)發(fā):Visual Studio,,簡(jiǎn)稱VS,是微軟提供和支持的集成開(kāi)發(fā)編譯環(huán)境,,用于開(kāi)發(fā)Windows上應(yīng)用,。 Mac/iOS應(yīng)用開(kāi)發(fā): Xcode,,是蘋果提供和支持的集成開(kāi)發(fā)編譯環(huán)境,,用于開(kāi)發(fā)Mac和iOS應(yīng)用。 Android應(yīng)用開(kāi)發(fā):Android Studio,,簡(jiǎn)稱AS,,是谷歌提供和支持的集成開(kāi)發(fā)環(huán)境,用于開(kāi)發(fā)Android應(yīng)用,。 Ubuntu等Linux應(yīng)用開(kāi)發(fā):沒(méi)有集成開(kāi)發(fā)環(huán)境,,工程管理使用CMake/GN/GYP…各類工具(也可以使用人腦)、代碼編輯使用vim等工具,,編譯器使用GCC,。
但實(shí)際上,這些集成開(kāi)發(fā)環(huán)境都不約而同的將編譯相關(guān)的細(xì)節(jié)很好的隱藏起來(lái)了,,因?yàn)檫@塊實(shí)在是太晦澀和繁瑣,,這里我們需要有一個(gè)很清晰的認(rèn)知,,那就是這些集成開(kāi)發(fā)環(huán)境之下,隱藏的編譯器基本上只有:微軟的msvc,,GNU的gcc,,蘋果的clang+llvm這三大類,讓我們?cè)俅握归_(kāi)這幾個(gè)集成開(kāi)發(fā)環(huán)境,,一窺的它的全貌 其中藍(lán)色部分就是跨平臺(tái)工具的主戰(zhàn)場(chǎng),,旨在脫離各平臺(tái)開(kāi)發(fā)環(huán)境的限制,統(tǒng)一進(jìn)行工程管理和工程設(shè)置,,最終由工具自動(dòng)生成統(tǒng)一的各平臺(tái)makefile文件,,調(diào)用各平臺(tái)編譯器完成編譯。
沿途的風(fēng)景
在通往終點(diǎn)的路上,,總有一些風(fēng)景值得欣賞,,如果有感興趣的同學(xué),可以品味下編譯器的技術(shù)歷史,,以及gcc+llvm這種畸形產(chǎn)物的閑聞逸事: 1.msvc,,對(duì)于微軟而言,閉源是它一直以來(lái)的做派,,如今有所改善,,而編譯器則是那時(shí)候的產(chǎn)物,所以理所當(dāng)然,,宇宙最強(qiáng)IDE中的編譯器,,只限于windows這個(gè)平臺(tái)上 2.gcc,是GNU開(kāi)發(fā)的編程語(yǔ)言編譯器,,以GPL及LGPL許可證所發(fā)行的自由軟件,,開(kāi)放給所有人,是最通用和廣泛的編譯器 3.gcc+llvm,,蘋果早期使用的gcc,,gcc非常通用,支持多種語(yǔ)言,,但對(duì)蘋果官方提出的Objective-C編譯器優(yōu)化的訴求愛(ài)答不理,,導(dǎo)致蘋果只能想辦法自己來(lái)做,一般而言,,按照保守的重構(gòu)做法一部分一部分漸進(jìn)式替換原有模塊是比較理性的思路,,蘋果也是采用這種策略,將編譯器分為前后兩端,,前端做語(yǔ)義語(yǔ)法分析,,后端做指令生成編譯,而后端則是蘋果實(shí)現(xiàn)自己愿望的第一步,由此llvm橫空出世,。 后端使用llvm,,在編譯期間針對(duì)性優(yōu)化,例如以顯卡特性為例,,將指令轉(zhuǎn)換為更為高效的GPU指令,,提升運(yùn)行性能。 前端使用gcc,,在編譯前期做語(yǔ)義語(yǔ)法分析等行為 4.clang+llvm,,由于gcc語(yǔ)義語(yǔ)法分析上,對(duì)于Objective-C做得不夠細(xì)致,,gcc+llvm這種組合無(wú)法提供更為高級(jí)的語(yǔ)法提示,、錯(cuò)誤糾正等能力,所以蘋果開(kāi)始更近一步,,開(kāi)發(fā)實(shí)現(xiàn)了編譯器前端clang,,所以到目前為止xcode中缺省設(shè)置是clang+llvm,clang在語(yǔ)義和語(yǔ)法上更為先進(jìn)和人性化,,但缺點(diǎn)顯而易見(jiàn),,Clang只支持C,C++和Objective-C,,Swift這幾種蘋果體系內(nèi)會(huì)用到的語(yǔ)言,。
跨平臺(tái)開(kāi)發(fā)遇到的問(wèn)題
一般而言,如果一個(gè)應(yīng)用需要在各個(gè)操作系統(tǒng)上都有實(shí)現(xiàn),,基本的方式都是將大部分能復(fù)用的代碼和邏輯封裝到底層,,減少冗余的開(kāi)發(fā)和維護(hù)工作,像微信,、QQ,、支付寶等知名軟件,都是這種方式,。由于各個(gè)操作系統(tǒng)平臺(tái)的集成開(kāi)發(fā)環(huán)境不同,,這部分底層工程需要在各個(gè)集成開(kāi)發(fā)環(huán)境中搭建和維護(hù)工程、編譯應(yīng)用,,這樣就會(huì)產(chǎn)生大量重復(fù)工作,,任何一個(gè)源碼文件的增加/刪除,、模塊調(diào)整都會(huì)涉及到多個(gè)集成開(kāi)發(fā)環(huán)境的工程調(diào)整,。這個(gè)時(shí)候,大多數(shù)人都會(huì)想,,如果有這么一個(gè)工具,,我們用它管理工程和編譯,一份工程能夠自動(dòng)編譯出各個(gè)平臺(tái)的應(yīng)用或底層庫(kù),那將是多么美好的一個(gè)事情,。
幸運(yùn)的是,,已經(jīng)有前人替我們感受過(guò)這個(gè)痛苦的過(guò)程,也有前人為我們種好了樹(shù),,我們只需要找好一個(gè)合適的樹(shù),,背靠著它,調(diào)整好坐姿,,懷著無(wú)比感恩的心情在它的樹(shù)蔭下乘涼,。樹(shù)有好多課,該選哪一顆,,大家興趣至上自行選擇,,這里我們主要介紹我個(gè)人傾向的GN這一棵。
原始跨平臺(tái): 編寫(xiě)makefile文件,,使用各平臺(tái)上的make(微軟的MS nmake,、GNU的make)來(lái)編譯makefile文件,這種做法的缺點(diǎn)是各平臺(tái)的make實(shí)現(xiàn)不同,,導(dǎo)致這種原始的做法其實(shí)復(fù)用度并不高,,需要針對(duì)各平臺(tái)單獨(dú)編寫(xiě)差異巨大的makefile文件,那為什么要介紹它呢,,因?yàn)檫@是跨平臺(tái)的根,,所有跨平臺(tái)工具,最終都是要依賴各平臺(tái)應(yīng)用的集成開(kāi)發(fā)環(huán)境的編譯器來(lái)執(zhí)行編譯,,這是固定不變的,,也就是說(shuō)各平臺(tái)的編譯,最終還是需要各平臺(tái)的makefile,,這一點(diǎn)是無(wú)法逃避的,,而怎么由人工轉(zhuǎn)為自動(dòng)化,才是跨平臺(tái)編譯的進(jìn)階之路,。 進(jìn)階跨平臺(tái): 使用cmake,,編寫(xiě)統(tǒng)一的makefile文件,最后由cmake自動(dòng)生成各平臺(tái)相關(guān)的makefile文件執(zhí)行編譯,,這一點(diǎn)上,,cmake已經(jīng)是比較好的跨平臺(tái)工具了,一般的跨平臺(tái)工程基本已經(jīng)滿足需求了,。 現(xiàn)代跨平臺(tái): 當(dāng)工程規(guī)模增大到難以想象的量級(jí)時(shí),,編譯速度和工程模塊的劃分變得尤為重要,其中chromium工程就遇到這兩個(gè)問(wèn)題,,于是最初誕生了gyp,,最后演化升級(jí)為gn,,其旨在追求工程更加清晰的模塊和結(jié)構(gòu)呈現(xiàn),以及更快的編譯速度,。前者通過(guò)語(yǔ)法層面實(shí)現(xiàn),,后者則依靠ninja來(lái)提升編譯速度,因?yàn)榇笮凸こ痰木幾g,,很大一部分時(shí)間都花在了源文件編譯依賴樹(shù)的分析這塊,,而ninja更像是一個(gè)編譯器的預(yù)處理,其主要目的是舍棄gcc,、msvc,、clang等編譯器在編譯過(guò)程中遞歸查找依賴的方式,因?yàn)檫@里存在很多重復(fù)的依賴查找,,而ninja改進(jìn)了這一過(guò)程,,提前生成編譯依賴樹(shù),編譯期間按照編譯依賴樹(shù)的順序依次編譯,,這樣就大大減少了編譯期間雜亂的編譯順序造成的重復(fù)依賴關(guān)系查找,。 關(guān)于一點(diǎn)說(shuō)明: 本文主要針對(duì)底層庫(kù)領(lǐng)域的跨平臺(tái)構(gòu)建,這也是比較常見(jiàn)和通用的跨平臺(tái)應(yīng)用方式,,但不排除有例外,,像Qt就是一套從底層開(kāi)發(fā)到UI框架,再到代碼編輯,、工程管理的C++整體跨平臺(tái)解決方案,,它的涵蓋面更大,只有應(yīng)用層使用Qt的UI框架時(shí)才能發(fā)揮它的真正威力(有興趣的同學(xué)可以研究它,,目前我所在團(tuán)隊(duì)研發(fā)的CCtalk客戶端Windows和Mac,,以及即將外發(fā)的Chromebook版都是使用它,我也打算在一個(gè)合適的時(shí)機(jī)從我最擅長(zhǎng)的UI框架領(lǐng)域來(lái)介紹Qt的實(shí)戰(zhàn)應(yīng)用),,而這篇文章尋求的是目前為止更通用化的跨平臺(tái)方式,,也就是C++跨平臺(tái)底層+原生UI應(yīng)用層的組合方式,所以本文跨平臺(tái)的切入重點(diǎn)也是針對(duì)底層庫(kù)這一塊,,做得好的應(yīng)用,,一般會(huì)把網(wǎng)絡(luò)、數(shù)據(jù),、業(yè)務(wù)上下文狀態(tài)管理歸屬到底層由跨平臺(tái)實(shí)現(xiàn),,這部分可以達(dá)到整個(gè)應(yīng)用程序代碼量的60~70%,完成這塊的復(fù)用是非常值得的,。
二,、跨平臺(tái)代碼編輯器
gn解決了跨平臺(tái)編譯問(wèn)題,但是各平臺(tái)的代碼編輯,,并不屬于底層C++跨平臺(tái)構(gòu)建工具的范疇(全框架C++流程的Qt例外),。一種做法是,通過(guò)gn生成各個(gè)平臺(tái)的工程(xcode工程,、vs工程)然后再進(jìn)行代碼編寫(xiě),,源文件和工程模塊的修改需要另外同步到gn工程文件中,;另一種做法是使用vscode,。顯而易見(jiàn),使用vscode+gn是最佳選擇,,這樣就相當(dāng)于一個(gè)各平臺(tái)統(tǒng)一的集成開(kāi)發(fā)環(huán)境,,這里我給大家預(yù)覽下vscode和gn配合之下的預(yù)覽圖:
- 代碼編寫(xiě)使用vscode,有各種插件提供了語(yǔ)法提示和檢測(cè),、自動(dòng)補(bǔ)齊、高亮等編輯器功能
- 代碼編譯使用gn,,在vscode中直接有內(nèi)置的命令行,,直接運(yùn)行編譯腳本執(zhí)行編譯,,編譯腳本中是gn的編譯命令的調(diào)用
- 在vscode+gn的使用過(guò)程中,,我們會(huì)慢慢體會(huì)到代碼編輯,、編譯的白盒流程,,而不是以前的黑盒,對(duì)編輯和編譯流程的理解也將越來(lái)越深刻,,這是軟件開(kāi)發(fā)越來(lái)越上層的當(dāng)下,,最難能可貴的地方,是抱有一顆求知之心的人最珍視的東西,。*
三,、GN入門
官方文檔
https://gn./gn/+/master/docs GN雖然有非常完善和詳細(xì)的官方文檔,但如果要真正搭建一個(gè)跨平臺(tái)的工程,,則往往寸步難行,,這在于GN終究只是chromium的附產(chǎn)物,在于更好的服務(wù)chromium工程,,所以缺少一些必要詳細(xì)的入門模版,,尤其是在各個(gè)編譯器下的編譯、鏈接選項(xiàng)設(shè)置這塊,,需要完全理解各個(gè)編譯器的編譯參數(shù)和設(shè)置,,這對(duì)于我們的精力來(lái)說(shuō),,幾乎是不可能一下子就能了解清楚的,而這些編譯選項(xiàng)往往是一次性工作,,一旦配置好了,,以后都不需要修改,或者僅需要修改個(gè)別選項(xiàng),。好在有大量成熟的跨平臺(tái)開(kāi)源項(xiàng)目,,以chromium為例,我們從中扒出GN的最小集,,再應(yīng)用到我們一個(gè)自定義的工程中,,搞清楚各個(gè)環(huán)節(jié)的含義,那么基本就算已經(jīng)掌握了GN了,。
示例工程
為了能夠?qū)N有一個(gè)全貌的了解,,我準(zhǔn)備了一個(gè)簡(jiǎn)單的demo工程,請(qǐng)大家務(wù)必下載下來(lái),,后續(xù)的介紹都是基于這個(gè)示例工程來(lái)做 騰訊微云:https://share./5ymLr5u 如果大家遇到下載不了或者最終編譯錯(cuò)誤的異常情況可以評(píng)論中聯(lián)系我,,目前已在windows、mac,、android編譯環(huán)境驗(yàn)證
鳥(niǎo)瞰一個(gè)GN工程
我們先從工程的全貌來(lái)看,,這是我們demo工程的全局視圖,GN組織一個(gè)跨平臺(tái)工程分為3大塊: 第1部分:整體工程入口,,這部分常年都不用做修改 第2部分:GN通用文件,,這部分常年都不用做修改 第3部分:GN源代碼工程文件,這部分與平常我們?cè)诩砷_(kāi)發(fā)環(huán)境中類似,,源文件的組織和管理就在這個(gè)部分
詳解各GN文件職責(zé)
四,、示范工程
一般而言,這一部分仔細(xì)閱讀后,,基本就可以依葫蘆畫(huà)瓢使用起GN了,,更加細(xì)節(jié)的部分則需要靠大家閱讀官方文檔以及后續(xù)的第五部分內(nèi)容
準(zhǔn)備環(huán)境
1.安裝vscode(主要是方便大家閱讀工程結(jié)構(gòu),也可以跳過(guò)這一步,,只是會(huì)增加工程的理解難度而已) vscode的使用非常簡(jiǎn)單,,指定打開(kāi)某個(gè)文件夾,然后整個(gè)文件夾的目錄結(jié)構(gòu)就自動(dòng)呈現(xiàn)在vscode的左側(cè)了,,同時(shí)插件安裝異常方便,,直接在vscode左側(cè)最后一個(gè)tab中搜索關(guān)鍵字就會(huì)有插件的在線安裝提示,根據(jù)提示安裝好安裝C++插件,、gn插件等插件后,,就可以作為代碼編輯器編寫(xiě)代碼了。 2.安裝python3 關(guān)于python的語(yǔ)法,,簡(jiǎn)單了解就好,,遇到看不懂的稍微查下語(yǔ)法,,腳本語(yǔ)言總是易于理解和使用的,大家要有信心,。
工程目錄結(jié)構(gòu)
各平臺(tái)編譯腳本執(zhí)行
windows:在gn_project目錄下,,命令行中運(yùn)行(vscode內(nèi)置的更方便)命令build_for_win.bat debug mac:gn_project目錄下,命令行中運(yùn)行(vscode內(nèi)置的更方便)命令./build_for_mac.sh debug android: gn_project目錄下,,命令行中運(yùn)行(vscode內(nèi)置的更方便)命令./build_for_android.sh debug arm
mac平臺(tái)編譯入口腳本
./build_for_mac.sh,,關(guān)鍵腳本如下(只是截取部分關(guān)鍵代碼,,旨在讓大家看到各平臺(tái)下gn編譯命令的調(diào)用):
# ninja files $gn gen $build_cache_path --args="is_debug=$debug_mode target_cpu=“x64"” if [ $? != 0 ]; then echo “generate ninja failed” exit 1 fi echo - echo - echo --------------------------------------------------------------- echo 第4步:開(kāi)始ninja編譯… echo --------------------------------------------------------------- # build $ninja -C $build_cache_path > $log_path if [ $? != 0 ]; then echo “build failed. log: $log_path” exit 1 fi
windows平臺(tái)編譯入口腳本
./build_for_win.sh 和 ./build_for_win.py,,關(guān)鍵腳本如下(只是截取部分關(guān)鍵代碼,旨在讓大家看到各平臺(tái)下gn編譯命令的調(diào)用):
# -------------------------------------------------------------------------------- # 3. build kernel # -------------------------------------------------------------------------------- # 1) generate ninja file for build # 2) build # 3) print end log # -------------------------------------------------------------------------------- print(“build kernel …”) gn_cmd = '"{}" gen “{}” --args=“is_debug={} target_cpu=\“x86\” win_vc=\”{}\" win_vc_ver=\"{}\" win_xp={}"’.format(gn, build_cache_path, debug_mode, win_vc, vs_ver, support_xp) proc = subprocess.run(gn_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=False) stdout = buildtools.console_to_str(proc.stdout) stderr = buildtools.console_to_str(proc.stderr) if 'Done.’ not in stdout: print(stdout) print(stderr) with open(os.path.join(build_cache_path, 'build.log’), 'w’) as log: log.write(stdout) sys.exit(1) ninja_cmd = '"{}" -C “{}”’.format(ninja, build_cache_path) proc = subprocess.run(ninja_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=False) stdout = buildtools.console_to_str(proc.stdout) stderr = buildtools.console_to_str(proc.stderr) if proc.returncode != 0: print(stdout) print(stderr) with open(os.path.join(build_cache_path, 'build.log’), 'w’) as log: log.write(stdout) sys.exit(1) with open(os.path.join(build_cache_path, 'build.log’), 'w’) as log: log.write(stdout)
android平臺(tái)編譯入口腳本
./build_for_android.sh,,關(guān)鍵腳本如下(只是截取部分關(guān)鍵代碼,,旨在讓大家看到各平臺(tái)下gn編譯命令的調(diào)用):
echo - echo - echo --------------------------------------------------------------- echo 第5步:使用gn生成ninja文件 echo --------------------------------------------------------------- # ninja file $gn gen $build_cache_path --args=" target_os = “android” target_cpu = “$target_config” use_qt_for_android = false ndk = “$ndk” qt_sdk = “$qt_sdk”" if [ $? != 0 ]; then echo “generate ninja failed” exit 1 fi echo - echo - echo --------------------------------------------------------------- echo 第6步:使用ninja開(kāi)始編譯… echo --------------------------------------------------------------- # build $ninja -C $build_cache_path > $log_path if [ $? != 0 ]; then exit 1 fi
整個(gè)工程的各個(gè)代碼模塊
工程總共有多少模塊,需要在./BUILD.gn文件中配置,,而各個(gè)模塊的工程則在各個(gè)模塊中,,關(guān)鍵代碼如下(只是截取部分關(guān)鍵代碼,旨在呈現(xiàn)整體工程各個(gè)模塊):
具體代碼模塊
以整體工程文件./BUILD.gn中各個(gè)模塊的配置為例,,"http://system_wrappers:system_wrappers"表示在system_wrappers目錄下,,尋找BUILD.gn文件,在這個(gè)BUILD.gn文件中定義了system_wrappers模塊,,以及該模塊的源文件,、編譯選項(xiàng)設(shè)置等,具體代碼如下:
library_template.gni文件
這是一個(gè)GN模版文件,,是我們自定義的一個(gè)GN文件,,文件名可以根據(jù)大家的喜好自行修改,確保在要使用的地方import進(jìn)來(lái)即可,,每個(gè)代碼模塊的編譯選項(xiàng)設(shè)置其實(shí)是有蠻多編譯選項(xiàng)和工程定義需要設(shè)置,,而這些編譯選項(xiàng)和定義的設(shè)置其實(shí)是重復(fù)的,如果每個(gè)模塊都寫(xiě)一遍,,會(huì)比較冗余,,所以我們抽取了公共的模版,使得各個(gè)代碼模塊的BUILD.gn文件可以只關(guān)注源代碼包含,、附件包含目錄,、附加包含庫(kù)等工程相關(guān)的事項(xiàng)。
五,、關(guān)鍵細(xì)節(jié)
這是GN比較進(jìn)階的一部分,,需要參考官方文檔中語(yǔ)法部分進(jìn)行解讀
https://gn./gn/+/master/docs/reference.md 官方語(yǔ)法文檔的查閱方法是,“關(guān)鍵字 + :”,,例如需要看template的定義時(shí),,文檔中搜索“template:”即可出現(xiàn)詳細(xì)的定義和解釋
1.template
template(模版)的作用是抽取公共gn代碼,,節(jié)省整體的gn代碼量,使得工程文件更容易閱讀,,以library_template.gni為例,,有幾個(gè)關(guān)鍵語(yǔ)法我們需要熟知:
- 模版中的變量和模版實(shí)例之間的數(shù)據(jù)傳遞,是通過(guò)invoker來(lái)承載,,例如以sources的值為例,,模版中可以通過(guò)sources = invoker.sources來(lái)傳遞值
- 模版實(shí)例的名稱,則通過(guò)target_name來(lái)承載,,例如,,我們定義了模版libaray,那么library(“aysnevent”)的target_name值就是”aysnevent"
template(“l(fā)ibrary”) { assert(defined(invoker.sources), “Need sources in $target_name listing the source files.”) type = “static_library” if (is_win) { type = “shared_library” } if (is_mac || is_android) { type = “source_set” } target(type, target_name) { configs += [ “//:qt_config”, ] sources = invoker.sources # output_prefix_override = true output_dir = “$root_out_dir” lib_dirs = [ rebase_path("$output_dir/libs"), ] if (type == “static_library”) { output_dir += “/libs” } if (defined(invoker.deps)) { if (defined(deps)) { deps += invoker.deps } else { deps = invoker.deps } } if (defined(invoker.data_deps)) { if (defined(data_deps)) { data_deps += invoker.data_deps } else { data_deps = invoker.data_deps } } if (defined(invoker.include_dirs)) { if (defined(include_dirs)) { include_dirs += invoker.include_dirs } else { include_dirs = invoker.include_dirs } } if (defined(invoker.configs)) { if (defined(configs)) { configs += invoker.configs } else { configs = invoker.configs } } if (defined(invoker.public_configs)) { if (defined(public_configs)) { public_configs += invoker.public_configs } else { public_configs = invoker.public_configs } } if (defined(invoker.cflags)) { if (defined(cflags)) { cflags += invoker.cflags } else { cflags = invoker.cflags } } if (defined(invoker.cflags_cc)) { if (defined(cflags_cc)) { cflags_cc += invoker.cflags_cc } else { cflags_cc = invoker.cflags_cc } } if (defined(invoker.del_configs) && defined(configs)) { configs -= invoker.del_configs } if (defined(invoker.del_cflags_cc) && defined(cflags_cc)) { cflags_cc -= invoker.del_cflags_cc } if (defined(invoker.libs)) { if (defined(libs)) { libs += invoker.libs } else { libs = invoker.libs } } if (defined(invoker.defines)) { if (defined(defines)) { defines += invoker.defines } else { defines = invoker.defines } } }
2.BUILD.gn和xxx.gni
BUILD.gn一般作為模塊的工程入口文件,,可以配合.gni文件來(lái)劃分源碼或模塊,。
- 當(dāng)模塊比較大時(shí),可以用.gni來(lái)劃分內(nèi)部子模塊,,減輕工程文件的負(fù)擔(dān),;
- 當(dāng)多個(gè)模塊之間差異很小時(shí),可以利用BUILD.gn來(lái)統(tǒng)一管理這些模塊的設(shè)置,,利用.gni來(lái)專屬負(fù)責(zé)各個(gè)模塊源文件管理,。
以./modules/BUILD.gn為例,它包含了多個(gè)代碼模塊,,各個(gè)模塊的源文件和工程配置則在各個(gè).gni中管理
3.source_set/static_library/shared_library
source_set:編譯后的代碼集合,,它與靜態(tài)庫(kù)的唯一區(qū)別是沒(méi)有鏈接的符號(hào)文件,就是一般編譯后生成的.o文件,,目前我們demo工程里頭,,非windows系統(tǒng)上使用的是source_set(具體參見(jiàn) library_template.gni),編譯完成后,,使用ar和ranlib命令重新打包成帶符號(hào)表的.a靜態(tài)庫(kù) 特別說(shuō)明:最終打包出來(lái)的是.a庫(kù),,那么為什么demo中不直接用static_library呢,這是因?yàn)?,整個(gè)工程的各個(gè)模塊,,由于沒(méi)有一個(gè)統(tǒng)一的地方調(diào)用這些模塊,這就會(huì)導(dǎo)致編譯出來(lái)的結(jié)果是各個(gè)分散的.a庫(kù),,使用source_set,,然后再手動(dòng)打包生成符號(hào)表最終可以生成一個(gè)統(tǒng)一的.a庫(kù) static_library:靜態(tài)庫(kù),windows上是.lib庫(kù),,其他平臺(tái)是.a庫(kù) shared_library: 動(dòng)態(tài)庫(kù),,windows上是.dll庫(kù),mac和iOS上是.dylib庫(kù),andorid是.so庫(kù)
4.”.gn“根文件
這是一個(gè)入口設(shè)置的文件,,是GN中的固定規(guī)則文件,,會(huì)自動(dòng)被GN讀取,但由于它沒(méi)有文件名,,只有擴(kuò)展名,,各個(gè)系統(tǒng)上如果沒(méi)有打開(kāi)文件夾選項(xiàng)中查看擴(kuò)展名的設(shè)置,,會(huì)看不到,,這里需要注意,。如果使用的vscode,,則不需要擔(dān)心這個(gè)問(wèn)題,在vscode中可以默認(rèn)看到該文件
5.windows上對(duì)XP支持
如果windows上安裝的是VS2010以上版本,,默認(rèn)編譯出來(lái)的結(jié)果是不支持XP系統(tǒng)的,,如果需要支持,,則需要手動(dòng)增加支持選項(xiàng),,在./gn/toolchain/BUILD.gn中對(duì)應(yīng)修改即可 具體參見(jiàn)關(guān)鍵代碼(只截取了部分關(guān)鍵代碼)
修改1:./gn/BUILDCONFIG.gn中declare_args中增加X(jué)P參數(shù)聲明“win_xp = false”,,這樣windows上調(diào)用ninja編譯命令時(shí),,傳入是否支持XP的選項(xiàng)
# -------------------------------------------------------------------------------- # 3. build kernel # -------------------------------------------------------------------------------- # 1) generate ninja file for build # 2) build # 3) print end log # -------------------------------------------------------------------------------- print(“build kernel …”) gn_cmd = '"{}" gen “{}” --args=“is_debug={} target_cpu=\“x86\” win_vc=\”{}\" win_vc_ver=\"{}\" win_xp={}"’.format(gn, build_cache_path, debug_mode, win_vc, vs_ver, support_xp)
修改2:./gn/toolchain/BUILD.gn中toolchain("msvc”) 下tool(“cc”)中增加”/D_USING_V110_SDK71_”編譯選項(xiàng)
# Label names may have spaces so pdbname must be quoted. command = “cmd /c “$env_setup $cc_wrapper $cl /FS /nologo /showIncludes /FC @$rspfile /c {{source}} /Fo{{output}} /Fd”$pdbname”"" if (win_xp) { command += " /D_USING_V110_SDK71_" }
修改3:./gn/toolchain/BUILD.gn中toolchain("msvc”) 下tool(“cxx”)中增加”/D_USING_V110_SDK71_”編譯選項(xiàng)
# Label names may have spaces so pdbname must be quoted. command = “cmd /c “$env_setup $cc_wrapper $cl /FS /nologo /showIncludes /FC @$rspfile /c {{source}} /Fo{{output}} /Fd”$pdbname”"" if (win_xp) { command += " /D_USING_V110_SDK71_" }
修改4:./gn/toolchain/BUILD.gn中toolchain(“msvc”) 下tool(“alink”)中增加”/SUBSYSTEM:WINDOWS,5.01”編譯選項(xiàng)
command = “cmd /c “$env_setup
l
i
b
/
n
o
l
o
g
o
/
i
g
n
o
r
e
:
4221
a
r
f
l
a
g
s
/
O
U
T
:
o
u
t
p
u
t
@
lib /nologo /ignore:4221 {{arflags}} /OUT:{{output}} @
lib/nologo/ignore:4221arflags/OUT:output@rspfile”” if (win_xp) { command += " /SUBSYSTEM:WINDOWS,5.01" }
修改5:./gn/toolchain/BUILD.gn中toolchain(“msvc”) 下tool(“solink”)中增加”/SUBSYSTEM:WINDOWS,5.01”編譯選項(xiàng)
command = “cmd /c “$env_setup $lnk /nologo /IMPLIB:$libname /DLL /OUT:$dllname /PDB:$pdbname @$rspfile”” if (win_xp) { command += " /SUBSYSTEM:WINDOWS,5.01" }
修改6:./gn/toolchain/BUILD.gn中toolchain(“msvc”) 下tool(“l(fā)ink”)中增加”/SUBSYSTEM:WINDOWS,5.01”編譯選項(xiàng)
command = “cmd /c “$env_setup $lnk /nologo /OUT:$exename /PDB:$pdbname @$rspfile”” if (win_xp) { command += " /SUBSYSTEM:WINDOWS,5.01" }
6.是否生成調(diào)試信息
release編譯選項(xiàng)下,,生成調(diào)試信息的情況下,才會(huì)對(duì)應(yīng)生成符號(hào)文件,,用于crash時(shí)分析崩潰,,例如windows上的pdb、mac上的dSYM
為了調(diào)用方便,,我們?cè)?/gn/BUILD.gn中自定義了配置選項(xiàng),,具體代碼如下
# ------------------------------------------------------------------------- # debug_symbols # 調(diào)試符號(hào)文件 # ------------------------------------------------------------------------- config(“debug_symbols”) { # It’s annoying to wait for full debug symbols to push over # to Android devices. -gline-tables-only is a lot slimmer. if (is_android) { cflags = [ “-gline-tables-only”, “-funwind-tables”, # Helps make in-process backtraces fuller. ] } else if (is_win) { cflags = [ “/Zi” ] ldflags = [ “/DEBUG” ] } else { cflags = [ “-g” ] } }
然后在./gn/BUILDCONFIG.gn中,將該自定義配置增加到gn的缺省配置中,,具體代碼如下
# Default configs default_configs = [ “//gn:default”, # “//gn:no_exceptions”, # “//gn:no_rtti”, # “//gn:warnings”, “//gn:warnings_except_public_headers”, ] if (!is_debug) { default_configs += [ “//gn:release” ] } default_configs += [ “//gn:debug_symbols” ] default_configs += [ “//gn:extra_flags” ]
7.dispatch_for_ide.py
這是我們自定義的編譯完成后執(zhí)行動(dòng)作,,通過(guò)python實(shí)現(xiàn),主要實(shí)現(xiàn)非windows下.a庫(kù)打包和生成文件同步到指定輸出目錄,,這里有幾個(gè)基礎(chǔ)的概念需要理解,,才能明白這個(gè)編譯完成后事件調(diào)用的機(jī)制
action:gn中的執(zhí)行動(dòng)作,它用于定義一個(gè)執(zhí)行的腳本,,一般配合python來(lái)使用,,在./BUILD.gn中,我們定義了action("dispatch_for_ide”),,從而指定了需要執(zhí)行的腳本是dispatch_for_ide.py,。 action中的deps:指定腳本的執(zhí)行需要在這些依賴項(xiàng)完成之后,而我們定義的action中依賴項(xiàng)是project,,也就是整個(gè)工程編譯完成后才能開(kāi)始執(zhí)行腳本,。 我們可以從過(guò)官方文檔中看到具體的定義描述(頁(yè)面中查找關(guān)鍵字“action:”即可看到) the “deps” and “public_deps” for an action will always be completed before any part of the action is run so it can depend on the output of previous steps.
為了便于理解,我們先看下action("dispatch_for_ide”)的調(diào)用代碼,,代碼位于./BUILG.gn
group(“ccore”){ deps = projects deps += [ “//:dispatch_for_ide” ] }
然后,,我們?cè)倏碼ction("dispatch_for_ide”)具體定義代碼,,代碼位于./BUILG.gn中
# 編譯完成后執(zhí)行動(dòng)作 action(“dispatch_for_ide”) { arg = [ “–kernel”, rebase_path("http://"), “–outpath”, rebase_path("http://out"), “–cachepath”, rebase_path("$root_out_dir"), ] if (is_debug) { arg += [ “–buildmode”, “Debug” ] } else { arg += [ “–buildmode”, “Release” ] } if (is_mac) { arg += ["–platform", “mac”] } else if (is_ios) { arg += ["–platform", “ios”] } else if (is_android) { arg += ["–platform", “android”] } else if (is_win) { arg += ["–platform", “win”] } arg += ["–targetcpu", “${target_cpu}”] script = “dispatch_for_ide.py” outputs = [ “$root_out_dir/libccore.a” ] args = arg deps = projects }
8.data_deps和deps
data_deps: 非鏈接依賴關(guān)系,如果依賴的模塊是一個(gè)靜態(tài)庫(kù),,則需要代碼中手動(dòng)通過(guò)命令引入對(duì)應(yīng)lib庫(kù),,鏈接時(shí)才能成功, 例如,,代碼中#pragma comment(lib, “xxx.lib”),,一般用于特指windows上隱式鏈接(靜態(tài)鏈接)的dll時(shí),需要代碼中如是操作 Specifies dependencies of a target that are not actually linked into the current target. Such dependencies will be built and will be available at runtime. This is normally used for things like plugins or helper programs that a target needs at runtime. Note: On iOS and macOS, create_bundle targets will not be recursed into when gathering data_deps. See “gn help create_bundle” for details. deps: 私有鏈接依賴關(guān)系,,如果依賴的模塊是一個(gè)靜態(tài)庫(kù),,則不需要代碼中再人工鏈入指定的lib庫(kù)文件,一般特指依賴的靜態(tài)庫(kù) Specifies private dependencies of a target. Private dependencies are propagated up the dependency tree and linked to dependent targets, but do not grant the ability to include headers from the dependency. Public configs are not forwarded.
9.Android下編譯環(huán)境
Android下編譯,,需要額外的環(huán)境依賴設(shè)置,,具體在./build_for_android.sh中設(shè)置,然后在./gn/BUILDCONFIG.gn中應(yīng)用
./build_for_android.sh中對(duì)應(yīng)代碼如下
ndk="/Library/android-ndk-r16b" PATH=$PATH:$ndk/toolchains/arm-linux-androideabi-4.9/prebuilt/darwin-x86_64/bin echo ndk路徑:$ndk #qt_sdk="/Users/taoshu/Qt5.9.6/5.9.6" #echo qt路徑:$qt_sdk … # ninja file $gn gen $build_cache_path --args=" target_os = “android” target_cpu = “$target_config” use_qt_for_android = false ndk = “$ndk” qt_sdk = “$qt_sdk”" if [ $? != 0 ]; then echo “generate ninja failed” exit 1 fi
NDK路徑設(shè)置:
編譯針對(duì)Android環(huán)境的目標(biāo)文件時(shí),,NDK是必不可少的設(shè)置,,它提供了各種針對(duì)Android標(biāo)準(zhǔn)庫(kù)以及交叉編譯的工具集
Qt路徑設(shè)置:
應(yīng)用層是Qt框架時(shí): 需要將編譯參數(shù)use_qt_for_android設(shè)置為true,并設(shè)置好Qt路徑,,這是由于Qt的UI線程和java主線程不在一個(gè)線程,,是自定義實(shí)現(xiàn),所以多線程轉(zhuǎn)異步處理依賴Qt的消息循壞實(shí)現(xiàn) 應(yīng)用層是Java原聲框架時(shí): 需要將編譯參數(shù)use_qt_for_android設(shè)置為false即可,,Qt路徑可以忽略
10.Windows下Visual Stuido的版本
由于GN中需要指定windows下編譯時(shí)使用的VS版本,,這是GN之外我們需要做的事情,目前是通過(guò)python腳本去檢查指定安裝目錄,,從而自動(dòng)獲取本機(jī)的VS版本,,這塊理論上不會(huì)有問(wèn)題,但電腦環(huán)境千差萬(wàn)別,,難免會(huì)有問(wèn)題,,所以demo工程中,直接強(qiáng)制指定使用的是VS2015,,如果大家需要修改VS版本,,需要在如下腳本文件中做稍許調(diào)整
./build_for_win.py:其中有參數(shù)指定強(qiáng)制使用VS2015
# -------------------------------------------------------------------------------- # 3. build kernel # -------------------------------------------------------------------------------- # 1) generate ninja file for build # 2) build # 3) print end log # -------------------------------------------------------------------------------- print(“build kernel …”) gn_cmd = '"{}" gen “{}” --args=“is_debug={} target_cpu=\“x86\” win_vc=\”{}\" win_vc_ver=\"{}\" win_xp={}"’.format(gn, build_cache_path, debug_mode, win_vc, vs_ver, support_xp) proc = subprocess.run(gn_cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=False) stdout = buildtools.console_to_str(proc.stdout) stderr = buildtools.console_to_str(proc.stderr)
./gn/BUILDCONFIG.gn:其中有根據(jù)用戶安裝目錄去搜尋VS版本和VS環(huán)境的代碼
# ********************************************************************************* # Config msvc builder # 配置MSVC編譯,首先會(huì)嘗試從系統(tǒng)中自動(dòng)尋找vc編譯器,, # 沒(méi)有匹配到結(jié)果時(shí),,再?gòu)娜笔∮簿幋a配置路徑中讀取 # ********************************************************************************* msvc = win_vc_ver if (target_os == “win”) { # By default we look for 2017 (Pro & Community), then 2015. If MSVC is installed in a # non-default location, you can set win_vc to inform us where it is. vc_2017_pro_default = “C:/Program Files (x86)/Microsoft Visual Studio/2017/Professional/VC” vc_2017_com_default = “C:/Program Files (x86)/Microsoft Visual Studio/2017/Community/VC” vc_2017_bt_default = “C:/Program Files (x86)/Microsoft Visual Studio/2017/BuildTools/VC” vc_2015_default = “C:/Program Files (x86)/Microsoft Visual Studio 14.0/VC” vc_2013_default = “C:/Program Files (x86)/Microsoft Visual Studio 12.0/VC” if (win_vc == “”) { if (“True” == exec_script("http://gn/BUIDCONFIG_win_check_vcdir.py", [ “$vc_2017_pro_default” ], “trim string”)) { win_vc = vc_2017_pro_default msvc = 2017 } else if (“True” == exec_script("http://gn/BUIDCONFIG_win_check_vcdir.py", [ “$vc_2017_com_default” ], “trim string”)) { win_vc = vc_2017_com_default msvc = 2017 } else if (“True” == exec_script("http://gn/BUIDCONFIG_win_check_vcdir.py", [ “$vc_2017_bt_default” ], “trim string”)) { win_vc = vc_2017_bt_default msvc = 2017 } else if (“True” == exec_script("http://gn/BUIDCONFIG_win_check_vcdir.py", [ “$vc_2015_default” ], “trim string”)) { win_vc = vc_2015_default msvc = 2015 } else if (“True” == exec_script("http://gn/BUIDCONFIG_win_check_vcdir.py", [ “$vc_2013_default” ], “trim string”)) { win_vc = vc_2013_default msvc = 2013 } } assert(win_vc != “”) # Could not find VC installation. Set win_vc to your VC directory. if (msvc == “”) { if (“True” == exec_script("http://gn/BUIDCONFIG_win_check_vcdir.py", [ “$win_vc/Tools” ], “trim string”)) { msvc = 2017 } else { msvc = 2015 } } } if (target_os == “win”) { if (msvc == 2017 && win_toolchain_version == “”) { win_toolchain_version = exec_script("http://gn/BUIDCONFIG_win_highest_version_vcdir.py", [ “$win_vc/Tools/MSVC”, “[0-9]{2}.[0-9]{2}.[0-9]{5}”, ], “trim string”) } if (win_sdk_version == “”) { win_sdk_version = exec_script("http://gn/BUIDCONFIG_win_highest_version_vcdir.py", [ “$win_sdk/Include”, “[0-9]{2}.[0-9].[0-9]{5}.[0-9]”, ], “trim string”) } }
六、結(jié)語(yǔ) [編譯器選項(xiàng)]
這篇文章講到這里,,使用GN已經(jīng)不在話下,,但是要真正做到無(wú)惑,則和GN無(wú)關(guān),因?yàn)榫幾g器選項(xiàng)不是跨平臺(tái)工具要解決問(wèn)題的范疇,,文章開(kāi)頭也有做說(shuō)明,。在./gn/BUILD.gn和./gn/BUILDCONFIG.gn這兩個(gè)文件中,有大量編譯器相關(guān)的選項(xiàng)設(shè)置,,這些選項(xiàng)設(shè)置由于各平臺(tái)編譯器的差異,,導(dǎo)致千差萬(wàn)別,非?;逎?,包括我自己也只了解其中一小部分。幸運(yùn)的是,,這部分內(nèi)容我們有大量的時(shí)間去慢慢熟悉它,,我們也無(wú)需記住每一項(xiàng),其中99%以上設(shè)置項(xiàng)是我們也許永遠(yuǎn)不會(huì)關(guān)注到的,,我相信經(jīng)過(guò)實(shí)際工作開(kāi)發(fā)后,,有那么一小部分選項(xiàng)我們會(huì)接觸到,并去調(diào)整它,,屆時(shí)我們只需要掌握那一部分就可以了,。
|