当前位置: 首页 > 工具软件 > makefile2dot > 使用案例 >

makefile详解

邵阳
2023-12-01

大型项目中 makefile 的具体应用

基本规则

target:prerequisites
    command

以上 是 makefile 最核心的规则,target 是目标集,prerequisites 是为了生成 target 的依赖集,command 是为了生成 target 需要执行的命令

#根据 main.o 生成 main.out
main.out : main.o
    gcc main.o -o main.out
#生成 main.o
main.o: main.c
    gcc -c main.c

其中 prerequisites 和 command 都可以为空, 如果需要的 prerequisites 不存在,就会按照 prerequisites 的生成规则来先生成 prerequisites

#为了生成 all, 最后会执行 script.sh 脚本,这种方式是间接执行脚本
all:
    ./script.sh

#为了生成 all, 需要先生成 program1 和 program2
all: program1 program2

业界通用的编译规则

下面以业界常用的编译规则为例

#指定 c 编译器
CC := gcc

#定义一个空的目标变量,表示最终需要生成的程序
PROGRAM = 

#下面是根据最终需要生成的目标,设置不同的编译选项
#如果是生成的动态链接库,则目标文件为 PROGRAM=$(SO_TARGET)
ifdef SO_TARGET
    CFLAGS += -shared -fPIC
    CXXFLAGS += -shared -fPIC
    PROGRAM=$(SO_TARGET)
    CSRES := $(shell echo $(SO_TARGET) | awk -F. '{print $$1}')
	REPORTDIR := $(shell echo $(SO_TARGET) | awk -F. '{print $$1}')
endif

#如果是生成的二进制程序,则目标文件为 PROGRAM=$(APP_TARGET)
ifdef APP_TARGET
    CFLAGS += -rdynamic
    CXXFLAGS += -rdynamic
    PROGRAM=$(APP_TARGET)
    CSRES := $(APP_TARGET)
    REPORTDIR := $(APP_TARGET)
endif

#如果是生成的静态链接库,则目标文件为 PROGRAM=$(LIB_TARGET)
ifdef LIB_TARGET
    PROGRAM=$(LIB_TARGET)
    CSRES := $(shell echo $(LIB_TARGET) | awk -F. '{print $$1}')
	REPORTDIR := $(shell echo $(LIB_TARGET) | awk -F. '{print $$1}')
endif


## Stable Section: usually no need to be changed. But you can add more.  
##==========================================================================  
SHELL   = /bin/sh  
EMPTY   =  
SPACE   = $(EMPTY) $(EMPTY)  

# 如果 PROGRAM 为空,则默认的输出结果为 a.out
ifeq ($(PROGRAM),)  
  CUR_PATH_NAMES = $(subst /,$(SPACE),$(subst $(SPACE),_,$(CURDIR)))  
  PROGRAM = $(word $(words $(CUR_PATH_NAMES)),$(CUR_PATH_NAMES))  
  ifeq ($(PROGRAM),)  
    PROGRAM = a.out  
  endif  
endif  

# 如果 SRCDIRS 变量为空,则默认当前目录为编译目录
ifeq ($(SRCDIRS),)  
  SRCDIRS = .  
endif  

# 目标目录下所有的源文件的集合
SOURCES = $(foreach d,$(SRCDIRS),$(wildcard $(addprefix $(d)/*,$(SRCEXTS))))  
# 目标目录下所有的头文件的集合
HEADERS = $(foreach d,$(SRCDIRS),$(wildcard $(addprefix $(d)/*,$(HDREXTS))))  
# 目标目录下所有的 .c 源文件的集合
SRC_CXX = $(filter-out %.c,$(SOURCES))  
# 目标目录下所有的 .o 文件的集合 (注意== 这些只是文件名集合的变量, 实际上这些 .o 文件还没有生成)
OBJS    = $(addsuffix .o, $(basename $(SOURCES)))  

#定义编译的命令和参数,展开之后其实就是 gcc -g -Wall -Werror -Wshadow.... -c 
COMPILE.c   = $(CC)  $(CFLAGS)  $(INCFLAGS)  -c  
COMPILE.cxx = $(CXX) $(CXXFLAGS) $(INCFLAGS) -c  
LINK.c      = $(CC)  $(CFLAGS)  $(LDFLAGS)  
LINK.cxx    = $(CXX) $(CXXFLAGS) $(LDFLAGS)  
AR          = ar -cr #用于生成静态库的 ar 

#伪目标,防止当前目录下有与目标名冲突的文件或文件夹,导致这些目标生成不了
.PHONY: all objs tags ctags clean distclean help show  

#将默认的前缀置空
.SUFFIXES:  

#生成最终目标 all
#由于 all 又依赖于 precommand/$(PROGRAM)/postcommand, 因此会依次先生成这三个目标
all: precommand $(PROGRAM)  postcommand

#以下是生成 .o 文件的规则
#----------------------------------------  
objs:$(OBJS)  

#将所有的 .c 文件编译生成对应的 .o 文件, 这里生成之后, $(OBJS) 就可以用了, 也就是 objs 构建完成
#  $< 表示所有的依赖目标集(也就是 .c 集合),$@ 表示目标集(也就是 .o 集合)
%.o:%.c
    $(COMPILE.c) $< -o $@  

%.o:%.C
    $(COMPILE.cxx) $< -o $@  

%.o:%.cc
    $(COMPILE.cxx) $< -o $@  

%.o:%.cpp
    $(COMPILE.cxx) $< -o $@  

%.o:%.CPP
    $(COMPILE.cxx) $< -o $@  

%.o:%.c++
    $(COMPILE.cxx) $< -o $@  

%.o:%.cp
    $(COMPILE.cxx) $< -o $@  

%.o:%.cxx
    $(COMPILE.cxx) $< -o $@  

# Rules for generating the tags.  
#----------------------------------------  
tags: $(HEADERS) $(SOURCES)
    $(ETAGS) $(ETAGSFLAGS) $(HEADERS) $(SOURCES)  

ctags: $(HEADERS) $(SOURCES)
    $(CTAGS) $(CTAGSFLAGS) $(HEADERS) $(SOURCES)  

# Rules for generating the executable.  
#----------------------------------------  
# 这里的 $(PRECOMMAND)/$(POSTCOMMAND)/$(CLEANCOMMAND) 可以是脚本或需要执行的命令
precommand:
    $(PRECOMMAND) 
postcommand:
    $(POSTCOMMAND)
cleancommand:
    $(CLEANCOMMAND)

#生成最终目标 $(PROGRAM),依赖目标集为 $(OBJS) 
#根据最终是生成库文件还是生成二进制文件,来决定是执行链接, 还是 ar
$(PROGRAM):$(OBJS)
ifdef LIB_TARGET  #如果是生成 lib 的话就是用 ar 
    $(AR) $@ $(OBJS)
else
ifeq ($(wildcard *.cpp *.cc),)  # C program
    $(LINK.c)   $(OBJS) $(LIBFLAGS) -o $@
    @echo Type $@ to execute the program.
else                            # C++ program
    $(LINK.cxx) $(OBJS) $(LIBFLAGS) -o $@
    @echo Type $@ to execute the program.  
endif  
endif

clean: cleancommand
    $(RM) $(OBJS) $(PROGRAM) TAGS  


# Show help.  
help:
    @echo 'Generic Makefile for C/C++ Programs (gcmakefile) version 0.5'
    @echo 'Copyright (C) 2007, 2008 whyglinux <whyglinux@hotmail.com>'
    @echo
    @echo 'Usage: make [TARGET]'
    @echo 'TARGETS:'
    @echo '  all       (=make) compile and link.'
    @echo '  NODEP=yes make without generating dependencies.'
    @echo '  objs      compile only (no linking).'
    @echo '  tags      create tags for Emacs editor.'
    @echo '  ctags     create ctags for VI editor.'
    @echo '  clean     clean objects and the executable file.'
#   @echo '  distclean clean objects, the executable and dependencies.'
    @echo '  show      show variables (for debug use only).'
    @echo '  help      print this message.'
    @echo
    @echo 'Report bugs to <whyglinux AT gmail DOT com>.'

# Show variables (for debug use only.)  
show:
    @echo 'PROGRAM     :' $(PROGRAM)
    @echo 'SRCDIRS     :' $(SRCDIRS)
    @echo 'HEADERS     :' $(HEADERS)
    @echo 'SOURCES     :' $(SOURCES)
    @echo 'SRC_CXX     :' $(SRC_CXX)
    @echo 'OBJS        :' $(OBJS)
    @echo 'COMPILE.c   :' $(COMPILE.c)
    @echo 'COMPILE.cxx :' $(COMPILE.cxx)
    @echo 'link.c      :' $(LINK.c)
    @echo 'link.cxx    :' $(LINK.cxx)

具体项目引用通用的编译规则

以上是针对所有项目的通用的编译规则,那么具体到整个项目,该如何构造编译树逐级编译呢?

假设有一个大型项目 project , 下面有各级子项目如下

.
└── project
    ├── projectA
    │   └── projectA1
    │       └── projectA2
    │           └── projectA3
    ├── projectB
    │   └── projectB1
    │       └── projectB2
    │           └── projectB3
    └── rules.make

那么 rules.make 可以作为最基本的编译规则放在项目根目录下。 各级目标依次写 makefile 去向下递归编译

假设 projectA3 的目录树如下

.
└── source
    ├── apps
    │   ├── app1
    │   ├── app2
    ├── libs
    ├── makefile

那么我们如何编写 projectA 的 makefile 以及如何编译 projectA 呢

首先是 source 下的 makefile ,因为有 apps 和 libs 目录,因此要依次对 apps 和 libs 目录进行编译

all: libs apps 
libs:
    make -C ./libs all
apps:
    make -C ./apps all
clean:
    make -C ./libs clean
    make -C ./apps clean
scan:
    make -C ./libs scan
    make -C ./apps scan
.PHONY: all libs apps scan clean 

那么对于apps 下的 app1,app2,appN ….. 也是一样的递归向下编译

DIRS = app1 app2 appN...
all:
    @for i in ${DIRS}; \
    do \
    make -C $${i}; \
	if [ $$? != 0 ] ; then \
        exit 1; \
    fi \
    done

clean:
    @for i in ${DIRS}; \
    do \
    make -C $${i} clean -j; \
    done

scan:
    @for i in ${DIRS}; \
    do \
    make -C $${i} scan; \
    done

.PHONY: all clean scan $(DIRS)

一直递归到最后一层包含源代码的目录,就可以直接 include 项目根目录下的编译规则了,

#找到根目录
ROOTDIR=$(shell while true; do if [ -f rules.make ]; then pwd;exit; else cd ..;fi;done;)
#make 之前需要执行的命令
PRECOMMAND = ./script.sh
#make 之后需要清理的命令
CLEANCOMMAND = make -C ./protodef/ clean
#可以自己加一些自定义的链接库
LDFLAGS  +=  -lxxx
#指定最终生成的目标的地方
APP_TARGET=$(APPDIR)/appproxywrk

#以上定义的变量,在inlcude 根目录下的编译规则之后,都会传递到 rules.make 中去
include $(ROOTDIR)/rules.make

最后直接在projectA 下执行一下,就可以将projectA 下的所有项目逐级编译好,并放在指定的文件夹下

make -C source/ 

一些语法

上述 makefile 的一些语法细节如下:

  • 伪目标:用于防止目标与当前目录下的文件或文件夹产生重名冲突
.PHONY: all objs tags ctags clean distclean help show  
  • make 命令不指定目标的时候将会只构建第一个目标, 也就是 如果第一个目标是 all , 即使后面还有 clean 之类的,如果不显示指定, 最后只会构建 all, 当然,all 的依赖目标集也会被构建。

  • -C 是指定需要编译的路径

make -C  source/
  • $<$@ 是自动化变量,$< 表示所有的依赖目标集,$@表示目标集, 以下命令中,依赖目标集为所有的 . c 文件,目标集则为对应的 .o 文件
%.o:%.c
    $(COMPILE.c) $< -o $@  
  • 关于缩进,tab 的区分很严格

  • 关于 foreach 语法,自行百度

 类似资料: