由于工作原因镶蹋,常常會(huì)在各種編程語(yǔ)言技術(shù)棧下切換。每次切回到C/C++技術(shù)棧下赏半,都會(huì)為C/C++語(yǔ)言缺乏一個(gè)好用的包管理器而不適應(yīng)好一陣贺归。
包管理器的存在可以讓程序功能單元的組織滿(mǎn)足閉包化(隱藏源碼、依賴(lài)和構(gòu)建細(xì)節(jié))断箫、契約化(顯示的API導(dǎo)出拂酣、變更和版本管理)以及標(biāo)準(zhǔn)化(體驗(yàn)一致的本地客戶(hù)端、中央倉(cāng)仲义、及其在工具約束下的標(biāo)準(zhǔn)開(kāi)發(fā)活動(dòng)等等)婶熬。這些都能夠讓軟件功能的復(fù)用變得更加黑盒和簡(jiǎn)單,降低程序的復(fù)用成本埃撵。
這些優(yōu)點(diǎn)對(duì)于社區(qū)化開(kāi)發(fā)是非常重要的赵颅。離散的社區(qū)團(tuán)隊(duì)之間需要有一種標(biāo)準(zhǔn)和契約,可以低成本的信任和依賴(lài)別人發(fā)布的代碼和更新暂刘。關(guān)于C/C++語(yǔ)言需要一個(gè)好用的包管理器這件事饺谬,已經(jīng)被社區(qū)呼吁很久了。
大多數(shù)C/C++程序員都羨慕過(guò)NodeJS下有npm谣拣、RUST下有cargo募寨,就連一貫采用單一代碼庫(kù)的google也因?yàn)樯鐓^(qū)的需要從golang 1.11版本開(kāi)始引入了go modules
機(jī)制來(lái)支持包管理∩回過(guò)頭來(lái)我們不禁會(huì)問(wèn)拔鹰,為C/C++語(yǔ)言做一款好用的包管理器真的有那么難嗎?
答案是確實(shí)挺難贵涵!這里面涉及到多方面的原因列肢,而且不僅僅是技術(shù)的原因恰画。
首先是C/C++語(yǔ)言自身的問(wèn)題。C/C++程序的構(gòu)建是底層相關(guān)的例书,這導(dǎo)致當(dāng)你創(chuàng)建一個(gè)包的時(shí)候锣尉,必須考慮目標(biāo)的操作系統(tǒng)刻炒、體系架構(gòu)决采、以及構(gòu)建時(shí)使用的編譯器類(lèi)型和版本、構(gòu)建類(lèi)型(Debug/Release)等一系列影響依賴(lài)方能否正常使用的因素坟奥。
此外树瞭,你還需要關(guān)注一些包自身的屬性:是純頭文件庫(kù)、靜態(tài)庫(kù)還是動(dòng)態(tài)庫(kù)日麸,以及包的構(gòu)建參數(shù)(比如優(yōu)化級(jí)別目胡、是否開(kāi)啟exception和rtti的編譯選項(xiàng)...)毒返、還有指定裁剪性(特性宏)等配置。
另外凉敲,由于C/C++的標(biāo)準(zhǔn)庫(kù)(glibc和libstdc++)存在版本兼容性問(wèn)題,以及C++存在ABI兼容性問(wèn)題寺旺,這會(huì)讓包的版本管理超越語(yǔ)義化版本(SemVer)所能解決的問(wèn)題范圍爷抓,這導(dǎo)致包的創(chuàng)建者需要在發(fā)包的時(shí)候?yàn)榘募嫒菪宰龈嗟目紤]。
最后阻塑,由于C/C++語(yǔ)言在語(yǔ)法上缺乏包級(jí)別的模塊化機(jī)制蓝撇,會(huì)讓包的符號(hào)依賴(lài)與沖突解決變得困難。
如果上述還不夠陈莽,那么在這些的基礎(chǔ)上渤昌,再加上交叉編譯的場(chǎng)景,絕對(duì)會(huì)讓一個(gè)通用C/C++包管理器的復(fù)雜度超過(guò)其它任何語(yǔ)言走搁。
上述問(wèn)題独柑,整個(gè)C/C++社區(qū)中的組織和開(kāi)發(fā)者一直都在努力解決。然而不像別的語(yǔ)言(golang屬于google私植,rust屬于社區(qū))群嗤,C/C++是由標(biāo)準(zhǔn)委員會(huì)和各個(gè)編譯器工具背后的商業(yè)組織共同推動(dòng)的(主要是C++,但是C受制于不同的Linux發(fā)行版本和工具鏈)兵琳,所以無(wú)論是從效率還是結(jié)果上都不是那么好狂秘。
所以這個(gè)社區(qū)是分裂的,只用看看有多少種編譯構(gòu)建系統(tǒng)就知道了:gcc躯肌、clang者春、intel、qcc清女、Visual Studio(MSBuild)钱烟、Makefiles、Ninja、Scons拴袭、CMake...读第。同樣,在缺少通用包管理的情況下拥刻,大家對(duì)于代碼復(fù)用的解決方式也發(fā)展出了各種模式怜瞒。
首先是基于源碼的復(fù)用方式。項(xiàng)目只要?jiǎng)澓媚K般哼,定義好各自的模塊目錄以及share的頭文件目錄吴汪,然后就可以分工合作了。
這種方式的問(wèn)題是代碼都在單一代碼庫(kù)中蒸眠,可以直接看到對(duì)方的源碼漾橙。由于互相之間的依賴(lài)是隱式的,導(dǎo)致不容易對(duì)代碼做溯源和裁剪楞卡。當(dāng)然這首先是個(gè)設(shè)計(jì)問(wèn)題霜运,但是這種復(fù)用方式讓工具不容易對(duì)現(xiàn)狀作出有效的可視化和約束管理。
在這種方式下蒋腮,大家很容易商量出一個(gè)公共的common頭文件目錄淘捡,將每個(gè)模塊公開(kāi)的頭文件都放里面(因?yàn)槌杀竞艿停瑹o(wú)論是手動(dòng)還是構(gòu)建過(guò)程中自動(dòng)完成)徽惋。任何一個(gè)模塊依賴(lài)別人似乎都很簡(jiǎn)單案淋,但是最后所有模塊都耦合到了一起。
而且這種方式下险绘,代碼庫(kù)會(huì)膨脹的很快踢京,所有變更最終都會(huì)擁擠到一條效率不高的持續(xù)集成流水線(xiàn)上。由于依賴(lài)的隱式化宦棺,為持續(xù)集成流水線(xiàn)做分層和優(yōu)化需要花費(fèi)比較大的精力瓣距。
后來(lái)圍繞著Git,人們發(fā)展出了一些能夠優(yōu)化“基于源碼復(fù)用”的工具代咸,如git submodule
蹈丸、git subtree
、git repo
等呐芥。這些工具可以把代碼分布到不同的git倉(cāng)庫(kù)和分支中逻杖,能為每個(gè)代碼倉(cāng)搭建自己的CI流水線(xiàn)。但是這些方式?jīng)]有從根本上解決依賴(lài)白盒化的問(wèn)題思瘟。由于在使用這些工具的時(shí)候荸百,大家仍然優(yōu)先傾向?qū)⑺性创a拉到一起后再進(jìn)行構(gòu)建,因此每個(gè)庫(kù)的獨(dú)立構(gòu)建滨攻、測(cè)試和發(fā)布其實(shí)是缺乏原動(dòng)力的够话。
一些構(gòu)建工具的發(fā)展蓝翰,為C/C++的代碼復(fù)用引入了更好的方式。例如CMake從3.0版本開(kāi)始被稱(chēng)之為“Modern CMake”女嘲,是因?yàn)樗肓藅arget的概念畜份,以及基于target建立起了構(gòu)建的依賴(lài)可見(jiàn)性和傳播控制機(jī)制。這些都更好的支持了代碼在構(gòu)建上的模塊化欣尼,號(hào)稱(chēng)“everything is a (self-contained) target”爆雹。另外,借助CMake的ExternalProject和find_package特性媒至,使得我們可以從指定的http或者git分支下載顶别、構(gòu)建谷徙、安裝和引用代碼庫(kù)拒啰。由于CMake的廣泛流行,目前這已經(jīng)成為C/C++開(kāi)源社區(qū)的事實(shí)標(biāo)準(zhǔn)完慧。關(guān)于Modern CMake的用法和最佳實(shí)踐谋旦,可以看看這篇文章:《Modern CMake最佳實(shí)踐》。
另外屈尼,Google的Bazel也具有類(lèi)似的模塊化構(gòu)建和依賴(lài)管理的能力册着,在某些方面它還要更強(qiáng)大一些,并且支持云構(gòu)建和緩存脾歧。但是由于其它一些原因甲捏,并沒(méi)有大規(guī)模流行起來(lái)。我的好朋友劉光聰寫(xiě)過(guò)系列文章對(duì)bazel做過(guò)分析和介紹鞭执,具體可以看看: 《Bazel是把雙刃劍》司顿。
上述構(gòu)建工具提供的代碼復(fù)用能力,使得C/C++從代碼的白盒復(fù)用往黑盒復(fù)用上邁進(jìn)了一大步:代碼的發(fā)布方至少要保證自己代碼庫(kù)的構(gòu)建閉包性兄纺。但是這種復(fù)用方式大溜,對(duì)于間接依賴(lài)的管理仍舊是不足的。我們需要一種能力估脆,可以通過(guò)全鏈條的依賴(lài)解析钦奋,進(jìn)行依賴(lài)溯源、沖突判決疙赠,以及基于變更進(jìn)行最小范圍的重構(gòu)建和發(fā)布管理付材。
所以,包管理器在C/C++社區(qū)很早就有了圃阳。包管理器通過(guò)讓包顯示化的描述自己的元信息:名稱(chēng)厌衔、版本、構(gòu)建方式限佩、以及所有的依賴(lài)包的版本信息葵诈,標(biāo)準(zhǔn)化了包的構(gòu)建裸弦、發(fā)布和復(fù)用方式,以及自動(dòng)化的對(duì)依賴(lài)和變更做管理作喘。
遺憾的是如我們前面所說(shuō)理疙,C/C++的構(gòu)建以及二進(jìn)制兼容性的外部影響因素太多,所以現(xiàn)有被廣泛使用的包管理器往往是局限于某種系統(tǒng)類(lèi)型內(nèi)的泞坦。例如Linux下主流的rpm和deb就分別面向不同的linux發(fā)行版(如Fedora和Ubuntu窖贤,當(dāng)然可以擴(kuò)展)。這種方式簡(jiǎn)化了C/C++的構(gòu)建和二進(jìn)制兼容性的管理(還包括標(biāo)準(zhǔn)庫(kù)的兼容性管理)贰锁,因此讓包管理器的設(shè)計(jì)和使用變得容易赃梧。遺憾的是,這樣的包管理器對(duì)于更廣泛的社區(qū)化開(kāi)發(fā)是不夠的豌熄。
不過(guò)授嘀,社區(qū)一直沒(méi)有停止過(guò)努力的腳步。Biicode是一款探索以源碼發(fā)包的現(xiàn)代化C/C++包管理器锣险,但遺憾的是這個(gè)項(xiàng)目由于經(jīng)營(yíng)原因在2015年關(guān)閉了蹄皱。Biicode在關(guān)閉前開(kāi)源了它所有的源碼,剛好那個(gè)時(shí)候我也和幾個(gè)朋友也一起創(chuàng)建了一個(gè)C/C++包管理的項(xiàng)目CUP
芯肤,遺憾的是由于精力原因這個(gè)項(xiàng)目一直未能完成巷折。
幸運(yùn)的是,后來(lái)我看到了conan崖咨,一款出色的開(kāi)源C/C++包管理器锻拘。它吸收了很多現(xiàn)代化包管理器的設(shè)計(jì)思想,探索解決通用C/C++包管理器的各種挑戰(zhàn)击蹲,而且每個(gè)方面都解決的很不錯(cuò)署拟。
借用Conan文檔中的介紹:“Conan is a dependency and package manager for C and C++ languages. It is free and open-source, and it works in all platforms,also integrates with all build systems...”际邻。
Conan支持交叉編譯芯丧,如果獲取匹配的二進(jìn)制包失敗會(huì)嘗試從源碼進(jìn)行構(gòu)建。除了基本的包管理能力外世曾,conan試圖內(nèi)置以包管理為中心的開(kāi)發(fā)最佳實(shí)踐缨恒,包括內(nèi)置的代碼布局(layout)、構(gòu)建轮听、包測(cè)試骗露、發(fā)布、以及與Git血巍、IDE萧锉、CI和部署工具的集成。這些都讓C/C++開(kāi)發(fā)逐漸有了類(lèi)似于在RUST下使用Cargo的感覺(jué)述寡。我把這些歸為是現(xiàn)代化包管理器應(yīng)有的能力柿隙,當(dāng)然conan還有一些工作要做叶洞,包括語(yǔ)言自身的完善(例如C++20標(biāo)準(zhǔn)引入的module機(jī)制),但目前的使用體驗(yàn)已經(jīng)不錯(cuò)了禀崖。唯獨(dú)可能會(huì)對(duì)使用者造成門(mén)檻的是衩辟,conan的包配置描述需要使用python。
https://ccup.github.io/conan-docs-zh/是我在官網(wǎng)學(xué)習(xí)Conan的過(guò)程中波附,一邊學(xué)習(xí)一邊翻譯記錄的結(jié)果艺晴。最初的目的是通過(guò)翻譯讓自己對(duì)看過(guò)的東西加深印象,雖然還沒(méi)有完全完成掸屡,但還是先稍加整理提供給有需要的同學(xué)吧封寞。
在翻譯記錄的過(guò)程中,我根據(jù)個(gè)人的感覺(jué)對(duì)內(nèi)容做了些取舍仅财。中間有很小的部分加了點(diǎn)個(gè)人的理解狈究,以使得整體更加易懂。因此满着,這個(gè)手冊(cè)不保證更新以及和官網(wǎng)完全一致谦炒,有精力的同學(xué)還是推薦大家盡可能閱讀官方文檔贯莺。
最后风喇,還想討論一個(gè)話(huà)題,那就是在集中管控式的大型C/C++項(xiàng)目中有沒(méi)有必要將類(lèi)似于Conan這樣的包管理能力內(nèi)置于開(kāi)發(fā)過(guò)程中缕探。
和社區(qū)化開(kāi)發(fā)不同魂莫,這些項(xiàng)目可以通過(guò)集中的項(xiàng)目管理手段協(xié)調(diào)內(nèi)部的協(xié)作和復(fù)用,再加上一些我們前面提的源碼和模塊化構(gòu)建的技術(shù)手段爹耗,大多數(shù)時(shí)候確實(shí)可以不需要包管理器耙考。但是我見(jiàn)過(guò)很多大型C/C++項(xiàng)目,代碼動(dòng)輒百萬(wàn)潭兽、千萬(wàn)倦始,涉及很多可復(fù)用的功能單元,由于缺乏包管理器對(duì)依賴(lài)進(jìn)行顯示化管理山卦,最后內(nèi)部依賴(lài)混亂復(fù)雜鞋邑,以至于源碼的追溯性和構(gòu)建的可重復(fù)性都變得困難。這些項(xiàng)目為了解決問(wèn)題账蓉,會(huì)自行制定代碼標(biāo)準(zhǔn)枚碗,開(kāi)發(fā)內(nèi)部工具,但是做的很多工作在我看來(lái)都是使用一款包管理器就可以解決的铸本。
當(dāng)然肮雨,這些項(xiàng)目寧愿自行定義標(biāo)準(zhǔn)和開(kāi)發(fā)工具,而不使用包管理箱玷,是有原因的怨规。首先陌宿,包管理一般具有侵入性,引入包管理勢(shì)必需要改造現(xiàn)有的開(kāi)發(fā)和協(xié)作模式波丰,對(duì)于遺留系統(tǒng)的改造成本可能會(huì)比較大限番。另外,采用包管理還可能會(huì)讓跨模塊的變更變得低效呀舔。
大多數(shù)項(xiàng)目在初期時(shí)候弥虐,變化方向不明確,因此系統(tǒng)內(nèi)部結(jié)構(gòu)是不穩(wěn)定的媚赖。在中期結(jié)構(gòu)穩(wěn)定后霜瘪,可能又缺乏演進(jìn)式設(shè)計(jì)和重構(gòu)能力,對(duì)軟件結(jié)構(gòu)的劃分未必能保證低耦合惧磺。如果當(dāng)大多數(shù)變更都需要跨越多個(gè)包的時(shí)候颖对,采用包管理這種隔離性強(qiáng)的方式,反而會(huì)增大協(xié)作溝通成本磨隘,降低效率缤底。幸運(yùn)的是,conan提供了editable mode package和workspace的特性(RUST的cargo也提供了這個(gè)特性)番捂,來(lái)讓多包協(xié)作的修改變得稍微容易一些个唧。
許多編程語(yǔ)言都把包管理器作為一個(gè)抓手,圍繞著包開(kāi)發(fā)來(lái)打造貫穿整個(gè)開(kāi)發(fā)過(guò)程的最佳實(shí)踐和輔助工具设预。包管理的引入會(huì)將原有的軟件模塊團(tuán)隊(duì)的交付終點(diǎn)徙歼,從僅僅將代碼合入到代碼庫(kù),延長(zhǎng)到了需要保證構(gòu)建鳖枕、測(cè)試魄梯、打包和發(fā)布成功,并且滿(mǎn)足包版本的發(fā)布契約(驗(yàn)收測(cè)試和契約測(cè)試)宾符,從而真正意義上的使能團(tuán)隊(duì)獨(dú)立流水線(xiàn)酿秸,推動(dòng)了團(tuán)隊(duì)的devops能力。
因此我覺(jué)得魏烫,隨著C/C++包管理器的成熟辣苏,以及對(duì)軟件開(kāi)發(fā)過(guò)程支持的更加完善,會(huì)有越來(lái)越多的C/C++新項(xiàng)目逐步開(kāi)始使用包管理器则奥。而那些改造負(fù)擔(dān)大考润,或者已經(jīng)有自己的標(biāo)準(zhǔn)和工具來(lái)替代包管理能力的項(xiàng)目,也不妨多關(guān)注C/C++社區(qū)現(xiàn)代化包管理的現(xiàn)狀和進(jìn)展读处,從中學(xué)習(xí)和借鑒一些經(jīng)驗(yàn)糊治,讓自己的標(biāo)準(zhǔn)和工具做的更好。
最后罚舱,再說(shuō)一句井辜,包管理只是一系列工具以及基于這些工具所構(gòu)建的公共能力绎谦,它早已被證明不是銀彈!包劃分的好不好依然依賴(lài)于軟件設(shè)計(jì)能力粥脚,這在某種程度上和微服務(wù)是一樣的窃肠。你一定聽(tīng)說(shuō)過(guò)很多關(guān)于服務(wù)拆分不好帶來(lái)問(wèn)題的故事吧,幸運(yùn)的是包劃分不好的成本比這低一些刷允,但仍舊是有成本的冤留。
Conan官方文檔中文翻譯版的github庫(kù)地址:https://github.com/ccup/conan-docs-zh/。