用 `make` 編譯 C 程式(給非 C 程式設計師)
原文由 Julia Evans 于 發布,訂閱此部落格
我從來不是個 C 程式設計師,但每隔一陣子就得從原始碼編譯 C/C++ 程式。這對我來說一直有點辛苦:很長一段時間,我的做法基本上就是「先裝好相依套件,然後跑 make,如果跑不起來,就去找別人編好的執行檔,不然就放棄」。
「指望別人已經編好了」這招在用 Linux 的時候還算管用,但過去幾年改用 Mac 之後,就越來越常遇到得自己動手編譯的情況。
來聊聊編譯 C 程式時可能會需要做的事吧!我會用幾個自己實際編過的 C 程式當例子,談談可能會出錯的地方。我們會提到的三個程式是:
步驟一:安裝 C 編譯器
這一步很簡單:在 Ubuntu 系統上,如果還沒裝 C 編譯器,我會這樣安裝:
sudo apt-get install build-essential
這會裝好 gcc、g++ 和 make。在 Mac 上的情況就比較混亂一點,大概就是「安裝 Xcode command line tools」之類的。
步驟二:安裝程式的相依套件
跟一些比較新的程式語言不同,C 沒有相依套件管理器。所以如果程式有相依套件,就得自己去找出來。還好也因為這樣,C 程式設計師通常會把相依套件壓到最少,而且這些套件往往在你用的套件管理器裡就找得到。
README 裡幾乎都會有一個段落說明要怎麼取得相依套件,舉例來說,paperjam 的 README 就寫道:
要編譯 PaperJam,你需要 libqpdf 和 libpaper 這兩個函式庫的標頭檔(通常是 libqpdf-dev 和 libpaper-dev 套件)。
你可能會需要
a2x(可在 AsciiDoc 中找到)來建置說明手冊頁面。
所以在 Debian 系的系統上,你可以這樣安裝相依套件。
sudo apt install -y libqpdf-dev libpaper-dev
如果 README 給的套件名稱像是 libqpdf-dev,我基本上都會假設它指的是「在 Debian 系的 Linux 發行版上」:如果你用的是 Mac,brew install libqpdf-dev 是行不通的。我在 Mac 上開發還沒有完全上手,所以這方面還沒太多心得可以分享。我猜在這個例子裡,如果是用 Homebrew 的話,應該是 brew install qpdf。
步驟三:執行 ./configure(如果需要的話)
有些 C 程式會附 Makefile,有些則是附一個叫做 ./configure 的腳本。舉例來說,如果你下載 sqlite 的原始碼,裡面就不是 Makefile,而是一個 ./configure 腳本。
我對這個 ./configure 腳本的理解是:
- 你執行它,它會印出一大堆有點讓人摸不著頭緒的輸出,然後不是產生出一個
Makefile,就是因為少了某個相依套件而失敗 - 這個
./configure腳本是一套叫做 autotools 的系統的一部分,我從來不需要學它,除了「執行它來產生Makefile」這件事以外。
我想應該可以傳一些選項讓 ./configure 腳本產生出不同的 Makefile,不過我從來沒這樣做過。
步驟四:執行 make
下一步就是執行 make 來嘗試建置程式。關於 make 有幾點要說明:
- 有時候可以用
make -j8來平行化建置,讓編譯快一點 - 編譯時它通常會噴出一大堆編譯器警告。我一向直接無視,反正又不是我寫的程式!編譯器警告才不關我的事。
編譯器錯誤往往是相依套件的問題
這是我在 Mac 上編譯 paperjam 時遇到的錯誤:
/opt/homebrew/Cellar/qpdf/12.0.0/include/qpdf/InputSource.hh:85:19: error: function definition does not declare parameters
85 | qpdf_offset_t last_offset{0};
| ^
這些年來我學到,像這樣的問題通常最好別想得太複雜:如果錯誤訊息裡提到 qpdf,很有可能只是代表我在引入 qpdf 相依套件的方式上弄錯了什麼。
接下來聊聊要怎麼用正確的方式把 qpdf 相依套件引進來。
關於編譯器與連結器的世界最短入門
在談怎麼修相依套件問題之前:建置 C 程式會分成兩個步驟:
- 編譯程式碼成目的檔(用
gcc或clang) - 用
ld把那些目的檔連結成最終的執行檔
建置 C 程式時知道這點很重要,因為有時候你得把正確的旗標傳給編譯器和連結器,告訴它們要去哪裡找你要編譯的程式所需的相依套件。
make 用環境變數來設定編譯器與連結器
如果我在 Mac 上執行 make 來安裝 paperjam,會得到這個錯誤:
c++ -o paperjam paperjam.o pdf-tools.o parse.o cmds.o pdf.o -lqpdf -lpaper
ld: library 'qpdf' not found
這不是因為我的系統沒安裝 qpdf(其實有裝!)。而是編譯器和連結器不知道要怎麼找到 qpdf 函式庫。要修正這個問題,我們需要:
- 傳
"-I/opt/homebrew/include"給編譯器(告訴它標頭檔在哪裡) - 傳
"-L/opt/homebrew/lib -liconv"給連結器(告訴它函式庫檔案在哪裡,並連結iconv)
而我們可以用環境變數讓 make 把這些額外參數傳給編譯器和連結器!要了解運作方式:看看 paperjam 的 Makefile 裡,就能看到一堆環境變數,像是這裡的 LDLIBS:
paperjam: $(OBJS)
$(LD) -o $@ $^ $(LDLIBS)
你放到 LDLIBS 環境變數裡的任何東西,都會以命令列引數的形式傳給連結器(ld)。
秘密環境變數:CPPFLAGS
Makefiles 有時會定義自己的環境變數來傳給編譯器/連結器,但 make 也有一堆「隱含的」環境變數,會自動傳給 C 編譯器和連結器。這裡有一份隱含環境變數的完整清單,其中之一就是 CPPFLAGS,它會自動傳給 C 編譯器。
(嚴格來說,這種情況用 CXXFLAGS 會比較正常,但這個 Makefile 把 CXXFLAGS 寫死了,所以設定 CPPFLAGS 是我找到唯一不用修改 Makefile 就能設定編譯器旗標的方法)
兩種把環境變數傳給 make 的方法
多虧了 @zwol 我才知道,其實有兩種把環境變數傳給 make 的方式:
CXXFLAGS=xyz make(常見的做法)make CXXFLAGS=xyz
兩者的差別在於,make CXXFLAGS=xyz 會覆蓋 Makefile 裡設定的 CXXFLAGS 值,但 CXXFLAGS=xyz make 不會。
我不確定哪一種才是常規,不過在這篇文章裡我會用第一種方式。
如何用 CPPFLAGS 和 LDLIBS 來修正這個編譯錯誤
既然已經談過 CPPFLAGS 和 LDLIBS 是怎麼傳給編譯器和連結器的,這裡就是我最後用來成功建置程式的咒語!
CPPFLAGS="-I/opt/homebrew/include" LDLIBS="-L/opt/homebrew/lib -liconv" make paperjam
這會把 -I/opt/homebrew/include 傳給編譯器,並把 -L/opt/homebrew/lib -liconv 傳給連結器。
另外我也不想假裝自己「神奇地」就知道該傳這些正確的引數,搞清楚它們其實經歷了一堆讓人一頭霧水的 Google 搜尋,只是我在這篇文章裡略過了。我想說明的是:
-I這個編譯器旗標是告訴編譯器要去哪個目錄找標頭檔,像是/opt/homebrew/include/qpdf/QPDF.hh-L這個連結器旗標是告訴連結器要去哪個目錄找函式庫,像是/opt/homebrew/lib/libqpdf.a-l這個連結器旗標是告訴連結器要連結哪些函式庫,像是-liconv就是指「連結iconv函式庫」,而-lm就是指「連結math」
小技巧:只建置某一個特定檔案:make $FILENAME
昨天我發現了一個很酷的工具叫做 qf,可以用來從 ripgrep 的輸出快速開啟檔案。
qf 位在一個放滿各種工具的大目錄裡,但我只想編譯 qf。所以我就只編譯 qf,像這樣:
make qf
基本上,如果你知道(或猜得到)想建置的檔案的輸出檔名,就可以透過執行 make $FILENAME 來叫 make 只建置那個檔案
小技巧:你不需要 Makefile
我有時會寫一些只有 5 行、沒有任何相依套件的 C 程式,而我最近才知道,如果有個叫 blah.c 的檔案,就算不建立 Makefile,也可以這樣直接編譯:
make blah
它會自動展開成 cc -o blah blah.c,可以省下一點打字。我不確定自己會不會記得這招(搞不好還是會繼續打 gcc -o blah blah.c),不過感覺是個有趣的小技巧。
小技巧:看看其他打包系統是怎麼建置同一個 C 程式的
如果你在建置某個 C 程式時遇到困難,也許別人也曾為它傷過腦筋!每個 Linux 發行版都會為它們建置的每個套件準備建置檔,所以就算你沒辦法直接安裝那個發行版的套件,也許還是能從那個發行版的建置方式中得到一些提示。意識到這點(感謝我的朋友 Dave)對我來說真是個恍然大悟的時刻。
舉例來說,這一行來自 paperjam 的 nix 套件寫道:
env.NIX_LDFLAGS = lib.optionalString stdenv.hostPlatform.isDarwin "-liconv";
這基本上就是在說「在 Mac 上建置時要傳連結器旗標 -liconv」,所以這就是我們可以用來建置它的線索。
同一個檔案還寫著 env.NIX_CFLAGS_COMPILE = "-DPOINTERHOLDER_TRANSITION=1";。我不太確定這是什麼意思,不過當我嘗試建置 paperjam 套件時,確實會得到一個關於某個叫 PointerHolder 的錯誤,所以我想這應該跟「PointerHolder transition」有關。
步驟五:安裝執行檔
一旦成功編譯出程式,你大概會想把它安裝到某個地方!有些 Makefile 有 install 目標,讓你可以用 make install 把工具安裝到系統上。我對這個總是有點害怕(它到底會把檔案放到哪裡?以後如果想移除要怎麼辦?),所以如果編譯的是比較簡單的程式,我通常會直接手動複製執行檔來安裝,像這樣:
cp qf ~/bin
步驟六:也許可以自己做一個套件!
搞懂這一切之後,我發現自己可以用新學到的 make 知識,來為 Homebrew 貢獻一個 paperjam 套件!這樣以後在別的系統上就可以直接 brew install paperjam 了。
好處是,就算各種不同打包系統的細節有所不同,它們在本質上都是在使用 C 編譯器和連結器。
即使你不是 C 程式設計師,懂一點 C 還是很有用的
我覺得這一切是個很有趣的例子,說明了就算你一輩子都不打算寫什麼像樣的 C 程式,了解一點 C 程式是怎麼運作的(像是「它們有標頭檔」這種基本概念)還是很有用。
能夠自己編譯 C/C++ 程式的感覺真不錯,雖然我對所有的編譯器和連結器旗標還是沒有完全掌握,而且我還是打算除了「執行 ./configure 來產生 Makefile」之外,什麼都不去學 autotools 是怎麼運作的。
這篇文章有兩件事我沒提到:
LD_LIBRARY_PATH / DYLD_LIBRARY_PATH(用來在執行時告訴動態連結器要去哪裡找動態連結的檔案),因為我已經想不起來上次遇到LD_LIBRARY_PATH問題是什麼時候了,也找不到例子。pkg-config,我覺得它很重要,但我還沒搞懂
隨機一篇部落格
留言
登入後參與討論