我們也說說Android.mk(4) - 依賴:目標編程的模式
另一種范式
我一直覺得猿推,Makefile確實是C/C++程序員的良配怕轿,因為Makefile所使用的兩種范式都是C/C++程序員不熟悉的勇皇,一種是函數(shù)式的思想雕凹,一種是依賴構(gòu)成的目標鏈的模式腐宋。
Makefile從最基本上來說喇完,可以抽象成下面這樣的:
target ... : prerequisites ...
command
...
...
如大家所熟悉的,這段的意義是:當(dāng)prerequisites有更新的時候蹬耘,執(zhí)行command命令芝雪。如果target是一個真實的目標,也就是對應(yīng)一個真實的文件综苔,那么就生成這個文件惩系。如果是偽目標,可以被用來做為一個入口如筛,比如clean堡牡,也可以成為一個真實目標的依賴。
可以明顯地分為兩個部分:一個是target依賴鏈的范式杨刨,這與過程式語言的C語言非常不同晤柄。用蔣軍的話講,跟Prolog有點像妖胀。有著它自己的一套邏輯系統(tǒng)可免。
后面的command,我們前面講了不少了做粤,我個人是希望大家以函數(shù)式的思想來寫。
我們落地到一個實際的例子中:
$(BUILT_SYSTEMIMAGE): $(FULL_SYSTEMIMAGE_DEPS) $(INSTALLED_FILES_FILE)
$(call build-systemimage-target,$@)
這個$(BUILT_SYSTEMIMAGE),是個真實的目標捉撮,對應(yīng)了要生成的文件system.img怕品,如下:
BUILT_SYSTEMIMAGE := $(systemimage_intermediates)/system.img
下面來看看,system.img所依賴目標巾遭,先看第一個肉康,結(jié)果這一個實際上又是兩個:
FULL_SYSTEMIMAGE_DEPS := $(INTERNAL_SYSTEMIMAGE_FILES) $(INTERNAL_USERIMAGES_DEPS)
然后闯估,我們發(fā)現(xiàn)這個依賴在一層層地擴張:
INTERNAL_SYSTEMIMAGE_FILES := $(filter $(TARGET_OUT)/%, \
$(ALL_PREBUILT) \
$(ALL_COPIED_HEADERS) \
$(ALL_GENERATED_SOURCES) \
$(ALL_DEFAULT_INSTALLED_MODULES) \
$(PDK_FUSION_SYSIMG_FILES) \
$(RECOVERY_RESOURCE_ZIP))
擴張還在繼續(xù),比如吼和,對于ALL_PREBUILT涨薪,各個模塊不斷地把自己的東西增加進去:
ALL_PREBUILT += $(TARGET_OUT)/bin/monkey
其他的目標原理相通,這里就不多浪費篇幅了炫乓。
總而言之刚夺,這一大套目標中,只要有任何一個有變化末捣,system.img就要重新生成了侠姑。如何生成?在下一行中寫著呢:調(diào)用build-systemimage-target啊箩做。
Makefile寫作指南
目標式和函數(shù)式兩種范式都學(xué)好了莽红,下面是我們?nèi)绾谓M織材料來完成我們的工程的時候了。
- 先定義總目標
Makefile的總的目的是輸出一個或多個結(jié)果文件邦邦,先把總的目標定義好安吁。
然后假設(shè)子目標都已經(jīng)構(gòu)建好了,下面寫一個將這些中間產(chǎn)品變成最終目標的腳本燃辖。
比如編譯一個簡單的C程序鬼店,總的目標是一個可執(zhí)行文件,最終加工時的材料是已經(jīng)編譯好的.o文件和輸入的第三方的庫文件等郭赐,我們先不管它們是如何編譯的薪韩,假設(shè)它們已經(jīng)做好了,我們只要寫一個鏈接的腳本就好了捌锭。
像我們上面的system.img的例子俘陷,反正依賴多,就分門別類的列吧观谦,最終我們只需要把它們打個包就好了拉盾。 - 層層分解,逐步完成
然后去尋找豁状,構(gòu)成這個大目標的第一層的構(gòu)件是什么捉偏,像上面我們所看過的一樣,逐層擴張泻红。
對于C文件夭禽,這時候才考慮每一個.o是如何從源文件編譯的。 - 模塊化谊路、函數(shù)化
上面兩步都是目標模式的讹躯,這一步開始搞函數(shù)模式了。將各目標中可重用的函數(shù)抽象出來,該分文件就分文件潮梯,該整理代碼就整理代碼等 - 測試調(diào)優(yōu)
當(dāng)一個工程大到一定程度的時候骗灶,Makefile的可讀性會嚴重下降。
這時候我們還是按目標式和函數(shù)式兩條主線來降低復(fù)雜度秉馏。目標是層次式的耙旦,我們可以一層一層地調(diào)試,比如先調(diào)從.c到.o的編譯過程萝究,再調(diào)將.o鏈接起來的總裝部分.
哪一個子模塊出問題免都,就專門調(diào)那一部分的。
對于功能部分糊肤,我們一直強調(diào)函數(shù)式思想就是希望琴昆,對于某一個確實性的輸入,能有一個確定性的輸出馆揉,沒有副作用业舍,這樣能夠?qū)⒄{(diào)試的難度降低,我們可以一個函數(shù)一個函數(shù)地調(diào)試升酣。
Makefile的調(diào)試以打日志為主舷暮,還可以通過make -p來輸出完整的變量和目標列表。
make -p噩茄,看看make都做了些什么
下面是我在cygwin下的make -p的輸出結(jié)果的節(jié)選
Make工具的信息
首先是Make工具匯報下自己的基礎(chǔ)情況:
The files is:main.cpp
# GNU Make 4.1
# Built for x86_64-unknown-cygwin
# Copyright (C) 1988-2014 Free Software Foundation, Inc.
# License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
# This is free software: you are free to change and redistribute it.
# There is NO WARRANTY, to the extent permitted by law.
# make 數(shù)據(jù)基礎(chǔ)下面,打印在 Tue May 3 17:54:36 2016
變量
下面是變量的列表,包含我們自己定義的绩聘,也包含make自動為我們生成的沥割。
# 變量
# 'override' directive
GNUMAKEFLAGS :=
# 自動
<D = $(patsubst %/,%,$(dir $<))
# 自動
?F = $(notdir $?)
# 默認
.SHELLFLAGS := -c
# makefile (from 'Makefile', line 30)
result_findString2 :=
# makefile
MAKEFLAGS = p
# 默認
CWEAVE = cweave
# 自動
?D = $(patsubst %/,%,$(dir $?))
# 環(huán)境
!:: = ::\
# 自動
@D = $(patsubst %/,%,$(dir $@))
# 環(huán)境
HOMEDRIVE = C:
# 自動
@F = $(notdir $@)
# 自動
^D = $(patsubst %/,%,$(dir $^))
# makefile
CURDIR := /cygdrive/d/working/codeBlocks/Hello
# makefile
SHELL = /bin/sh
# 默認
RM = rm -f
# 默認
CO = co
...
目錄信息
# 目錄
# SCCS:無法對其進行 stat 操作。
# . (設(shè)備 114478965凿菩,i-節(jié)點 1688849860268365):10 文件机杜, 19 不可能.
# RCS:無法對其進行 stat 操作。
# 10 文件衅谷, 19 不可能在 3 目錄中椒拗。
隱含規(guī)則信息
# 隱含規(guī)則。
%.out:
%.a:
%.ln:
%.o:
%: %.o
# recipe to execute (內(nèi)置):
$(LINK.o) $^ $(LOADLIBES) $(LDLIBS) -o $@
%.c:
%: %.c
# recipe to execute (內(nèi)置):
$(LINK.c) $^ $(LOADLIBES) $(LDLIBS) -o $@
%.ln: %.c
# recipe to execute (內(nèi)置):
$(LINT.c) -C$* $<
%.o: %.c
# recipe to execute (內(nèi)置):
$(COMPILE.c) $(OUTPUT_OPTION) $<
%.cc:
%: %.cc
# recipe to execute (內(nèi)置):
$(LINK.cc) $^ $(LOADLIBES) $(LDLIBS) -o $@
%.o: %.cc
# recipe to execute (內(nèi)置):
$(COMPILE.cc) $(OUTPUT_OPTION) $<
%.C:
%: %.C
# recipe to execute (內(nèi)置):
$(LINK.C) $^ $(LOADLIBES) $(LDLIBS) -o $@
%.o: %.C
# recipe to execute (內(nèi)置):
$(COMPILE.C) $(OUTPUT_OPTION) $<
%.cpp:
%: %.cpp
# recipe to execute (內(nèi)置):
$(LINK.cpp) $^ $(LOADLIBES) $(LDLIBS) -o $@
%.o: %.cpp
# recipe to execute (內(nèi)置):
$(COMPILE.cpp) $(OUTPUT_OPTION) $<
%.p:
%: %.p
# recipe to execute (內(nèi)置):
$(LINK.p) $^ $(LOADLIBES) $(LDLIBS) -o $@
%.o: %.p
# recipe to execute (內(nèi)置):
$(COMPILE.p) $(OUTPUT_OPTION) $<
...
文件目標和假目標
# 文件
# 不是一個目標:
.web.p:
# Builtin rule
# 對隱含規(guī)則的搜索尚未完成获黔。
# 從不檢查修改時間蚀苛。
# 文件尚未被更新。
# recipe to execute (內(nèi)置):
$(TANGLE) $<
# 不是一個目標:
.l.r:
# Builtin rule
# 對隱含規(guī)則的搜索尚未完成玷氏。
# 從不檢查修改時間堵未。
# 文件尚未被更新。
# recipe to execute (內(nèi)置):
$(LEX.l) $< > $@
mv -f lex.yy.r $@
all8:
# 假目標 (.PHONY的前提)盏触。
# 對隱含規(guī)則的搜索尚未完成渗蟹。
# 文件不存在侦厚。
# 文件尚未被更新。
# recipe to execute (from 'Makefile', line 66):
@echo $(filter-out default interpreter jit optimizing,xoc)
@echo $(filter-out default interpreter jit optimizing,default)
all9:
# 假目標 (.PHONY的前提)拙徽。
# 對隱含規(guī)則的搜索尚未完成。
# 文件不存在诗宣。
# 文件尚未被更新膘怕。
# recipe to execute (from 'Makefile', line 75):
$(eval ARCH_OF_BOOT_OAT := $(lastword $(subst /, ,$(dir $(BOOT_ART_SRC)))))
$(eval OAT_TEMP := $(PRODUCT_OUT)/data/dalvik-cache/temp-oat/system/framework/$(ARCH_OF_BOOT_OAT))
$(eval OAT_SRC := $(patsubst %.art,%.oat,$(BOOT_ART_SRC)))
$(eval OAT_DIST := $(patsubst %.art,%.oat,$(BOOT_ART_DST)))
@echo $(ARCH_OF_BOOT_OAT)
@echo $(OAT_TEMP)
@echo $(OAT_SRC)
@echo $(OAT_DIST)
...