Linux Makefile:快速入门

1、说明

1.1 Makefile是什么

Makefile是Linux(UNIX)环境下管理工程文件的一种文件,它用来描述项目中哪些文件需要编译,哪些不需要编译,哪些文件需要先编译,哪些后编译,哪些文件怎样编译等等。如果不使用Makefile文件来管理,在项目中就需要逐个文件进行编译或其他操作,这样工作量就会非常庞大,而使用了Makefile来管理,只需要一个make或make xxx命令,就可以对整个项目工程进行相应的操作,使得平时的工程管理变得简单化、自动化。


1.2 书写规则

目标: 依赖1 依赖2 ...
<Tab键>命令

说明:当“目标”不存在或某个“依赖”比“目标”的时间要新,则执行“命令”。

  • 目标:可以是文件,也可以是标签;
  • 依赖:为生成“目标”的文件或其他“目标”,可以多个,也可以没有;
  • 命令:需要执行的命令等,可以多个,但不能没有,每条命令前都是<Tab键>,命令间可以空行。

(另,Makefile中的注释符是“#”)


1.3 文件命名

Makefile一般的命名方式有GNUmakefile、makefile、Makefile三种,但是一般都会选择命名为Makefile。当执行make命令的时候,就会在当前路径下查找这几个Makefile文件,它们的查找顺序是GNUmakefile->makefile->Makefile。当然,也可以在make的时候使用-f参数来指定其他名称的"Makefile"文件。


1.4 工作流程

默认情况下,make会执行Makefile文件中出现的第一个依赖关系,也称为“最终目标”或“终极目标”。当发现依赖关系中的“依赖”比“目标”新的时候,它又会先去处理这些以这些“依赖”为“目标”的规则,处理完成之后就回过头来处理之前的“目标”。


2、逐步进入Makefile世界

2.1 示例1:最简单的Makefile

linrm@linrm-VirtualBox:~/MakefileTest/01$ ls
main.c  Makefile

最简单的Makefile搭配最简单的代码。代码内容如下:

/* main.c */
#include <stdio.h>

int main(int argc, char **argv)
{
    printf("hello, world!\n");
    return 0;
}

最简单的Makefile如下:

main: main.c
	gcc main.c -o main

使用:

linrm@linrm-VirtualBox:~/MakefileTest/01$ ls
main.c  Makefile
linrm@linrm-VirtualBox:~/MakefileTest/01$ make
gcc main.c -o main
linrm@linrm-VirtualBox:~/MakefileTest/01$ ls
main  main.c  Makefile
linrm@linrm-VirtualBox:~/MakefileTest/01$ ./main
hello, world!
linrm@linrm-VirtualBox:~/MakefileTest/01$

2.2 示例2:同一个目录多个源文件

linrm@linrm-VirtualBox:~/MakefileTest/02$ ls
main.c  Makefile  test.c  test.h

test.c/test.h内容如下:

/* test.c */
#include <stdio.h>
#include "test.h"

int test(void)
{
    printf("%s\n", STRING);
}

/* test.h */
#ifndef _TEST_H_
#define _TEST_H_

#define STRING  "hello, world!"

int test(void);

#endif

main.c内容如下:

/* main.c */
#include "test.h"

int main(int argc, char **argv)
{
    test();
    return 0;
}

Makefile内容如下:

main: main.o test.o
	gcc main.o test.o -o main

main.o: main.c
	gcc -c main.c -o main.o

test.o: test.c test.h
	gcc -c test.c -o test.o

使用:

linrm@linrm-VirtualBox:~/MakefileTest/02$ ls
main.c  Makefile  test.c  test.h
linrm@linrm-VirtualBox:~/MakefileTest/02$ make
gcc -c main.c -o main.o
gcc -c test.c -o test.o
gcc main.o test.o -o main
linrm@linrm-VirtualBox:~/MakefileTest/02$ ls
main  main.c  main.o  Makefile  test.c  test.h  test.o
linrm@linrm-VirtualBox:~/MakefileTest/02$
linrm@linrm-VirtualBox:~/MakefileTest/02$ ./main
hello, world!
linrm@linrm-VirtualBox:~/MakefileTest/02$ 

可以看到这次的目标文件中就有.o汇编文件,这种生成中间文件的方式有利于减少编译花费的时间,因为Makefile的核心规则是当“依赖”比“目标”新的时候才会执行“命令”。这样下来当只修改test.c时就不会连main.c也重复编译,例如对比下面和上面的gcc编译命令次数即可知道:

linrm@linrm-VirtualBox:~/MakefileTest/02$ touch test.c # 更新文件的时间戳
linrm@linrm-VirtualBox:~/MakefileTest/02$ make
gcc -c test.c -o test.o
gcc -o main main.o test.o
linrm@linrm-VirtualBox:~/MakefileTest/02$

2.3 伪目标介绍,并在示例2的基础上继续改良

示例2中make编译之后得到一些.o格式的中间汇编文件,有时候只是想看下源文件就不是很清晰了。这时可以自己添加一个“清理”的命令,修改后的Makefile如下:

main: main.o test.o
	gcc main.o test.o -o main

main.o: main.c
	gcc -c main.c -o main.o

test.o: test.c test.h
	gcc -c test.c -o test.o

clean:
	rm *.o main -rf
.PHONY: clean

这个clean是一个“目标”,但是发现它没有“依赖”,那怎样判断“目标”和“依赖”谁比较新呢?其实,clean并不是我们想要生成的一个文件,我们只是想让clean这个“目标”来执行它后面的“命令”,所以使用“.PHONY”关键字定义clean为假想目标,也就是“伪目标”。

经过改良之后,功能已经稍微增强了那么一丢丢。如果接着再新建一个c文件并且也要一起编译,那么还是要继续在Makefile里添加一条编译规则,但是如果需要添加多个文件呢,那显得有点麻烦了。没关系,Makefile里还提供了简便的解决方法,那就继续改良,修改后的Makefile内容:

main: main.o test.o
	gcc -o main $^

%.o: %.c
	gcc -c $< -o $@

clean:
	rm *.o main -rf
.PHONY: clean

这样,再添加c文件也只需要在“目标”main中添加一个“依赖”即可。其中,以上使用到的符号含义如下:

符号含义
$^所有依赖文件
$<第一个依赖文件
$@目标文件
%.o匹配到所有的.o文件

2.4 变量介绍,并在示例2的基础上继续改良

变量类型说明
:=即时变量
=延时变量
?=延时变量,第一次定义才起效,如果前面已经定义后面就不起作用
+=附加,即时/延时取决于该变量的定义

先看变量的使用示例:

A := $(C)
B = $(C)
C = abc
D = arm
D ?= linux

all:
	@echo A = $(A)
	@echo B = $(B)
	@echo D = $(D)

C += 123

测试结果:

A =
B = abc 123
D = arm

了解了基本的变量类型之后,就可以在示例2的基础上继续改良,修改后Makefile内容如下:

objs = main.o test.o

CC = gcc
CFLAGS = -Werror

main: $(objs)
	$(CC) $(CFLAGS) -o main $^

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

clean:
	rm *.o main -rf
.PHONY: clean

使用示例:

linrm@linrm-VirtualBox:~/MakefileTest/02$ make
gcc -Werror -c main.c -o main.o
gcc -Werror -c test.c -o test.o
gcc -Werror -o main main.o test.o
linrm@linrm-VirtualBox:~/MakefileTest/02$ file main
main: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=4bf30f4b9232d5f5eea573ba2d05bbfea4ec35d9, not stripped
linrm@linrm-VirtualBox:~/MakefileTest/02$
linrm@linrm-VirtualBox:~/MakefileTest/02$ make clean
rm *.o main -rf
linrm@linrm-VirtualBox:~/MakefileTest/02$ make CC=arm-linux-gnueabihf-gcc
arm-linux-gnueabihf-gcc -Werror -c main.c -o main.o
arm-linux-gnueabihf-gcc -Werror -c test.c -o test.o
arm-linux-gnueabihf-gcc -Werror -o main main.o test.o
linrm@linrm-VirtualBox:~/MakefileTest/02$ 
linrm@linrm-VirtualBox:~/MakefileTest/02$ file main
main: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 2.6.32, BuildID[sha1]=a1fc72638a44da6e0302f2b20d2218b6b4dcdc27, with debug_info, not stripped
linrm@linrm-VirtualBox:~/MakefileTest/02$

可以看到,通过变量的方法使得编译变得更加智能了,并且在make的时候也可以手动指定变量来覆盖Makefile文件中的变量,以上示例就指定了CC变量来编译得到不同平台的可执行程序。


2.5 函数介绍,并在示例2的基础上继续改良

函数介绍

格式:

$(函数名 参数) 	# 函数名与参数用空格或Tab键隔开,参数之间用逗号隔开,空格和逗号不作为参数的一部分

常见的函数(更多函数需自行探索):

$(foreach var, list, text) 		# 对于list的每个变量var执行text
$(filter pattern..., text) 		# 在text中取出符合patten格式的值
$(filter-out pattern..., text) 	# 在text中取出不符合patten格式的值
$(wildcard pattern) 			# pattern定义了文件名的格式
								# wildcard函数就是取出真实存在的文件
$(patsubst pattern, replacement, $(var))
								# 从$(var)中取出每一个值
								# 如果符合pattern则替换为replacement
$(subst from, to, text) 		# 将text中使用to替换每一处from
$(findstring find, in) 			# 在字符串in中搜索find并返回find

在示例2的基础上继续改良

在前面的2.4中,如果修改了头文件,重新编译时就会发现并没有对包含该头文件的源文件进行重新编译,修改之前的具体情况如下:

linrm@linrm-VirtualBox:~/MakefileTest/02$ touch test.h
linrm@linrm-VirtualBox:~/MakefileTest/02$ make
make: 'main' is up to date.
linrm@linrm-VirtualBox:~/MakefileTest/02$

修改之后的Makefile内容如下(后面再分析为什么这么改):

objs = main.o test.o

CC = gcc
CFLAGS = -Werror

depend_files := $(patsubst %, .%.d, $(objs))
depend_files := $(wildcard $(depend_files))

main: $(objs)
	$(CC) $(CFLAGS) -o main $^

ifneq ($(depend_files), )
include $(depend_files)
endif

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@ -MD -MF .$@.d

clean:
	rm *.o .*.d main -rf
.PHONY: clean

使用:

linrm@linrm-VirtualBox:~/MakefileTest/02$ make
gcc -Werror -c main.c -o main.o -MD -MF .main.o.d
gcc -Werror -c test.c -o test.o -MD -MF .test.o.d
gcc -Werror -o main main.o test.o
linrm@linrm-VirtualBox:~/MakefileTest/02$
linrm@linrm-VirtualBox:~/MakefileTest/02$ touch test.h
linrm@linrm-VirtualBox:~/MakefileTest/02$ make
gcc -Werror -c main.c -o main.o -MD -MF .main.o.d
gcc -Werror -c test.c -o test.o -MD -MF .test.o.d
gcc -Werror -o main main.o test.o
linrm@linrm-VirtualBox:~/MakefileTest/02$ 

可以看到,只要修改了某个头文件,其他包含该头文件的源文件就会被重新编译了。这里gcc涉及了2个参数:“-MD”选项的目的是生成目标文件依赖的所有文件详细信息;“-MF filename”就是将前面的“-MD”选项生的内容写入到文件filename中(参考文章:[gcc的参数解释1] [gcc的参数解释2])。查看一下.main.o.d文件的内容:

main.o: main.c /usr/include/stdc-predef.h test.h

这个文件的内容看起来就是一个Makefile的“目标”和“依赖”,而文件的本身是被include关键字将它包含进来。需要注意的是,include前面可以是一个或多个空格,反正读取时都会被忽略掉,但是就不能是Tab键,因为这样就会被认为是一个命令而不是普通的语句。另外,在一个Makefile文件中使用include关键字时,它会暂停读取本文件的内容,等到读取include包含的文件时才返回继续往下读。所以,include编译生成的依赖文件就相当于为我们编写了一个Makefile的规则,所以只要它依赖的头文件被修改了,也就比“目标”新,就会“触发”该文件被重新编译。


2.6 示例3:VPATH/vpath的使用(可用于多目录查找文件)

linrm@linrm-VirtualBox:~/MakefileTest/03$ tree
.
├── include
│   └── test.h
├── Makefile
└── src
    ├── main.c
    └── test.c

其中源文件都是直接从前面示例2中拷贝过来的,这种目录架构有利于代码的管理。项目的结构变了,相应的Makefile当然也要跟着变:

objs = main.o test.o

CC = gcc
CFLAGS = -Werror -Iinclude

VPATH = src include
#vpath %.c src
#vpath %.h include

depend_files := $(patsubst %, .%.d, $(objs))
depend_files := $(wildcard $(depend_files))

main: $(objs)
	$(CC) $(CFLAGS) -o main $^

ifneq ($(depend_files), )
include $(depend_files)
endif

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@ -MD -MF .$@.d

clean:
	rm *.o .*.d main -rf
.PHONY: clean

搜索文件比较重要的两个变量:

  • VPATH:它是一个环境变量,使用时指定文件的路径,多路径可以使用空格或者冒号隔开,并且是按照前后顺序进行搜索;
  • vpath:它是一个关键字,使用方法和VPATH差不多,但它是按照条件搜索,除了指定文件的路径,还要加上条件,格式为“vpath 条件 目录1:目录2…”;除此之外,如果不加目录则代表清楚符合条件的目录,如果条件与目录都不加则代表清除所有已被设置的文件搜索路径。

这种编译方式是将所有的规则都写到一个Makefile里了,这种方式对于文件较多的工程来讲还是不够方便的,还可以参考后面的编写方式。


2.7 示例4:参考完整项目的Makefile

(整理自韦东山老师教学视频)

a. 项目顶层Makefile

CROSS_COMPILE = 
AS		= $(CROSS_COMPILE)as
LD		= $(CROSS_COMPILE)ld
CC		= $(CROSS_COMPILE)gcc
CPP		= $(CC) -E
AR		= $(CROSS_COMPILE)ar
NM		= $(CROSS_COMPILE)nm

STRIP		= $(CROSS_COMPILE)strip
OBJCOPY		= $(CROSS_COMPILE)objcopy
OBJDUMP		= $(CROSS_COMPILE)objdump

export AS LD CC CPP AR NM
export STRIP OBJCOPY OBJDUMP

CFLAGS := -Wall -O2 -g
CFLAGS += -I $(shell pwd)/include

LDFLAGS := 

export CFLAGS LDFLAGS

TOPDIR := $(shell pwd)
export TOPDIR

TARGET := test


obj-y += main.o
obj-y += sub.o
obj-y += a/


all : start_recursive_build $(TARGET)
	@echo $(TARGET) has been built!

start_recursive_build:
	make -C ./ -f $(TOPDIR)/Makefile.build

$(TARGET) : built-in.o
	$(CC) -o $(TARGET) built-in.o $(LDFLAGS)

clean:
	rm -f $(shell find -name "*.o")
	rm -f $(TARGET)

distclean:
	rm -f $(shell find -name "*.o")
	rm -f $(shell find -name "*.d")
	rm -f $(TARGET)

b. 项目顶层Makefile.build

PHONY := __build
__build:


obj-y :=
subdir-y :=
EXTRA_CFLAGS :=

include Makefile

# obj-y := a.o b.o c/ d/
# $(filter %/, $(obj-y))   : c/ d/
# __subdir-y  : c d
# subdir-y    : c d
__subdir-y	:= $(patsubst %/,%,$(filter %/, $(obj-y)))
subdir-y	+= $(__subdir-y)

# c/built-in.o d/built-in.o
subdir_objs := $(foreach f,$(subdir-y),$(f)/built-in.o)

# a.o b.o
cur_objs := $(filter-out %/, $(obj-y))
dep_files := $(foreach f,$(cur_objs),.$(f).d)
dep_files := $(wildcard $(dep_files))

ifneq ($(dep_files),)
  include $(dep_files)
endif


PHONY += $(subdir-y)


__build : $(subdir-y) built-in.o

$(subdir-y):
	make -C $@ -f $(TOPDIR)/Makefile.build

built-in.o : $(cur_objs) $(subdir_objs)
	$(LD) -r -o $@ $^

dep_file = .$@.d

%.o : %.c
	$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<
	
.PHONY : $(PHONY)

c. 子目录Makefile

obj-y += xxx.o
obj-y += yyy.o

3、总结

其实,在实际使用中,很好会从0开始写一个Makefile,一般有现成的搬过来修修改改基本上就可以使用了,但前提也还是需要看得懂。另外,目前也有一些自动生成Makefile文件的开源软件,就比如autotools和cmake,两者的使用可以参考专栏:【Linux:Makefile】

参考文章

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

R-QWERT

你的鼓励是我最大的动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值