--- 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 ``` * 編譯順序 & 關係圖 >  * 使用 `make` 指令就可以生成 SimpleMakefile 文件 (上面有說明輸出的文件名稱) ```shell= # 執行 Makefile (SimpleMakefile) 文件 make # 查看多了哪些文件 tree . # 執行剛剛輸出的 SimpleMakefile 文件 ./SimpleMakefile ``` >  ### 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 輸出結果--** >  ### make 內建規則 * make 有自己的內建規則 1. **自動尋找對應檔**: 當只有指定 `.o` 檔,但沒有指定 `.c` 檔時,它會 **自己取找 `.o` 檔的對應 `.c` 檔案**;範例如下: ```shell= OBJS=main.o utility.o SimpleMakefile: $(OBJS) gcc $(OBJS) -o SimpleMakefile ``` 呼叫 make 開始建構 ```shell= make ``` >  2. **迭代增量**:它會檢測原始檔是否有更改,**如果沒有更改並已經有 `.o` 檔,則不進行編譯**,這是一種典型的 **反應鏈** ```shell= make # 由於第二次編譯原始檔沒有更動,所以也不會更新 .o 檔 make ``` >  ### 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. 查看編譯時會編譯的檔案(查看指令) >  2. 指定編譯腳本檔為 MyFile >  ### 標準巨集、變數 * 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 ``` >  * 常見變數:變數會隨著建構目標的設置不同而改變,通常不用在編譯指令時指定 | 巨集 | 說明 | | -------- | -------- | | `$@` | 寫在規則裡時,表示當前目標 (TARGET) | | `$*` | 取得目前目標的名稱 | 範例: ```shell= OBJS=main.o utility.o testParam: $(OBJS) echo "Target name: $@, base name: $*" ``` >  <!-- ### 常規目標 --> ## 從 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 ``` >  :::danger * **預防惡意程式碼** 預先查看壓縮包其實相當重要,如果預覽中方現絕對路徑的檔案,那非常危險(如果是強制改變你的 `/etc/passwd`, `/etc/inetd.conf`... 等等設定就相當危險!) ::: ### 源碼文件 - sqlite * 接著我們來安裝剛剛解壓的 `sqlite` 源碼包 1. 在進入解壓目錄後我們首先要做的 **觀察、閱讀檔案**;重點檔案如下 | 重點檔案 | 功能 | | - | - | | README | 該檔案是一定要看的檔案,基本上會有包的 **描述、手冊、安裝提示... 等等資訊** | | INSTALL | 內有編譯、安裝軟體的指令 | ```shell= # 閱讀 README 檔案 cat README.md | less ``` 其中一段內容可以看到它教我們如何編譯該應用 >  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` 前 >  產出 `configure` 後,可以看到 `Makefile` 的產生,並有許多 Make 任務 ```shell= ./configure ``` >  :::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 ``` >  確認 myDir 是否已經設定進 Makefile >  2. **安裝 sqlite 包** ```shell= # 先確認會安裝哪些東西 make -n install make install ls -laF myDir/ tree myDir/ ``` 可以看到可執行(二進位檔)檔安裝在指定目錄下的 `/bin` 目錄之下(`sqlite3`) >  ### 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 ``` >  :::success * **找不到 `.pc` 檔**? 1. 可以創建 `.pc` 連接檔案到目標 `/lib/pkgconfig` 目錄中 2. 指定環境變數 `PKG_CONFIG_PATH` ::: ## Appendix & FAQ :::info ::: ###### tags: `Linux 基礎` `C`
×
Sign in
Email
Password
Forgot password
or
Sign in via Google
Sign in via Facebook
Sign in via X(Twitter)
Sign in via GitHub
Sign in via Dropbox
Sign in with Wallet
Wallet (
)
Connect another wallet
Continue with a different method
New to HackMD?
Sign up
By signing in, you agree to our
terms of service
.