《跟我一起寫Makefile》摘要 1-3


0. 前言

  • 想成爲C++工程師,需要系統學習開發工具,就從Makefile和cmake入手吧。
  • 跟我一起寫Makefile
  • 目錄:
    1. 概述:簡單介紹makefile的來源、作用,以及介紹程序編譯與鏈接的基本過程
    2. makefile介紹:簡單介紹Makefile的基本功能
    3. 書寫規則:介紹規則相關的內容,即targets/commands/prerequisites相關

1. 概述

1.1. Makefile 基本介紹

  • 作用:關係到整個項目的編譯規則:
    • 指定哪些文件先編譯,哪些文件後編譯,哪些重新編譯。
    • 類似於shell腳本,執行操作系統的命令。
  • 好處:自動化編譯
    • 通過make命令,使得整個工程自動編譯,提高軟件開發效率
    • 大多數IDE都自帶make。
  • 其他:不同廠商的make略有不同,本書選用的是 RedHat Linux 8.0,make版本3.80。

1.2. C++程序的編譯與鏈接

  • 總過程:源文件先編譯成爲中間目標文件,再由中間目標文件生成執行文件。
  • 編譯(compile):將原文件編譯成中間代碼文件,即.obj.o文件,即object file。
    • 主要工作:確認語法正確、函數與變量的聲明正確。對於後者,主要是確認頭文件單的位置。
    • 一般來說,每個原文件對應一箇中間目標文件。
  • 鏈接(link):將大量的object file合成執行文件。
    • 主要是鏈接函數與全局變量,會用到中間目標文件。
    • 鏈接器不管函數所在的源文件,只管中間目標文件。
    • 有時候,由於原文件太多 ,編譯生成的中間文件太多,而在鏈接時需要明顯地之處中間目標文件名稱,這很不方便,所以需要對中間文件打包,生成庫文件/Archive File,win下名爲.lib,UNIX下名爲.a

2. makefile介紹

2.1. makefile基本規則與舉例

  • 基本規則:
    • 如果這個工程沒有編譯過,那麼所有c文件都要編譯並被鏈接。
    • 如果工程的某幾個c文件被修改,那麼只編譯被修改的文件,並鏈接目標程序。
    • 如果工程的頭文件被更改,那麼需要重新編譯引用了這幾個頭文件的c文件,並鏈接目標程序。
  • makefile 的基本格式
    • target:可以是目標文件、執行文件,還可以是標籤(可能指的是 clean/install 這些吧)。
    • prerequisites:生成該target所依賴的文件或target。
    • command:該target要執行的命令(任意shell命令)
    • prerequisites中如果有一個以上的文件比target文件要新的話,command所定義的命令就會被執行。
target ...: prerequisites...
    command ...
  • 舉例
    • 執行make命令後,就會得到執行文件edit
    • 如果要刪除執行文件和中間文件,可通過make clean實現。
    • command行必須以TAB開頭
    • make會比較target文件和prerequisites文件的修改日期,如果prerequisites文件的日期比targets文件新,或target文件不存在,則會執行下面的command。
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.o

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

2.2. make的工作流程

  • 前提:在默認條件下,即只執行make命令時:
    • 在當前目錄下找名字叫Makefilemakefile的文件。
    • 如果找到,它會找文件中的第一個目標文件(target)。
      • 在上面的例子中,會找到edit,並把這個target作爲最終的目標文件。
    • 如果edit文件不存在,或是edit所依賴的後面的 .o 文件的文件修改時間要比 edit 這個文件新,那麼,他就會執行後面所定義的命令來生成 edit 這個文件。
    • 如果 edit 所依賴的 .o 文件也不存在,那麼make會在當前文件中找目標爲 .o 文件的依賴性,如果找到則再根據那一個規則生成 .o 文件。(這有點像一個堆棧的過程)
    • 當然,你的C文件和H文件是存在的啦,於是make會生成 .o 文件,然後再用 .o 文件生成make的終極任務,也就是執行文件 edit 了。
    • 整個過程就是一層一層找文件的依賴關係,直到最終編譯出第一個目標文件。
  • 類似於clean這些標籤,沒有被一個目標文件直接/間接關聯,那麼後面定義的命令也就不會被自動執行,但可以通過 make clean 來顯示執行。
  • 當修改了其中一個源文件,那麼對應的中間目標文件以及最初的edit都會重新編譯、鏈接。

2.3. makefile中使用變量

  • makefile的變量就是一個字符串,也可理解爲C語言中的宏。
  • 定義:
objects = main.o kbd.o command.o display.o \
     insert.o search.o files.o utils.o
  • 使用:$(objects)

2.4. make自動推導依賴關係

  • make可實現自動推導文件以及文件依賴關係,不在每一個.o文件後都加上 prerequisites。
  • make看到一個.o文件,會自動把.c文件加到依賴關係中。
    • 例如,有一個whatever.o,那麼自動會把whatever.c作爲其依賴文件。
    • 同時,cc -c wahtever.c也會被自動推導出來。
  • 舉例
    • .PHONY 表示 clean 是個僞目標文件。
objects = main.o kbd.o command.o display.o \
    insert.o search.o files.o utils.o

edit : $(objects)
    cc -o edit $(objects)

main.o : defs.h
kbd.o : defs.h command.h
command.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h

.PHONY : clean
clean :
    rm edit $(objects)

2.5. 合併依賴項

  • 上面的例子中,.h文件非常重複,有方法可以實現將.h出現次數減少。
  • 舉例
    • 這樣簡潔不少,但依賴關係也凌亂了(比如,要知道某個.o文件依賴哪些內容,就必須一個個看過去了)。
    • 魚和熊掌不可兼得,沒得法子。
objects = main.o kbd.o command.o display.o \
    insert.o search.o files.o utils.o

edit : $(objects)
    cc -o edit $(objects)

$(objects) : defs.h
kbd.o command.o files.o : command.h
display.o insert.o search.o files.o : buffer.h

.PHONY : clean
clean :
    rm edit $(objects)

2.6. 清空目標文件規則

  • 每個Makefile都應該應該對應一個clean,方便重新編譯。
    • 感覺類似於寫代碼都應該寫註釋,大家都知道應該這樣做,但……
  • 形式:
    • 第二種方式更爲穩健(但也沒寫啥東西……)。
    • -rm中多了一個-主要是爲了,如果某些文件除了問題也不要管,繼續做後面的事。
clean:
    rm edit $(objects)

.PHONY : clean
clean :
    -rm edit $(objects)

2.7. makefile裏有什麼

  • 顯式規則:說明了如何生成一個或多個目標文件。
    • 這是由Makefile的書寫者明顯指出要生成的文件、文件的依賴文件和生成的命令。
  • 隱晦規則:由於make有自動推導的功能,所以隱晦的規則可以讓我們比較簡略地書寫 Makefile。
  • 變量的定義:變量一般都是字符串,類似於C語言中的宏。
  • 文件指示:包括了三個部分
      1. 在一個Makefile中引用另一個Makefile,就像C語言中的include一樣
      1. 另一個是指根據某些情況指定Makefile中的有效部分,就像C語言中的預編譯#if一樣
      1. 多行的命令,後面再介紹
  • 註釋:Makefile中只有行註釋,和UNIX的Shell腳本一樣,其註釋是用 # 字符。
  • 其他:Makefile中的命令必須以TAB開頭。

2.8. Makefile 文件名

  • 默認情況下,會按“GNUmakefile”、“makefile”、“Makefile”這個循序尋找。
    • 建議使用Makefile,比較醒目。
    • 不推薦使用 GNUmakefile,因爲只支持GNU的make。
    • 大多數make都支持makefileMakefile
  • 也可以指定文件名,通過-f, --file來指定,如make -f Make.Linux

2.9. 引用其它的Makefile

  • 基本格式:include <filename>
  • filename可以是當前操作系統Shell的文件格式,包括路徑和通配符。
  • include前可以有空格,但不能是TAB。
  • 可以通過include多個文件,通過一個或多個空格隔開。
    • 例如include foo.make *.mk $(bar)
  • 原理:
    • 當make命令開始時,會尋找include所指出的其他Makefile,並把其內容安置在當前的位置,類似於C/C++中的#include
    • 如果文件都沒有絕對路徑,而是相對路徑,那麼首先會在當前目錄下找。如果找不到,則:
      • 如果make有指定參數-I, --include-dir,則會在這些目錄下去尋找。
      • 如果有<prefix>/include,例如/usr/local/bin/usr/include,make也會尋找。
    • 如果實在找不到:
      • 不會馬上報錯,而會生成warning信息。
      • 加載剩餘其他文件,完成makefile讀取,make會重新尋找,如果還是找不到,會出現致命錯誤。
      • 如果要跳過上面所說的致命信息,可通過-include <filename>實現。

2.10 環境變量MAKEFILES

  • 該環境變量的作用是:make將該變量中的值做一個類似於include的動作。
  • 該環境變量中的值是其他 Makefile,用空格分割。
  • 與普通include的區別:
    • 從這個環境變量中引入的target不會起作用。
    • 如果環境變量中定義的文件發現錯誤,make也不會理會。
  • 不建議使用
  • 放這是提醒大家,如果有錯,不妨看看是不是有人設置了這個環境變量。

2.11 make的工作方式

  • GNU make工作時執行的步驟如下:
    • 讀入所有的Makefile。
    • 讀入被include的其它Makefile。
    • 初始化文件中的變量。
    • 推導隱晦規則,並分析所有規則。
    • 爲所有的目標文件創建依賴關係鏈。
    • 根據依賴關係,決定哪些目標要重新生成。
    • 執行生成命令。
  • 1-5爲第一階段,6-7爲第二階段。
    • 第一個階段中,如果定義的變量被使用了,那麼make會把其展開在使用的位置。
    • make不會馬上展開,使用的策略是,如果變量出現在依賴關係的規則中,只有當前依賴關係決定要使用了,纔會內部展開。

3. 書寫規則

3.1. 概述

  • 規則包括兩部分:
    • 依賴關係
    • 生成目標的方法
  • 規則的順序很重要
    • Makefile只有一個最終目標,其他目標都是被這個目標帶出來,所以首先需要明確最終目標是什麼。
    • Makefile中目標很多,但第一條規則將被確立爲最終目標。
    • 如果第一條規則中目標數量很多,那第一個目標就是最終目標。
  • 規則舉例
    • 前兩章也有類似實例。
    • foo.o 是最終目標,foo.c defs.h 是目標依賴原文件,執行命令是cc -c -g foo.c
    • 由於foo.o依賴於foo.c defs.h,如果後兩者的文件日期比目標要新,或者目標不存在,則會執行命令。
foo.o: foo.c defs.h       # foo模塊
    cc -c -g foo.c

3.2. 規則詳解

  • targets是文件名、label,以空格分開,可以使用通配符。
  • command是命令行,如果不與target:prerequisites在一行,那必須以TAB開頭,如果在一行可以用分號分隔。
  • prerequisites是依賴文件或其他target,如果其中某個文件比當前target新,則當前target就被認爲是過時的,需要重新生成。
  • 命令太長可以通過 \ 作爲換行符。
targets : prerequisites
    command

# 或者

targets : prerequisites ; command
    command

3.3. 在規則中使用通配符

  • make支持三個通配符
    • *:任意長度任意字符,長度可以爲0。
    • ?:任意單個字符,必須是1個。
    • ~:表示$HOME。在Windows下沒有宿主目錄,那麼指的就是環境變量HOME的取值。
  • 如果要使用三個字符本身,而不是作爲通配符使用,可以通過轉義字符,\*, \?, \~
  • 通配符可以出現在command、依賴中。
  • 如果要在變量中使用通配符,不能直接通過 objects = *.o 的形式。
    • 如果通過上面這種形式,那變量就是 *.o 而不會展開。
    • 如果要定義所有.o文件的集合,可以使用 objects := $(wildcard *.o)
  • 有一個需求:想獲得所有.c文件對應的.o文件合集,該如何處理?
    • $(patsubst %.c, %.o, $(wildcard *.c))
    • patsubst是個模式替換函數,格式爲 $(patsubst <pattern>,<replacement>,<text> )
      • 查看<text>中的單詞(以空格、TAB、回車分隔)是否符合<pattern>,如果匹配則用<replacement>替換。

3.4. 文件搜索

  • 需求:源碼很多,分多個文件夾,make尋找依賴關係時可以加上搜索路徑,但最好的方法是吧一個路徑告訴make,讓其自動尋找。
  • VPATH
    • 如果沒有指定該變量,make只會在當前目錄中尋找依賴文件和目標文件。
    • 如果定義了這個變量,那麼make會在當前目錄找不到的情況下,去指定的目錄中尋找。
    • 舉例:VPATH = src:../headers
      • 這裏指定了兩個目錄src../headers,make會按這個順序尋找。
      • 多個目錄通過冒號分隔。
  • vpath
    • 這個與上面的不同,注意大小寫。
    • vpath不是變量,而是關鍵字,可以指定不同的文件在不同的目錄中搜索。
    • 基本用法:
      • vpath <pattern> <directories>:符合<pattern>的文件搜索目錄<directories>
      • vapth <pattern>:清除符合模式<pattern>的文件的搜索目錄。
      • vpath:清除所有已被設置好了的文件搜索目錄。
    • <pattern>中包含%字符,用於表示零活若干匹配
    • 舉例:
      • vpath %.h ../headers:如果在當前文件夾中找不到,make會在../headers目錄下搜索所有以.h結尾的文件。
      • 如果vpath出現多次,同一文件被多次匹配,那麼會按vpath的先後順序執行。如下面代碼中,如果搜索.c文件,則搜索順序爲 foo -> blish -> bar。
vpath %.c foo
vpath %   blish
vpath %.c bar

3.5. 僞目標

  • 所謂的僞目標,應該就是指target爲標籤的情況,例如clean
  • 特點
    • 僞目標不是一個文件,只是一個標籤,make無法生成它的依賴關係和決定是否需要執行。
    • 只能通過顯示地指明這個目標,才能讓其生效。
    • 僞目標不能與文件重名,否則就沒有意義了。
  • 爲了避免和文件重名的情況,可以通過 .PHONY 來顯示地指明一個目標是僞目標。
  • 僞目標一般沒有依賴文件,但也可以指定依賴文件,也可作爲默認目標。
  • 僞目標也可以作爲依賴。

3.6. 多目標

  • 多個目標同時依賴一個文件,且生成的命令大體相同。
  • 但由於多個目標生成規則的執行命令不是同一個,可能會帶來麻煩,但可以通過自動化變量 $@ 處理。
    • 自動化變量在後面的章節中有介紹。
  • 舉例
    • 其中,-$(subst output,,$@) 中的 $ 表示執行一個Makefile函數,函數名爲subst,後面的爲參數。該函數用於替換字符串。
    • $@ 表示目標的集合,就像一個數組,依次讀取目標並執行命令。
bigoutput littleoutput : text.g
    generate text.g -$(subst output,,$@) > $@

# 上面的命令等價於

bigoutput : text.g
    generate text.g -big > bigoutput
littleoutput : text.g
    generate text.g -little > littleoutput

3.7. 靜態模式

  • 基本結構
    • targets 定義了一系列目標文件,可以有通用符,是目標的集合。
    • target-pattern指明瞭targets的模式。
    • prereq-patterns是目標依賴的模式,對target-pattern形成的模式再進行一次依賴目標的定義。
<targets ...> : <target-pattern> : <prereq-patterns ...>
    <commands>
    ...
  • 舉個例子
    • $<$@ 都是自動化變量,前者表示第一個依賴文件,後者表示目標集合。
objects = foo.o bar.o

all: $(objects)

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


# 上面的例子等價於

foo.o : foo.c
    $(CC) -c $(CFLAGS) foo.c -o foo.o
bar.o : bar.c
    $(CC) -c $(CFLAGS) bar.c -o bar.o
  • 另外一個例子
    • filter 函數用於過濾 $files 集,只選擇其中符合%.o模式的內容。
files = foo.elc bar.o lose.o

$(filter %.o,$(files)): %.o: %.c
    $(CC) -c $(CFLAGS) $< -o $@
$(filter %.elc,$(files)): %.elc: %.el
    emacs -f batch-byte-compile $<

3.8. 自動生成依賴性

  • 在不使用自動生成依賴時:
    • 如果是一個大型項目,必須清楚知道每個源文件包含哪些頭文件,並且在加入/刪除頭文件時修改Makefile。
  • 編譯器的-M/-MM選項
    • 作用:自動尋找源文件中包含的頭文件,並生成一個依賴關係。
    • 在GNU的C/C++編譯器中要使用-MM選項,不然會包含一些標準庫的頭文件。
  • 如何將編譯器的功能與Makefile聯繫到一起
    • 讓Makefile依賴於源文件的方案並不現實。
    • GNU建議把編譯器爲每一個源文件的自動生成的依賴關係放到一個文件中,例如每個.c文件都對應一個.d文件。
  • 舉例
    • 細節
      • 所有 .d 文件都依賴於 .c 文件。
      • 先刪除所有目標,即.d文件。
      • 爲每個依賴文件 $<(即.c文件)生成依賴文件,$@表示模式%.d文件,$$$$表示隨機編號。
      • 第三行是一個替換命令,具體查看sed文件本身。
      • 第四行是刪除臨時文件。
    • 目標:在編譯器生成的依賴關係中加入.d文件的依賴,即把依賴關係 main.o : main.c defs.h 轉換爲 main.o main.d: main.c defs.h
      • 這樣.d文件會自動更新。
      • 之後吧這些生成的規則放到Makefile中,通過include命令。要注意順序,不然可能就會作爲make的默認target了。
%.d: %.c
    @set -e; rm -f $@; \
    $(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
    sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
    rm -f $@.$$$$

# 下面是通過include引入.d文件
sources = foo.c bar.c
include $(sources:.c=.d)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章