---
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`