1. 程式人生 > >linux之makefile的學習(一)

linux之makefile的學習(一)

為什麼要學習makefile

在linux下開發專案,如果想要完成一個大型專案的開發,可能在windows環境下,有許多編譯器就已經替代了makefile功能,但在linux下想要合理管理程式碼,學會編寫makefile就非常重要了。

makefile 關係到了整個工程的編譯規則。一個工程中的原始檔不計數,其按型別、功能、模組分別放在若干個目錄中, makefile 定義了一系列的規則來指定,哪些檔案需要先編譯,哪些檔案需要後編譯,哪些檔案需要重新編譯,甚至於進行更復雜的功能操作,因為makefile 就像一個 Shell 指令碼一樣,其中也可以執行作業系統的命令。

編寫makefile檔案本質上是幫組make如何一鍵編譯,進行批處理,makefile檔案包含的規則命令使我們不需要繁瑣的操作,提高了開發效率。

其實,在makefile檔案的編寫,我們參照的是linux核心管理龐大程式碼的風格編寫makefile。

makefile在核心中主要分為三大類:

  1. 總控makefile
  2. 功能目錄makefile
  3. scripts下的makefile

    如此管理程式碼,自然有非常多的好處。一是提高了程式碼的維護性,二是間接的提高了程式碼的可讀性。

    下面介紹一下makefile體系的工作原理
    在scripts目錄下的makefile就類似c語言中的標頭檔案,裡面主要定義了變數。在總控makefile下一般有如下一行程式碼:

include scripts/Makefile

當我們啟用make命令時,首先尋找總控makefile並執行makefile檔案,在執行上述程式碼時,將定義的變數在總控makefile中展開,然後在進入各個功能目錄下makefile執行,最總生成我們最終需要的目標。
下面我以一個簡單的例項想大家展示makefile的編寫。程式碼在全文最後貼出。主要實現一個加減乘除的計算器功能。

首先先建立以下幾個功能目錄

mkdir -p main/src sub/src add/src mul/src div/src//用這條命令建立各個功能目錄

mkdir scripts//建立這個目錄存放變數的Makefile

touch Makefile //建立總控makefile

touch main/src/Makefile
touch ....
//分別在各個功能目錄src下建立Makefile目錄。

見圖

tree//將makefile以如下顯示出來

這裡寫圖片描述

這樣就可以清楚的管理程式碼,間接提高開原始檔程式碼的閱讀性。

如何填寫makefile的內容

下面講一下makefile的編寫,其實就是makefile的規則。
target … : prerequisites …
command


target 也就是一個目標檔案,可以是 Object File,也可以是執行檔案。還可以是一個標籤( Label)。
prerequisites 就是,要生成那個 target 所需要的檔案或是目標。command 也就是 make 需要執行的命令。(任意的 Shell 命令)
這是一個檔案的依賴關係,也就是說, target 這一個或多個的目標檔案依賴於 prerequisites中的檔案,其生成規則定義在 command 中。說白一點就是說, prerequisites 中如果有一個以上的檔案比 target 檔案要新的話, command 所定義的命令就會被執行。這就是 Makefile的規則。也就是 Makefile 中最核心的內容。

目標、依賴、命令就是makefile規則編寫的三要素。

下面舉一個簡單的例子(摘自網路)

如果一個工程有 3 個頭檔案,和 8 個 C 檔案,我們為了完成前面所述的那三個規則,我們的 Makefile 應該是下面的這個樣子的。

edit : main.o kbd.o command.o display.o /
insert.o search.o files.o utils.o
cc -o edit main.o kbd.o command.o display.o /
insert.o search.o files.o utils.o4
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
clean :
rm edit main.o kbd.o command.o display.o /
insert.o search.o files.o utils.o

反斜槓( /)是換行符的意思。這樣比較便於 Makefile 的易讀。我們可以把這個內容儲存在檔案為“ Makefile”或“ makefile”的檔案中,然後在該目錄下直接輸入命令“ make”就可以生成執行檔案 edit。如果要刪除執行檔案和所有的中間目標檔案,那麼,只要簡單地執行
一下“ make clean”就可以了。
在這個 makefile 中,目標檔案( target)包含:執行檔案 edit 和中間目標檔案( *.o),依賴檔案( prerequisites)就是冒號後面的那些 .c 檔案和 .h 檔案。每一個 .o 檔案都有一組依賴檔案,而這些 .o 檔案又是執行檔案 edit 的依賴檔案。依賴關係的實質上就是說明了目標檔案是由哪些檔案生成的,換言之,目標檔案是哪些檔案更新的。

在定義好依賴關係後,後續的那一行定義瞭如何生成目標檔案的作業系統命令,一定要以一個 Tab 鍵作為開頭。

記住, make 並不管命令是怎麼工作的,他只管執行所定義的命令。 make會比較 targets 檔案和 prerequisites 檔案的修改日期,如果 prerequisites 檔案的日期要比targets 檔案的日期要新,或者 target 不存在的話,那麼, make 就會執行後續定義的命令。這裡要說明一點的是, clean 不是一個檔案,它只不過是一個動作名字,有點像 C 語言中的lable 一樣,其冒號後什麼也沒有,那麼, make 就不會自動去找檔案的依賴性,也就不會自動執行其後所定義的命令。要執行其後的命令,就要在 make 命令後明顯得指出這個 lable的名字。這樣的方法非常有用,我們可以在一個 makefile 中定義不用的編譯或是和編譯無關的命令,比如程式的打包,程式的備份,等等。

make如何工作

在預設的方式下,也就是我們只輸入 make 命令。那麼,
1、 make 會在當前目錄下找名字叫“ Makefile”或“ makefile”的檔案。
2、如果找到,它會找檔案中的第一個目標檔案( target),在上面的例子中,他會找到“ edit”
這個檔案,並把這個檔案作為最終的目標檔案。
3、如果 edit 檔案不存在,或是 edit 所依賴的後面的 .o 檔案的檔案修改時間要比 edit 這個檔案新,那麼,他就會執行後面所定義的命令來生成 edit 這個檔案。
4、如果 edit 所依賴的.o 檔案也不存在,那麼 make 會在當前檔案中找目標為.o 檔案的依賴性,如果找到則再根據那一個規則生成.o 檔案。(這有點像一個堆疊的過程)
5、當然,你的 C 檔案和 H 檔案是存在的啦,於是 make 會生成 .o 檔案,然後再用 .o 檔案生成make 的終極任務,也就是執行檔案 edit 了。

這就是整個 make 的依賴性, make 會一層又一層地去找檔案的依賴關係,直到最終編譯出第一個目標檔案。在找尋的過程中,如果出現錯誤,比如最後被依賴的檔案找不到,那麼make 就會直接退出,並報錯,而對於所定義的命令的錯誤,或是編譯不成功, make 根本不理。 make 只管檔案的依賴性,即,如果在我找了依賴關係之後,冒號後面的檔案還是不在,那麼對不起,我就不工作啦。

通過上述分析,我們知道,像 clean 這種,沒有被第一個目標檔案直接或間接關聯,那麼它後面所定義的命令將不會被自動執行,不過,我們可以顯示要 make 執行。即命令—— “ makeclean”,以此來清除所有的目標檔案,以便重編譯。於是在我們程式設計中,如果這個工程已被編譯過了,當我們修改了其中一個原始檔,比如 file.c,那麼根據我們的依賴性,我們的目標 file.o 會被重編譯(也就是在這個依性關係後面所定義的命令),於是 file.o 的檔案也是最新的啦,於是 file.o 的檔案修改時間要比 edit 要新,所以edit 也會被重新連結了(詳見 edit 目標檔案後定義的命令)。而如果我們改變了“ command.h”,那麼, kdb.o、command.o 和 files.o 都會被重編譯,並且,edit 會被重連結。

如何在scripts下的Makefile定義變變數

變數的定義說明

變數在宣告時需要給予初值,而在使用時,需要在變數名前加上“$”符號,但最好用小括號“()”或是花括號”{}”把變數給引用起來。

下面Makefile等同:
objects=program.o foo.o utils.o
program:(objects)ccoprogram(objects)
$(objects):defs.h
等同於:
objects=program.o foo.o utils.o
program:program.o foo.o utils.o
cc -o program program.o foo.o utils.o
program.o foo.o utils.o:defs.h

賦值變數
1、使用“=”操作符,使得前面的變數可以通過後面的變數來定義。
1、使用“:=”操作符,使得前面的變數不能使用後面的變數,只能使用前面已定義好了的變數。
2、使用”?=”操作符:例如foo ?= bar 如果foo沒有被定義過,那麼變數foo的值就是“bar”,否則此語句什麼也不做。
3、使用“+=”操作符,可以追加值
objects=main.o foo.o bar.o
objects+=another.o
等同於
objects=main.o foo.o bar.o
objects:=$(objects) another.o

下面給出之前給出的計算器的makefile的寫法

總控makefile

include scripts/Makefile

modules_make = $(MAKE) -C $(1);
modules_clean = $(MAKE) clean -C $(1);

.PHONY: all mm mc clean


all : $(Target)

mm:
    @ $(foreach n,$(Modules),$(call modules_make,$(n)))
mc:
    @ $(foreach n,$(Modules),$(call modules_clean,$(n)))

$(Target) : mm
    @$(CC) -o  $(Target) $(Allobjs)  -lsqlite3 -lpthread

clean : mc
    @rm -rf $(Target)

scripts下makefile

CC := gcc -lpthread -lsqlite3
Target := cal
Source := $(wildcard src/*.c)
Objs := $(patsubst %.c,%.o,$(Source))
Modules += main sub div mul add
Allobjs := $(addsuffix /src/*.o,$(Modules))

各個功能目錄下makefile類似。

include ../scripts/Makefile

all:    $(Objs)

clean:
    rm -rf $(Objs)

下面對上述makefile做的一個補充

巢狀執行

subsystem:
            cd subdir && $(MAKE)

其等價於:

subsystem:
        $(MAKE) -C subdir

這句是Makefile的巢狀執行:這裡的$(MAKE)就相當於make,-C 選項的作用是指將當前工作目錄轉移到你所指定的位置。

一些make函式的補充

函式補充

上面程式碼的分析,以後再做補充。
以下為原始碼

main.c檔案

#include <stdio.h>

int main()
{
    printf("add = %d\n",add(6,3));
    printf("sub = %d\n",sub(6,3));
    printf("mul = %d\n",mul(6,3));
    printf("div = %d\n",div(6,3));

    return 0;
}

add.c

int add(int a,int b)
{
    return a+b;
}

sub.c

int sub(int a,in b)
{
     return a - b;
}

mul.c

int mul(int a,int b)
{  
   return a * b;
}

div.c

int div(int a,int b)
{ 
   return a/b;
}

今日總結:
makefile的編寫最近是個難點,希望不要懼怕,把每個細節都要搞懂,畢竟makefile的編寫不是一蹴而就的。