羽夏 MakeFile 簡明教程

寂靜的羽夏發表於2022-05-11

寫在前面

  此係列是本人一個字一個字碼出來的,包括示例和實驗截圖。該文章根據 GNU Make Manual 進行漢化處理並作出自己的整理,一是我對 Make 的學習記錄,二是對大家學習 MakeFile 有更好的幫助。如對該博文有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我。本篇博文可能十分冗長,請耐心閱讀和學習。

當你讀到後面可能感覺名詞有點怪怪的,那個就是我的譯文。因為老外表述翻譯到中文有些突兀,谷歌翻譯也比較離譜,我語文不行也不好翻譯,如果有能力看看原文吧。

make 概述

  make實用程式自動確認需要重新編譯大型程式的哪些部分,並執行哪些命令來重新編譯。本篇博文使用的示例是C程式,但你可以將make與任何程式語言結合使用,這些語言的編譯器可以通過shell命令執行。事實上,make並不侷限於程式。你可以用它來描述任何一項任務,當其他檔案發生變化時,相關檔案必須自動從其他檔案中來進行更新。

準備和執行 make

  要準備使用make,必須編寫一個名為makefile的檔案,該檔案描述程式中檔案之間的關係,並提供更新每個檔案的命令。在程式中,可執行檔案通常是從目標檔案更新而來的,而目標檔案又是通過編譯原始檔來實現的。
  一旦存在合適的makefile,每次更改一些原始檔時,下面這個簡單的shell命令:

make

  這足以執行所有必要的重新編譯。make程式使用makefile資料庫和檔案的最後修改時間來決定哪些檔案需要更新。對於這些當中的每一個檔案,都會被記錄在資料庫中。

Makefiles 介紹

  您需要一個名為makefile的檔案來告訴make要做什麼。大多數情況下,makefile告訴make如何編譯和連結程式。但是功能不僅僅侷限於此,它還可以告訴make如何遇到讓它執行某個操作的時候如何去做,比如刪除某些檔案作為清理操作。
  在本篇博文,我們將寫一個makefile,來編譯和連結一個簡單的由C編寫的文字編輯器。如果你有能力訪問GitHub,你可以去 mazarf/editor 去下載克隆。當然作者已經把makefile寫好了,我建議你在學習的時候刪掉它,去獨立寫一個,這對於你學習本篇博文有很大的幫助。
  如果你訪問有困難,我提供了一個沒有makefile版本的 原始碼 。這是一個藍奏雲網盤分享,如果你要獲取該檔案,需要密碼:haoj
  當make重新編譯我們所謂上述的編輯器時,每個更改的C原始檔都必須重新編譯。如果標頭檔案已更改,則必須重新編譯包含該標頭檔案的每個C原始檔以確保安全。每次編譯都會生成一個與原始檔對應的目標檔案。最後,如果任何原始檔已被重新編譯,則所有目標檔案,無論是新建立的還是從以前的編譯中儲存的,都必須連結在一起以生成新的可執行的文字編輯器。
  下面我們來開始學習makefile的編寫:

編寫入門篇

概述

  一個簡單的makefile包含著一系列的規則,它的大體模樣如下:

[目標 (target)]:[條件 (prerequisites)]
    [配置 (recipe)]

  目標通常是程式生成的檔案的名稱,例如可執行檔案或物件 (object)檔案。 目標也可以是要執行的操作的名稱,例如clean
  條件是用作建立目標的輸入的檔案。一個目標通常需要幾個檔案來製作。
  配置是執行的動作。 一個配置可能有多個命令,或者在同一行上,或者每個在自己的行上,一定要注意的是你需要在每條配置行的開頭放置一個製表符,也就是你在鍵盤上按下一個Tab。但如果您喜歡使用製表符以外的字元作為配置的字首(也就是除製表符以外的字元),則可以修改.RECIPEPREFIX變數來設定成其他字元。
  通常,配置位於含有各種條件的規則中,用於在任何條件發生變化時建立目標檔案。 但是,為目標指定配置的規則不需要條件。 例如,包含與目標clean關聯的刪除命令的規則沒有條件
  一條規則解釋瞭如何以及何時重新制作作為特定規則目標的某些檔案。make根據條件執行配置以建立或更新目標。規則還可以解釋如何以及何時執行操作,這個東西之後再說。
  makefile可以包含除規則之外的其他文字,但簡單的makefile只需要包含規則。規則可能看起來比此示例看起來要複雜一些,但都或多或少都會有相似之處。
  下面我們來寫一個簡單的makefile,它描述了名為 text的可執行檔案依賴於八個目標檔案的方式,而這些目標檔案又依賴於對應的C原始碼檔案和標頭檔案,如下所示:

text:line.o page.o prompt.o text.o
    gcc -o text line.o page.o \
        prompt.o text.o -lncurses

line.o: line.c line.h 
    gcc -c line.c
page.o: page.c page.h line.h
    gcc -c page.c
prompt.o:prompt.c prompt.h
    gcc -c prompt.c
text.o: text.c text.h prompt.h page.h line.h
    gcc -c text.c
clean:
    rm text line.o page.o prompt.o text.o

  是不是看不太明白,我們來畫一個示意圖:

graph TD A(text);B(line.o);C(page.o); D(prompt.o);E(text.o); E---A;D---A;C---A;B---A; 1[line.c];2[line.h];3[page.c];4[page.h]; 5[prompt.c];6[prompt.h];7[text.c];8[text.h]; 1 ==>B; 2 ==>B; 3 -->C;4-->C;1-->C; 5 -...->D;6-...->D; 7-.->E;8-.->E;6-.->E;4-.->E;2-.->E;

  如上展示的就是所謂的依賴關係,如果有關編譯器命令不會的話,建議自己查詢。在一條生成語句中,我們使用反斜槓加換行符將一行分成兩行,作用和一行是一樣的,但增加的可讀性。要使用此 makefile建立名為text的可執行檔案,請轉到該檔案的當前目錄下,輸入:

make

  效果如下:

wingsummer@wingsummer-PC editor → make
gcc -c line.c
gcc -c page.c
gcc -c prompt.c
gcc -c text.c
gcc -o text line.o page.o \
        prompt.o text.o -lncurses

  在你的當前資料夾中會有這些東西,如下圖所示:

羽夏 MakeFile 簡明教程

  我們這個程式就可以拿到控制檯執行了,是一個控制檯的文字編輯器。如果要使用這個makefile從目錄中刪除可執行檔案和所有目標檔案,輸入:

make clean

  所有的檔案將會恢復到初始狀態。
  當目標是一個檔案時,如果它的條件發生變化,則需要重新編譯或重新連結。此外,應首先更新本身自動生成的任何條件配置可以遵循包含目標和條件的每一行。這些配置說明了如何更新目標檔案。製表符或.RECIPEPREFIX變數指定的任何字元必須出現在配置中每一行的開頭,以將配置makefile中的其他行區分開來。請記住,make配置的工作原理一無所知,由你提供將正確更新目標檔案的配置。所有make所做的只是在需要更新目標檔案時執行您指定的配置
  上面有一個特例,目標clean不是一個檔案,而僅僅是一個動作的名稱。由於您通常不想執行此規則中的操作,因此clean不是任何其他規則的條件。因此,除非你明確告訴它,否則make永遠不會對它做任何事情。請注意,此規則不僅不是條件,它也沒有任何條件,因此該規則的唯一目的是執行指定的配置。這樣不引用檔案而只是動作的目標被稱為假目標。
  那麼make是如何處理makefile的呢?
  預設情況下,make從第一個目標開始(不是名稱以.頭的目標),這稱為預設目標,但你可以使用.DEFAULT_GOAL特殊變數修改。
  在我們前面的簡單的例子當中,我們的目標是重新編譯一個text可執行程式,因此我們首先得建立幾個規則,然後在命令列呼叫make。當你輸入這條指令的時候,make讀取當前目錄中的makefile並從處理第一條規則開始。在示例中,此規則用於重新連結text程式。但是在make可以完全處理這個規則之前,它必須處理編輯所依賴的檔案的規則,也就是所謂的目標檔案。這些檔案中的每一個都根據自己的規則進行處理,這些規則通過編譯其原始檔來更新每個.o檔案。如果原始檔或任何條件的標頭檔案比目標檔案更新,或者目標檔案不存在,則必須進行重新編譯。
  處理其他規則是因為它們的目標需要目標的條件。如果目標不依賴其他規則或者依賴項,則不會處理該規則,除非告訴make這樣做,比如make clean之類的命令。
  在重新編譯目標檔案之前,make會考慮更新其條件、原始檔和標頭檔案。這個makefile沒有指定要為它們做的任何事情,.c.h檔案不是任何規則的目標,所以make對這些檔案什麼都不做。但是此時make會按照自己的規則更新自動生成的C程式,例如BisonYacc製作的C程式。
  在重新編譯任何需要的目標檔案後,make決定是否重新連結我們上面的編輯器text。如果text不存在,或者任何目標檔案比它新,則必須這樣做。如果一個目標檔案剛剛被重新編譯,它現在比text新,所以text被重新連結。因此,如果我們更改檔案line.c並執行makemake將編譯該檔案以更新line.o,然後進行連結程式text
  

變數簡化

  在我們的示例中,我們必須在規則中列出所有目標檔案兩次以編譯text

text:line.o page.o prompt.o text.o
    gcc -o text line.o page.o \
        prompt.o text.o -lncurses

  這種重複很容易出錯。如果一個新的目標檔案被新增到我們的編譯系統中,我們很可能會丟三落四導致錯誤。此時,我們可以通過使用變數來消除風險並簡化生成檔案。只需要定義一次,我們在之後就可以隨意使用。
  每個makefile都有一個名為objectsOBJECTSobjsOBJSobjOBJ的變數,這是所有物件檔名的列表,這是標準的做法。我們將在makefile中用這樣的一行定義這樣的變數物件:

OBJS=text.o page.o line.o prompt.o

  然後,每個我們想要放置目標檔名列表的地方,我們可以通過使用變數來替換變數的值,如下所示:

OBJS=text.o page.o line.o prompt.o

text:$(OBJS)
    gcc -o text $(OBJS) -lncurses

line.o: line.c line.h 
    gcc -c line.c
page.o: page.c page.h line.h
    gcc -c page.c
prompt.o:prompt.c prompt.h
    gcc -c prompt.c
text.o: text.c text.h prompt.h page.h line.h
    gcc -c text.c
clean:
    rm text $(OBJS)

  我們同樣繼續呼叫make,發現效果是一樣的。

讓 make 簡化 配置

  沒有必要詳細說明編譯單個C原始檔的方法,因為make可以弄清楚它們。它有一個隱含的規則,用於使用cc從相應命名的.c檔案更新.o檔案-c命令。因此,我們可以從目標檔案的規則中簡化配置
  當以這種方式自動使用.c檔案時,它也會自動新增到條件列表中。因此,只要我們省略了配置,我們就可以從條件中省略.c檔案。
  到目前的更改如下所示:

OBJS=text.o page.o line.o prompt.o

text:$(OBJS)
    gcc -o text $(OBJS) -lncurses

line.o: line.c line.h 
page.o: page.c page.h line.h
prompt.o:prompt.c prompt.h
text.o: text.c text.h prompt.h page.h line.h

.PHONY: clean
clean:
    rm text $(OBJS)

  效果如下:

wingsummer@wingsummer-PC editor → make
cc    -c -o text.o text.c
cc    -c -o page.o page.c
cc    -c -o line.o line.c
cc    -c -o prompt.o prompt.c
gcc -o text text.o page.o line.o prompt.o -lncurses

  因為隱式規則非常方便,所以它們很重要。 你會看到它們經常被使用。

clean

  編譯程式並不是您可能想要為其編寫規則的唯一事情。Makefiles通常告訴除了編譯程式之外如何做一些其他事情。例如,如何刪除所有目標檔案和可執行檔案以使目錄恢復到乾淨的狀態。
  以下是我們如何編寫清理示例的make規則:

clean:
    rm text $(OBJS)

  在實踐中,我們可能希望以更復雜的方式編寫規則來處理意料之外的情況。我們會這樣做:

.PHONY: clean
clean:
    -rm text $(OBJS)

  這可以防止make被一個名為clean的實際檔案混淆,並導致它繼續執行。我們使用它的時候不應該將這樣的規則放在makefile的開頭,因為我們不希望它預設執行。 因此,在示例makefile中,我們希望重新編譯text的編輯規則保持預設目標。因為clean不是text條件,所以如果我們給出不帶引數的命令make,這條規則根本不會執行。為了使規則執行,我們必須輸入make clean

編寫高階篇

  當你學會前面的入門的時候,想要看懂真正的makefile還是有一定的差距,如下是我們示例自帶的內容:

# makefile for text.c

CC=gcc
CFLAGS=-Wall -g
OBJS=text.o page.o line.o prompt.o
HEADERS=$(subst .o,.h,$(OBJS)) # text.h page.h ...
LIBS=-lncurses

text: $(OBJS)
    $(CC) $(CFLAGS) -o text $(OBJS) $(LIBS)

text.o: text.c $(HEADERS)
    $(CC) $(CFLAGS) -c text.c

page.o: page.c page.h line.h
    $(CC) $(CFLAGS) -c page.c

# '$<' expands to first prerequisite file
# NOTE: this rule is already implicit
%.o: %.c %.h
    $(CC) $(CFLAGS) -c $< -o $@ 

.PHONY: cleanall clean cleantxt
cleanall: clean cleantxt

clean:
    rm -f $(OBJS) text

cleantxt:
    rm -f *.txt

  雖然有一些部分我們已經能看懂了,但還有我們看不懂的地方,下面我們來開始介紹詳細部分。

包含內容有什麼

  Makefile包含五種內容:顯式規則、隱式規則、變數定義、指令和註釋。規則、變數和指令將在後面的章節中詳細描述。
  顯式規則說明何時以及如何重新制作一個或多個檔案,稱為規則的目標。它列出了目標所依賴的其他檔案,稱為目標的條件,還可能提供用於建立或更新目標的配置
  隱式規則說明了何時以及如何根據檔名重新制作一類檔案。它描述了目標如何依賴於名稱與目標相似的檔案,並提供了建立或更新此類目標的方法。
  變數定義是為變數指定文字字串值的行,該變數可以稍後替換到文字中。在我們前面的入門篇示例用到過,這裡就不贅述了。
  指令是make在讀取makefile時執行某些特殊操作的指令。這些包括:讀取另一個makefile、決定(基於變數的值)是使用還是忽略makefile的一部分、從包含多行的逐字字串定義變數。
  #的作用是makefile單行註釋標誌,作用和c//作用是一樣的。如果您想要#作為文字,請使用反斜槓對其進行轉義。註釋可能會出現在makefile的任何行上,儘管在某些情況下會特別處理它們。你不能在變數引用或函式呼叫中使用註釋,任何#例項都將在變數引用或函式呼叫中按字面意思(而不是作為註釋的開頭)處理。
  配置中的註釋被傳遞到shell,就像任何其他配置文字一樣,是由shell決定如何解釋它。
  在定義指令中,在變數定義期間不會忽略註釋,而是在變數值中保持原樣。擴充套件變數時,它們將被視為註釋或配置文字,具體取決於評估變數的上下文。

多行分割

  Makefile使用基於行的語法,其中換行符是特殊的並標記語句的結尾。GNU make對語句行的長度沒有限制,最多不超過你計算機中的記憶體量。
  但是,如果不換行,則很難閱讀太長而無法顯示的行。因此,可以通過在語句中間新增換行符來格式化makefile以提高可讀性。反斜槓\字元轉義內部換行符可以實現此功能。在需要區分的地方,我們將物理行稱為以換行符結尾的單行(不管它是否被轉義),而邏輯行是一個完整的語句,包括所有轉義的換行符,直到第一個非轉義換行符。
  處理反斜槓/換行符組合的方式取決於語句是配置行還是非配置行。在配置行之外,反斜槓/換行符被轉換為單個空格字元。完成後,反斜槓/換行符周圍的所有空格都將壓縮為一個空格,這包括反斜槓之前的所有空格、反斜槓/換行符之後行首的所有空格以及任何連續的反斜槓/換行符組合。如果定義了.POSIX特殊目標,則反斜槓/換行符處理會稍作修改以符合POSIX.2:首先,不刪除反斜槓之前的空格,其次,不壓縮連續的反斜槓/換行符
  如果您需要拆分一行但不希望新增任何空格,您可以利用一個巧妙的技巧:將反斜槓/換行符替換為三個字元美元符號/反斜槓/換行符:

var := one$\
       word

  在make刪除反斜槓/換行符並將以下行壓縮為一個空格之後,這相當於:

var := one$ word

  然後make會進行變數擴充套件。變數引用$指的是一個不存在的具有單字元名稱的變數,因此擴充套件為空字串,給出最終賦值,相當於:

var := oneword

Makefile 命名

  預設情況下,當make查詢makefile時,它​​會依次嘗試以下名稱:GNUmakefilemakefileMakefile。如果您有一個特定於GNU makemakefile,並且不會被其他版本的make理解,您應該使用這個名稱。其他make程式尋找makefileMakefile,但不是GNUmakefile
  如果make沒有找到這些名稱,它就不會使用任何makefile。然後您必須使用命令引數指定目標,make將嘗試找出如何僅使用其內建的隱式規則來重新制作它。如果你想為你的makefile使用一個非標準的名字,你可以使用-f--file選項來指定makefile的名字。如果指定上面的引數,則不會自動檢查預設的makefile名稱。

包含其他 Makefiles

  include指令告訴make暫停讀取當前的makefile並在繼續之前讀取一個或多個其他makefile。該指令是makefile中的一行,如下所示

include 檔名

  檔名可以包含shell模式檔名。如果為空,則不包含任何內容並且不列印錯誤。
  行首允許並忽略多餘的空格,但第一個字元不能是製表符或.RECIPEPREFIX的值。如果該行以製表符開頭,它將被視為配置行。include和檔名之間以及檔名之間需要空格,額外的空格會被忽略。允許在行尾新增以“#”開頭的註釋,如果檔名包含任何變數或函式引用,它們將被擴充套件。
  例如,如果您有三個mk檔案,a.mkb.mkc.mk,並且$(bar)擴充套件為bish bash,則以下表示式:

include foo *.mk $(bar)

  等價於:

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

  當make處理一個include指令時,它會暫停讀取包含的makefile並依次從每個列出的檔案中讀取。完成後,make繼續讀取指令出現的makefile
  使用include指令的一個場合是,由不同目錄中的單個makefile處理的多個程式需要使用一組通用的變數定義或模式規則。
  另一個這樣的場合是當您想從原始檔自動生成條件時,條件可以放在主makefile包含的檔案中。這種做法通常比以某種方式將條件附加到主makefile末尾的做法更乾淨,就像傳統上使用其他版本的make所做的那樣。
  如果指定的名稱不以斜槓開頭,並且在當前目錄中沒有找到該檔案,則會搜尋其他幾個目錄。首先,搜尋您使用-I--include-dir選項指定的任何目錄。然後按以下順序搜尋以下目錄(如果存在):prefix/include(通常是/usr/local/include)、/usr/gnu/include/usr/local/include/usr/include
  如果在任何這些目錄中都找不到包含的makefile,則會生成警告訊息,但這不是立即致命的錯誤,繼續處理包含包含的 makefile。一旦它完成了對makefile的讀取,make將嘗試重新制作任何過時或不存在的檔案。只有在它試圖找到一種方法來重新制作makefile並失敗後,才會將丟失的makefile診斷為致命錯誤。
  如果您希望make簡單地忽略不存在或無法重新制作的makefile,並且沒有錯誤訊息,請使用-include指令而不是include,如下所示:

-include 檔名

  除了任何檔名或任何檔名的任何條件不存在或無法重新制作時,這就像在所有方面都包含在內,但沒有錯誤(甚至沒有警告)。
  為了與其他一些make實現相容,sinclude-include的另一個名稱。

MAKEFILES

  如果定義了環境變數MAKEFILES,則make將其值視為要在其他檔案之前讀取的其他makefile的名稱列表(由空格分隔)。這很像include指令。此外,預設目標永遠不會從這些makefile中獲取,如果沒找到MAKEFILES中列出的檔案,這不是錯誤。
  MAKEFILES的主要用途是在make的遞迴呼叫之間進行通訊。通常不希望在頂級呼叫make之前設定環境變數,因為通常最好不要弄亂外部的makefile。但是,如果您在沒有特定makefile的情況下執行make,則MAKEFILES中的makefile可以做一些有用的事情來幫助內建的隱式規則更好地工作,例如定義搜尋路徑。
  一些使用者很想在登入時自動在環境中設定MAKEFILES,並且程式makefile期望這樣做。這是一個非常糟糕的主意,因為如果由其他人執行,這樣的makefile將無法工作,在makefile中編寫顯式包含指令要好得多。

重寫另一個 Makefile 的一部分

  有時,有一個與另一個makefile基本相同的makefile是很有用的。你可以經常使用include指令將其中一個包含到另一箇中,並新增更多的目標或變數定義。但是,兩個makefile為同一個目標提供不同的配置是無效的,不過還有另一種方法。
  在包含的makefile中(想要包含makefile的另一者),你可以使用match-anything模式規則來描述,表示要重新編譯無法從包含makefile中的資訊生成的任何目標,make應該在另一個makefile中查詢。
  例如,如果你有一個makefile,它告訴你如何建立目標foo(和其他目標),你可以寫一個名為GNUmakefilemakefile,它內容如下:

foo:
        frobnicate > foo

%: force
        @$(MAKE) -f Makefile $@
force: ;

  如果你輸入命令make foomake會找到 GNUmakefile並掃描,發現要生成foo,它需要執行frobnicate > foo。 如果你說make barmake將無法在GNUmakefile中建立bar,因此它將使用模式規則中的配置make -f Makefile bar。 如果Makefile提供了更新bar的規則,make將應用該規則。對於GNUmakefile未說明如何製作的任何其他目標也是如此。
  它的工作方式是模式規則的模式只有%,所以它匹配任何目標。該規則指定了一個條件,以保證即使目標檔案已經存在也將執行配方。我們給強制目標一個空配置,以防止make搜尋隱式規則來構建它,否則它將應用相同的match-anything規則來強制自身並建立一個條件迴圈。

make 是如何解析 Makefile

  GNU make在兩個不同的階段完成它的工作。在第一階段,它讀取所有makefile、包含的makefile等,並內化所有變數及其值以及隱式和顯式規則,並構建所有目標及其先決條件的依賴關係圖。在第二階段,make使用這些內部化資料來確定需要更新哪些目標並執行更新它們所需的配置
  理解這種兩階段方法很重要,因為它直接影響變數和函式擴充套件的發生方式。在編寫makefile時,這通常是一些混亂的根源。下面是可以在makefile中找到的不同構造的摘要,以及構造的每個部分發生擴充套件的階段。
  如果它發生在第一階段,我們說擴充套件是立即的,make將在解析makefile時擴充套件構造的那部分。我們說,如果不是立即擴充套件,擴充套件就會被推遲。延遲構造部分的擴充套件會延遲到使用擴充套件之前,無論是在直接上下文中引用它時,還是在第二階段需要它時。

變數賦值

  變數定義解析如下:

immediate = deferred    #普通賦值
immediate ?= deferred   #如果未賦值則賦值
immediate := immediate  #覆蓋賦值
immediate ::= immediate #等同於:=
immediate += deferred or immediate  #追加賦值
immediate != immediate  #結果執行,返回賦值

define immediate
  deferred
endef

define immediate =
  deferred
endef

define immediate ?=
  deferred
endef

define immediate :=
  immediate
endef

define immediate ::=
  immediate
endef

define immediate +=
  deferred or immediate
endef

define immediate !=
  immediate
endef

  對於附加操作符+=,如果變數之前被設定為簡單變數(:=或'::=),則認為右邊是即時的,否則是延遲的。
  對於shell的賦值操作符!=時,將立即計算右邊的值並將其傳遞給shell。結果儲存在左側命名的變數中,該變數成為一個簡單變數(因此將在每次引用時重新計算)。

條件指令

  條件指令立即被解析。這意味著,例如,自動變數不能在條件指令中使用,因為自動變數直到該規則的配方被呼叫時才會被設定。如果你需要在條件指令中使用自動變數,你必須將條件移動到配方中,並使用shell條件語法。

規則定義

  規則總是以同樣的方式展開,無論其形式如何:

immediate : immediate ; deferred
        deferred

  也就是說,目標條件部分將立即展開,而用於構建目標的配置總是延遲。對於顯式規則、模式規則、字尾規則、靜態模式規則和簡單的條件定義來說是這樣的。

自動變數

  假設你正在編寫一個模式規則,將一個.c檔案編譯成一個.o檔案。那麼如何編寫cc命令,以便它在正確的原始檔名上操作?您不能在配置中寫入名稱,因為每次應用隱式規則時名稱都是不同的。
  你要做的是使用一個特殊的特性,自動變數。這些變數具有根據規則的目標和先決條件重新計算的每個規則的值。在本例中,您將使用$@作為物件檔名,使用$<作為原始檔名。
  認識到自動變數值的可用範圍是有限的,這一點非常重要。它們只在配置中有值。特別是,你不能在規則的目標列表中使用它們,它們沒有值,會擴充套件為空字串。而且,不能在規則的條件列表中直接訪問它們。一個常見的錯誤是試圖在先決條件列表中使用$@,這行不通。然而,GNU make有一個特殊的特性,即二次擴充套件,它將允許在先決條件列表中使用自動變數值,這將會在後面進行介紹。
  下面是自動變數表:

自動變數符號 含義
$@ 規則目標的檔名。如果目標是存檔成員,則$@是存檔檔案的名稱。在有多個目標的模式規則中,$@是導致規則配置執行的任何目標的名稱。
$% 目標成員名。例如,如果目標是foo.abar.o),那麼$%就是bar.o$@foo.a。當目標不是存檔成員時,$%為空。
$< 第一個條件的名稱。如果目標從隱式規則獲得配置,這將是隱式規則新增的第一個條件
$? 比目標更新的所有條件的名稱,名稱之間用空格隔開。如果目標不存在,則將包括所有條件。對於作為歸檔成員的條件,只使用命名的成員。
$^ 所有條件的名稱,它們之間有空格。對於作為歸檔成員的條件,只使用命名的成員。目標對它所依賴的其他檔案只有一個條件,無論每個檔案作為條件列出多少次。因此,如果您多次為目標列出條件,那麼$^的值只包含名稱的一個副本。此列表不包含任何order-only條件
$+ 這類似於$^,但是不止一次列出的先決條件會按照它們在makefile中列出的順序重複出現。這在連結命令時非常有用,當需要以特定順序重複庫檔名時。
`$ `
$* 隱式規則匹配詞幹。如果目標為dir/a.foo.b,目標匹配模式是a.%.b,則詞幹為dir/foo

  當然,自動變數不僅僅是上面這些,還有有關DF的變體,比如$(@D)等,如果有需要請參考原文。

二次展開

  前面我們瞭解到GNU在兩個不同的階段中製作:讀取階段和目標更新階段。GNU也能夠為Makefile中定義的某些或所有目標啟用條件的第二次擴充套件。為了使第二次擴充套件發生,必須在使用此功能的第一個條件列表之前定義特殊的目標。
  如果定義了該特殊目標,則在上述兩個階段之間,就在讀取階段的末尾,第二次擴充套件了特殊目標後定義的目標的所有條件。在大多數情況下,這種次要擴充套件將無效,因為在MakeFiles的初始解析過程中,所有變數和功能參考都將進行擴充套件。為了利用解析器的二級擴充套件階段,有必要逃避makefile中的變數或函式參考。在這種情況下,第一個擴充套件僅取消參考,但並未擴充套件,並且擴充套件到次級擴充套件階段。例如,考慮這個makefile

.SECONDEXPANSION:
ONEVAR = onefile
TWOVAR = twofile
myfile: $(ONEVAR) $$(TWOVAR)

  在第一個擴充套件階段之後,MyFile目標的先決條件列表將為單一和$(TWOVAR); 擴充套件了對ONEVAR的第一個unescaped變數引用,而第二個escaped變數引用簡單地保留,而未被識別為變數參考。現在,在次要擴充套件期間,第一個單詞再次擴充套件,但是由於它不包含變數或函式引用,因此它仍然是一個值的值,而第二個單詞現在是對變數TWOVAR的正常引用,該引用將擴充套件到twofile的值。最終的結果是有兩個條件,即onefiletwofile
  顯然,這不是一個非常有趣的案例,因為僅通過在先決條件列表中顯示兩個變數,可以更輕鬆地實現相同的結果。如果變數重置,則會顯而易見。考慮此示例:

.SECONDEXPANSION:
AVAR = top
onefile: $(AVAR)
twofile: $$(AVAR)
AVAR = bottom

  在這裡,onefile條件將立即擴充套件,並解析到到值top,而在二次擴充套件併產生底部值之前,twofile的先決條件將不完整。
  這更加令人興奮,但是隻有當您發現次要擴充套件始終發生在該目標的自動變數範圍內時,此功能的真正力量才變得顯而易見。這意味著您可以在第二個擴充套件過程中使用$@$*等的變數,並且它們將具有預期值,就像在配置中一樣。 您要做的就是通過逃脫$來推遲擴充套件。 同樣,對於顯式和隱式(模式)規則,都會發生次要擴充套件。 知道這一點,此功能的可能使用急劇增加。 例如:

.SECONDEXPANSION:
main_OBJS := main.o try.o test.o
lib_OBJS := lib.o api.o

main lib: $$($$@_OBJS)

  在這裡,在初次擴充套件之後,主和LIB目標的條件將為$($@_OBJS)。在二次擴充套件期間,$@變數設定為目標名稱,因此主目標的擴充套件將產生$(main_OBJS)main.o try.o test.o,而LIB的二次擴充套件目標將產生$(lib_OBJS)lib.o api.o
  您也可以在此處混合功能,只要它們正確的轉義:

main_SRCS := main.c try.c test.c
lib_SRCS := lib.c api.c

.SECONDEXPANSION:
main lib: $$(patsubst %.c,%.o,$$($$@_SRCS))

  有關二次擴充套件的內容就介紹這麼多,由於其比較複雜,也無法在一次說明白,建議閱讀官方文件。

假目標 (Phony Targets)

  假目標是真正不是檔名的目標。 相反,當您提出明確請求時,它只是要執行的配置的名稱。使用虛假目標有兩個原因:避免與同名檔案發生衝突,並提高效能。
  如果您編寫了一條規則,該規則將無法建立目標檔案,則每當目標出現進行重新制作時,將執行配置。這是一個示例:

clean:
        rm *.o temp

  因為rm命令沒有建立名為clean的檔案,因此可能永遠都不存在此類檔案。因此,每次執行make clean時,rm命令將被執行。
  在此示例中,如果在此目錄中建立了名為clean的檔案,則將無法正常工作。 由於它沒有條件,因此將始終考慮clean最新生成狀態而不會執行其配置。為了避免此問題,您可以通過使其成為特殊目標的條件,來明確宣告該目標為假。如下:

.PHONY: clean
clean:
        rm *.o temp

  完成此操作後,無論是否有名為clean的檔案,clean就會被正確的執行。
  假目標也與make遞迴呼叫相結合。在這種情況下,MakeFile通常會包含一個變數,該變數列出了要構建的許多子目錄。處理此操作的一種簡單方法是用迴圈在子目錄上的配置定義一個規則,例如:

SUBDIRS = foo bar baz

subdirs:
        for dir in $(SUBDIRS); do \
          $(MAKE) -C $$dir; \
        done

  但是,這種方法存在問題。 首先,該規則忽略了子make中檢測到的任何錯誤,因此即使在失敗的情況下,它也會繼續構建其餘目錄。可以通過新增shell命令來注意錯誤和退出,但是即使使用-k選項呼叫,這很不好。其次,也許更重要的是,您無法利用make可以並行構建目標的能力,因為只有一個規則。
  
通過將子目錄宣佈為.PHONY目標,這必須這樣做,因為該子目錄顯然總是存在,否則不會構建。您可以消除這些問題:

SUBDIRS = foo bar baz

.PHONY: subdirs $(SUBDIRS)

subdirs: $(SUBDIRS)

$(SUBDIRS):
        $(MAKE) -C $@

foo: baz

  在這裡,我們還宣告,直到baz子目錄完成後才能構建foo子目錄。嘗試並行構建時,這種關係宣告尤其重要。隱式規則搜尋被跳過。這就是為什麼將目標宣佈為.PHONY對效能有益的原因,即使您不擔心存在的實際檔案。
  假目標不應是真實目標檔案的條件。如果是這樣,每次更新該檔案時都會執行其配置。只要假目標絕不是真正目標的條件,只有當假目標是指定目標goal時,假目標的配置才會執行。
  假目標可以有條件。當一個目錄包含多個程式時,最方便地將所有程式描述為一個makefile ./Makefile。由於預設情況下的目標將是makefile中的第一個,因此通常將其作為名為all的假目標,並作為條件將其授予所有單獨的程式。例如:

all : prog1 prog2 prog3
.PHONY : all

prog1 : prog1.o utils.o
        cc -o prog1 prog1.o utils.o

prog2 : prog2.o
        cc -o prog2 prog2.o

prog3 : prog3.o sort.o utils.o
        cc -o prog3 prog3.o sort.o utils.o

  現在,您可以使用make以重新編譯這三個程式,也可以將其指定為重製的引數。假(Phoniness)不是繼承,除非明確宣佈是這樣的,否則假目標的條件本身不是假的。
  當一個假目標是另一個目標的條件時,它將用作另一個子例程。例如,這裡的meake cleanall將刪除物件檔案,差異檔案和檔案程式:

.PHONY: cleanall cleanobj cleandiff

cleanall : cleanobj cleandiff
        rm program

cleanobj :
        rm *.o

cleandiff :
        rm *.diff

小結

  學會了上面的內容,我們來回去看自帶的 makefile

# makefile for text.c

CC=gcc
CFLAGS=-Wall -g
OBJS=text.o page.o line.o prompt.o
HEADERS=$(subst .o,.h,$(OBJS)) # text.h page.h ...
LIBS=-lncurses

text: $(OBJS)
    $(CC) $(CFLAGS) -o text $(OBJS) $(LIBS)

text.o: text.c $(HEADERS)
    $(CC) $(CFLAGS) -c text.c

page.o: page.c page.h line.h
    $(CC) $(CFLAGS) -c page.c

# '$<' expands to first prerequisite file
# NOTE: this rule is already implicit
%.o: %.c %.h
    $(CC) $(CFLAGS) -c $< -o $@ 

.PHONY: cleanall clean cleantxt
cleanall: clean cleantxt

clean:
    rm -f $(OBJS) text

cleantxt:
    rm -f *.txt

  上面比較難理解的就下面的部分:

HEADERS=$(subst .o,.h,$(OBJS)) 

%.o: %.c %.h
    $(CC) $(CFLAGS) -c $< -o $@ 

  substmakefile的裡的函式,意為字元替換,但本篇並沒有介紹,因為makefile是在是太複雜了,要想徹底弄懂還是需要大量的時間的,具體查閱源文件的Functions for Transforming Text部分,本篇僅僅起到拋磚引玉的作用。對於$(subst FROM, TO, TEXT),它的意思是將字串TEXT中的子串FROM變為TO。對於我們的示例就是將$(OBJS)變數的值中的.o字串替換為.h
  %.o: %.c %.h%就是一個匹配符號,使用它可以嘗試編譯當前檔案下的所有對應.c.h生成.o檔案。$<用人話講,意思就是構造所需檔案列表的第一個檔案的名字,$@是目標的名字。我們可以make一下看看這個被替換成了什麼:

wingsummer@wingsummer-PC editor → make
gcc -Wall -g -c text.c
text.c: In function ‘load_file’:
text.c:234:5: warning: this ‘if’ clause does not guard... [-Wmisleading-indentation]
     if(size < PAGE_SIZE)
     ^~
text.c:237:2: note: ...this statement, but the latter is misleadingly indented as if it were guarded by the ‘if’
  init_page(p, filename, size);
  ^~~~~~~~~
text.c: In function ‘main’:
text.c:87:49: warning: ‘sprintf’ may write a terminating nul past the end of the destination [-Wformat-overflow=]
                 sprintf(status, "Saved as \'%s\'", page.filename);
                                                 ^
text.c:87:17: note: ‘sprintf’ output between 12 and 267 bytes into a destination of size 266
                 sprintf(status, "Saved as \'%s\'", page.filename);
                 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
text.c:81:49: warning: ‘sprintf’ may write a terminating nul past the end of the destination [-Wformat-overflow=]
                 sprintf(status, "Saved as \'%s\'", page.filename);
                                                 ^
text.c:81:17: note: ‘sprintf’ output between 12 and 267 bytes into a destination of size 266
                 sprintf(status, "Saved as \'%s\'", page.filename);
                 ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
gcc -Wall -g -c page.c
gcc -Wall -g -c line.c -o line.o 
gcc -Wall -g -c prompt.c -o prompt.o 
gcc -Wall -g -o text text.o page.o line.o prompt.o -lncurses

  重點注意的是下面幾句:

gcc -Wall -g -c line.c -o line.o 
gcc -Wall -g -c prompt.c -o prompt.o 

  這兩句是makefile並沒有明確宣告的,也就是我們%.o: %.c %.h規則對應的執行。至此,本教程暫告一段落。
  如果學會了上面的部分,如果有時間,如果有能力,建議把原文完完整整的看一遍,這樣的話可能一天的時間就沒了。對需要的重點部分看一看,剩下的如果用到就查。就算熟練使用makefile維護專案,也未必能用到make所有的功能,所以沒必要為自己學不完makefile而苦惱。

相關文章