跳转至

Make

导言

虽然大型项目会使用cmake,手写makefile的情况比较少,但是基本语法还是要熟悉的。

简介

make 和 makefile 是 Linux 系统下 C/C++ 工程的编译工具,它们用来自动化配置实验环境,编译项目文件。makefile 文件描述了项目文件之间的依赖关系和编译规则,make 命令根据 makefile 文件的内容执行编译任务123。相对于 cmake 等其他的编译工具,make 和 makefile 有以下几个特点:

  • make 和 makefile 是 GNU 的标准工具,可以在多种平台上使用。
  • make 和 makefile 可以根据文件时间戳自动发现更新过的文件而减少编译的工作量。
  • make 和 makefile 可以灵活地定义变量、函数和条件判断等,实现复杂的编译逻辑

对于复杂的项目,能通过makefile文件将

  • 实验环境Build,(提前设置相关路径参数)
    • wget/git 下载相关文件到对应目录
    • apt install
  • 样例测试test,
  • 整体运行/Benchmark运行run,
  • 环境清理clean 统一起来

执行流程

  • make的默认目标是makefile中的第一个目标,而其它目标一般是由这个目标连带出来的。

选项

# 不实际运行,但是打印会运行的命令
make -n
make --dry-run

# 命令的 -C 选项用于指定一个目录,在该目录中执行 Makefile。
make -C 

# 指定makefile
make -f makefile.llvm

# -W 假设某个文件是最新的,不需要重新编译。inj是目标
make inj svm.inj

debug

打印debug信息,makefile如何选择决策

  • --debug=v 输出的信息包括哪个makefile被解析,不需要被重编译的依赖文件(或是依赖目标)等。
  • --debug=i implicit,输出使用的隐含规则过程。
  • --debug=m 输出make读取makefile,更新makefile,执行makefile的信息。

基本语法

  1. $(foreach var,list,text)的语法来对list中的每个元素执行text,并用var来引用当前元素1。你也可以用
  2. $(wildcard pattern)的语法来匹配指定模式的文件,并返回其列表
  3. $(subst from,to,text)的语法来把text中的from替换为to
  4. $(patsubst pattern,replacement,text)的语法来把text中匹配pattern的部分替换为replacement
  5. include <filenames> ,make 在处理程序的时候,文件列表中的任意一个文件不存在的时候或者是没有规则去创建这个文件的时候,make 程序将会提示错误并保存退出;
  6. -include <filenames>,当包含的文件不存在或者是没有规则去创建它的时候,make 将会继续执行程序,只有真正由于不能完成终极目标重建的时候我们的程序才会提示错误保存退出;
  7. addprefix的功能增加前缀,例如$(addprefix -I,./Inc)执行后为 -I ./Inc
    1. OUTPUT_FILES = $(addsuffix .out, $(addprefix $(OUTPUT_DIR)/, $(BENCH_MAINNAME)))
  8. 文件名处理函数 dir, notdir, suffix, basename (网站basename例子写错了)
  9. .PHONY: clean是为了有clean名称的文件时,防止把make clean理解成生成clean文件,而不是使用clean规则。
  10. 一个Makefile文件里通常会有多个目标,一般会选择第一个作为默认目标。所以一般第一个写all
  11. 赋值符号
= 是最基本的赋值
:= 是覆盖之前的值
?= 是如果没有被赋值过就赋予等号后面的值
+= 是添加等号后面的值
  1. makefile中命令前加一个@的作用是不让make显示出要执行的命令
  2. $@ is the name of the target being generated, and$< the first prerequisite (usually a source file). You can find a list of all these special variables in the GNU Make manual.

For example, consider the following declaration:

all: library.cpp main.cpp

In this case:

$@ evaluates to all

$< evaluates to library.cpp

$^ evaluates to library.cpp main.cpp

常见问题

Makefile:22: *** missing separator.  Stop.

命令开头要用Tab,不是空格。别用vscode,用vim写

Makefile 隐含规则

  • “隐含规则”也就是一种惯例,make会按照这种“惯例”心照不喧地来运行,那怕我们的Makefile中没有书写这样的规则。
    • 例如,把[.c]文件编译成[.o]文件这一规则,你根本就不用写出来,make会自动推导出这种规则,并生成我们需要的[.o]文件。
    • 将头文件变成.gch precompiled headers
  • 许多的隐含规则都是使用了“后缀规则”来定义的

    • 变量.SUFFIXES存储了默认的依赖目标,可以修改。默认的后缀列表是:.out,.a, .ln, .o, .c, .cc, .C, .p, .f, .F, .r, .y, .l, .s, .S, .mod, .sym, .def, .h, .info, .dvi, .tex, .texinfo, .texi, .txinfo, .w, .ch .web, .sh, .elc, .el
    • 要让make知道一些特定的后缀,我们可以使用伪目标".SUFFIXES"来定义或是删除,如:
  • 把后缀.hack和.win加入后缀列表中的末尾。

SUFFIXES: .hack .win
  1. 先删除默认后缀,后定义自己的后缀列表。
.SUFFIXES: # 删除默认的后缀
.SUFFIXES: .c .o .h # 定义自己的后缀

模式规则

  • 常见使用模式规则来定义一个隐含规则
  • 至少在规则的目标定义中要包含"%"
%.o : %.c ; <command ......>

老式风格的"后缀规则"

  • 后缀规则是一个比较老式的定义隐含规则的方法。
  • 后缀规则会被模式规则逐步地取代。因为模式规则更强更清晰。为了和老版本的Makefile兼容,GNU make同样兼容于这些东西。
  • 后缀规则有两种方式:"双后缀"和"单后缀"。
  • 双后缀规则定义了一对后缀:目标文件的后缀和依赖目标(源文件)的后缀。如".c.o"相当于"%o : %c"。
  • 单后缀规则只定义一个后缀,也就是源文件的后缀。如".c"相当于"% : %.c"。
.cpp.o:
  $(CPPCOMPILE) -c $(COMPILEOPTION) $(INCLUDEDIR) $<

.c.o:
 $(CCOMPILE) -c $(COMPILEOPTION) $(INCLUDEDIR) $<

Makefile 手写技巧示例

框架

SIM_ROOT ?= $(shell readlink -f "$(CURDIR)") #当前工作目录的绝对路径。
CLEAN=$(findstring clean,$(MAKECMDGOALS)) #???

include common/Makefile.common

$(STANDALONE): $(LIB_CARBON) $(LIB_SIFT) $(LIB_DECODER)
 @$(MAKE) $(MAKE_QUIET) -C $(SIM_ROOT)/standalone #进入standalone目录,执行Makefile文件

Makefile.common文件中的内容如下:

include $(SIM_ROOT)/Makefile.config

DIRECTORIES := ${shell find $(SIM_ROOT)/common -type d -print} #查找common目录下的所有子目录
LIBCARBON_SOURCES = $(foreach dir,$(DIRECTORIES),$(wildcard $(dir)/*.cc)) \
 $(wildcard $(SIM_ROOT)/common/config/*.cpp) # 把变量DIRECTORIES中的每个目录下的所有.cc文件和$(SIM_ROOT)/common/config目录下的所有.cpp文件赋值给变量LIBCARBON_SOURCES

# FLAGS
ifeq ($(SNIPER_TARGET_ARCH),ia32)
  # Add -march=i686 to enable some extra instructions that allow for implementation of 64-bit atomic adds
  CXXFLAGS += -m32 -march=i686 -DTARGET_IA32 # Include paths
  LD_FLAGS += -m32
endif
ifeq ($(SNIPER_TARGET_ARCH),intel64)
  CXXFLAGS += -fPIC -DTARGET_INTEL64
  LD_FLAGS +=
endif

# Build rules for dependency generation
%.d: %.cpp
 $(_MSG) '[DEP   ]' $(subst $(shell readlink -f $(SIM_ROOT))/,,$(shell readlink -f $@)) #把$@的绝对路径中的$(SIM_ROOT)的绝对路径部分替换为空字符串,便于打印
 $(_CMD) $(CXX) -MM -MG $(CPPFLAGS) $(CXXFLAGS) $< | sed -n "H;$$ {g;s@.*:\(.*\)@$*.o $@: \$$\(wildcard\1\)@;p}" >$@
 # 用$(CXX)编译器对$<文件进行依赖分析,并把结果通过sed命令进行处理,然后输出到$@文件

Makefile.config 如下

# 架构判断
ARCH_QUERY=$(shell uname -m)
ifeq ($(ARCH_QUERY),i686)
SNIPER_TARGET_ARCH = ia32
else
ifeq ($(ARCH_QUERY),x86_64)
SNIPER_TARGET_ARCH ?= intel64
#SNIPER_TARGET_ARCH = ia32
else
$(error Unknown target arch: $(ARCH_QUERY))
endif
endif

# 全局的路径
PIN_HOME ?= xxx
PIN_ROOT := $(PIN_HOME)

# 编译器
CC ?= gcc
CXX ?= g++

# DEBUG输出
ifneq ($(DEBUG_SHOW_COMPILE),)  # 如果变量DEBUG_SHOW_COMPILE不为空,如make DEBUG_SHOW_COMPILE=1 使用
  SHOW_COMPILE=1
  MAKE_QUIET=     # 使用情形:$(MAKE) $(MAKE_QUIET) -C pin clean
  _MSG=@echo >/dev/null   # 使用情形:$(_MSG) '[CLEAN ] pin'
  _CMD=       # 使用情形:$(_CMD) git clone --quiet xxx
else
  SHOW_COMPILE=
  MAKE_QUIET=--quiet
  _MSG=@echo
  _CMD=@
endif

打印make的详细信息make DEBUG_SHOW_COMPILE=1 DEBUG=1 -j 16|tee make.log |my_hl

打印make时每项依赖文件的情况

# Sums up number of passes and fails
# cut 命令用于从文件的每一行剪切字节、字符和字段并将这些字节、字符和字段写至标准输出。
# 其中 -d (--delimiter)参数指定分隔符(默认TAB),-f (--fields)参数指定要显示的字段。
# 因此,cut -d\  -f 2 的意思是以空格为分隔符,显示第二个字段。
# uniq -c 统计不重复词(PASS\|FAIL)的出现
test-score: test-all
 @$(MAKE) test-all | cut -d\  -f 2 | grep 'PASS\|FAIL' | sort | uniq -c

# Result output strings
PASS = \033[92mPASS\033[0m
FAIL = \033[91mFAIL\033[0m

# Need to be able to build kernels, if this fails rest not run
test-build: all
 @echo " $(PASS) Build"

实现在Makefile里下载

根据是否存在文件来执行makefile语句

ROAD_URL = http://www.dis.uniroma1.it/challenge9/data/USA-road-d/USA-road-d.USA.gr.gz
$(RAW_GRAPH_DIR)/USA-road-d.USA.gr.gz:
 wget -P $(RAW_GRAPH_DIR) $(ROAD_URL)

$(RAW_GRAPH_DIR)/USA-road-d.USA.gr: $(RAW_GRAPH_DIR)/USA-road-d.USA.gr.gz
 cd $(RAW_GRAPH_DIR)
 gunzip < $< > $@

$(GRAPH_DIR)/road.sg: $(RAW_GRAPH_DIR)/USA-road-d.USA.gr converter
 ./converter -f $< -b $@

每个文件是单独EXE

目录下所有源文件编译成单独的可执行文件

#before run, mkdir build && cd build

CFLAGS = -g -O0

#source files dir
SRC_DIR = ../
#Makefile dir, generally build
BUILD_DIR = ./

#get ../example.c 不能寻找子文件夹的文件,需要shell find
SRCS = $(wildcard $(SRC_DIR)*.c)

#delete .c, then become ../example
# :.c = .o的意思是C文件對應相應的.o文件
FILENAME = $(SRCS:.c=)

#replace src dir with dest dir, then become ./example
PROGS = $(addprefix $(BUILD_DIR),$(notdir $(FILENAME)))

.PHONY: all
all: $(PROGS)
    @echo "-- build all .c files" # makefile中命令前加一个@的作用是不让make显示出要执行的命令
 @echo Building for x86 architecture # echo 无需“”

$(BUILD_DIR)%: $(SRC_DIR)%.c
    gcc $(CFLAGS) -o $@ $<

另一种写法

CC=mpicc
CXX=mpicxx
OPENMP=-fopenmp
SOURCES:=$(shell find $(.) -name '*.c')
SOURCESCXX:=$(shell find $(.) -name '*.cpp')
# SRCS := $(shell find $(SRC_DIRS) -name *.cpp -or -name *.c -or -name *.s)
LIB=-lm
OBJS=$(SOURCES:%.c=%)
OBJS+=$(SOURCESCXX:%.cpp=%)
DEBUG=-g

SRC_DIR_1 = ./Lab1
SRCS_1 = $(shell find $(.) -name '*.c')
FILENAME_1 = $(SRCS_1:.c=)

# lab1 : $(FILENAME_1)
#  @echo $(SRCS_1)
#  @echo "lab1编译完成"
#  if [ ! -d "build" ]; then mkdir build; fi
#  mv $(FILENAME_1) build

all : $(OBJS)
 @echo $(SOURCES) $(SOURCESCXX)
 @echo "编译完成"
 @echo $(OBJS)
 if [ ! -d "build" ]; then mkdir build; fi
 mv $(OBJS) build

%: %.c
 $(CC) $(DEBUG) $(OPENMP) $< $(LIB) -o $@

%: %.cpp
 $(CXX) $(DEBUG) $(OPENMP) $< $(LIB) -o $@

.PHONY: clean showVariable

showVariable:
 @echo $(SOURCES)

clean: 
 rm -rf build

多个文件变一个可执行文件

MPICC = mpicc

LIB = -lm 
C_FLAGS= -O3 -mavx2 -fopenmp $(LIB)

SRC_DIR = src
BUILD_DIR = build/bin
SRCS = $(wildcard $(SRC_DIR)/*.c)
OBJ = $(SRCS:.c=.o)

pivot: ${SRCS}
 echo "compiling $(SRC_DIR) ${FILENAME}"
 $(MPICC) $^ $(C_FLAGS) -o $(BUILD_DIR)/pivot

运行结果

$ make pivot -C ..
make: 进入目录“/home/shaojiemike/github/IPCC2022-preliminary”
echo "compiling src src/Combination src/heapSort src/SunDistance src/MPI src/pivot"
compiling src src/Combination src/heapSort src/SunDistance src/MPI src/pivot
mpicc src/Combination.c src/heapSort.c src/SunDistance.c src/MPI.c src/pivot.c -O3 -mavx2 -fopenmp -lm  -o build/bin/pivot

更复杂

保留.o文件,区分Flag与lib

CC = g++
MPICC = mpicc

C_FLAGS= -O3 -fopenmp 
LIB = -lgomp

INCLUDEPATH = feGRASS
SRC_DIR = feGRASS
BUILD_DIR = build/bin

SRCS = $(wildcard $(SRC_DIR)/*.cpp)
OBJ = $(SRCS:.cpp=.o)
DEBUG_OBJ = $(SRCS:.cpp=_debug.o)
TIME_OBJ = $(SRCS:.cpp=_time.o)
FILENAME = $(SRCS:.cpp=)

.DEFAULT_GOAL := all
all : ${OBJ}
 echo "compiling $(SRC_DIR) ${FILENAME}"
 $(CC) $^ $(LIB) -o $(BUILD_DIR)/main

debugPrint: ${DEBUG_OBJ}
 echo "compiling $(SRC_DIR) ${FILENAME}"
 $(CC) $^ $(LIB) -o $(BUILD_DIR)/main

timePrint: ${TIME_OBJ}
 echo "compiling $(SRC_DIR) ${FILENAME}"
 $(CC) $^ $(LIB) -o $(BUILD_DIR)/main

%.o: %.cpp
 $(CC) $(C_FLAGS) -c $< -o $@ 
%_debug.o: %.cpp
 $(CC) -DDEBUG -DTIME $(C_FLAGS) -c $< -o $@ 
%_time.o: %.cpp
 $(CC) -DTIME $(C_FLAGS) -c $< -o $@ 


checkdirs: $(BUILD_DIR)
$(BUILD_DIR):
 @mkdir -p $@

.PHONY: clean
clean:
 rm -rf $(BUILD_DIR)/main ${SRC_DIR}/*.o

将obj与src分离

CC = g++
MPICC = mpicc

C_FLAGS= -O3 -fopenmp 
LIB = -lgomp
# C_FLAGS= -fopenmp $(LIB) $(debugFlag)
# C_FLAGS= -O3 -march=znver1 -mavx2 -fopenmp $(LIB) $(debugFlag)

INCLUDEPATH = feGRASS
SRC_DIR = feGRASS
BUILD_DIR = build/bin
OBJ_DIR = build/obj

SRCS = $(wildcard $(SRC_DIR)/*.cpp)
TMP = $(patsubst %.cpp,${OBJ_DIR}/%.cpp,$(notdir ${SRCS}))
# example: $(patsubst %.cpp,%.d,$(patsubst %.c,%.d,$(patsubst %.cc,%.d,$(LIBCARBON_SOURCES)))) 
# 意思是把$(LIBCARBON_SOURCES)变量中的所有.cpp,.c,.cc文件名替换为.d文件名
HEADERS = $(wildcard $(SRC_DIR)/*.h)
OBJ = $(TMP:.cpp=.o)
DEBUG_OBJ = $(TMP:.cpp=_debug.o)
TIME_OBJ = $(TMP:.cpp=_time.o)
FILENAME = $(TMP:.cpp=)

$(info    SRCS is: $(SRCS))
$(info    OBJ is: $(OBJ))
$(info    HEADERS is: $(HEADERS))

.DEFAULT_GOAL := main
main : $(OBJ)
 $(CC) $(OBJ) $(LIB) -o $(BUILD_DIR)/main

debugPrint: $(DEBUG_OBJ)
 $(CC) $(DEBUG_OBJ) $(LIB) -o $(BUILD_DIR)/main

timePrint: $(TIME_OBJ)
 $(CC) $(TIME_OBJ) $(LIB) -o $(BUILD_DIR)/main

mpi: $(SRCS)
 $(MPICC) $^ $(C_FLAGS) -o $(BUILD_DIR)/main

debugMpi: $(SRCS)
 $(MPICC) -DDEBUG -DTIME $^ $(C_FLAGS) -o $(BUILD_DIR)/main

timeMpi: $(SRCS)
 $(MPICC) -DTIME $^ $(C_FLAGS) -o $(BUILD_DIR)/main

${OBJ_DIR}/%.o: ${SRC_DIR}/%.cpp $(HEADERS)
 $(CC) $(C_FLAGS) -c $< -o $@ 
${OBJ_DIR}/%_debug.o: ${SRC_DIR}/%.cpp $(HEADERS)
 $(CC) -DDEBUG -DTIME $(C_FLAGS) -c $< -o $@ 
${OBJ_DIR}/%_time.o: ${SRC_DIR}/%.cpp $(HEADERS)
 $(CC) -DTIME $(C_FLAGS) -c $< -o $@ 


checkdirs: $(BUILD_DIR)
$(BUILD_DIR):
 @mkdir -p $(BUILD_DIR)
 @mkdir -p $(OBJ_DIR)


.PHONY: clean
clean:
 rm -rf $(BUILD_DIR)/main $(OBJ_DIR)/*.o

复杂项目的例子

Victima模拟器(sniper拓展地址翻译的模拟器)

分析复杂的Makefile

# Victima/sniper/Makefile
STANDALONE=$(SIM_ROOT)/lib/sniper
$(STANDALONE): $(LIB_CARBON) $(LIB_SIFT) $(LIB_DECODER)
    @$(MAKE) $(MAKE_QUIET) -C $(SIM_ROOT)/standalone

# vscode-remote://ssh-remote%2Bicarus3/staff/shaojiemike/test/Victima/src/Victima/sniper/standalone/Makefile
LD_LIBS += -lcarbon_sim -lpthread -ldw
SOURCES = $(shell ls $(SIM_ROOT)/standalone/*.cc)
OBJECTS = $(patsubst %.c,%.o,$(patsubst %.cc,%.o,$(SOURCES)))
## build rules
TARGET = $(SIM_ROOT)/lib/sniper
$(TARGET): $(SIM_ROOT)/lib/libcarbon_sim.a $(SIM_ROOT)/sift/libsift.a $(SIM_ROOT)/decoder_lib/libdecoder.a
$(TARGET): $(OBJECTS)
    $(_MSG) '[LD    ]' $(subst $(shell readlink -f $(SIM_ROOT))/,,$(shell readlink -f $@))
    $(_CMD) $(CXX) $(LD_FLAGS) -o $@ $(OBJECTS) $(LD_LIBS) $(OPT_CFLAGS) -std=c++0x

解释

第一个规则:$(TARGET): $(SIM_ROOT)/lib/libcarbon_sim.a $(SIM_ROOT)/sift/libsift.a $(SIM_ROOT)/decoder_lib/libdecoder.a 这行定义了 $(TARGET)(即 $(SIM_ROOT)/lib/sniper)依赖于三个库文件。这意味着在构建 $(TARGET) 之前,必须首先存在或构建这些库文件。

第二个规则:$(TARGET): $(OBJECTS) 这行则说明 $(TARGET) 还依赖于由 $(OBJECTS) 定义的一系列对象文件。这是实际编译生成可执行文件所需要的对象文件。

参考文献

https://blog.csdn.net/ET_Endeavoring/article/details/98989066