Makefile - 快速指南



為什麼使用 Makefile?

編譯原始碼檔案可能很繁瑣,尤其是在您需要包含多個原始檔並且每次需要編譯時都必須鍵入編譯命令的情況下。Makefile 是簡化此任務的解決方案。

Makefile 是特殊的格式檔案,可以幫助自動構建和管理專案。

例如,假設我們有以下原始檔。

  • main.cpp
  • hello.cpp
  • factorial.cpp
  • functions.h

main.cpp

以下是 main.cpp 原始檔的程式碼 -

#include <iostream>

using namespace std;

#include "functions.h"

int main(){
   print_hello();
   cout << endl;
   cout << "The factorial of 5 is " << factorial(5) << endl;
   return 0;
}

hello.cpp

以下程式碼是 hello.cpp 原始檔 -

#include <iostream>

using namespace std;

#include "functions.h"

void print_hello(){
   cout << "Hello World!";
}

factorial.cpp

factorial.cpp 的程式碼如下 -

#include "functions.h"

int factorial(int n){
   
   if(n!=1){
      return(n * factorial(n-1));
   } else return 1;
}

functions.h

以下是 fnctions.h 的程式碼 -

void print_hello();
int factorial(int n);

編譯檔案並獲得可執行檔案的簡單方法是執行以下命令 -

gcc  main.cpp hello.cpp factorial.cpp -o hello

此命令生成 hello 二進位制檔案。在此示例中,我們只有四個檔案,並且知道函式呼叫的順序。因此,鍵入上述命令並準備最終二進位制檔案是可行的。

但是,對於一個擁有數千個原始碼檔案的大型專案,維護二進位制構建變得很困難。

make 命令允許您管理大型程式或程式組。當您開始編寫大型程式時,您會注意到重新編譯大型程式比重新編譯短程式花費的時間更長。此外,您會注意到您通常只處理程式的一小部分(例如單個函式),而程式的大部分內容保持不變。

在下一節中,我們將瞭解如何為我們的專案準備 Makefile。

Makefile - 宏

make 程式允許您使用宏,它類似於變數。宏在 Makefile 中定義為 = 對。下面顯示了一個示例 -

MACROS  = -me
PSROFF  = groff -Tps
DITROFF = groff -Tdvi
CFLAGS  = -O -systype bsd43
LIBS    = "-lncurses -lm -lsdl"
MYFACE  = ":*)"

特殊宏

在目標規則集中發出任何命令之前,都預定義了一些特殊宏 -

  • $@ 是要建立的檔案的名稱。

  • $? 是已更改的依賴項的名稱。

例如,我們可以使用如下規則 -

hello: main.cpp hello.cpp factorial.cpp
   $(CC) $(CFLAGS) $? $(LDFLAGS) -o $@

Alternatively:

hello: main.cpp hello.cpp factorial.cpp
   $(CC) $(CFLAGS) $@.cpp $(LDFLAGS) -o $@

在此示例中,$@ 表示 hello,而 $? 或 $@.cpp 拾取所有已更改的原始檔。

隱式規則中還有另外兩個特殊的宏。它們是 -

  • $< 導致操作的相關檔案的名稱。

  • $* 目標檔案和依賴檔案共享的字首。

常見的隱式規則是根據 .cpp(原始檔)構建 .o(物件)檔案。

.cpp.o:
   $(CC) $(CFLAGS) -c $<

Alternatively:

.cpp.o:
   $(CC) $(CFLAGS) -c $*.c

常規宏

有各種預設宏。您可以透過鍵入“make -p”來列印預設值。大多數從它們使用的規則中都很明顯。

這些預定義變數,即隱式規則中使用的宏,分為兩類。它們如下 -

  • 程式名稱的宏(例如 CC)

  • 包含程式引數的宏(例如 CFLAGS)。

下表列出了一些在 Makefile 的內建規則中用作程式名稱的常用變數 -

序號 變數和描述
1

AR

維護檔案的程式;預設為 `ar`。

2

AS

編譯彙編檔案的程式;預設為 `as`。

3

CC

編譯 C 程式的程式;預設為 `cc`。

4

CO

從 RCS 中檢出檔案的程式;預設為 `co`。

5

CXX

編譯 C++ 程式的程式;預設為 `g++`。

6

CPP

執行 C 預處理器的程式,結果輸出到標準輸出;預設為 `$(CC) -E`。

7

FC

編譯或預處理 Fortran 和 Ratfor 程式的程式;預設為 `f77`。

8

GET

從 SCCS 中提取檔案的程式;預設為 `get`。

9

LEX

用於將 Lex 語法轉換為原始碼的程式;預設為 `lex`。

10

YACC

用於將 Yacc 語法轉換為原始碼的程式;預設為 `yacc`。

11

LINT

用於對原始碼執行 lint 的程式;預設為 `lint`。

12

M2C

用於編譯 Modula-2 原始碼的程式;預設為 `m2c`。

13

PC

編譯 Pascal 程式的程式;預設為 `pc`。

14

MAKEINFO

將 Texinfo 原始檔轉換為 Info 檔案的程式;預設為 `makeinfo`。

15

TEX

從 TeX 原始碼建立 TeX dvi 檔案的程式;預設為 `tex`。

16

TEXI2DVI

從 Texinfo 原始碼建立 TeX dvi 檔案的程式;預設為 `texi2dvi`。

17

WEAVE

將 Web 轉換為 TeX 的程式;預設為 `weave`。

18

CWEAVE

將 C Web 轉換為 TeX 的程式;預設為 `cweave`。

19

TANGLE

將 Web 轉換為 Pascal 的程式;預設為 `tangle`。

20

CTANGLE

將 C Web 轉換為 C 的程式;預設為 `ctangle`。

21

RM

刪除檔案的命令;預設為 `rm -f`。

下表列出了值是上述程式的其他引數的變數。所有這些變數的預設值為空字串,除非另有說明。

序號。 變數和描述
1

ARFLAGS

要提供給檔案維護程式的標誌;預設為 `rv`。

2

ASFLAGS

在顯式呼叫 `.s` 或 `.S` 檔案上的彙編程式時要提供的額外標誌。

3

CFLAGS

要提供給 C 編譯器的額外標誌。

4

CXXFLAGS

要提供給 C 編譯器的額外標誌。

5

COFLAGS

要提供給 RCS co 程式的額外標誌。

6

CPPFLAGS

要提供給 C 預處理器和使用它的程式(例如 C 和 Fortran 編譯器)的額外標誌。

7

FFLAGS

要提供給 Fortran 編譯器的額外標誌。

8

GFLAGS

要提供給 SCCS get 程式的額外標誌。

9

LDFLAGS

當編譯器應該呼叫連結器 `ld` 時要提供的額外標誌。

10

LFLAGS

要提供給 Lex 的額外標誌。

11

YFLAGS

要提供給 Yacc 的額外標誌。

12

PFLAGS

要提供給 Pascal 編譯器的額外標誌。

13

RFLAGS

要提供給 Fortran 編譯器以用於 Ratfor 程式的額外標誌。

14

LINTFLAGS

要提供給 lint 的額外標誌。

注意 - 您可以使用 `-R` 或 `--no-builtin-variables` 選項取消隱式規則使用的所有變數。

您也可以在命令列中定義宏,如下所示 -

make CPP = /home/courses/cop4530/spring02

在 Makefile 中定義依賴關係

最終二進位制檔案通常依賴於各種原始碼和源標頭檔案。依賴關係很重要,因為它們讓 make 知道任何目標的原始碼。考慮以下示例 -

hello: main.o factorial.o hello.o
   $(CC) main.o factorial.o hello.o -o hello

在這裡,我們告訴 make hello 依賴於 main.o、factorial.o 和 hello.o 檔案。因此,每當這些物件檔案中的任何一個發生更改時,make 都會採取措施。

同時,我們需要告訴 make 如何準備 .o 檔案。因此,我們還需要定義這些依賴關係,如下所示 -

main.o: main.cpp functions.h
   $(CC) -c main.cpp

factorial.o: factorial.cpp functions.h
   $(CC) -c factorial.cpp

hello.o: hello.cpp functions.h
   $(CC) -c hello.cpp

在 Makefile 中定義規則

現在我們將學習 Makefile 的規則。

Makefile 目標規則的通用語法為 -

target [target...] : [dependent ....]
[ command ...]

在上面的程式碼中,括號中的引數是可選的,省略號表示一個或多個。這裡要注意,每個命令前面都需要製表符。

下面給出了一個簡單的示例,其中您定義了一個規則,用於從其他三個檔案建立目標 hello。

hello: main.o factorial.o hello.o
   $(CC) main.o factorial.o hello.o -o hello

注意 - 在此示例中,您必須提供規則以從原始檔建立所有物件檔案。

語義非常簡單。當您說“make target”時,make 會找到適用的目標規則;並且,如果任何依賴項比目標更新,make 會依次執行命令(宏替換後)。如果需要建立任何依賴項,則會先建立(因此您有一個遞迴)。

如果任何命令返回失敗狀態,Make 將終止。在這種情況下,將顯示以下規則 -

clean:
   -rm *.o *~ core paper

Make 忽略以連字元開頭的命令列上的返回狀態。例如,誰在乎是否有核心檔案?

Make 會回顯命令(宏替換後),以向您顯示正在發生的事情。有時您可能希望關閉它。例如 -

install:
   @echo You must be root to install

人們已經開始期待 Makefile 中的某些目標。您應該始終先瀏覽。但是,合理地期望找到 all(或僅 make)、install 和 clean 目標。

  • make all - 它編譯所有內容,以便您可以在安裝應用程式之前進行本地測試。

  • make install - 它將應用程式安裝到正確的位置。

  • make clean - 它清理應用程式,清除可執行檔案、任何臨時檔案、物件檔案等。

Makefile 隱式規則

該命令應該在所有情況下都能正常工作,我們從中構建可執行檔案 x 的原始碼為 x.cpp。這可以表示為隱式規則 -

.cpp:
   $(CC) $(CFLAGS) $@.cpp $(LDFLAGS) -o $@

此隱式規則說明了如何根據 x.c 建立 x - 對 x.c 執行 cc 並將輸出命名為 x。該規則是隱式的,因為沒有提到特定目標。它可以在所有情況下使用。

另一個常見的隱式規則是根據 .cpp(原始檔)構建 .o(物件)檔案。

.cpp.o:
   $(CC) $(CFLAGS) -c $<

alternatively

.cpp.o:
   $(CC) $(CFLAGS) -c $*.cpp

在 Makefile 中定義自定義字尾規則

Make 可以自動建立 .o 檔案,使用 cc -c 在相應的 .c 檔案上。這些規則是內建在 make 中的,您可以利用此優勢來縮短 Makefile。如果您在 Makefile 的依賴項行中僅指示當前目標所依賴的 .h 檔案,make 將知道相應的 .c 檔案是必需的。您不必包含編譯器的命令。

這進一步減少了 Makefile,如下所示 -

OBJECTS = main.o hello.o factorial.o
hello: $(OBJECTS)
   cc $(OBJECTS) -o hello
hellp.o: functions.h

main.o: functions.h 
factorial.o: functions.h 

Make 使用一個名為 .SUFFIXES 的特殊目標,允許您定義自己的字尾。例如,參考下面給出的依賴行 -

.SUFFIXES: .foo .bar

它通知 make 您將使用這些特殊字尾來建立自己的規則。

類似於 make 已經知道如何從 .c 檔案建立 .o 檔案,您可以如下方式定義規則 -

.foo.bar:
   tr '[A-Z][a-z]' '[N-Z][A-M][n-z][a-m]' < $< > $@
.c.o:
   $(CC) $(CFLAGS) -c $<

第一條規則允許您根據 .foo 檔案建立 .bar 檔案。它基本上會對檔案進行混淆。第二條規則是 make 用於根據 .c 檔案建立 .o 檔案的預設規則。

Makefile - 指令

有許多指令以各種形式提供。您系統上的 make 程式可能不支援所有指令。因此,請檢查您的 make 是否支援我們在此處解釋的指令。GNU make 支援這些指令。

條件指令

條件指令為 -

  • ifeq 指令開始條件,並指定條件。它包含兩個引數,用逗號分隔並用括號括起來。對這兩個引數執行變數替換,然後進行比較。如果這兩個引數匹配,則遵循 ifeq 的 Makefile 行將被遵守;否則將被忽略。

  • ifneq 指令開始條件,並指定條件。它包含兩個引數,用逗號分隔並用括號括起來。對這兩個引數執行變數替換,然後進行比較。如果這兩個引數不匹配,則遵循 ifneq 的 Makefile 行將被遵守;否則將被忽略。

  • ifdef 指令開始條件,並指定條件。它包含單個引數。如果給定的引數為真,則條件變為真。

  • ifndef 指令開始條件判斷,並指定條件。它包含一個引數。如果給定的引數為假,則條件變為真。

  • else 指令導致在之前的條件判斷失敗時執行以下行。在上面的示例中,這意味著只要第一個備選連結命令未使用,就會使用第二個備選連結命令。在條件判斷中使用 else 是可選的。

  • endif 指令結束條件判斷。每個條件判斷都必須以 endif 結束。

條件指令的語法

沒有 else 的簡單條件判斷的語法如下所示:

conditional-directive
   text-if-true
endif

如果條件為真,則 text-if-true 可以是任何文字行,被視為 Makefile 的一部分;如果條件為假,則不使用任何文字。

複雜條件判斷的語法如下所示:

conditional-directive
   text-if-true
else
   text-if-false
endif

如果條件為真,則使用 text-if-true;否則,使用 text-if-false。text-if-false 可以是任意數量的文字行。

無論條件判斷是簡單還是複雜,條件指令的語法都是相同的。有四種不同的指令可以測試各種條件,如下所示:

ifeq (arg1, arg2)
ifeq 'arg1' 'arg2'
ifeq "arg1" "arg2"
ifeq "arg1" 'arg2'
ifeq 'arg1' "arg2" 

上述條件的反向指令如下:

ifneq (arg1, arg2)
ifneq 'arg1' 'arg2'
ifneq "arg1" "arg2"
ifneq "arg1" 'arg2'
ifneq 'arg1' "arg2" 

條件指令示例

libs_for_gcc = -lgnu
normal_libs =

foo: $(objects)
ifeq ($(CC),gcc)
   $(CC) -o foo $(objects) $(libs_for_gcc)
else
   $(CC) -o foo $(objects) $(normal_libs)
endif

include 指令

include 指令允許 make 暫停讀取當前 Makefile 並讀取一個或多個其他 Makefile,然後再繼續。該指令是 Makefile 中的一行,如下所示:

include filenames...

檔名可以包含 shell 檔名模式。行首允許額外的空格並忽略,但不允許製表符。例如,如果您有三個 `.mk' 檔案,即 `a.mk'、`b.mk' 和 `c.mk',以及 $(bar),則它擴充套件為 bish bash,然後是以下表達式。

include foo *.mk $(bar)

is equivalent to:

include foo a.mk b.mk c.mk bish bash

make 處理 include 指令時,它會暫停讀取 Makefile 並依次讀取每個列出的檔案。完成後,make 會恢復讀取包含該指令的 Makefile。

override 指令

如果一個變數已使用命令引數設定,則 Makefile 中的普通賦值將被忽略。如果您希望在 Makefile 中設定變數,即使它已使用命令引數設定,也可以使用 override 指令,它是一行如下所示的程式碼:

override variable = value

or

override variable := value

Makefile - 重新編譯

make 程式是一個智慧實用程式,並根據您在原始檔中所做的更改進行工作。如果您有四個檔案 main.cpp、hello.cpp、factorial.cpp 和 functions.h,則所有剩餘檔案都依賴於 functions.h,而 main.cpp 依賴於 hello.cpp 和 factorial.cpp。因此,如果您對 functions.h 做了任何更改,則 make 將重新編譯所有原始檔以生成新的目標檔案。但是,如果您對 main.cpp 進行了任何更改,因為這並不依賴於任何其他檔案,則只會重新編譯 main.cpp 檔案,而 help.cpp 和 factorial.cpp 不會。

在編譯檔案時,make 會檢查其目標檔案並比較時間戳。如果原始檔的時間戳比目標檔案新,則它會生成新的目標檔案,假設原始檔已更改。

避免重新編譯

可能存在一個由數千個檔案組成的專案。有時您可能更改了一個原始檔,但可能不想重新編譯所有依賴它的檔案。例如,假設您向其他檔案依賴的標頭檔案中添加了一個宏或宣告。出於謹慎考慮,make 假設標頭檔案中的任何更改都需要重新編譯所有依賴檔案,但您知道它們不需要重新編譯,並且您寧願不浪費時間等待它們編譯。

如果您在更改標頭檔案之前預料到了問題,可以使用 `-t' 標誌。此標誌告訴 make 不要執行規則中的命令,而是透過更改其上次修改日期來標記目標為最新。您需要遵循以下步驟:

  • 使用命令 `make' 重新編譯確實需要重新編譯的原始檔。

  • 更改標頭檔案。

  • 使用命令 `make -t' 將所有目標檔案標記為最新。下次執行 make 時,標頭檔案中的更改不會導致任何重新編譯。

如果您已經在某些檔案確實需要重新編譯時更改了標頭檔案,那麼現在為時已晚。相反,您可以使用 `-o 檔案' 標誌,該標誌將指定的檔案標記為“舊的”。這意味著,檔案本身不會重新生成,並且不會因其而重新生成其他任何內容。您需要遵循以下步驟:

  • 使用 `make -o 標頭檔案' 重新編譯由於獨立於特定標頭檔案的原因而需要編譯的原始檔。如果涉及多個頭檔案,請為每個標頭檔案使用單獨的 `-o' 選項。

  • 使用 `make -t' 更新所有目標檔案。

Makefile - 其他特性

在本章中,我們將研究 Makefile 的各種其他功能。

遞迴使用 Make

遞迴使用 make 指的是在 Makefile 中使用 make 作為命令。當您希望為構成更大系統的各種子系統建立單獨的 Makefile 時,此技術非常有用。例如,假設您有一個名為 `subdir' 的子目錄,它有自己的 Makefile,並且您希望包含目錄的 Makefile 對子目錄執行 make。您可以透過編寫以下程式碼來實現:

subsystem:
   cd subdir && $(MAKE)

or, equivalently:
 	
subsystem:
   $(MAKE) -C subdir

您可以透過複製此示例來編寫遞迴 make 命令。但是,您需要了解它們的工作原理以及原因,以及子 make 與頂級 make 的關係。

將變數傳遞給子 Make

頂級 make 的變數值可以透過環境透過顯式請求傳遞給子 make。這些變數在子 make 中被定義為預設值。除非您使用 `-e' 開關,否則您無法覆蓋子 make 使用的 Makefile 中指定的變數。

為了傳遞或匯出變數,make 將變數及其值新增到環境中,以便執行每個命令。然後,子 make 使用環境來初始化其變數值表。

特殊變數 SHELL 和 MAKEFLAGS 始終被匯出(除非您取消匯出它們)。如果您將 MAKEFILES 設定為任何值,則會匯出它。

如果您希望將特定變數匯出到子 make,請使用 export 指令,如下所示:

export variable ...

如果您希望阻止變數被匯出,請使用 unexport 指令,如下所示:

unexport variable ...

變數 MAKEFILES

如果定義了環境變數 MAKEFILES,則 make 會將其值視為要讀取的其他 Makefile 的名稱列表(以空格分隔),然後再讀取其他檔案。這與 include 指令的工作方式非常相似:各種目錄將搜尋這些檔案。

MAKEFILES 的主要用途是在 make 的遞迴呼叫之間進行通訊。

從不同目錄包含標頭檔案

如果您將標頭檔案放在不同的目錄中,並且您在不同的目錄中執行 make,則需要提供標頭檔案的路徑。這可以透過在 Makefile 中使用 -I 選項來完成。假設 functions.h 檔案位於 /home/tutorialspoint/header 資料夾中,其餘檔案位於 /home/tutorialspoint/src/ 資料夾中,則 Makefile 將如下所示:

INCLUDES = -I "/home/tutorialspoint/header"
CC = gcc
LIBS =  -lm
CFLAGS = -g -Wall
OBJ =  main.o factorial.o hello.o

hello: ${OBJ}
   ${CC} ${CFLAGS} ${INCLUDES} -o $@ ${OBJS} ${LIBS}
.cpp.o:
   ${CC} ${CFLAGS} ${INCLUDES} -c $<

將更多文字追加到變數

通常,將更多文字新增到已定義變數的值中很有用。您可以使用包含 `+=' 的一行來執行此操作,如下所示:

objects += another.o

它獲取變數 objects 的值,並在其前面新增文字 `another.o',前面帶有一個空格,如下所示。

objects = main.o hello.o factorial.o
objects += another.o

以上程式碼將 objects 設定為 `main.o hello.o factorial.o another.o'。

使用 `+=' 類似於

objects = main.o hello.o factorial.o
objects := $(objects) another.o

Makefile 中的續行

如果您不喜歡 Makefile 中太長的行,則可以使用反斜槓 "\" 來換行,如下所示:

OBJ =  main.o factorial.o \
   hello.o

is equivalent to

OBJ =  main.o factorial.o hello.o

從命令提示符執行 Makefile

如果您已準備了一個名為“Makefile”的 Makefile,則只需在命令提示符下輸入 make,它就會執行 Makefile 檔案。但是,如果您為 Makefile 指定了其他名稱,則使用以下命令:

make -f your-makefile-name

Makefile - 示例

這是一個編譯 hello 程式的 Makefile 示例。該程式包含三個檔案:main.cppfactorial.cpphello.cpp

# Define required macros here
SHELL = /bin/sh

OBJS =  main.o factorial.o hello.o
CFLAG = -Wall -g
CC = gcc
INCLUDE =
LIBS = -lm

hello:${OBJ}
   ${CC} ${CFLAGS} ${INCLUDES} -o $@ ${OBJS} ${LIBS}

clean:
   -rm -f *.o core *.core

.cpp.o:
   ${CC} ${CFLAGS} ${INCLUDES} -c $<

現在您可以使用 make 構建您的程式 hello。如果您發出命令 make clean,則它會刪除當前目錄中所有目標檔案和核心檔案。

廣告

© . All rights reserved.