天天看點

ESP8266工程Makefile分析

最近重新溫習了一下《跟我一起學Makefile》的文檔,剛好又做了ESP8266的開發,正所謂學而不思則罔,時而不學者殆,是以就來總結下ESP8266裡面工程是怎麼編譯的,Makefile是怎麼玩的。當然我也沒全部搞懂,好多知識也是别人這樣說的我也就這麼說了。有什麼不對的地方望大家指出,交流學習學習,我菜鳥一枚哈

首先說一下我的工程目錄結構:
Project
	|--bin
	 	|--upgrade	 存放編譯出來的更新檔案	
	|--sdk      	 直接将原版SDK放到這裡的
	|--app       	 存放使用者代碼
		|--app    	 真正的使用者代碼
		|--driver  	 驅動代碼,官網上也有
		|--include   頭檔案
		|--user      隻放了user_main.c檔案(使用者函數入口),本來所有的使用者代碼應該放在這裡,但是我是移植舊的項目,就另起了一個app目錄
           

說完了目錄結構,再來講講編譯,很簡單,就是進入app目錄(第一級),shell下敲 ./gen_misc.sh就可以編出更新檔案了(實際我是在總目錄起了一個shell來完成)。當然我對這個腳本做了些改動的,原本的例子是需要首先修改兩個宏:

export SDK_PATH=$(pwd)/../sdk
export BIN_PATH=$(pwd)/../bin
           

另外,運作過程中會有些選項。我将這選項固定了,下面貼其中一個改動:

echo "Please check SDK_PATH & BIN_PATH, enter (Y/y) to continue:"
#read input
input=Y 
if [[ $input != Y ]] && [[ $input != y ]]; then
    exit
fi
           

這說完,該言歸正傳了,直接看到gen_misc.sh腳本最後的,首先make clean,然後make,并且将前面選項參數傳遞給該目錄下的makefile。

echo "start..."
echo ""

make clean

make BOOT=$boot APP=$app SPI_SPEED=$spi_speed SPI_MODE=$spi_mode SPI_SIZE_MAP=$spi_size_map
           

剛開始接觸的時候(很久沒摸makefile了),我去找makefile我是有點蒙的,app目錄(第一級)下makefile沒有一個目标檔案,僞目标都沒有,隻是定義了寫變量而已,看到最後才知道,原來是引用了sdk下的makefile:

INCLUDES := $(INCLUDES) -I $(PDIR)include -I $(PDIR)include/driver -I $(PDIR)include/app
sinclude $(SDK_PATH)/Makefile
           

正似懂非懂的時候,懵逼的又來了,user目錄的下makefie還有app目錄(第二級)(我抄user目錄的)和driver目錄的下的makefile都是簡單到令人咋舌:

ifndef PDIR
GEN_LIBS = libapp.a
endif

INCLUDES := $(INCLUDES) -I $(PDIR)include
PDIR := ../$(PDIR)
sinclude $(PDIR)Makefile
           

可以看到唯一不同的隻是GEN_LIBS這個變量,那麼問題來,全部都引用SDK目錄下的一個makefile(後面統稱主makefile),怎麼做到的呢,這就是今天的重點了。重點就是SDK目錄下這個主makefile,終于找到個稍微正常點的makefile了,雖然也是一堆看不到的東西,但是至少有僞目标,熟悉的all,clean:

all:	.subdirs $(OBJS) $(OLIBS) $(OIMAGES) $(OBINS) $(SPECIAL_MKTARGETS)
	
clean:
	$(foreach d, $(SUBDIRS), $(MAKE) -C $(d) clean;)
	$(RM) -r $(ODIR)/$(TARGET)/$(FLAVOR)

clobber: $(SPECIAL_CLOBBER)
	$(foreach d, $(SUBDIRS), $(MAKE) -C $(d) clobber;)
	$(RM) -r $(ODIR)

.subdirs:
	set -e; $(foreach d, $(SUBDIRS), $(MAKE) -C $(d);)
           

回到剛才講的前面app目錄(第一級),先執行make clean,跳轉到SDK目錄下的clean僞目标,先執行:

$(foreach d, $(SUBDIRS), $(MAKE) -C $(d) clean;)
           

這是一個for循環,将變量SUBDIRS中的參數依次代入到臨時變量d中,去執行$(MAKE) -C $(d) clean。我們可以在app目錄(第一級)中找到變量的定義:

ifndef PDIR # {
GEN_IMAGES= eagle.app.v6.out
GEN_BINS= eagle.app.v6.bin
SPECIAL_MKTARGETS=$(APP_MKTARGETS)
SUBDIRS=    \
	user    \
    driver	\
	app

endif # } PDIR
           

也就是分别在這三個目錄執行了make clean。正如前面所講,這三個目錄其實還是引用的主makefile,也就是繼續執行僞目标clean下的操作,但是第一個for循環不會再執行了,精妙之處就在于,SUBDIRS在makefile引用的過程中并未定義。我們來看user下的makefile最後兩句:

PDIR := ../$(PDIR)
sinclude $(PDIR)Makefile
           

PDIR被定義為上一級目錄,是以include了上一級目錄的makefile,也即是app目錄(第一級),注意看我前面貼出的代碼,SUBDIRS變量是在PDIR變量沒定義時候才被定義,而此時PDIR已被定義,是以app目錄(第一級)下的makefile再次跳轉到主makefile時,$(SUBDIRS)為空。是以for就不執行,而繼續執行rm操作,将編譯參數的檔案夾.output删除

這裡我們回想一下,這幾個makefile的調用相當于轉了一圈又回來了,而且中間夾着這變量控制,以前沒見過(可能我見識短淺吧),簡直秀的我頭皮發麻:

gen_misc.sh -> app目錄(第一級) -> 主makefile -> user下makefile -> app目錄(第一級) -> 主makefile

講完make clean執行流程,再來說make。make會找到第一個目标,也就是all來執行,all的依賴有很多,先說下第一個.subdirs這個依賴的僞目标,主要也是一個for循環:

$(foreach d, $(SUBDIRS), $(MAKE) -C $(d);)
           

跟前面的clean很像,也是分别進入三個目錄進行make,那麼它們最終會執行主makefile中的第一個目标all,還是一樣SUBDIRS未定義,是以.subdirs相當于沒用,接着執行到$(OBJS):

CSRCS ?= $(wildcard *.c)
CPPSRCS ?= $(wildcard *.cpp)
ASRCs ?= $(wildcard *.s)
ASRCS ?= $(wildcard *.S)
SUBDIRS ?= $(patsubst %/,%,$(dir $(wildcard */Makefile)))

ODIR := .output
OBJODIR := $(ODIR)/$(TARGET)/$(FLAVOR)/obj

OBJS := $(CSRCS:%.c=$(OBJODIR)/%.o) \
        $(CPPSRCS:%.cpp=$(OBJODIR)/%.o) \
        $(ASRCs:%.s=$(OBJODIR)/%.o) \
        $(ASRCS:%.S=$(OBJODIR)/%.o)

DEPS := $(CSRCS:%.c=$(OBJODIR)/%.d) \
        $(CPPSRCS:%.cpp=$(OBJODIR)/%.d) \
        $(ASRCs:%.s=$(OBJODIR)/%.d) \
        $(ASRCS:%.S=$(OBJODIR)/%.d)
           

看到這裡可能有人會說SUBDIRS不是在這裡定義了嗎,但是patsubst函數執行完後還是為空的。回到$(OBJS),這裡用到靜态模式,簡單的說就是将該目錄下的.c都找出來,替換成.o,編譯.o之前呢,首先需要include .d檔案:

ifneq ($(MAKECMDGOALS),clean)
ifneq ($(MAKECMDGOALS),clobber)
ifdef DEPS
sinclude $(DEPS)
endif
endif
endif
           

非clean和clobber目标時都會去include,此時就用下面的代碼自動生成依賴的 .d檔案:

$(OBJODIR)/%.d: %.c
	@mkdir -p $(OBJODIR);
	@echo DEPEND: $(CC) -M $(CFLAGS) $<
	@set -e; rm -f [email protected]; \
	$(CC) -M $(CFLAGS) $< > [email protected]$$$$; \
	sed 's,\($*\.o\)[ :]*,$(OBJODIR)/\1 [email protected] : ,g' < [email protected]$$$$ > [email protected]; \
	rm -f [email protected]$$$$
           

這是标準的生成流程哈,大緻就是 -M參數生成依賴關系導入到檔案中,至于裡面的随機檔案名,sed替換,删除中間檔案啥的操作,别問我哈。。。

然後就編譯.o了,使用隐含規則和自動化變量:

$(OBJODIR)/%.o: %.c
	@mkdir -p $(OBJODIR);
	$(CC) $(if $(findstring $<,$(DSRCS)),$(DFLAGS),$(CFLAGS)) $(COPTS_$(*F)) -o [email protected] -c $<
           

OBJS運作完了,接着是$(OLIBS),根據下面的代碼可得結果是.output/eagle/debug/lib/libuser.a:

LIBODIR := $(ODIR)/$(TARGET)/$(FLAVOR)/lib
OLIBS := $(GEN_LIBS:%=$(LIBODIR)/%)
           

但是Makefile沒有libuser.a這個目标啊,我又蒙了一會,然後看到這裡:

$(foreach lib,$(GEN_LIBS),$(eval $(call MakeLibrary,$(basename $(lib)))))

$(foreach image,$(GEN_IMAGES),$(eval $(call MakeImage,$(basename $(image)))))
           

有一個以前沒見過的進階函數eval函數,函數原型 $(eval text),它的意思是 text 的内容将作為makefile的一部分而被make解析和執行。call調用的MakeLibrary就會被展開在makefile裡面:

define MakeLibrary
DEP_LIBS_$(1) = $$(foreach lib,$$(filter %.a,$$(COMPONENTS_$(1))),$$(dir $$(lib))$$(LIBODIR)/$$(notdir $$(lib)))
DEP_OBJS_$(1) = $$(foreach obj,$$(filter %.o,$$(COMPONENTS_$(1))),$$(dir $$(obj))$$(OBJODIR)/$$(notdir $$(obj)))
$$(LIBODIR)/$(1).a: $$(OBJS) $$(DEP_OBJS_$(1)) $$(DEP_LIBS_$(1)) $$(DEPENDS_$(1))
	@mkdir -p $$(LIBODIR)
	$$(if $$(filter %.a,$$?),mkdir -p $$(EXTRACT_DIR)_$(1))
	$$(if $$(filter %.a,$$?),cd $$(EXTRACT_DIR)_$(1); $$(foreach lib,$$(filter %.a,$$?),$$(AR) xo $$(UP_EXTRACT_DIR)/$$(lib);))
	$$(AR) ru [email protected] $$(filter %.o,$$?) $$(if $$(filter %.a,$$?),$$(EXTRACT_DIR)_$(1)/*.o)
	$$(if $$(filter %.a,$$?),$$(RM) -r $$(EXTRACT_DIR)_$(1))
endef
           

這時,一個紅色字型.a映入眼簾,大概就能知道目标在這裡了,根據這段代碼來編出lib檔案

那另外剩下的三個依賴目标呢OIMAGES OBINS SPECIAL_MKTARGETS,其實這三個都是空字元串,道理跟之前一樣。是以連續循環三遍後,三個使用者目錄就搞定了,都編出lib出來了

這時又回到主makefile需要編譯下一個目标$(OIMAGES)了,同樣是采用了eval函數,代碼上面已經截圖過,調用的MakeImage

define MakeImage
DEP_LIBS_$(1) = $$(foreach lib,$$(filter %.a,$$(COMPONENTS_$(1))),$$(dir $$(lib))$$(LIBODIR)/$$(notdir $$(lib)))
DEP_OBJS_$(1) = $$(foreach obj,$$(filter %.o,$$(COMPONENTS_$(1))),$$(dir $$(obj))$$(OBJODIR)/$$(notdir $$(obj)))
$$(IMAGEODIR)/$(1).out: $$(OBJS) $$(DEP_OBJS_$(1)) $$(DEP_LIBS_$(1)) $$(DEPENDS_$(1))
	@mkdir -p $$(IMAGEODIR)
	$$(CC) $$(LDFLAGS) $$(if $$(LINKFLAGS_$(1)),$$(LINKFLAGS_$(1)),$$(LINKFLAGS_DEFAULT) $$(OBJS) $$(DEP_OBJS_$(1)) $$(DEP_LIBS_$(1))) -o [email protected] 
endef
           

執行了.output/eagle/debug/image/eagle.app.v6.out這個目标

接就是$(OBINS)這個目标了.output/eagle/debug/bin/eagle.app.v6.bin,代碼入口比較直接,代碼有很多,貼一點吧

$(BINODIR)/%.bin: $(IMAGEODIR)/%.out
	@mkdir -p $(BIN_PATH)
	@mkdir -p $(BINODIR)
           

到這裡,整個工程就編完了。講到這裡也差不多了,主要突出一個流程,裡面還有很多變量怎麼來,函數怎麼用的,都沒去細說,因為我也沒有的一個個分析。有興趣的可以看看裡面幾個函數的實作。

繼續閱讀