一、makefile快速开始

1. 什么是makefile、make

Makefile是一个名为make软件所需要的脚本文件,该脚本文件可以指导make软件控制gcc等工具链去编译工程文件最终得到可执行文件,几乎所有的Linux发行版都内置了GNU-Make软件,VScode等多种IED也内置了Make程序。

你见到的xxx.mk文件或者Makefile都统称为Makefile脚本文件。大多数的make都支持“makefile”和“Makefile”这两种默认文件名称。

一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。

make是一个命令工具,它解释Makefile 中的指令。Makefile用来调用各种命令做自动化构建工具非常方便。

1.1 make命令 工作流程

  1. 读入所有的Makefile。
  2. 读入被include的其它Makefile。
  3. 初始化文件中的变量。
  4. 推导隐晦规则,并分析所有规则。
  5. 为所有的目标文件创建依赖关系链。
  6. 根据依赖关系,决定哪些目标要重新生成。
  7. 执行生成命令。

当我们在执行 make 条命令的时候,make 就会去当前文件下找要执行的编译规则,也就是 Makefile 文件。我们编写 Makefile 的时可以使用的文件的名称 “GNUmakefile” 、“makefile” 、“Makefile” ,make 执行时回去寻找 Makefile 文件,推荐使用Makefile。

1.2 关于程序的编译和链接

一般来说,无论是C、C++、还是pas,首先要把源文件编译成中间代码文件,在Windows下也就是 .obj 文件,UNIX下是 .o 文件,即 Object File,这个动作叫做编译(compile)。然后再把大量的Object File合成执行文件,这个动作叫作链接(link)。

编译时,编译器需要的是语法的正确,函数与变量的声明的正确。对于后者,通常是你需要告诉编译器头文件的所在位置(头文件中应该只是声明,而定义应该放在C/C++文件中),只要所有的语法正确,编译器就可以编译出中间目标文件。一般来说,每个源文件都应该对应于一个中间目标文件(O文件或是OBJ文件)。

链接时,主要是链接函数和全局变量,所以,我们可以使用这些中间目标文件(O文件或是OBJ文件)来链接我们的应用程序。链接器并不管函数所在的源文件,只管函数的中间目标文件(Object File),在大多数时候,由于源文件太多,编译生成的中间目标文件太多,而在链接时需要明显地指出中间目标文件名,这对于编译很不方便,所以,我们要给中间目标文件打个包,在Windows下这种包叫“库文件”(LibraryFile),也就是 .lib文件,在UNIX下,是Archive File,也就是.a文件。

总结: 源文件首先会生成中间目标文件,再由中间目标文件生成执行文件。在编译时,编译器只检测程序语法,和函数、变量是否被声明。如果函数未被声明,编译器会给出一个警告,但可以生成Object File。而在链接程序时,链接器会在所有的Object File中找寻函数的实现,如果找不到,那到就会报链接错误码(Linker Error)

2. 掌握 Makefile 常用语法基础

2.1 Makefile 规则语法

规则:用于说明如何生成一个或多个目标文件
规则的格式:

targets:prerequisites
    command
目标: 依赖
[tab键]命令
  • target 目标
    可以是一个object file(目标文件),也可以是一个执行文件,还可以是一个标签(伪目标)。

  • prerequisites
    生成该target所依赖的文件或target。生成目标所要依赖的文件,可能不止一个。

  • command
    该target要执行的命令(任意的shell命令)。生成目标所要执行的命令,注意所有的命令必须要以tab 键开头。

总结:target这一个或多个的目标文件依赖于prerequisites中的文件, 其生成规则定义在command中。

2.2 伪目标 | Makefile中.PHONY的作用

.PHONY(伪目标)

伪目标是这样一个目标:它不代表一个真正的文件名,在执行make时可以指定这个目标来执行所在规则定义的命令,有时也可以将一个伪目标称为标签。伪目标通过PHONY来指明。

如果我们指定的目标不是创建目标文件,而是使用makefile执行一些特定的命令,例如:

clean:
        rm *.o temp

我们希望,只要输入make clean后,rm *.o temp命令就会执行。但是,当前目录中存在一个和指定目标重名的文件时,例如clean文件,结果就不是我们想要的了。输入make clean后,rm *.o temp 命令一定不会被执行。

解决的办法:将目标clean定义成伪目标就成了。 无论当前目录下是否存在clean这个文件,输入make clean后,rm *.o temp命令都会被执行。

这种做法的带来的好处还不止此,它同时提高了make的执行效率,因为将clean定义成伪目标后,make的执行程序不会试图寻找clean的隐含规则。

举例如下:

clean:
	rm -rf $(CLEAN) $(BINS)
.PHONY: clean

2.3 变量赋值

Makefile变量的定义和使用
参考URL: http://c.biancheng.net/view/7096.html

变量的使用在我们的 Makefile 编写中还是非常广泛的,可以说我们的 Makefile 中必不可少的东西。

Makefile 的变量的四种基本赋值方式:

  • 简单赋值 ( := ) 编程语言中常规理解的赋值方式,只对当前语句的变量有效。
  • 递归赋值 ( = ) 赋值语句可能影响多个变量,所有目标变量相关的其他变量都受影响。
  • 条件赋值 ( ?= ) 如果变量未定义,则使用符号中的值定义变量。如果该变量已经赋值,则该赋值语句无效。
  • 追加赋值 ( += ) 原变量用空格隔开的方式追加一个新值。
  1. 简单赋值
x:=foo
y:=$(x)b
x:=new
test:
        @echo "y=>$(y)"
        @echo "x=>$(x)"

:=与我们实际上更符合我们平时写代码的思维,也就是顺序声明变量,使用这种赋值方式,前面的变量不能使用后面的变量,只能使用前面已定义好了的变量。
比较常用。

  1. 递归赋值
x=foo
y=$(x)b
x=new
test:
        @echo "y=>$(y)"
        @echo "x=>$(x)"

结果:

[root@centos7 ~]# make test
y=>newb
x=>new

使用= 需要注意2点:
1)这种赋值方式是有先后顺序的,后面的赋值会覆盖掉前面的赋值。因此这种赋值方式,比较好用。不太符合我们的书写逻辑。
2) 右侧中的变量不一定非要是已定义好的值,其也可以使用后面定义的值。

x = $(y)
y = hello
  1. 条件赋值
x:=foo
y:=$(x)b
x?=new
test:
        @echo "y=>$(y)"
        @echo "x=>$(x)"
  1. 追加赋值
x:=foo
y:=$(x)b
x?=new
test:
        @echo "y=>$(y)"
        @echo "x=>$(x)"
  1. 追加赋值
x:=foo
y:=$(x)b
x+=$(y)
test:
        @echo "y=>$(y)"
        @echo "x=>$(x)"

结果:

[root@centos7 ~]# make test
y=>foob
x=>foo foob

+=的含义是:为变量追加值,但是如果变量之前没有没定义过,那么+=会自动转化为=,如果前面变量的定义形式为:=那么+=会以:=为其赋值。

+= 连接的字符串中间有空格。

总结:
Makefile中并不要求赋值运算符两边一定要有空格或者无空格,这一点比shell的格式要求要松一些。
不同的赋值方式会产生不同的结果,我们使用的时候应该根据具体的情况选择相应的赋值规则。

自动变量(Automatic Variables)

除了上面提到的几种形式的变量外,make还提供了一系列的自动变量,这些自动变量是根据make自动根据规则生成的,不需要显示指出相应的文件或目标名。
下面列出一些比较常见的自动变量的形式:
在这里插入图片描述(1)$@

$@指代当前目标,就是Make命令当前构建的那个目标。比如,make foo的 $@ 就指代foo。

a.txt b.txt: 
    touch $@

等同于下面的写法。

a.txt:
    touch a.txt
b.txt:
    touch b.txt

(2)$*

如果目标文件的后缀是make所识别的,那么"$*"就是除了后缀的那一部分

内置变量(Implicit Variables)

Make命令提供一系列内置变量,比如, ( C C ) 指 向 当 前 使 用 的 编 译 器 , (CC) 指向当前使用的编译器, (CC)使(MAKE) 指向当前使用的Make工具。这主要是为了跨平台的兼容性,详细的内置变量清单见手册

output:
    $(CC) -o output input.c

2.4 makefile条件语句

日常使用 Makefile 编译文件时,要根据判断,分条件执行语句。

关键字 功能
ifeq 判断参数是否不相等,相等为 true,不相等为 false
ifneq 判断参数是否不相等,不相等为 true,相等为 false
ifdef 判断是否有值,有值为 true,没有值为 false
ifndef 判断是否有值,没有值为 true,有值为 false

2.5 Makefile 常用函数

参考URL: http://www.ruanyifeng.com/blog/2015/02/make.html

Makefile 还可以使用函数,格式如下。

$(function arguments)
# 或者
${function arguments}

(1)shell 函数

ifeq ($(origin OUTPUT_DIR),undefined)
OUTPUT_DIR := $(ROOT_DIR)/_output
$(shell mkdir -p $(OUTPUT_DIR))
endif

shell 函数用来执行 shell 命令

​ (2) makefile word函数

$(word 1,hello jello yello)

上面的语句执行后的结果为hello,意为取字符串的第一个单词

用来取单词的函数

​ (3) subst

用法是$(subst FROM,TO,TEXT),即将TEXT中的东西从FROM变为TO

​ (4) eval函数

函数原型:
$(eval text)
它的意思是 text 的内容将作为makefile的一部分而被make解析和执行。
需要注意的是该函数在执行时会对它的参数进行两次展开,第一次展开是由函数本身完成,第二次是函数展开后的结果被作为makefile内容时由make解析时展开.

(5) addprefix 函数

addprefix 是makefile中的函数,是添加前缀的函数

$(addprefix PREFIX,NAMES…)
函数名称:加前缀函数—addprefix。
函数功能:为“NAMES…”中的每一个文件名添加前缀“PREFIX”。参数“NAMES…”
是空格分割的文件名序列,将“SUFFIX”添加到此序列的每一个文件名
之前。
返回值:以单空格分割的添加了前缀“PREFIX”的文件名序列。

例如:
$(addprefix src/,foo bar)

返回值为“src/foo src/bar”.

(6) wildcard 函数

在Makefile规则中,通配符会被自动展开。但在变量的定义和函数引用时,通配符将失效。这种情况下如果需要通配符有效,就需要使用函数“wildcard”,它的用法是:$(wildcard PATTERN…) 。

一般我们可以使用“$(wildcard *.c)”来获取工作目录下的所有的.c文件列表。

(7) foreach函数
foreach作为makefile中的函数,相当于一个循环函数。循环处理文件列表。

 $(foreach var text commond)

var:局部变量
text:文件列表,空格隔开,每一次取一个值赋值为变量var
commond:对var变量进行操作(一般会使用var变量,不然没意义),每次操作结果都会以空格隔开,最后返回空格隔开的列表。

(8)notdir函数
notdir用于去掉文件的绝对路径,只保留文件名。剥离文件的绝对路径,只保留文件名。

$(notdir 文件列表)

(9)反过滤函数—filter-out。

$(filter-out PATTERN…,TEXT) 

函数名称 :反过滤函数—filter-out。

函数功能 :和“filter”函数实现的功能相反。过滤掉字串“TEXT”中所有符合模式“PATTERN”的单词,保留所有不符合此模式的单词。可以有多个模式。存在多个模式时,模式表达式之间使用空格分割。

返回值 :空格分割的“TEXT”字串中所有不符合模式“PATTERN”的字串。

函数说明: “filter-out”函数也可以用来去除一个变量中的某些字符串(实现和“filter”函数相反)。

objects=main1.o foo.o main2.o bar.o 
mains=main1.o main2.o 

$(filter-out $(mains),$(objects)) 

实现了去除变量“objects”中“mains”定义的字串(文件名)功能。它的返回值为“foo.o bar.o”。

6-9 实战综合demo

# Determine image files by looking into build/docker/*/Dockerfile
IMAGES_DIR ?= $(wildcard ${ROOT_DIR}/build/docker/*)
# Determine images names by stripping out the dir names
IMAGES ?= $(filter-out tools,$(foreach image,${IMAGES_DIR},$(notdir ${image})))
函数名 功能描述
$(origin ) 告诉变量的“出生情况”,有如下返回值:
  • undefined: 从来没有定义过
  • default: 是一个默认的定义
  • environment: 是一个环境变量
  • file: 这个变量被定义在 Makefile中
  • command line: 这个变量是被命令行定义的
  • override: 是被 override 指示符重新定义的
  • automatic: 是一个命令运行中的自动化变量
$(addsuffix ,<names…>) 把后缀加到中的每个单词后面,并返回加过后缀的文件名序列。
$(addprefix ,<names…>) 把前缀加到中的每个单词前面,并返回加过前缀的文件名序列。
$(wildcard ) 扩展通配符,例如:$(wildcard ${ROOT_DIR}/build/docker/*)
$(word , ) 取字符串 中第个单词(从一开始),并返回字符串 中第个单词。如 比 中的单词数要大,那么返回空字符串
$(subst ,, ) 把字串 中的 字符串替换成 ,并返回被替换后的字符串
$(eval ) 的内容将作为makefile的一部分而被make解析和执行。
$(firstword ) 取字符串 中的第一个单词,并返回字符串 的第一个单词
$(lastword ) 取字符串 中的最后一个单词,并返回字符串 的最后一个单词
$(abspath ) 中的各路径转换成绝对路径,并将转换后的结果返回
$(shell cat foo) 执行操作系统命令,并返回操作结果
$(info <text …>) 输出一段信息
$(warning <text …>) 出一段警告信息,而 make 继续执行
$(error <text …>) 产生一个致命的错误,<text …> 是错误信息
$(filter <pattern…>, ) 以模式过滤 字符串中的单词,保留符合模式的单词。可以有多个模式。返回符合模式的字串
$(filter-out <pattern…>, ) 以模式过滤 字符串中的单词,去除符合模式的单词。可以有多个模式,并返回不符合模式的字串
$(dir <names…>) 从文件名序列中取出目录部分。目录部分是指最后一个反斜杠(/)之前的部分。返回文件名序列的目录部分。
$(notdir <names…>) 从文件名序列中取出非目录部分。非目录部分是指最後一个反斜杠(/)之后的部分。返回文件名序列的非目录部分。
$(strip ) 去掉字串中开头和结尾的空字符,并返回去掉空格后的字符串
$(suffix <names…>) 从文件名序列中取出各个文件名的后缀。返回文件名序列的后缀序列,如果文件没有后缀,则返回空字串。
$(foreach ,, ) 把参数中的单词逐一取出放到参数所指定的变量中,然后再执行 所包含的表达式。每一次 会返回一个字符串,循环过程中 的所返回的每个字符串会以空格分隔,最后当整个循环结束时, 所返回的每个字符串所组成的整个字符串(以空格分隔)将会是foreach函数的返回值。

2.6 makefile日志打印调试

使用info/warning/error增加调试信息
info

$(info “here add the debug info”)

注,info信息,不打印信息所在行号
warning

$(warning “here add the debug info”)

error

$(error “error: this will stop the compile”)

这个可以停止当前makefile的编译

打印变量的值

$(info $(TARGET_DEVICE) )

使用echo增加调试信息

注:echo只能在target:后面的语句中使用,且前面是个TAB,

验证demo:

parent_dir_fname:=$(lastword $(MAKEFILE_LIST))

$(info "xxx" $(parent_dir_fname) )


all:
        @echo $(parent_dir_fname)

2.7 其他常见必看基础

  • .DEFAULT_GOAL := all
    all应该是make文件中定义的第一个目标,或者被指定为默认目标。

这是一个惯例,你提供了一个“全部”规则,GNU Make版本3.81引入了一个名为.DEFAULT_GOAL的特殊变量,可用于告知如果在命令行中未指定目标,应该构建哪个目标(或目标)

.DEFAULT_GOAL := all

通过.DEFAULT_GOAL实际上可以在Makefile中的其他位置读取$(.DEFAULT_GOAL)的值。测试表明,它是否明确设置或者是否通过选择第一条规则来确定没有区别。

all目标通常是makefile中的第一个目标,因为如果你只是在命令行写入make ,而不指定目标,它将会build立第一个目标。 你期望它是all 。

all通常也是一个.PHONY目标。
GNU Make手册给出了标准目标列表中 all的清晰定义。

如果Makefile的作者遵循这个约定,那么目标应该是:
- 编译整个程序,但不能编译文档。
- 成为默认的目标。all应该是make文件中定义的第一个目标,或者被指定为默认目标 .DEFAULT_GOAL := all

  • include 引用其它makefile文件
include scripts/make-rules/common.mk
  • Makefile 变量 MAKEFILE_LIST
    makefile使用MAKEFILE_LIST获取inlcude文件所在的目录
parent_dir_fname:=$(shell pwd)/$(lastword $(MAKEFILE_LIST))

all:
	@echo $(parent_dir_fname)

利用shell dirname命令
dirname命令去除文件名中的非目录部分,仅显示与目录有关的内容。dirname命令读取指定路径名保留最后一个/及其后面的字符,删除其他部分,并写结果到标准输出。如果最后一个/后无字符,dirname 命令使用倒数第二个/,并忽略其后的所有字符

  • @$(MAKE)
    make 定义了很多默认变量,像常用的命令或者是命令选项之类的,什么CC啊,CFLAGS啊之类。
    ${MAKE} 就是预设的 make 这个命令的名称(或者路径)。
    make -p可以查看所有预定义的变量的当前值。

  • makefile export的用法 //设置环境变量

export FLASK_ENV=dev
export FLASK_DEBUG=1

dev:
    @echo $(FLASK_ENV)
    @echo $(FLASK_DEBUG)

运行make dev时则返回:

$ make dev
dev

需求:在一个目标中设置一个环境变量,在依赖目标或命令中使用该环境变量。

因为在依赖目标或命令中经常需要根据不同的变量值做不同的处理。

设置环境变量值有三种方法:

  1. Makefile中直接: export 变量
  2. 目标命令中,在命令前:变量=值 command
  3. 单独定义一个目标(不能有命令),目标1: export 变量=值
  • makefile define
    对makefile里的define补充说明
    参考URL: https://blog.csdn.net/seven_tree/article/details/106060613
    makefile里可能会用到define来打包一些可能会重用的指令,但是因为makefile里实际上会重用代码的情况并不多

  • Makefile之文件名操作函数 dir
    取目录函数——dir

解释: 从文件名序列中取出目录部分。目录部分是指最后一个反斜杠(“/”)之前的部分
示例: $(dir src/foo.c hacks)返回值是“src/, ./”。
注意: 函数说明:如果文件名中没有斜线,认为此文件为当前目录(“./” )下的文件。

  • Makefile之origin函数
    origin 函数不像其它的函数,他并不操作变量的值,他只是告诉你你的这个变量是哪里来的?其语法是:
    $(origin )

注意,是变量的名字,不应该是引用。所以你最好不要在中使用“$”字符。Origin 函数会以其返回值来告诉你这个变量的“出生情况”,下面,是 origin函数的返回值:
“undefined”
如果从来没有定义过,origin 函数返回这个值“undefined”。

ifeq ($(origin VERSION), undefined)
VERSION := $(shell git describe --tags --always --match='v*')
endif
  • make 定义了很多默认变量,${MAKE} 就是预设的 make 这个命令的名称(或者路径)。
    make -p 可以查看所有预定义的变量的当前值。
    @表示在执行命令时不输出命令本身(不回显),只输出命令执行的结果。

3. makefile demo举例

我们以fastdfs libfastcommon项目举例:

.SUFFIXES: .c .o .lo

COMPILE = $(CC) $(CFLAGS)
INC_PATH = 
LIB_PATH = $(LIBS)
TARGET_LIB = $(TARGET_PREFIX)/$(LIB_VERSION)

FAST_SHARED_OBJS = hash.lo chain.lo shared_func.lo ini_file_reader.lo \
                   logger.lo sockopt.lo base64.lo sched_thread.lo \
                   http_func.lo md5.lo pthread_func.lo local_ip_func.lo \
                   avl_tree.lo ioevent.lo ioevent_loop.lo fast_task_queue.lo \
                   fast_timer.lo process_ctrl.lo fast_mblock.lo \
                   connection_pool.lo fast_mpool.lo fast_allocator.lo  \
                   fast_buffer.lo multi_skiplist.lo flat_skiplist.lo \
                   system_info.lo fast_blocked_queue.lo id_generator.lo \
				   char_converter.lo char_convert_loader.lo common_blocked_queue.lo \
                   multi_socket_client.lo skiplist_set.lo json_parser.lo \
                   buffered_file_writer.lo

FAST_STATIC_OBJS = hash.o chain.o shared_func.o ini_file_reader.o \
                   logger.o sockopt.o base64.o sched_thread.o \
                   http_func.o md5.o pthread_func.o local_ip_func.o \
                   avl_tree.o ioevent.o ioevent_loop.o fast_task_queue.o \
                   fast_timer.o process_ctrl.o fast_mblock.o \
                   connection_pool.o fast_mpool.o fast_allocator.o \
                   fast_buffer.o multi_skiplist.o flat_skiplist.o  \
                   system_info.o fast_blocked_queue.o id_generator.o \
				   char_converter.o char_convert_loader.o common_blocked_queue.o \
                   multi_socket_client.o skiplist_set.o json_parser.o \
                   buffered_file_writer.o

HEADER_FILES = common_define.h hash.h chain.h logger.h base64.h \
               shared_func.h pthread_func.h ini_file_reader.h _os_define.h \
               sockopt.h sched_thread.h http_func.h md5.h local_ip_func.h \
               avl_tree.h ioevent.h ioevent_loop.h fast_task_queue.h \
               fast_timer.h process_ctrl.h fast_mblock.h \
               connection_pool.h fast_mpool.h fast_allocator.h \
               fast_buffer.h skiplist.h multi_skiplist.h flat_skiplist.h \
               skiplist_common.h system_info.h fast_blocked_queue.h \
               php7_ext_wrapper.h id_generator.h char_converter.h \
			   char_convert_loader.h common_blocked_queue.h  \
               multi_socket_client.h skiplist_set.h fc_list.h \
               json_parser.h buffered_file_writer.h

ALL_OBJS = $(FAST_STATIC_OBJS) $(FAST_SHARED_OBJS)

ALL_PRGS = 
SHARED_LIBS = libfastcommon.so
STATIC_LIBS = libfastcommon.a
ALL_LIBS = $(SHARED_LIBS) $(STATIC_LIBS)

all: $(ALL_OBJS) $(ALL_PRGS) $(ALL_LIBS)
libfastcommon.so: $(FAST_SHARED_OBJS)
	$(COMPILE) -o $@ -shared $(FAST_SHARED_OBJS) $(LIB_PATH)
libfastcommon.a: $(FAST_STATIC_OBJS)
	ar rcs $@ $(FAST_STATIC_OBJS)
.o:
	$(COMPILE) -o $@ $<  $(FAST_STATIC_OBJS) $(LIB_PATH) $(INC_PATH)
.c:
	$(COMPILE) -o $@ $<  $(FAST_STATIC_OBJS) $(LIB_PATH) $(INC_PATH)
.c.o:
	$(COMPILE) -c -o $@ $<  $(INC_PATH)
.c.lo:
	$(COMPILE) -c -fPIC -o $@ $<  $(INC_PATH)
install:
	mkdir -p $(TARGET_LIB)
	mkdir -p $(TARGET_PREFIX)/lib
	mkdir -p $(TARGET_PREFIX)/include/fastcommon

	install -m 755 $(SHARED_LIBS) $(TARGET_LIB)
	install -m 644 $(HEADER_FILES) $(TARGET_PREFIX)/include/fastcommon
	if [ ! -e $(TARGET_PREFIX)/lib/libfastcommon.so ]; then ln -s $(TARGET_LIB)/libfastcommon.so $(TARGET_PREFIX)/lib/libfastcommon.so; fi
clean:
	rm -f $(ALL_OBJS) $(ALL_PRGS) $(ALL_LIBS)

C语言下“.lo”和“.o” .a 的区别

.lo文件是libtool生成的文件,被libtool用来生成共享库的;而.o 文件是标准的目标文件。

.a 为静态库,是好多个.o合在一起,用于静态连接

lo: 使用libtool编译出的目标文件,其实就是在o文件中添加了一些信息

创建.a库文件和.o库文件:

$ gcc -c mylib.c
$ ar -r mylib.a mylib.o

makefile 中 $@ $^ %< 使用

makefile 中 $@ $^ %< 使用
参考URL: https://www.cnblogs.com/baiduboy/p/6849587.html

Makefile有三个非常有用的变量。分别是 @ , @, @^,$<代表的意义分别是:

@ − − 目 标 文 件 , @--目标文件, @^–所有的依赖文件,$<–第一个依赖文件。

二、go项目makefile编写实战

代码变成可执行文件,叫做编译(compile);先编译这个,还是先编译那个(即编译的安排),叫做构建(build)。

Make是最常用的构建工具,诞生于1977年,主要用于C语言的项目。但是实际上 ,任何只要某个文件有变化,就要重新构建的项目,都可以用Make构建。

本节,我们将介绍如何使用Makefile在Go(GoLang)支持的任何操作系统 和平台上运行 ,构建和编译应用程序。

1. 如何编写高质量的Makefile?

14 | 项目管理:如何编写高质量的Makefile?
参考URL: https://time.geekbang.org/column/article/388920

要写出一个优雅的 Go 项目,不仅仅是要开发一个优秀的 Go 应用,而且还要能够高效地管理项目。有效手段之一,就是通过 Makefile 来管理我们的项目,这就要求我们要为项目编写 Makefile 文件。

本节,推荐查看原文!

2. go项目makefile编写实战

思路:

  1. 全局Makefile定义
    ROOT_PACKAGE=mygoapp
    VERSION_PACKAGE=mygoapp/pkg/version
  2. common.mk 中,自定义一些通用的变量
  3. golang.mk 中为项目定义的 go相关环境变量。
    定义伪目标 go.build.%
  4. image.mk 中,定义 docker镜像制作相关。
    1)定义相应docker镜像命令一些变量。
    2)伪目标image.verify 通过shell docker version 结合awk取版本号,然后判断docker api版本是否满足要求(大于1.32)。
    Linux三剑客之awk命令
    参考URL: https://www.cnblogs.com/ginvip/p/6352157.html
    3)伪目标image.daemon.verify 判断 docker 是否支持 docker 开启experimental功能
    4)然后执行目标: image.build.%
    在makefile替换DockerFile中 基础镜像为 centos8
    然后执行 docker build --platform 命令
docker  build --platform linux --pull -t $(REGISTRY_PREFIX)/$(IMAGE)-$(ARCH):$(VERSION) $(TMP_DIR)/$(IMAGE)

实际组装命令如下:

docker  build --platform linux --pull -t mygoapp/file-boom-amd64:v1.23.4 /root/code/mygoapp/_output/tmp/file-boom

构建完成后删除docker镜像

其中go编译,用到以下3个点:

使用ldflags设置Go应用程序的版本信息

go ldflags设置
-w 去掉调试信息
-s 去掉符号表
-X 注入变量, 编译时赋值

可以在go install 、go build、go run 、go test中使用

常用变量

Module=github.com/pubgo/xxx
GOPATH=$(shell go env GOPATH)
Version=$(shell git tag --sort=committerdate | tail -n 1)
GoVersion=$(shell go version)
BuildTime=$(shell date "+%F %T")
CommitID=$(shell git rev-parse HEAD)
LDFLAGS:=-ldflags "-X 'github.com/pubgo/xxx/version.GoVersion=${GoVersion}' \
-X 'github.com/pubgo/xxx/version.BuildTime=${BuildTime}' \
-X 'github.com/pubgo/xxx/version.GoPath=${GOPATH}' \
-X 'github.com/pubgo/xxx/version.CommitID=${CommitID}' \
-X 'github.com/pubgo/xxx/version.Module=${Module}' \
-X 'github.com/pubgo/xxx/version.Version=${Version:-v0.0.1}'"

编译

go build ${LDFLAGS} -mod vendor -race -v -o main main.go
go build -ldflags "-w -s" -mod vendor -race -v -o main main.go

-w -s
如果使用这两个将会看不见文件名、行号, 对于调试不利
gdb看不到源码。

golang编译时选择json解析库 -tags=jsoniter

golang官方为我们提供了标准的json解析库–encoding/json,大部分情况下,使用它已经够用了。不过这个解析包有个很大的问题–性能。它不够快,如果我们开发高性能、高并发的网络服务就无法满足,这时就需要高性能的json解析库,目前性能比较高的有json-iteratoreasyjson

json-iterator

https://github.com/json-iterator/go

jsoniter 最大的优势就在于:能够 100% 兼容标准库,因此代码能够非常方便地进行迁移。

实现思路: 利用条件编译,实现灵活选择json解析库的目的。

编译构建的话,改为go build -tags=jsoniter .即可,即可生成使用json-iterator解析的可执行文件。

什么时候使用jsoniter,网友讨论:

没有瓶颈的前提下就用标准库。
直接用标准库的就可以,性能有问题的时候尝试切换。
一般是这么个操作。不过还是标准库第一位。
其实性能影响没那么大的

golang编译时的参数传递 -gcflags “all=-N -l”

go build 可以用-gcflagsgo编译器传入参数,也就是传给go tool compile的参数,因此可以用go tool compile --help查看所有可用的参数。

 -gcflags "all=-N -l"
  • -N 参数代表禁止优化,

  • -l 参数代表禁止内联,

go在编译目标程序的时候会嵌入运行时(runtime)的二进制,
禁止优化和内联可以让运行时(runtime)中的函数变得更容易调试。

三、参考

跟我一起写Makefile
参考URL: https://seisman.github.io/how-to-write-makefile/overview.html
14 | 项目管理:如何编写高质量的Makefile?
参考URL: https://time.geekbang.org/column/article/388920

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐