Makefile 这个东西最早是在大学学习 Linux 下编写 C 程序的时候接触到的,但是,基本上都是用完就还给老师了。最近也是因为使用 skynet 框架开发服务器,才想起重新拾起这个技能。今天就以 skynet 里面的 makefile 为例,大概解读一遍。
make
在了解 Makefile 之前,需要先了解 make
,代码生成可执行文件的过程,会经历 编译
(源文件生成中间文件)和 链接
(中间文件合并生成可执行文件),而编译的安排(编译顺序),叫做 构建
(build)。make
便是常用的构建工具,主要用于 C 语言的项目。
make
维基百科的解释:在软件开发中,make是一个工具程序(Utility software),经由读取叫做 “makefile” 的文件,自动化建构软件。它是一种转化文件形式的工具,转换的目标称为 “target” ;与此同时,它也检查文件的依赖关系,如果需要的话,它会调用一些外部软件来完成任务。
它的依赖关系检查系统非常简单,主要根据依赖文件的修改时间进行判断。大多数情况下,它被用来编译源代码,生成结果代码,然后把结果代码连接起来生成可执行文件或者库文件。
它使用叫做 “makefile” 的文件来确定一个 target 文件的依赖关系,然后把生成这个 target 的相关命令传给 shell 去执行。
—— 维基百科 make
那么,可以简单理解为: make
是自动构建工具或依赖关系检查工具,而 makefile
的构建规则(依赖关系)文件。
make 程序被多次重写/改写衍生出来的结果:
GNU make
仿照make的标准功能(通过 clean-room 工程)重新改写,并加入作者觉得值得加入的新功能,常和 GNU 编译系统一起被使用,是大多数 GNU Linux 安装的一部分。
BSD make
它编译目标的时候有并发计算的能力。它在 FreeBSD ,NetBSD 和 OpenBSD 中不同程度的修改下存活了下来。
Microsoft nmake
微软的 nmake 是 Visual Studio 随附的命令行工具。
在非 Unix 系统下进行程序开发,例如: Windows ,集成开发环境(IDE)中已经继承了相应的构建工具,所以不再需要程序员手动去管理依赖关系检查。
Makefile 的通用格式:
# 注释内容 target ...: dependencies(或prerequisites) common 1 # 前面是一个 tab 距离而非空格 common 2 common 3前半段 \ common 3后半段 ... common n
以 "#"
开始的改行后续内容,只有单行注释;
可以使用“\”表示续行。注意,“\”之后不能有空格,例如命令3;
target 并非关键字,这是一个字符串,可以是我们要生成目标文件的名称,也可以是一个执行文件,还可以是一个或多个标签 (伪目标时使用)。一个 makefile 中可以有多个 target ,在 shell 中执行 make 指令时可以带上指定的 target 名称来指定要进行编译的 target ,例如:make test1
,假如 make 不带参数,则默认执行第一个 target 。target 也可以是要求make完成的动作,执行这种 target 后并不能得到和 target 同名的文件,因此,也称为伪 target(phony target) ;
dependencies(或prerequisites) 也不是关键字,而是生成 target 所需要依赖的文件名或前提目标名(target)列表。依赖可以为空,常用的 "clean" target
就常常没有依赖,只有命令;
common 1~common n 可以是任何一个能被 shell 运行的命令。
下面是测试样例:
test1: main.o test.o gcc -o test1 main.o test.o main.o: main.c main.h gcc -c main.c test.o: test.c test.h gcc -c test.c move: test1 mv test1 /usr/local
在 shell 中输入 make
或者 make test1
,会执行下述流程:
当 test1 这个 target 文件不存在,或 main.o 、test.o 这两个依赖文件被修改,则会执行后面的命令 gcc -o test1 main.o test.o
来重新生成 test1 文件;
而 main.o 依赖于 main.c 和 main.h ,make 会检查这两个文件是否有更新,假如有,则执行后面的命令 gcc -c main.c
来重新生成 main.o ;
同理检查 test.o 。
因此,同样的 make 指令,在不同情况下会执行的命令并非完全相同,取决于发生变化的文件情况。
假如在 shell 中输入 make move
,make 先检查 test1 文件是否是最新编译结果:
假如 test1 不是最新的,则自动执行 make test1
的过程;
假如 test1 是最新的,则执行 mv test1 /usr/local
将 test1 移至目录 /usr/local
下。
此时 target 便是伪 target ,不生成任何名字为 “move” 的目标文件,只是执行了一个 shell 命令。
从上述流程也可以看出:make 可以根据程序模块的修改情况重新编译链接目标代码,以保证目标文件总是由它的最新代码模块组成的。
用一个字符串代替另一个字符串,通常使用于用简短字符串代替比较长字符串且该字符串复用性较高的情况下。
使用 "(宏名)=(字符串内容)"
号来定义宏
使用 "$(宏名)"
来使用字符串,
使用 "$(宏名) += (追加内容)"
来追加宏的内容,
通常宏都是使用全部大写的规则,例如:
DEFOBJECTS = main.o test.o test1: $(DEFOBJECTS) gcc -o test1 $(DEFOBJECTS) main.o: main.c main.h gcc -c main.c test.o: test.c test.h gcc -c test.c move: test1 mv test1 /usr/local
大致了解了 makefile 的基础知识之后,我们开始来读一遍 skynet 中的 makefile 。查看 skynet 官方构建文档 了解到:
“skynet 所有代码以及引用的第三方库都可以被支持 C99 的编译器编译。所以你需要先安装 gcc 4.4及以上版本。(Clang 应该也没有问题)。它还需要 GNU Make 以运行 Makefile 脚本。”
gcc
是指 GNU 编译器套装(GNU Complier Collection),是一套编程语言编译器,原名 GNU C 语言编译器(GNU C Complier),因为其原本只能处理 C 语言,后来扩展了对其他语言(例如:C++ 、Java 和 Go 等)的支持,就成了现在的编译器套装。
所以,skynet 使用到的 make 版本是 GNU make ,直接打开 skynet 根目录下的 Makefile
文件:
include platform.mk LUA_CLIB_PATH ?= luaclib CSERVICE_PATH ?= cservice SKYNET_BUILD_PATH ?= . CFLAGS = -g -O2 -Wall -I$(LUA_INC) $(MYCFLAGS) # CFLAGS += -DUSE_PTHREAD_LOCK # lua LUA_STATICLIB := 3rd/lua/liblua.a LUA_LIB ?= $(LUA_STATICLIB) LUA_INC ?= 3rd/lua $(LUA_STATICLIB) : cd 3rd/lua && $(MAKE) CC='$(CC) -std=gnu99' $(PLAT) # jemalloc JEMALLOC_STATICLIB := 3rd/jemalloc/lib/libjemalloc_pic.a JEMALLOC_INC := 3rd/jemalloc/include/jemalloc all : jemalloc .PHONY : jemalloc update3rd MALLOC_STATICLIB := $(JEMALLOC_STATICLIB) $(JEMALLOC_STATICLIB) : 3rd/jemalloc/Makefile cd 3rd/jemalloc && $(MAKE) CC=$(CC) 3rd/jemalloc/autogen.sh : git submodule update --init 3rd/jemalloc/Makefile : | 3rd/jemalloc/autogen.sh cd 3rd/jemalloc && ./autogen.sh --with-jemalloc-prefix=je_ --disable-valgrind jemalloc : $(MALLOC_STATICLIB) update3rd : rm -rf 3rd/jemalloc && git submodule update --init # skynet CSERVICE = snlua logger gate harbor LUA_CLIB = skynet \ client \ bson md5 sproto lpeg LUA_CLIB_SKYNET = \ lua-skynet.c lua-seri.c \ lua-socket.c \ lua-mongo.c \ lua-netpack.c \ lua-memory.c \ lua-profile.c \ lua-multicast.c \ lua-cluster.c \ lua-crypt.c lsha1.c \ lua-sharedata.c \ lua-stm.c \ lua-mysqlaux.c \ lua-debugchannel.c \ lua-datasheet.c \ \ SKYNET_SRC = skynet_main.c skynet_handle.c skynet_module.c skynet_mq.c \ skynet_server.c skynet_start.c skynet_timer.c skynet_error.c \ skynet_harbor.c skynet_env.c skynet_monitor.c skynet_socket.c socket_server.c \ malloc_hook.c skynet_daemon.c skynet_log.c all : \ $(SKYNET_BUILD_PATH)/skynet \ $(foreach v, $(CSERVICE), $(CSERVICE_PATH)/$(v).so) \ $(foreach v, $(LUA_CLIB), $(LUA_CLIB_PATH)/$(v).so) $(SKYNET_BUILD_PATH)/skynet : $(foreach v, $(SKYNET_SRC), skynet-src/$(v)) $(LUA_LIB) $(MALLOC_STATICLIB) $(CC) $(CFLAGS) -o $@ $^ -Iskynet-src -I$(JEMALLOC_INC) $(LDFLAGS) $(EXPORT) $(SKYNET_LIBS) $(SKYNET_DEFINES) $(LUA_CLIB_PATH) : mkdir $(LUA_CLIB_PATH) $(CSERVICE_PATH) : mkdir $(CSERVICE_PATH) define CSERVICE_TEMP $$(CSERVICE_PATH)/$(1).so : service-src/service_$(1).c | $$(CSERVICE_PATH) $$(CC) $$(CFLAGS) $$(SHARED) $$< -o $$@ -Iskynet-src endef $(foreach v, $(CSERVICE), $(eval $(call CSERVICE_TEMP,$(v)))) $(LUA_CLIB_PATH)/skynet.so : $(addprefix lualib-src/,$(LUA_CLIB_SKYNET)) | $(LUA_CLIB_PATH) $(CC) $(CFLAGS) $(SHARED) $^ -o $@ -Iskynet-src -Iservice-src -Ilualib-src $(LUA_CLIB_PATH)/bson.so : lualib-src/lua-bson.c | $(LUA_CLIB_PATH) $(CC) $(CFLAGS) $(SHARED) -Iskynet-src $^ -o $@ -Iskynet-src $(LUA_CLIB_PATH)/md5.so : 3rd/lua-md5/md5.c 3rd/lua-md5/md5lib.c 3rd/lua-md5/compat-5.2.c | $(LUA_CLIB_PATH) $(CC) $(CFLAGS) $(SHARED) -I3rd/lua-md5 $^ -o $@ $(LUA_CLIB_PATH)/client.so : lualib-src/lua-clientsocket.c lualib-src/lua-crypt.c lualib-src/lsha1.c | $(LUA_CLIB_PATH) $(CC) $(CFLAGS) $(SHARED) $^ -o $@ -lpthread $(LUA_CLIB_PATH)/sproto.so : lualib-src/sproto/sproto.c lualib-src/sproto/lsproto.c | $(LUA_CLIB_PATH) $(CC) $(CFLAGS) $(SHARED) -Ilualib-src/sproto $^ -o $@ $(LUA_CLIB_PATH)/lpeg.so : 3rd/lpeg/lpcap.c 3rd/lpeg/lpcode.c 3rd/lpeg/lpprint.c 3rd/lpeg/lptree.c 3rd/lpeg/lpvm.c | $(LUA_CLIB_PATH) $(CC) $(CFLAGS) $(SHARED) -I3rd/lpeg $^ -o $@ clean : rm -f $(SKYNET_BUILD_PATH)/skynet $(CSERVICE_PATH)/*.so $(LUA_CLIB_PATH)/*.so cleanall: clean cd 3rd/jemalloc && $(MAKE) clean && rm Makefile cd 3rd/lua && $(MAKE) clean rm -f $(LUA_STATICLIB)
首先,include
字段引入了 platform.mk
文件:
include platform.mk
这是另外的一份 Makefile 文件,用于处理一些平台相关的配置信息:
PLAT ?= none PLATS = linux freebsd macosx CC ?= gcc .PHONY : none $(PLATS) clean all cleanall #ifneq ($(PLAT), none) .PHONY : default default : $(MAKE) $(PLAT) #endif none : @echo "Please do 'make PLATFORM' where PLATFORM is one of these:" @echo " $(PLATS)" SKYNET_LIBS := -lpthread -lm SHARED := -fPIC --shared EXPORT := -Wl,-E linux : PLAT = linux macosx : PLAT = macosx freebsd : PLAT = freebsd macosx : SHARED := -fPIC -dynamiclib -Wl,-undefined,dynamic_lookup macosx : EXPORT := macosx linux : SKYNET_LIBS += -ldl linux freebsd : SKYNET_LIBS += -lrt # Turn off jemalloc and malloc hook on macosx macosx : MALLOC_STATICLIB := macosx : SKYNET_DEFINES :=-DNOUSE_JEMALLOC linux macosx freebsd : $(MAKE) all PLAT=$@ SKYNET_LIBS="$(SKYNET_LIBS)" SHARED="$(SHARED)" EXPORT="$(EXPORT)" MALLOC_STATICLIB="$(MALLOC_STATICLIB)" SKYNET_DEFINES="$(SKYNET_DEFINES)"
下面简单解析一下:
宏定义部分:
PLAT ?= none PLATS = linux freebsd macosx CC ?= gcc SKYNET_LIBS := -lpthread -lm SHARED := -fPIC --shared EXPORT := -Wl,-E
=
、?=
、:=
和 +=
的区别是:
=
是最基本的赋值;
?=
是如果没有被赋值过就赋予等号后面的值;
:=
是覆盖之前的值,使用 :=
使得前面的變量不能使用後面的變量;
+=
是添加等号后面的值(追加内容)。
即 PLAT
定义当前的编译平台,PLATS
定义了当前支持的平台列表,CC ?= gcc
指定了当前使用的编译器是 gcc
。
$(MAKE)
适用于递归调用 make 命令的情况,make 命令将在每次调用时传递所有的标志。
伪目标:
以 .PHONY :
关键字用来显式地说明伪目标,伪目标都是用于定义一些只有命令操作没有目标文件生成的命令,即只有规则没有依赖的目标。是如何都会执行的目标。例如:
.PHONY : none $(PLATS) clean all cleanall
这里会同时说明了后面定义的 none
、linux
、 freebsd
、 macosx
、 clean
、 all
和 cleanall
这多个伪目标标签,伪目标的内容是:
none : @echo "Please do 'make PLATFORM' where PLATFORM is one of these:" @echo " $(PLATS)" linux : PLAT = linux macosx : PLAT = macosx freebsd : PLAT = freebsd macosx : SHARED := -fPIC -dynamiclib -Wl,-undefined,dynamic_lookup macosx : EXPORT := macosx linux : SKYNET_LIBS += -ldl linux freebsd : SKYNET_LIBS += -lrt # Turn off jemalloc and malloc hook on macosx macosx : MALLOC_STATICLIB := macosx : SKYNET_DEFINES :=-DNOUSE_JEMALLOC linux macosx freebsd : $(MAKE) all PLAT=$@ SKYNET_LIBS="$(SKYNET_LIBS)" SHARED="$(SHARED)" EXPORT="$(EXPORT)" MALLOC_STATICLIB="$(MALLOC_STATICLIB)" SKYNET_DEFINES="$(SKYNET_DEFINES)" all : jemalloc all : \ $(SKYNET_BUILD_PATH)/skynet \ $(foreach v, $(CSERVICE), $(CSERVICE_PATH)/$(v).so) \ $(foreach v, $(LUA_CLIB), $(LUA_CLIB_PATH)/$(v).so) clean : rm -f $(SKYNET_BUILD_PATH)/skynet $(CSERVICE_PATH)/*.so $(LUA_CLIB_PATH)/*.so cleanall: clean cd 3rd/jemalloc && $(MAKE) clean && rm Makefile cd 3rd/lua && $(MAKE) clean rm -f $(LUA_STATICLIB)
在这里
linux
、freebsd
、macosx
不再是目标文件名或执行文件,而是伪目标标签。
常见的伪目标:
all
是所有目标的伪目标,功能是编译所有目标clean
删除所有被 make 创建的文件install
安装已经编译好的程序,其实就是把目标执行文件拷贝到指定目标中print
这个伪目标的功能是列出改变过的源文件tar
把源程序打包备份,就是一个 tar 文件dist
创建一个压缩文件,一般把 tar 文件压缩成 Z 文件或 gz 文件TAGS
更新所有目标,以备完整的重编译使用check/test
测试 makefile 流程
平台判断:
#ifneq ($(PLAT), none) .PHONY : default default : $(MAKE) $(PLAT) #endif
#ifneq ($(PLAT), none)
指令用于判断括号内两个字符串是否不相等,即:PLAT
不为 none 则使用.PHONY
来说明后续的伪目标 default
。
要理解这里的逻辑需要知道 skynet 编译 make 指令是需要输入对应的编译平台的,例如:
make linux
此时会执行所有包含 linux
标签的目标,其中 linux : PLAT = linux
目标去给 PLAT
赋值,执行完第一个目标之后,根据扩展规则(分析依赖),顺序执行 makefile 中的目标,那么执行 PLAT ?= none
的时候由于 PlAT
已被赋值,则不会再被赋值为 none
,假如直接输入 make
,则 shell 会打印出:
$ Please do 'make PLATFORM' where PLATFORM is one of these: linux freebsd macosx
同名目标:
在 platform.mk
中有多个 macosx :
目标,这是用于进行平台区分的变量赋值的做法,即他们都属于 macosx
平台下才执行的内容,也就是说同一个目标可以有多个句,在 shell 中输入 make macosx
这些目标都会被执行。
特殊变量:
上面的大部分通过 .PHONY :
说明的伪目标,是必定会被执行的内容,其中在 linux macosx freebsd
中使用到了一个特殊的变量号 $@
,这是一个自动化变量,代码目标,例如 shell 中输入的是 make linux
则此时目标为 "linux" ,所以 $@
此时等价于 linux
。其他的特殊变量:
$@
代表目标文件
$^
代表所有依赖文件
$<
代表第一个依赖文件
例如简化范例:
DEFOBJECTS = main.o test.o test1: $(DEFOBJECTS) gcc -o $@ $^ main.o: main.c main.h gcc -c $< test.o: test.c test.h gcc -c $< move: test1 mv $^ /usr/local
由于在 platform.mk
最后调用 linux macosx freebsd
伪目标时,通过 $(MAKE) all
调用了 all
目标:
linux macosx freebsd : $(MAKE) all PLAT=$@ SKYNET_LIBS="$(SKYNET_LIBS)" SHARED="$(SHARED)" EXPORT="$(EXPORT)" MALLOC_STATICLIB="$(MALLOC_STATICLIB)" SKYNET_DEFINES="$(SKYNET_DEFINES)"
设置了一些跟平台相关的基本参数的内容,具体的内容在 Makefile 中,这才是真正编译流程的开始:
all : jemalloc all : \ $(SKYNET_BUILD_PATH)/skynet \ $(foreach v, $(CSERVICE), $(CSERVICE_PATH)/$(v).so) \ $(foreach v, $(LUA_CLIB), $(LUA_CLIB_PATH)/$(v).so)
all
目标包括两个部分依赖: jemalloc
和 skynet
,但其实 skynet
有依赖于 lua
源码,所以此过程需要编译三个部分内容:
jemalloc
:
这是一种内存分配算法,比 C 语言的 malloc 效率高。
# jemalloc JEMALLOC_STATICLIB := 3rd/jemalloc/lib/libjemalloc_pic.a JEMALLOC_INC := 3rd/jemalloc/include/jemalloc all : jemalloc .PHONY : jemalloc update3rd MALLOC_STATICLIB := $(JEMALLOC_STATICLIB) $(JEMALLOC_STATICLIB) : 3rd/jemalloc/Makefile cd 3rd/jemalloc && $(MAKE) CC=$(CC) 3rd/jemalloc/autogen.sh : git submodule update --init 3rd/jemalloc/Makefile : | 3rd/jemalloc/autogen.sh cd 3rd/jemalloc && ./autogen.sh --with-jemalloc-prefix=je_ --disable-valgrind jemalloc : $(MALLOC_STATICLIB) update3rd : rm -rf 3rd/jemalloc && git submodule update --init
大概的依赖链关系是:
jemalloc
依赖 MALLOC_STATICLIB
MALLOC_STATICLIB
依赖JEMALLOC_STATICLIB
JEMALLOC_STATICLIB
依赖 3rd/jemalloc/Makefile
,且会通过执行 cd 3rd/jemalloc && $(MAKE) CC=$(CC)
来找到 jemalloc
这个第三方库的 Makefile 文件来编译 jemalloc
3rd/jemalloc/Makefile
又依赖于 3rd/jemalloc/autogen.sh
,且需要执行 cd 3rd/jemalloc && ./autogen.sh --with-jemalloc-prefix=je_ --disable-valgrind
借助 git 来更新获取最新版本的 jemalloc
源码(即 3rd/jemalloc/autogen.sh
的内容)。
所以,此部分完成了 jemalloc 库的更新和编译。
skynet
:
# skynet CSERVICE = snlua logger gate harbor LUA_CLIB = skynet \ client \ bson md5 sproto lpeg LUA_CLIB_SKYNET = \ lua-skynet.c lua-seri.c \ lua-socket.c \ lua-mongo.c \ lua-netpack.c \ lua-memory.c \ lua-profile.c \ lua-multicast.c \ lua-cluster.c \ lua-crypt.c lsha1.c \ lua-sharedata.c \ lua-stm.c \ lua-mysqlaux.c \ lua-debugchannel.c \ lua-datasheet.c \ \ SKYNET_SRC = skynet_main.c skynet_handle.c skynet_module.c skynet_mq.c \ skynet_server.c skynet_start.c skynet_timer.c skynet_error.c \ skynet_harbor.c skynet_env.c skynet_monitor.c skynet_socket.c socket_server.c \ malloc_hook.c skynet_daemon.c skynet_log.c all : \ $(SKYNET_BUILD_PATH)/skynet \ $(foreach v, $(CSERVICE), $(CSERVICE_PATH)/$(v).so) \ $(foreach v, $(LUA_CLIB), $(LUA_CLIB_PATH)/$(v).so) $(SKYNET_BUILD_PATH)/skynet : $(foreach v, $(SKYNET_SRC), skynet-src/$(v)) $(LUA_LIB) $(MALLOC_STATICLIB) $(CC) $(CFLAGS) -o $@ $^ -Iskynet-src -I$(JEMALLOC_INC) $(LDFLAGS) $(EXPORT) $(SKYNET_LIBS) $(SKYNET_DEFINES) $(LUA_CLIB_PATH) : mkdir $(LUA_CLIB_PATH) $(CSERVICE_PATH) : mkdir $(CSERVICE_PATH) define CSERVICE_TEMP $$(CSERVICE_PATH)/$(1).so : service-src/service_$(1).c | $$(CSERVICE_PATH) $$(CC) $$(CFLAGS) $$(SHARED) $$< -o $$@ -Iskynet-src endef $(foreach v, $(CSERVICE), $(eval $(call CSERVICE_TEMP,$(v)))) $(LUA_CLIB_PATH)/skynet.so : $(addprefix lualib-src/,$(LUA_CLIB_SKYNET)) | $(LUA_CLIB_PATH) $(CC) $(CFLAGS) $(SHARED) $^ -o $@ -Iskynet-src -Iservice-src -Ilualib-src $(LUA_CLIB_PATH)/bson.so : lualib-src/lua-bson.c | $(LUA_CLIB_PATH) $(CC) $(CFLAGS) $(SHARED) -Iskynet-src $^ -o $@ -Iskynet-src $(LUA_CLIB_PATH)/md5.so : 3rd/lua-md5/md5.c 3rd/lua-md5/md5lib.c 3rd/lua-md5/compat-5.2.c | $(LUA_CLIB_PATH) $(CC) $(CFLAGS) $(SHARED) -I3rd/lua-md5 $^ -o $@ $(LUA_CLIB_PATH)/client.so : lualib-src/lua-clientsocket.c lualib-src/lua-crypt.c lualib-src/lsha1.c | $(LUA_CLIB_PATH) $(CC) $(CFLAGS) $(SHARED) $^ -o $@ -lpthread $(LUA_CLIB_PATH)/sproto.so : lualib-src/sproto/sproto.c lualib-src/sproto/lsproto.c | $(LUA_CLIB_PATH) $(CC) $(CFLAGS) $(SHARED) -Ilualib-src/sproto $^ -o $@ $(LUA_CLIB_PATH)/lpeg.so : 3rd/lpeg/lpcap.c 3rd/lpeg/lpcode.c 3rd/lpeg/lpprint.c 3rd/lpeg/lptree.c 3rd/lpeg/lpvm.c | $(LUA_CLIB_PATH) $(CC) $(CFLAGS) $(SHARED) -I3rd/lpeg $^ -o $@
这里执行的起点是 all
目标,从此建立其编译的依赖关系链:
all
目标依赖于 $(SKYNET_BUILD_PATH)/skynet
目标、$(foreach v, $(CSERVICE), $(CSERVICE_PATH)/$(v).so)
C 服务模块 、$(foreach v, $(LUA_CLIB),$(LUA_CLIB_PATH)/$(v).so)
lua 库模块(一些底层的基础模块);
$(SKYNET_BUILD_PATH)/skynet
目标,其实就是 .skynet
(除非重新自定义了SKYNET_BUILD_PATH
的目录) ,依赖于 $(foreach v, $(SKYNET_SRC), skynet-src/$(v))
skynet-src
目录下的 skynet 服务 C 源码、$(LUA_LIB)
Lua 源码 和 $(MALLOC_STATICLIB)
内存管理库。然后,执行 $(CC) $(CFLAGS) -o $@ $^ -Iskynet-src -I$(JEMALLOC_INC) $(LDFLAGS) $(EXPORT) $(SKYNET_LIBS) $(SKYNET_DEFINES)
来生成最终的 $@
对象,其实就是 skynet
服务器最终的执行文件;
这里会创建两个目录:mkdir $(LUA_CLIB_PATH)
和 mkdir $(CSERVICE_PATH)
分别用来放置 Lua 模块和 生 C 库模块成的 .so
中间文件,最终生成完 skynet
通过 clean
将这两个目录下的中间文件清除:
clean : rm -f $(SKYNET_BUILD_PATH)/skynet $(CSERVICE_PATH)/*.so $(LUA_CLIB_PATH)/*.so
lua
:
# lua LUA_STATICLIB := 3rd/lua/liblua.a LUA_LIB ?= $(LUA_STATICLIB) LUA_INC ?= 3rd/lua $(LUA_STATICLIB) : cd 3rd/lua && $(MAKE) CC='$(CC) -std=gnu99' $(PLAT)
由于 skynet
编译中依赖了 $(LUA_LIB)
,这就是 skynet 自带的修改版的 Lua5.3
的源码,与原版的区别是多了加载文件的缓存处理,减低启动一个 lua state 的成本。当然也可以直接使用原版的 Lua ,直接将 3rd/lua
改为自己的 Lua 地址即可。
这部分的逻辑比较简单,进入到 3rd/lua
目录中,使用 $(MAKE) CC='$(CC) -std=gnu99' $(PLAT)
指定 gnu99
的编译器来编译 Lua 源码,最终生成 3rd/lua/liblua.a
目标文件。
最后,当 skynet
执行文件编译完成后,会调用 cleanall
和 clean
来清理所有 .so
的中间文件,由于它们被 .PHONY
声明了所以是必然会执行的伪目标:
clean : rm -f $(SKYNET_BUILD_PATH)/skynet $(CSERVICE_PATH)/*.so $(LUA_CLIB_PATH)/*.so cleanall: clean
order-only Prerequisites
命令前提目标
上面写前提目标(依赖目标)的时候,出现了用 '|'
符号连接的情况,这里是分割两种类型的前提目标的符号:
正常前提目标(Normal Prerequisites
)此类前提目标内容发生变化时必须重新生成 target 目标,需要在每次编译立即更新到最终目标中的内容;
命令前提目标(order-only Prerequisites
)此类前提目标内容发生变化时不会引起 target 目标重新生成,属于在每次更新时无需立即更新到最终目标中的内容。
就像 jemalloc 中的 | 3rd/jemalloc/autogen.sh
,和 skynet 中的 | $(LUA_CLIB_PATH)
,它们都是第三方库,这部分内容通常不易变动,只会在最初编译的时候编译一次,之后假如这些库目标中的内容发生改变都,在重新编译 skynet 时不会立即自动引起最终目标(skynet 可执行文件)的重新编译生成,除非在 make 中进行目标指定。
通常的书写格式如下:
target : normal-prerequisites | order-only-prerequisites
竖线左边的正常前提目标列表可以是空。
foreach
语法
在 skynet 编译部分,使用了很多 $(foreach v, $(CSERVICE), $(CSERVICE_PATH)/$(v).so)
这样的语法,具体的作用就是从一个列表中遍历出内容,默认格式:$(foreach ,
,即从 list 中依次取出 var ,作用于 text。,
例如上述例子:从 CSERVICE
目录中取出 snlua
、logger
、 gate
和 harbor
内容,然后得到:cservice/snlua.so
、 cservice/logger.so
、 cservice/gate.so
和 cservice/harbor.so
这几个 .so
前提目标,而这些目标的生成过程在一下内容中定义:
$(CSERVICE_PATH) : mkdir $(CSERVICE_PATH) define CSERVICE_TEMP $$(CSERVICE_PATH)/$(1).so : service-src/service_$(1).c | $$(CSERVICE_PATH) $$(CC) $$(CFLAGS) $$(SHARED) $$< -o $$@ -Iskynet-src endef $(foreach v, $(CSERVICE), $(eval $(call CSERVICE_TEMP,$(v))))
同理可以解析 $(foreach v, $(LUA_CLIB), $(LUA_CLIB_PATH)/$(v).so)
,因此,all 目标最终可以得到很多 .so
的中间文件。
define
是定义命令包,以 endef
结尾,而 CSERVICE_TEMP
是命令包的名字,可以看做是宏,$$
代表真实的 $
而非调用变量。
此外,这里还用到了 eval
、 call
wildcard
和 addprefix
函数:
eval
:将字串应用到 Makefile 上下文,例如这里的 $(eval $(call CSERVICE_TEMP,$(v)))
就是为了让CSERVICE_TEMP
命令包中定义的 CSERVICE_PATH
所有 .so
的构建过程支持此 Makefile 中的全局调用;
call
:调用自定义宏,并传入参数,例如:$(call CSERVICE_TEMP,$(v))
就是将 $(v)
作为参数传入 CSERVICE_TEMP
命令包中,此参数最终会被赋值给 $(1)
变量;
wildcard
:扩展通配符,这里 ifneq (,$(wildcard 3rd/jemalloc/Makefile))
的目的是判断 jemalloc 中的 Makefile 文件是否还存在,存在的话对此库执行一次 clean
操作,然后移除 Makefile 文件;
addprefix
:加前缀,为传入参数中包含的每个文件名添加一个前缀,例如这里的 $(addprefix lualib-src/,$(LUA_CLIB_SKYNET))
就是对 LUA_CLIB_SKYNET
这个宏所包含的所有文件名添加一个 lualib-src/
目录前缀。
这里最终达到的效果就是动态生成 CSERVICE_PATH
所有 .so
的构建过程。
“C语言” 读书札记(六)之 Linux下C语言编程环境Make命令和Makefile
Makefile 中:= ?= += =的区别
Makefile有三个非常有用的变量。分别是,^,$<
Makefile依赖关系中的竖线“|” 和 Makefile 中,依赖关系里的一根竖线 "|" 是什么作用?
Makefile伪目标
讀一下skynet的Makefile
跟我一起写 Makefile
cloudwu/skynet/wiki/Build
维基百科 make