从零开始写 Makefile
本文旨在介绍,如何从零开始写Makfile,实现多文件、多目标、多级目录的代码编译。
目录
5、编译lib & 编译bin & 执行可执行文件 & 清除编译产物
一、Makefile基础
1、编写c代码
编写一个简单的main.c就行
#include <unistd.h>
#include <stdio.h>
int main(void)
{
printf("Hello world!\n");
return 0;
}
2、编写Makefile
同级目录下创建一个文件,名称为Makefile(建议文件名称就固定为Makefile)
Makefile的基础编写规则,我都写在下面的注释里面了^_^
# 在Makfile中,以'#'作为注释开头,不支持多行注释
#Makfile最朴素的用法就是,指定依赖文件,指定文件依赖关系,指定生成规则(如何将依赖文件转换成目标文件)
# 指定编译器
CC = gcc
# Makefile中,通常需要指定一个目标文件名词,即下面的变量 TARGET,后面可以借用 $(TARGET) 来获取 TARGET 的值
TARGET = main
#指定用于生成目标文件的依赖文件如下,目标文件是可执行文件,所以依赖文件应该是一个对象文件(.o文件)
#我们可以借助一个变量OBJS来指代它,后面可以使用 $(OBJS) 来获取变量 OBJS 的值
OBJS = main.o
#同样,生成对象文件(.o文件),也需要相应的源文件(.c或.cpp文件)
SRC = main.c
##指定编译选项,用于gcc编译
# 选项[-Wall]表示 将打印全部告警信息,[-g]表示 支持gdb调试,[-c]表示 只编译源文件但不链接,[-o]表示 指定输出的目标文件名称
CFLAGS = -Wall -g -c
# 需要指定一定的规则来生成目标文件,使用冒号指定依赖关系
# 下面一行gcc命令是生成规则(若要在makefile中执行命令,行首必须以tab键开头)
# 变量 $@ 指代冒号前面的参数,变量 $^ 指代冒号后面的所有参数
$(TARGET) : $(OBJS)
$(CC) -o $@ $^
$(OBJS) : $(SRC)
$(CC) -o $@ $(CFLAGS) $^
# 通常会在 Makfile 结尾处编写一个clean规则用于清除我们的中间文件和目标文件
# .PHONY 表示冒号后面的标签名 clean 是一个伪目标(只执行clean的命令,但不会生成一个名叫“clean”的目标文件)
.PHONY:clean
# 在命令前面加一个'@'符号,则只会执行命令,而不在终端打印出命令本身
clean:
$(RM) $(TARGET)
@echo "Clean target files done."
$(RM) $(OBJS)
@echo "Clean object files done."
3、编译 & 执行 & 清除编译产物
终端下执行命令make可以执行编译工作,执行make clean可以清除掉编译产物。
如果你的Makefile文件叫其他名称,可以使用 -f 参数指定文件名
现在就简单完成了一个Makefile的编译工程,可以通过make命令实现c代码的编译工作
二、多个依赖文件编译
通常来讲,我们编写的大型工程文件,代码都不可能放在同一个文件中,所以搭建如下图所示的一个代码结构
1、编写一个头文件public.h
#ifndef PUBLIC_H__
#define PUBLIC_H__
void tool1(void);
#endif
2、编写一个新的c文件main_lib.c
代码内容如下
#include <unistd.h>
#include <stdio.h>
#include "public.h"
void tool1(void)
{
printf("This is tool1.\n");
return;
}
3、修改main.c
修改main函数,使其能够调用main_lib.c中的函数
#include <unistd.h>
#include <stdio.h>
#include "public.h"
int main(void)
{
printf("Hello world!\n");
tool1();
return 0;
}
4、修改Makefile
##指定源文件
# 使用wildcard通配符,匹配所有 .c 文件
SRC = $(wildcard *.c)
##指定生成目标文件的依赖文件
# 使用patsubst 方法替换字符串,利用获取到的.c文件名,获得.o文件名
# %.c 即表示所有以 .c 结尾的字符串
OBJS = $(patsubst %.c, %.o, $(SRC))
##指定编译选项,用于gcc编译
# 选项[-Wall]表示 将打印全部告警信息,[-g]表示 支持gdb调试,[-c]表示 只编译源文件但不链接,[-o]表示 指定输出的目标文件名称
CFLAGS = -Wall -g -c
##指定编译器
CC = gcc
##指定目标文件名称
TARGET = main
##指定依赖关系
$(TARGET) : $(OBJS)
$(CC) -o $@ $^
##指定由 .c 文件生成 .o 的生成规则
%.o : %.c
$(CC) -o $@ $(CFLAGS) $^
##指定清除编译产物时的执行命令
.PHONY:clean
clean:
$(RM) $(TARGET)
@echo "Clean target files done."
$(RM) $(OBJS)
@echo "Clean object files done."
5、编译 & 执行 & 清除编译产物
以上就是多个依赖文件(源文件)生成同一个可执行文件的make规则
三、同一目录生成多个目标文件
1、新增一个main1.c
#include <unistd.h>
#include <stdio.h>
int main(void)
{
printf("This is in main1.\n");
return 0;
}
2、修改Makefile
##指定源文件
# 使用wildcard通配符,匹配所有 .c 文件
SRC = $(wildcard *.c)
##指定生成目标文件的依赖文件
# 使用patsubst 方法替换字符串,利用获取到的.c文件名,获得.o文件名
# %.c 即表示所有以 .c 结尾的字符串
OBJS = $(patsubst %.c, %.o, $(SRC))
##指定编译选项,用于gcc编译
# 选项[-Wall]表示 将打印全部告警信息,[-g]表示 支持gdb调试,[-c]表示 只编译源文件但不链接,[-o]表示 指定输出的目标文件名称
CFLAGS = -Wall -g -c
##指定编译器
CC = gcc
##指定目标文件名称
TARGET = main main1
##执行 make 命令时,缺省参数事实上就是 all
all: $(TARGET)
##指定依赖关系
##指定依赖关系
main : main.o main_lib.o
$(CC) -o $@ $^
main1 : main1.o
$(CC) -o $@ $^
##指定由 .c 文件生成 .o 的生成规则
%.o : %.c
$(CC) -o $@ $(CFLAGS) $^
##指定清除编译产物时的执行命令
.PHONY:clean
clean:
$(RM) $(TARGET)
@echo "Clean target files done."
$(RM) $(OBJS)
@echo "Clean object files done."
3、编译 & 分别执行 & 清除编译产物
这样就可以实现一个makefile编译生成多个可执行文件
四、静态库文件(.a文件)的编译打包和链接
前述代码组织方式都不够结构化,可执行文件和库文件杂糅在一起,下面整理一下代码文件组织方式,将 main.lib.c 打包成 .a 静态库文件供生成可执行文件时使用
1、更改代码目录结构
将所有代码文件划分为可执行文件、库文件、头文件,将可执行文件放到 bin 目录下,将库文件放到 lib 目录下,将 头文件统一放到 include 目录下。
除 include 目录下,其他目录下各自创建一个 Makefile 文件,另外在lib目录下新建一个targets目录用于统一存放生成的静态库文件
2、修改 lib 目录下的 main_lib.c
#include <unistd.h>
#include <stdio.h>
#include "../include/public.h"
void tool1(void)
{
printf("This is tool1.\n");
return;
}
3、编写 lib 目录下的 Makefile
lib 目录下的 .c 文件要生成 静态库文件 .a,所以目标文件不再是可执行文件main,而是libmain.a 文件
##指定源文件
# 使用wildcard通配符,匹配所有 .c 文件
SRC = $(wildcard *.c)
##指定生成目标文件的依赖文件
# 使用patsubst 方法替换字符串,利用获取到的.c文件名,获得.o文件名
# %.c 即表示所有以 .c 结尾的字符串
OBJS = $(patsubst %.c, %.o, $(SRC))
##指定编译选项,用于gcc编译
# 选项[-Wall]表示 将打印全部告警信息,[-g]表示 支持gdb调试,[-c]表示 只编译源文件但不链接,[-o]表示 指定输出的目标文件名称
CFLAGS = -Wall -g -c
##指定编译器
CC = gcc
##指定目标文件名称
TARGET = targets/libmain.a
##执行 make 命令时,缺省参数事实上就是 all
all: $(TARGET)
##指定依赖关系
##指定依赖关系
$(TARGET) : $(OBJS)
ar rcs $@ $^
ranlib $@
##指定由 .c 文件生成 .o 的生成规则
%.o : %.c
$(CC) -o $@ $(CFLAGS) $^ -I:$(INC_DIR)
##指定清除编译产物时的执行命令
.PHONY:clean
clean:
$(RM) $(TARGET)
@echo "Clean target files done."
$(RM) $(OBJS)
@echo "Clean object files done."
4、编写 bin 目录下的 Makefile
##指定头文件目录
INC_DIR = ../public/
##指定源文件
# 使用wildcard通配符,匹配所有 .c 文件
SRC = $(wildcard *.c)
##指定生成目标文件的依赖文件
# 使用patsubst 方法替换字符串,利用获取到的.c文件名,获得.o文件名
# %.c 即表示所有以 .c 结尾的字符串
OBJS = $(patsubst %.c, %.o, $(SRC))
##指定静态库文件所在路径
LIB_DIR = ../lib/targets
##指定静态库文件名称
# notdir 方法用于去除路径名
LIB_FILES = $(notdir $(wildcard $(LIB_DIR)/*.a))
##指定编译选项,用于gcc编译
# 选项[-Wall]表示 将打印全部告警信息,[-g]表示 支持gdb调试,[-c]表示 只编译源文件但不链接,[-o]表示 指定输出的目标文件名称
CFLAGS = -Wall -g -c
##指定编译器
CC = gcc
##指定目标文件名称
TARGET = main main1
##执行 make 命令时,缺省参数事实上就是 all
all: $(TARGET)
##指定依赖关系
#生成可执行文件 main 需要链接静态库文件
main : main.o
$(CC) -o $@ $^ -I$(INC_DIR) -L$(LIB_DIR) -l:$(LIB_FILES)
main1 : main1.o
$(CC) -o $@ $^
##指定由 .c 文件生成 .o 的生成规则
%.o : %.c
$(CC) -o $@ $(CFLAGS) $^
##指定清除编译产物时的执行命令
.PHONY:clean
clean:
$(RM) $(TARGET)
@echo "Clean target files done."
$(RM) $(OBJS)
@echo "Clean object files done."
5、编译lib & 编译bin & 执行可执行文件 & 清除编译产物
五、多层目录递归编译
上面虽然已经实现了将可执行文件、库文件、头文件分别存放,但是编译和清理都需要分别切换目录,使用起来太不方便了,所以我们需要实现让makefile自动切换目录去编译和清理。
在根目录下创建一个新的 Makefile 文件,最终的代码目录结构如下图所示。
1、在根目录下编写 Makefile 文件
##指定当前目录下存在的子目录
SUBDIRS = lib bin
##将执行当前 Makefile 文件时传入的参数都作为伪目标向下一级传递
.PHONY: all clean $(SUBDIRS)
# 注意 往下传递的参数 尽量不要与子目录中的相同(除非是提供给子目录 Makefile 使用时)
# 我的子目录的指代目标文件使用的变量都是 TARGET ,所以这里这个变量名称 BIN 无论如何不能改成 TARGET
all clean:
$(MAKE) $(SUBDIRS) BIN=$@
##使用make -C 命令实现自动切换编译目录,注意参数 -C 必须是大写 C
$(SUBDIRS):
$(MAKE) -C $@ $(BIN)
2、编译 & 清理编译产物
以上就基本完成了一个比较完整的由Makefile组织的代码工程。
六、参考
学习过程中自然离不开各位大佬、前辈们写的资料,学习期间网上查阅了不少的博客、文章,但有许多没有收藏or点赞,链接已经找不到了,就放两个对我帮助最大的链接吧
学识尚浅,如有纰漏,望不吝赐教!
以上,End.