--- title: 'make 編譯管理、編譯軟體' disqus: kyleAlien --- make 編譯管理、編譯軟體 === ## Overview of Content [TOC] ## make 概述 大部分源碼中都不會是單一檔案,所以如果有手動編譯所有檔案、進行連結,那會是一件頗滿煩的事情;而 Unix 中出現了一個 **編譯管理工具 make**,透過它可以幫我們組織要編譯的檔案,並進行有條理的編譯工作 > 通常你在源碼中看到檔名為 `makefile`、`Makefile` 的檔案時,就說明該應用使用 make 進行編譯管理 ### Makefile 概述 * Makefile 是 make 指令的腳本,make 指令會讀取該腳本做相對應的處理 * Makefile 它能更有效的組織編譯工作(**但它並 ++不是編譯++**),它算是一種腳本(類似於 `Window's bat` & `Linux's Shell` 或是 `Python`) **所以 Makefile 重點是在學習它的規則** 1. Makefile 由 Make 工具進行解析,所以必須安裝 `make` 工具 (必要) ```shell= sudo apt install make ``` 2. 安裝 `gcc`、`tree` (下面範例會用到) ```shell= sudo apt install gcc sudo apt install tree ``` :::danger * Window's ubuntu 安裝 gcc 有問題 ? 可以試試看以下方法 ```shell= # 升級軟體 sudo apt-get update # 修正錯誤 sudo apt-get -f install # 設定 Debian configure sudo dpkg --configure -a # 重新安裝 sudo apt install gcc ``` ::: ### Makefile - 腳本格式 * Makefile 腳本格式如下 ```shell= TARGET: Prerequisites Commands ``` 1. `TARGET`(目標): 目標,可以是一個實際檔案,也可以是另一個規則的目標 2. `Prerequisites`(規則): 目標所依賴的所有文件或規則 (可多個) 3. `Commands`(行為、命令): 當 `Prerequisites` 有任何一個比 `TARGET` 新時都會觸發到 Command 命令的執行,具體命令是依照使用者需求所設置 > eg. 調用 `shell`, `gcc`, `java`... 等等 :::warning * **Commands 前必須要有一個 Tab 空格!** ::: ### Makefile 入門 - 簡單使用 * 需要創建 4 個檔案:^1^ `main.c`、^2^ `utility.c`、^3^ `utility.h`、^4^ `Makefile` 文件 | 文件名稱 | 功能 | | -------- | -------- | | `main.c` | 主要檔案,會依賴 utility 標頭檔案 | | `utility.c` | **實現** getNumber() 函數 | | `utility.h` | **宣告** getNumber() 函數 | | `Makefile` | 編譯過程的主導者(組織編譯) | 1. `main.c` 主要在呼叫 getNumber 函數 ```c= #include <stdio.h> // 系統 Lib #include "utility.h" // 自定義 Lib int main(int arg, char* argv[]) { printf("Hello, getNumber=%d\n", getNumber()); } ``` 2. `utility.h` 宣告函數,好讓其他檔案使用 ```c= #ifndef NUMBER #define NUMBER int getNumber(void); #endif ``` 3. `utility.c` 簡單實現函數 ```c= #include "utility.h" int getNumber() { return 2; } ``` 4. `Makefile` 編譯規劃者 ```shell= # 從上到下分析 # 變數:代表了要生成的物件 OBJS=main.o utility.o # 要製作出SimpleMakefile 檔案,就必須先要有 main.o、utility.o 兩個檔案 # 繼續往下尋找 main.o、utility.o 關鍵字 SMmgimpleMakefile: $(OBJS) # 指定 輸出名稱,並配合其他 .o 檔案 gcc $(OBJS) -o SimpleMakefile # 找到 main.o 的製作 main.o: main.c # 編譯 main.c,這個步驟會輸出 main.o gcc -c main.c # 找到 utility.o 的製作 utility.o: utility.c # 編譯 utility.c,這個步驟會輸出 utility.o gcc -c utility.c ``` * 編譯順序 & 關係圖 > ![](https://i.imgur.com/cN4CqJm.png) * 使用 `make` 指令就可以生成 SimpleMakefile 文件 (上面有說明輸出的文件名稱) ```shell= # 執行 Makefile (SimpleMakefile) 文件 make # 查看多了哪些文件 tree . # 執行剛剛輸出的 SimpleMakefile 文件 ./SimpleMakefile ``` > ![](https://i.imgur.com/wi14Pja.png) ### Makefile 引用參數 * 可以在外部宣告參數,在使用 `${}` or `$()` 引用該參數進來 使用 vim 修改 MakeFile 檔案 ```shell= FILE_NAME = SimpleMakefile_2 SimpleMakefile: main.o utility.o gcc -o $(FILE_NAME) main.o utility.o main.o:main.c gcc -c main.c utility.o:utility.c gcc -c utility.c ``` **--make 輸出結果--** > ![](https://i.imgur.com/C1pPraj.png) ### make 內建規則 * make 有自己的內建規則 1. **自動尋找對應檔**: 當只有指定 `.o` 檔,但沒有指定 `.c` 檔時,它會 **自己取找 `.o` 檔的對應 `.c` 檔案**;範例如下: ```shell= OBJS=main.o utility.o SimpleMakefile: $(OBJS) gcc $(OBJS) -o SimpleMakefile ``` 呼叫 make 開始建構 ```shell= make ``` > ![](https://hackmd.io/_uploads/ryMl-rvnh.png) 2. **迭代增量**:它會檢測原始檔是否有更改,**如果沒有更改並已經有 `.o` 檔,則不進行編譯**,這是一種典型的 **反應鏈** ```shell= make # 由於第二次編譯原始檔沒有更動,所以也不會更新 .o 檔 make ``` > ![](https://hackmd.io/_uploads/BkvWMBv2n.png) ### Makefile 高級用法 * Android 的 Makefile 有很多種高級用法,以下舉兩種較常見的用法 1. **`Target-Specific Variable`** (區域型變數): 這是一種局部作用域的變量,它只針對特定目標起效果,以下使用 `build/core/package internal.mk` [**internal.mk**](https://android.googlesource.com/platform/build/+/master/core/package_internal.mk#78) 的部分代碼有 `PRIVATE_DEX_FILE` 關鍵字 ```shell= ## 這裡的 built_dex 就是 `全局變量` ## 若執行 my_dex_jar(Command) 時就會使用到 built_dex,這時若全局變量 build_dex 受到其他的影響導致 ## 其改變時就會影響到 my_dex_jar 的結果 # # 所以需要一個 `區域變數` 也就是 PRIVATE_DEX_FILE ifdef LOCAL_DEX_PREOPT $(my_dex_jar): PRIVATE_DEX_FILE := $(built_dex) # PRIVATE_DEX_FILE 只存在 my_dex_jar 中 $(my_dex_jar): $(built_dex) $(SOONG_ZIP) $(hide) mkdir -p $(dir $@) && rm -f $@ $(call create-dex-jar,$@,$(PRIVATE_DEX_FILE)) endif ``` 2. **`Static Pattern Rules`** 它的經典格式中帶有兩個冒號 `:`,它可以用來過濾 & 產生真正需要依賴的對象 ```shell= ## 指令格式 TARGETS ...: TARGET-PATTERN: DEP-PATTERNS COMMANDS ``` 靜態模式語法,常常被用在多目標的場景 ```shell= # 抓取其中的 *.o 檔案,並輸出為 *.c 檔案 foo.c tcc.o bar.o : %.o : %.c ``` 上面的例子 `TARGET-PATTERN` 帶有 `%` 符號 1. 它會與 `TARGETS` 進行匹配得到 (STEM),也就是從 [foo.c tcc.o bar.o] 中匹配 .o 結尾,結果就是 [tcc.o bar.o] 2. 再與 `DEP-PATTERNS` 的 `%` 符號結合,**最終生成 tcc.c、bar.c 檔案**,原本的 foo.c 就不會進行任何動作 ## 其他 ### make 命令行 以下有有幾個常用的 make 命令 | make option | 功能 | | - | - | | `-n` | 不進行整合編譯,只會顯示建構時會使用到哪些指令(可以安全的檢查) | | `-f` | 改變 make 預設的腳本名 | 1. 查看編譯時會編譯的檔案(查看指令) > ![](https://hackmd.io/_uploads/Bk8SSSv23.png) 2. 指定編譯腳本檔為 MyFile > ![](https://hackmd.io/_uploads/HJqAHrv33.png) ### 標準巨集、變數 * make 指令有許多巨集、變數 :::info * 巨集:可以當作開始執行 make 後不可再改動的數(像是常數的概念) ::: * 常見巨集: | 巨集 | 說明 | 補充 | | -------- | -------- | -------- | | `CFLAGS` | 給 C 編譯器的選項 | make 會將該選項作為參數,用在所有 `.c` -> `.o` 的階段 | | `LDFLAGS` | 給 C 連結時的選項 | - | | `LDLIBS` | 連結時,指定函式庫名 | 配合 `LDFLAGS` 一起使用 | | `CC` | C 編譯器 | 可以指定 `gcc` or `clang`... 等等 | | `CPPFLAGS` | 給 C 預編譯階段的選項 | - | | `CXXFLAGS` | 給 C++ 編譯器的選項 | - | 範例:修改編譯器為 clang ```shell= OBJS=main.o utility.o SimpleMakefile: $(OBJS) # 編譯器改作 CC $(CC) $(OBJS) -o SimpleMakefile ``` > ![](https://hackmd.io/_uploads/SkvHFSvn3.png) * 常見變數:變數會隨著建構目標的設置不同而改變,通常不用在編譯指令時指定 | 巨集 | 說明 | | -------- | -------- | | `$@` | 寫在規則裡時,表示當前目標 (TARGET) | | `$*` | 取得目前目標的名稱 | 範例: ```shell= OBJS=main.o utility.o testParam: $(OBJS) echo "Target name: $@, base name: $*" ``` > ![](https://hackmd.io/_uploads/rJPosBDhn.png) <!-- ### 常規目標 --> ## 從 C 源碼編譯出可執行應用 我們在學習時往往最常忽略的就是 **建構專案的行為**,因為大部分 IDE 都協助我們快速、方便的建構出應用;但 **建構專案** 其實是學習程式設計、軟體開發的必要基礎 > IDE 隱藏了細節,讓我們快但可能也讓我們笨了(不知所以然) ### 安裝軟體 - 概述 在 Linux 系統中,幾乎所有東西都有他的 Source code(源碼),從核心、函式庫、網頁應用... 等等都有 我們大多可以透過 PackageManager 來幫我們安裝,但其實我們也可以自己下載並手動安裝;手動安裝有以下幾點特點 * **優點** 1. 自定義設定 2. 更清楚地知道你的應用細節(該應用需要哪些依賴、設置) 3. 可以安裝 **特定版本** 的應用 4. 可以訂製特別的設定選項,產出特定環境的軟體並分享(像是可以讓 Docker 環境使用) :::warning 但這並不代表建議都手動安裝! ::: * **缺點**:其實透過優點就可以反推缺點 1. 了解細節要付出時間成本的代價(訂製越多代價越大) > 關注細節常常會在一些小設定上出錯,花時間除錯... 2. 自行安裝軟體通常也不會自己更新 > 通常發行版都會跟隨開發者發佈新的軟體而更新 ### 下載源碼包 - sqlite * 我們嘗試自己下載 [**sqlite**](https://github.com/sqlite/sqlite) 源碼包; 1. 下載 sqlite 源碼 ```shell= wget https://github.com/sqlite/sqlite/archive/refs/heads/master.zip ``` 2. 解壓 zip 包 ```shell= # 先查看 zip 內容 # 如果沒有單獨資料夾,建議創建一個資料夾再解壓 unzip -l ./master.zip | head unzip -q ./master.zip cd sqlite-master ``` > ![](https://hackmd.io/_uploads/HJrJT9hh3.png) :::danger * **預防惡意程式碼** 預先查看壓縮包其實相當重要,如果預覽中方現絕對路徑的檔案,那非常危險(如果是強制改變你的 `/etc/passwd`, `/etc/inetd.conf`... 等等設定就相當危險!) ::: ### 源碼文件 - sqlite * 接著我們來安裝剛剛解壓的 `sqlite` 源碼包 1. 在進入解壓目錄後我們首先要做的 **觀察、閱讀檔案**;重點檔案如下 | 重點檔案 | 功能 | | - | - | | README | 該檔案是一定要看的檔案,基本上會有包的 **描述、手冊、安裝提示... 等等資訊** | | INSTALL | 內有編譯、安裝軟體的指令 | ```shell= # 閱讀 README 檔案 cat README.md | less ``` 其中一段內容可以看到它教我們如何編譯該應用 > ![](https://hackmd.io/_uploads/Hy6Bkoh23.png) 2. **有關 make 的檔案** | 檔案 | 說明 | | - | - | | Makefile | 如上面小節所說,用來集成源碼編譯的腳本 | | configure | `GNU autoconf` 讀取的設定檔 | | Makefile.in | 由 `GNU autoconf` 工具產出的腳本 | | CMakeList.txt | `CMake` 讀取的設定檔,最終產出 `Makefile` | 舊的軟體會自己編寫 `Makefile`,並有時候會需要我們自己手動去修改 `Makefile`,但現在大多數是透過某些工具自動產出 `Makefile` * **GNU autoconf**:透過 `configure` 設定檔來產出對應的 `Makefile.in` 檔案 * **CMake**:透過 `CMakeList.txt` 設定檔產出對應的 `Makefile` 檔案 3. 源碼相關檔案 | 檔案 | 說明 | | - | - | | `.c`、`.h`、`.cc` | 這些檔案大多(非一定)是 C 原始碼檔案 | | `.o` | `.o` 代表的是物件檔(二進制檔案),比較少出現,如果有的話通常代表該包的依賴庫 | ### 產出 Makefile 工具 - GNU autoconf * 在編譯源碼之前,我們先了解一下編譯腳本工具的發展 * 雖然 C 作為高級語言(擁有可移植不同平台的特性),但對於不同平台通常需要不同的參數、選項設置,以往對於不同平台會有不同平台的 `Makefile` 檔案 由於這種不方便性,導致衍生出了 **腳本分析系統**:它會針對不同平台自動產出對應的 `Makefile` 程式,而 `GNU autoconf` 就是其中一個工具 如果使用此系統的包含以下檔案,通常就是可使用 `GNU autoconf` | 檔案 | 說明 | | - | - | | configure | `GNU autoconf` 讀取的設定檔 | | Makefile.in | `Makefile` 範本 | | config.h.in | `config.h` 範本 | * 通常只要執行以下指令就可以從產出 `Makefile.in` 產出對應的 `Makefile` 產出 `configure` 前 > ![](https://hackmd.io/_uploads/SycHPo323.png) 產出 `configure` 後,可以看到 `Makefile` 的產生,並有許多 Make 任務 ```shell= ./configure ``` > ![](https://hackmd.io/_uploads/B1Rsvj233.png) :::success * 如果沒有這些功能請使用以下指令安裝必要的編譯工具 ```shell= sudo apt install build-essential ``` ::: * **auoconfig 重點資訊** 1. **`configure` 選項**:關於 `configure` 的一些設定,我們應該要知道一些常用的選項,選項、功能如下 | 選項 | 功能 | 補充 | | -------- | -------- | - | | `--prefix=<位置>` | 指定 Makefile 內對於應用設定的安裝位置 | 預設應用會裝到 `/usr/bin`、二進位檔案會安裝到 `/usr/local/bin`、函數庫會安裝到 `/usr/local/lib` 目錄 | | `--bindir=<dir>` | 指定二進位檔案放置的位置 | - | | `--sbindir=<dir>` | 指定系統二進位檔案放置的位置 | - | | `--libdir=<dir>` | 指定函式庫放置的位置 | - | | `--with-package=<dir>` | 指定編譯時需要依賴包的位置 | 通常是某個函式庫不在標準位置時使用 | | `--disable-shared` | 不產生共享函式庫(`.so` 檔) | | 2. **autoconf 自動產生的 Makefile 的 Make 任務**: | 選項 | 功能 | | -------- | -------- | | `clean` | 清除所有物件檔案(`.o` files)、可執行程式、函式庫 | | `distclean` | 清除自動產生的所有東西(`Makefile`、`config.h`、`config.log`... 等等),恢復至原始狀態 | | `check` | 執行內建的測試,**檢查編譯其產生的程式的可行性** | | `install-strip` | 安裝時移除函式庫中多餘的除錯資訊、多餘符號,這可以讓程式佔有較少空間 | 3. **autoconf 日誌檔案**: 如果 `configure` 過程出錯可以去查看 `config.log` 檔案 > 由於訊息量很大,建議從底部開始尋找 (搜尋看看 `for more details` 字串看看) ### 手動安裝 (指定位置) - sqlite * 根據上述的 `configure` 選項,我們來重新產出 `Makefile` 檔案,使用指令如下 1. 創建一個目標目錄(`myDir`),等等安裝時就安裝在該目錄下 ```shell= mkdir myDir # 安裝到指定目錄 ./configure --prefix=`pwd`/myDir ``` > ![](https://hackmd.io/_uploads/rkGunjh22.png) 確認 myDir 是否已經設定進 Makefile > ![](https://hackmd.io/_uploads/Hk2hhsn3n.png) 2. **安裝 sqlite 包** ```shell= # 先確認會安裝哪些東西 make -n install make install ls -laF myDir/ tree myDir/ ``` 可以看到可執行(二進位檔)檔安裝在指定目錄下的 `/bin` 目錄之下(`sqlite3`) > ![](https://hackmd.io/_uploads/rykUy2332.png) ### library 依賴 - pc 檔 * 一個應用基本上都會依賴於另 N 個可執行程式(不太會重造輪胎,除非有這個必要性);就像 Git 源碼的依賴包:`Curl`、`Zlib`、`Openssl`、`Expat`、`Libiconv`... ``` mermaid graph TD; Git應用-->Curl; Git應用-->Zlib; Git應用-->Openssl; Git應用-->Expat; Git應用-->Libiconv; ``` * 我們可以透過 `pkg-config` 指令查看目標應用都依賴些什麼庫 ```shell= pkg-config --libs ./myDir/bin/sqlite3 ``` 而 `pkg-config` 的運作方式,其實是去讀取應用對應的 `<應用>.pc` 檔 ```shell= cat ./myDir/lib/pkgconfig/sqlite3.pc ``` > ![](https://hackmd.io/_uploads/ry_qOh2n2.png) :::success * **找不到 `.pc` 檔**? 1. 可以創建 `.pc` 連接檔案到目標 `/lib/pkgconfig` 目錄中 2. 指定環境變數 `PKG_CONFIG_PATH` ::: ## Appendix & FAQ :::info ::: ###### tags: `Linux 基礎` `C`