菜单开关

周梦康 发表于 2019-12-23 326 次浏览

项目目录

.
├── http.c
├── http.h
├── log.c
├── log.h
├── mywget
├── mywget.c
├── progress.c
├── progress.h
├── test.php
├── url.c
├── url.h
├── wget.c
├── wget.h
├── wget_opt.c
└── wget_opt.h

原始方案

$ ls *.c
http.c  log.c  mywget.c  progress.c  url.c  wget.c  wget_opt.c
$ gcc http.c  log.c  mywget.c  progress.c  url.c  wget.c  wget_opt.c -std=gnu99 -o mywget

使用 shell 命令助力

gcc `ls *.c` -std=gnu99 -o mywget

一步到位,写着很简单,但是在大型软件工程中,可能有成千个c文件,如果修改一行代码所有的代码都需要重新编译,无异于噩梦。所以这才有了*.o文件。

gcc 模块化

$ ls *.c|awk '{print "gcc -std=gnu99 -c "$1}'
gcc -std=gnu99 -c http.c
gcc -std=gnu99 -c log.c
gcc -std=gnu99 -c mywget.c
gcc -std=gnu99 -c progress.c
gcc -std=gnu99 -c url.c
gcc -std=gnu99 -c wget.c
gcc -std=gnu99 -c wget_opt.c

所以我们可以这样执行,初次编译

$ ls *.c|awk '{print "gcc -std=gnu99 -c "$1}'|sh
$ gcc `ls *.o` -o mywget

假如又更新了url.c,那么我们只需要

$ gcc -std=gnu99 -c url.c
$ gcc `ls *.o` -o mywget

但是又有了新问题,如果一个文件更新所有依赖的文件的编译都需要更新,肯定有办法能扫描出来呢?
gcc -M 可以把依赖的头文件输出出来,包括系统头文件;gcc -MM 可以把依赖的头文件输出出来,排除系统头文件。

$ gcc -MM *.c
http.o: http.c http.h url.h log.h
log.o: log.c log.h
mywget.o: mywget.c url.h log.h wget.h wget_opt.h
progress.o: progress.c progress.h
url.o: url.c url.h log.h
wget.o: wget.c wget.h url.h log.h http.h progress.h
wget_opt.o: wget_opt.c wget_opt.h

从上面的结果可以看出,如果更新了log.h,基本所有的模块都要更新一遍。命令手动操作也还是比较麻烦,所以如此看来必须得有个新的工具来管理项目的编译。那就是 Makefile 了。

Makefile 基础

官方手册 https://www.gnu.org/software/make/manual/html_node/#toc-Overview-of-make
内容太多,官方也不推荐一个新手上来就细读,https://www.gnu.org/software/make/manual/html_node/Reading.html#Reading

规则

Makefile由n组规则组成,每组规则如下

target ... : prerequisites ... 
    command1
    command2
    ...

目标(target)和条件(Prerequisite)之间的关系是:欲更新目标,必须首先更新它的所有条件;所有条件中只要有一个条件被更新了,目标也必须随之被更新。所谓“更新”就是执行一遍规则中的命令列表,命令列表中的每条命令必须以一个Tab开头,注意不能是空格,Makefile的格式不像C语言的缩进那么随意,对于Makefile中的每个以Tab开头的命令,make会创建一个Shell进程去执行它。

那么我们最初的 gcc 的编译方式就可以这样来写

mywget : http.c  log.c  mywget.c  progress.c  url.c  wget.c  wget_opt.c
    gcc http.c  log.c  mywget.c  progress.c  url.c  wget.c  wget_opt.c -std=gnu99 -o mywget

复制我的代码到你的项目中应该会报错,记得把第二行的缩进删掉,在 vim 敲 tab 键才行

特殊变量

https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html#Automatic-Variables
https://mengkang.net/book-c/ch22s03.html

$@,表示规则中的目标。
$<,表示规则中的第一个条件。
$?,表示规则中所有比目标新的条件,组成一个列表,以空格分隔。
$^,表示规则中的所有条件,组成一个列表,以空格分隔。

所以我们的 Makefile 可以变为

mywget : http.c  log.c  mywget.c  progress.c  url.c  wget.c  wget_opt.c
    gcc $^ -std=gnu99 -o $@

这样当我们项目增加c文件时,需要编辑的就少了一行,有没有办法把条件里的文件也动态替换呢?

变量赋值

makefile 变量赋值的套路和我们c程序赋值不一样,有单行的、多行的;单行和多行的又分为 6 种,具体大家可以看官方手册 https://www.gnu.org/software/make/manual/html_node/Reading-Makefiles.html#Reading-Makefiles

根据需要我们介绍2种,:=运算符和=运算符的

a := 1$(c)
b  = 1$(c)
c  = 2

all :
    @echo a:$(a)
    @echo b:$(b)
$ make
a:1
b:12

解释

a := 1$(c) // 简单展开,此时 c 没有赋值所以 a = 1
b  = 1$(c) // 递归展开,c 后面赋值为 2 所以这里 b = 12

https://mengkang.net/book-c/ch22s03.html

使用 shell 函数

https://www.gnu.org/software/make/manual/html_node/Shell-Function.html#Shell-Function

例如

contents := $(shell cat foo)
files := $(shell echo *.c)

所以我们的 Makefile 可以写成这样

src = $(shell echo *.c)

mywget : $(src)
    @echo src: $(src)
    gcc $^ -std=gnu99 -o $@

执行

$ make
src: http.c log.c mywget.c progress.c url.c wget.c wget_opt.c
gcc http.c log.c mywget.c progress.c url.c wget.c wget_opt.c -std=gnu99 -o mywget

Makefile 模块化

按照方面命令行直接使用gcc生成.o文件的方式,照搬放在 Makefile

CFLAGS = -std=gnu99

mywget: http.o log.o mywget.o progress.o url.o wget.o wget_opt.o
    gcc $(CFLAGS) -o $@ $^
http.o: http.c http.h url.h log.h
    gcc $(CFLAGS) -c -o $@ $<
log.o: log.c log.h
    gcc $(CFLAGS) -c -o $@ $<
mywget.o: mywget.c url.h log.h wget.h wget_opt.h
    gcc $(CFLAGS) -c -o $@ $<
progress.o: progress.c progress.h
    gcc $(CFLAGS) -c -o $@ $<
url.o: url.c url.h log.h
    gcc $(CFLAGS) -c -o $@ $<
wget.o: wget.c wget.h url.h log.h http.h progress.h
    gcc $(CFLAGS) -c -o $@ $<
wget_opt.o: wget_opt.c wget_opt.h
    gcc $(CFLAGS) -c -o $@ $<

使用内置函数完成模块化

官方提供字符串替换函数

https://www.gnu.org/software/make/manual/html_node/Text-Functions.html#Text-Functions

$(var:suffix=replacement)
$(patsubst %suffix,%replacement,$(var))

当我们想通过*.c获取对应的*.o文件列表,我们可以通过下面两种方式来获取

src = $(shell echo *.c)

objects1 = $(src:.c=.o)
objects2 = $(patsubst %.c,%.o,$(src))

mywget :
    @echo $(objects1)
    @echo $(objects2)

执行测试下

$ make
http.o log.o mywget.o progress.o url.o wget.o wget_opt.o
http.o log.o mywget.o progress.o url.o wget.o wget_opt.o

隐含规则和模式规则

在 make 中定义了一些隐含规则,在隐含规则中有一条%.o: %.c的模式规则

https://mengkang.net/book-c/ch22s02.html

# default
OUTPUT_OPTION = -o $@

# default
CC = cc

# default
COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c

%.o: %.c
#  commands to execute (built-in):
        $(COMPILE.c) $(OUTPUT_OPTION) $<

所以gcc -MM *.c的结果

http.o: http.c http.h url.h log.h
log.o: log.c log.h
mywget.o: mywget.c url.h log.h wget.h wget_opt.h
progress.o: progress.c progress.h
url.o: url.c url.h log.h
wget.o: wget.c wget.h url.h log.h http.h progress.h
wget_opt.o: wget_opt.c wget_opt.h

都会去默认去执行cc编译

cc $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c -o $@ $<

cc 链接到了 gcc 在GNU/Linux可以认为是一个文件
CFLAGS CPPFLAGS TARGET_ARCH 都是空

cc -c -o $@ $<

我们把前面用到的gcc -MM *.c的结果添加到Makefile

src = $(shell echo *.c)
objects = $(src:.c=.o)

mywget : $(objects)
    gcc $^ -std=gnu99 -o $@

http.o: http.c http.h url.h log.h
log.o: log.c log.h
mywget.o: mywget.c url.h log.h wget.h wget_opt.h
progress.o: progress.c progress.h
url.o: url.c url.h log.h
wget.o: wget.c wget.h url.h log.h http.h progress.h
wget_opt.o: wget_opt.c wget_opt.h

编译会报错,我们需要的是

cc -std=gnu99 -c -o $@ $<

所以我们需要把 CFLAGS 设置为 -std=gnu99,如果我们全局都需要使用这个参数,直接统一赋值即可;如果只希望某种匹配模式才会设置 CFLAGS 变量,可以这样

%.o : CFLAGS = -std=gnu99

https://www.gnu.org/software/make/manual/make.html#Pattern_002dspecific

所以我们的 Makefile 可以写成

src = $(shell echo *.c)
objects = $(src:.c=.o)

mywget : $(objects)
    gcc $^ $(CFLAGS) -o $@

http.o: http.c http.h url.h log.h
log.o: log.c log.h
mywget.o: mywget.c url.h log.h wget.h wget_opt.h
progress.o: progress.c progress.h
url.o: url.c url.h log.h
wget.o: wget.c wget.h url.h log.h http.h progress.h
wget_opt.o: wget_opt.c wget_opt.h

CFLAGS = -std=gnu99

CFLAGS = -std=gnu99放最前面,最后面都行

动态管理自动生成先决条件

同样gcc -MM *.c的结果也是会变的,也不应该是每次执行完,然后拷贝到 Makefile 里,肯定有办法在 Makefile 里动态的管理。

方案是把gcc -MM xx.c的结果存发送到xx.d文件里,然后再通过include引入进来

下面这条规则来源于官方手册推荐写法,每次编译都会重新生成.d文件

https://www.gnu.org/software/make/manual/make.html#Automatic-Prerequisites

%.d: %.c
    set -e; rm -f $@; \
    $(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \
    sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
    rm -f $@.$$$$
$ gcc -MM mywget.c
mywget.o: mywget.c url.h log.h wget.h wget_opt.h
$ cat mywget.d
mywget.o mywget.d : mywget.c url.h log.h wget.h wget_opt.h

此处解释的非常清楚 https://mengkang.net/book-c/ch22s04.html

所以最后我们的 Makefile 文件是

src = $(shell echo *.c)
objects = $(src:.c=.o)

mywget : $(objects)
    gcc $^ $(CFLAGS) -o $@

include $(src:.c=.d)

%.d: %.c
    set -e; rm -f $@; \
    $(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \
    sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
    rm -f $@.$$$$

CFLAGS = -std=gnu99

执行如下

$ make
Makefile:7: http.d: No such file or directory
Makefile:7: log.d: No such file or directory
Makefile:7: mywget.d: No such file or directory
Makefile:7: progress.d: No such file or directory
Makefile:7: url.d: No such file or directory
Makefile:7: wget.d: No such file or directory
Makefile:7: wget_opt.d: No such file or directory
set -e; rm -f wget_opt.d; \
cc -MM  wget_opt.c > wget_opt.d.$$; \
sed 's,\(wget_opt\)\.o[ :]*,\1.o wget_opt.d : ,g' < wget_opt.d.$$ > wget_opt.d; \
rm -f wget_opt.d.$$
set -e; rm -f wget.d; \
cc -MM  wget.c > wget.d.$$; \
sed 's,\(wget\)\.o[ :]*,\1.o wget.d : ,g' < wget.d.$$ > wget.d; \
rm -f wget.d.$$
set -e; rm -f url.d; \
cc -MM  url.c > url.d.$$; \
sed 's,\(url\)\.o[ :]*,\1.o url.d : ,g' < url.d.$$ > url.d; \
rm -f url.d.$$
set -e; rm -f progress.d; \
cc -MM  progress.c > progress.d.$$; \
sed 's,\(progress\)\.o[ :]*,\1.o progress.d : ,g' < progress.d.$$ > progress.d; \
rm -f progress.d.$$
set -e; rm -f mywget.d; \
cc -MM  mywget.c > mywget.d.$$; \
sed 's,\(mywget\)\.o[ :]*,\1.o mywget.d : ,g' < mywget.d.$$ > mywget.d; \
rm -f mywget.d.$$
set -e; rm -f log.d; \
cc -MM  log.c > log.d.$$; \
sed 's,\(log\)\.o[ :]*,\1.o log.d : ,g' < log.d.$$ > log.d; \
rm -f log.d.$$
set -e; rm -f http.d; \
cc -MM  http.c > http.d.$$; \
sed 's,\(http\)\.o[ :]*,\1.o http.d : ,g' < http.d.$$ > http.d; \
rm -f http.d.$$
cc -std=gnu99   -c -o http.o http.c
cc -std=gnu99   -c -o log.o log.c
cc -std=gnu99   -c -o mywget.o mywget.c
cc -std=gnu99   -c -o progress.o progress.c
cc -std=gnu99   -c -o url.o url.c
cc -std=gnu99   -c -o wget.o wget.c
cc -std=gnu99   -c -o wget_opt.o wget_opt.c
gcc http.o log.o mywget.o progress.o url.o wget.o wget_opt.o -std=gnu99 -o mywget

增加 clean 规则

src = $(shell echo *.c)
objects = $(src:.c=.o)

mywget : $(objects)
    gcc $^ $(CFLAGS) -o $@

include $(src:.c=.d)

%.d: %.c
    set -e; rm -f $@; \
    $(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \
    sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
    rm -f $@.$$$$

clean :
    -rm mywget *.d *.o
    
CFLAGS = -std=gnu99

.PHONY: clean

这样就可以执行make clean

增加 install uninstall 规则

目录命名上规则参考 https://www.gnu.org/prep/standards/html_node/Directory-Variables.html#Directory-Variables
目标文件命名上规则参考 https://www.gnu.org/prep/standards/html_node/Standard-Targets.html#Standard-Targets

按照规范,默认是需要支持make all的,用来表示生成二进制文件、库文件、文档等。再加上安装和卸载,最后命令如下:

src = $(shell echo *.c)
objects = $(src:.c=.o)
bindir = /usr/local/bin

all : mywget

mywget : $(objects)
    $(CC) $^ $(CFLAGS) -o $@

include $(src:.c=.d)

%.d: %.c
    set -e; rm -f $@; \
    $(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \
    sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
    rm -f $@.$$$$

clean :
    -rm mywget *.d *.o
    
install :
    install -d $(bindir)
    install -m 0775 mywget $(bindir)
    
uninstall :
    rm $(bindir)/mywget
    
CFLAGS = -std=gnu99

.PHONY: clean install uninstall all

自测下吧

$ make
$ make all
$ make clean
$ sudo make install
$ sudo make uninstall

评论列表