从零开始写 Makefile

本文旨在介绍,如何从零开始写Makfile,实现多文件、多目标、多级目录的代码编译。

目录

一、Makefile基础

1、编写c代码

2、编写Makefile

3、编译 & 执行 & 清除编译产物

二、多个依赖文件编译

1、编写一个头文件public.h

2、编写一个新的c文件main_lib.c

3、修改main.c

4、修改Makefile

5、编译 & 执行 & 清除编译产物

三、同一目录生成多个目标文件

1、新增一个main1.c

2、修改Makefile

3、编译 & 分别执行 & 清除编译产物

四、静态库文件(.a文件)的编译打包和链接

1、更改代码目录结构

2、修改 lib 目录下的 main_lib.c

3、编写 lib 目录下的 Makefile

4、编写 bin 目录下的 Makefile

5、编译lib & 编译bin & 执行可执行文件 & 清除编译产物

五、多层目录递归编译

1、在根目录下编写 Makefile 文件

2、编译 & 清理编译产物

六、参考


一、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点赞,链接已经找不到了,就放两个对我帮助最大的链接吧

        跟我一起写Makefile

        Makefile总结(多级目录、多目标)

        学识尚浅,如有纰漏,望不吝赐教!

以上,End.