Skynet服务器框架(十一) 看懂 skynet 的 Makefile

Makefile 这个东西最早是在大学学习 Linux 下编写 C 程序的时候接触到的,但是,基本上都是用完就还给老师了。最近也是因为使用 skynet 框架开发服务器,才想起重新拾起这个技能。今天就以 skynet 里面的 makefile 为例,大概解读一遍。

什么是 Makefile

1. 什么是 make

在了解 Makefile 之前,需要先了解 make ,代码生成可执行文件的过程,会经历 编译 (源文件生成中间文件)和 链接 (中间文件合并生成可执行文件),而编译的安排(编译顺序),叫做 构建 (build)。make 便是常用的构建工具,主要用于 C 语言的项目。

make 维基百科的解释:

软件开发中,make是一个工具程序(Utility software),经由读取叫做 “makefile” 的文件,自动化建构软件。它是一种转化文件形式的工具,转换的目标称为 “target” ;与此同时,它也检查文件的依赖关系,如果需要的话,它会调用一些外部软件来完成任务。

它的依赖关系检查系统非常简单,主要根据依赖文件的修改时间进行判断。大多数情况下,它被用来编译源代码,生成结果代码,然后把结果代码连接起来生成可执行文件或者库文件

它使用叫做 “makefile” 的文件来确定一个 target 文件的依赖关系,然后把生成这个 target 的相关命令传给 shell 去执行。

—— 维基百科 make

那么,可以简单理解为: make 是自动构建工具或依赖关系检查工具,而 makefile 的构建规则(依赖关系)文件。

2. make 的不同版本

make 程序被多次重写/改写衍生出来的结果:

扫描二维码关注公众号,回复: 2367474 查看本文章
  • GNU make

    仿照make的标准功能(通过 clean-room 工程)重新改写,并加入作者觉得值得加入的新功能,常和 GNU 编译系统一起被使用,是大多数 GNU Linux 安装的一部分。

  • BSD make

    它编译目标的时候有并发计算的能力。它在 FreeBSDNetBSDOpenBSD 中不同程度的修改下存活了下来。

  • Microsoft nmake

    微软的 nmake 是 Visual Studio 随附的命令行工具。

在非 Unix 系统下进行程序开发,例如: Windows ,集成开发环境(IDE)中已经继承了相应的构建工具,所以不再需要程序员手动去管理依赖关系检查。

Makefile 基本语法

1.基础格式

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 运行的命令。

2. 示范样例:

下面是测试样例:


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 可以根据程序模块的修改情况重新编译链接目标代码,以保证目标文件总是由它的最新代码模块组成的。

3.宏

用一个字符串代替另一个字符串,通常使用于用简短字符串代替比较长字符串且该字符串复用性较高的情况下。

  • 使用 "(宏名)=(字符串内容)" 号来定义宏

  • 使用 "$(宏名)" 来使用字符串,

  • 使用 "$(宏名) += (追加内容)" 来追加宏的内容,

通常宏都是使用全部大写的规则,例如:


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

skynet 的 Makefile

大致了解了 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
ifneq (,$(wildcard 3rd/jemalloc/Makefile))
    cd 3rd/jemalloc && $(MAKE) clean && rm Makefile
endif
    cd 3rd/lua && $(MAKE) clean
    rm -f $(LUA_STATICLIB)

1. 平台相关设置:

首先,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   

    这里会同时说明了后面定义的 nonelinuxfreebsdmacosxcleanallcleanall 这多个伪目标标签,伪目标的内容是:

    
    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
    ifneq (,$(wildcard 3rd/jemalloc/Makefile))
        cd 3rd/jemalloc && $(MAKE) clean && rm Makefile
    endif
        cd 3rd/lua && $(MAKE) clean
        rm -f $(LUA_STATICLIB)

    在这里 linuxfreebsdmacosx 不再是目标文件名或执行文件,而是伪目标标签。

    常见的伪目标:

    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

2. 编译流程:

由于在 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 目标包括两个部分依赖: jemallocskynet ,但其实 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 执行文件编译完成后,会调用 cleanallclean 来清理所有 .so 的中间文件,由于它们被 .PHONY 声明了所以是必然会执行的伪目标:

clean :
    rm -f $(SKYNET_BUILD_PATH)/skynet $(CSERVICE_PATH)/*.so $(LUA_CLIB_PATH)/*.so
cleanall: clean

3. 其他

  • 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 <var>,<list>,<text>) ,即从 list 中依次取出 var ,作用于 text。

    例如上述例子:从 CSERVICE 目录中取出 snlualoggergateharbor 内容,然后得到:cservice/snlua.socservice/logger.socservice/gate.socservice/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 是命令包的名字,可以看做是宏,$$ 代表真实的 $ 而非调用变量。

    此外,这里还用到了 evalcall wildcardaddprefix 函数:

    • 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 的构建过程。

参考

猜你喜欢

转载自blog.csdn.net/linshuhe1/article/details/79391031