[TOC]

Linux源码包的制作教程

学习Linux源码包的制作,一篇文章就够了。

本文章从源码包的概念开始,详细讲解每个步骤:configure,makefile,make,make install,make clean。

教你怎么去编写makefile编译文件。

本教程同样适用于使用makefile为安装文件的二进制软件包制作.

并用具体案例来说明整个流程,内含案例源码。

如需将源码包制作成rpm安装包,可以参考教程:Linux二进制包(RPM包)制作教程(教程+资料+案例)

一.概述

1.linux源码包

实际上,源码包就是一大堆源代码程序,是由程序员按照特定的格式和语法编写出来的。

我们都知道,计算机只能识别机器语言,也就是二进制语言,所以源码包的安装需要一名“翻译官”将“abcd”翻译成二进制语言,这名“翻译官”通常被称为编译器。

“编译”指的是从源代码到直接被计算机(或虚拟机)执行的目标代码的翻译过程,编译器的功能就是把源代码翻译为二进制代码,让计算机识别并运行。

源码包的安装需要把源代码编译为二进制代码,因此安装时间较长,很费时间。

为了解决使用源码包安装方式的这些问题,Linux 软件包的安装出现了使用二进制包的安装方式。

2.linux二进制包

二进制包,也就是源码包经过成功编译之后产生的包。由于二进制包在发布之前就已经完成了编译的工作,因此用户安装软件的速度较快(同 Windows下安装软件速度相当),且安装过程报错几率大大减小。

二进制包是 Linux 下默认的软件安装包,因此二进制包又被称为默认安装软件包。目前主要有以下 2 大主流的二进制包管理系统:

  • RPM 包管理系统:功能强大,安装、升级、査询和卸载非常简单方便,因此很多 Linux 发行版都默认使用此机制作为软件安装的管理方式,例如 Fedora、CentOS、SuSE 等。
  • DPKG 包管理系统:由 Debian Linux 所开发的包管理机制,通过 DPKG 包,Debian Linux 就可以进行软件包管理,主要应用在 Debian 和 Ubuntu 中。

同时有些开发者,会将编译好的二进制文件和其他配置,安装脚本压缩打包,然后使用脚本安装,不过安装较为复杂。

2.为什么要使用make?

在开发学习过程中,经常需要编译,简单的项目构建起来很方便。但遇到较复杂的项目,比如要跨平台交叉编译,选择性单元测试,性能测试,命令就要附加很多参数,敲起来麻烦,还容易忘记,时常要查看help。为了解决这个问题,我们可以编写shell,将常用操作封装在脚本中,虽然实现简单,但每个人编写、阅读能力不一,不利于规范化。另一个更好的选择,就是使用make。

3.什么是makefile ?

​ 因为, makefile关系到了整个工程的编译规则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。

​ makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。 make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,makefile都成为了一种在工程方面的编译方法。

4.关于程序的编译和链接

​ 一般来说,无论是C、C++、还是pas,首先要把源文件编译成中间代码文件,在Windows下也就是 .obj 文件,UNIX下是 .o 文件,即 Object File,这个动作叫做编译(compile)。然后再把大量的Object File合成执行文件,这个动作叫作链接(link) 。

译时,编译器需要的是语法的正确,函数与变量的声明的正确。对于后者,通常是你需要告诉编译器头文件的所在位置(头文件中应该只是声明,而定义应该放在C/C++文件中),只要所有的语法正确,编译器就可以编译出中间目标文件。一般来说,每个源文件都应该对应于一个中间目标文件(O文件或是OBJ文件)。 链接时,主要是链接函数和全局变量,所以,我们可以使用这些中间目标文件(O文件或是OBJ文件)来链接我们的应用程序。链接器并不管函数所在的源文件,只管函数的中间目标文件(Object File),在大多数时候,由于源文件太多,编译生成的中间目标文件太多,而在链接时需要明显地指出中间目标文件名,这对于编译很不方便,所以,我们要给中间目标文件打个包,在Windows下这种包叫“库文件”(Library File),也就是 .lib 文件,在UNIX下,是Archive File,也就是 .a 文件。

​ 总结一下,源文件首先会生成中间目标文件,再由中间目标文件生成执行文件。在编译时,编译器只检测程序语法,和函数、变量是否被声明。如果函数未被声明,编译器会给出一个警告,但可以生成Object File。而在链接程序时,链接器会在所有的Object File中找寻函数的实现,如果找不到,那到就会报链接错误码(Linker Error),在VC下,这种错误一般是:Link 2001错误,意思说是说,链接器未能找到函数的实现。你需要指定函数的ObjectFile.

二.configure介绍

1.什么是configure?

​ configure 是一个shell脚本。用来检测你的安装平台的目标特征的,并自动生成makefile文件,如果手动编写了makefile文件,则configure非必须的。

​ 为下一步的编译做准备,你可以通过在 configure 后加上参数来对安装进行控制,比如代码:./configure –prefix=/usr 意思是将该软件安装在 /usr 下面,执行文件就会安装在 /usr/bin (而不是默认的 /usr/local/bin),资源文件就会安装在 /usr/share(而不是默认的/usr/local/share)。同时一些软件的配置文件你可以通过指定 –sys-config= 参数进行设定。有一些软件还可以加上 –with、–enable、–without、–disable 等等参数对编译加以控制,你可以通过允许 ./configure –help 察看详细的说明帮助。

2.configurep配置详细说明

配置项较多,可以参考文章:Linux 下configure配置详解

本教程使用自定义的Makefile,所以此步可以跳过。

三.Make介绍

1.什么是Make?

​ Make是最常用的构建工具,诞生于1977年,主要用于C语言的项目。但是实际上 ,任何只要某个文件有变化,就要重新构建的项目,都可以用Make构建。

​ make命令是GNU的工程化编译工具,用以实现工程化的管理,提高开发效率。

​ 构建规则都写在Makefile文件里面,要学会如何Make命令,就必须学会如何编写Makefile文件。

Make解释Makefile 中的指令(应该说是规则)。在Makefile文件中描述了整个工程所有文件的编译顺序、编译规则。Makefile 有自己的书写格式、关键字、函数。像C 语言有自己的格式、关键字和函数一样。而且在Makefile 中可以使用系统shell所提供的任何命令来完成想要的工作。

2.常用的Make命令

make一般命令如下:

1
2
3
4
5
6
#安装
make install
#卸载
make uninstall
#默认的进行源代码编译
make

四.Makefile介绍

1.什么是Makefile

​ make命令执行时,需要一个makefile文件,以告诉make命令需要怎么样的去编译和链接程序。

​ 首先,我们用一个示例来说明makefile的书写规则,以便给大家一个感性认识。这个示例来源于gnu 的make使用手册,在这个示例中,我们的工程有8个c文件,和3个头文件,我们要写一个makefile来告诉make命令如何编译和链接这几个文件。我们的规则是:

  1. 如果这个工程没有编译过,那么我们的所有c文件都要编译并被链接。
  2. 如果这个工程的某几个c文件被修改,那么我们只编译被修改的c文件,并链接目标程序。
  3. 如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的c文件,并链接目标程序。

只要我们的makefile写得够好,所有的这一切,我们只用一个make命令就可以完成,make命令会自动智能地根据当前的文件修改的情况来确定哪些文件需要重编译,从而自动编译所需要的文件和链接目标程序。

2.makefile的规则

1).makefile的规则

1
2
3
4
target ... : prerequisites ...
[tab] command
    ...
    ...
  • target

    一个目标,可以是文件名(object file(目标文件)),也可以是某个操作的名字(伪目标),还可以是一个标签(label),这个名字由自己定义,用来指明要构建的对象 。

  • prerequisites

    生成该target所依赖的文件和/或target

  • tab

    使用tab来缩进

  • command

    该target要执行的命令(任意的shell命令)

这就是makefile的规则,也就是makefile中最核心的内容。

2).伪目标

1
2
create:
    touch newfile

比如上面这条规则,伪目标为create,命令作用为创建一个文件。要想构建这个操作,调用make create即可。

但是如果目录下,存在一个文件名为create,那么构建命令就不会去执行。为了解决这个问题,当使用伪目标时,可以明确声明create是“伪目标“,告诉make跳过文件检查。

1
2
3
.PHONY: clean
create:
    touch newfile

如果Make命令运行时没有指定目标,默认会执行Makefile文件的第一个目标。

3).prerequisites先决条件

先决条件,通常是文件名,多个名字用空格分隔。

它定义了一个是否进行重新构建的判断标准: 如果有任何一个先决文件发生改变(时间戳更新),就要重新构建。

这是一个文件的依赖关系,也就是说,target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义在command中。说白一点就是说:

1
prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。
示例
1
2
result.txt: source.txt
    cp source.txt result.txt

上面代码中,构建 result.txt 的前置条件是 source.txt 。如果当前目录中,source.txt 已经存在,那么make result.txt可以正常运行,否则必须再写一条规则,来生成 source.txt 。

1
2
source.txt:
    echo "this is the source" > source.txt

上面代码中,source.txt后面没有前置条件,就意味着它跟其他文件都无关,只要这个文件还不存在,每次调用make source.txt,它都会生成。

1
2
$ make result.txt
$ make result.txt

如果需要生成多个文件,往往采用下面的写法。

1
source: file1 file2 file3

上面代码中,source 是一个伪目标,只有三个前置文件,没有任何对应的命令。

1
$ make source

执行make source命令后,就会一次性生成 file1,file2,file3 三个文件

4).commands命令

命令是构建目标时具体执行的指令,由一行或多行shell组成。每行命令之前必须有一个tab键缩进。如果想用其他键缩进,可以用内置变量.RECIPEPREFIX声明。

1
2
3
.RECIPEPREFIX = >
hello:
> echo Hello, world

需要注意的是,每行shell在一个单独的bash进程中执行,多进程间没有继承关系。

1
2
3
var:
    export name=wangpeng
    echo "myname is $name"

运行上面的构建 ,发现变量name是取不到的,因为两行shell在两个独立的bash中运行。

  • 最直接的方法就是将两行shell写到一行中

    1
    2
    
    var:
        export name=wangpeng; echo "myname is $name"
    
  • 第二种办法,在换行前加反斜杠\转义,

    1
    2
    3
    
    var:
        export name=wangpeng \
        echo "myname is $name"
    
  • 还有第三种办法是使用。ONESHELL内置命令。

    1
    2
    3
    4
    
    .ONESHELL:
    var:
        export name=wangpeng
        echo "myname is $name"
    

3.makefile文件语法

1).注释

行首井号(#)表示注释。

2).回显

回显是指,在执行到每行命令前,将命令本身打印出来。

1
2
test:
    # 这是测试

执行上面构建会输出

1
2
$ make test
# 这是测试

在命令的前面加上@,就可以关闭回声。

1
2
test:
    @# 这是测试

这下构建时就不会有任何输出。

3).通配符

Makefile 的通配符与 Bash 一致,主要有星号"*"、问号"?"。比如*.text 表示所有后缀名为text的文件。

4).模式匹配

Make命令允许对文件名,进行类似正则运算的匹配,主要用到的匹配符是%。比如,假定当前目录下有 f1.c 和 f2.c 两个源码文件,需要将它们编译为对应的对象文件。

1
%.o: %.c

等同于下面的写法。

1
2
f1.o: f1.c
f2.o: f2.c

使用匹配符%,可以将大量同类型的文件,只用一条规则就完成构建。

5).变量和赋值符

Makefile 允许自定义变量。

1
2
3
txt = Hello World
test:
    @echo $(txt)

调用shell中的变量,需要使用两个美元符号$$。

Makefile一共提供了四个赋值运算符 (=、:=、?=、+=),它们的区别请看StackOverflow

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
VARIABLE = value
# 在执行时扩展,允许递归扩展。

VARIABLE := value
# 在定义时扩展。

VARIABLE ?= value
# 只有在该变量为空时才设置值。

VARIABLE += value
# 将值追加到变量的尾端。

6).内置变量

Make命令提供一系列内置变量,比如,$(CC)指向当前使用的编译器,$(MAKE) 指向当前使用的Make工具。这主要是为了跨平台的兼容性,详细的内置变量清单见手册

1
2
output:
    $(CC) -o output input.c

7).判断和循环

Makefile使用 Bash 语法,完成判断和循环。

1
2
3
4
5
ifeq ($(CC),gcc)
  libs=$(libs_for_gcc)
else
  libs=$(normal_libs)
endif

上面代码判断当前编译器是否 gcc ,然后指定不同的库文件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
LIST = one two three
all:
    for i in $(LIST); do \
        echo $$i; \
    done

# 等同于

all:
    for i in one two three; do \
        echo $i; \
    done

上面代码的运行结果。

1
2
3
one
two
three

4.几个makefile示例

1).简单项目-执行多个目标

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
.PHONY: cleanall cleanobj cleandiff

cleanall: cleanobj cleandiff
        rm all

cleanobj:
        rm *.o

cleandiff:
        rm *.diff

上面代码可以调用不同目标,删除不同后缀名的文件,也可以调用一个目标(cleanall),删除所有指定类型的文件。

2).c语言项目示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
edit : main.o kbd.o command.o display.o \
        insert.o search.o files.o utils.o
    cc -o edit main.o kbd.o command.o display.o \
        insert.o search.o files.o utils.o

main.o : main.c defs.h
    cc -c main.c
kbd.o : kbd.c defs.h command.h
    cc -c kbd.c
command.o : command.c defs.h command.h
    cc -c command.c
display.o : display.c defs.h buffer.h
    cc -c display.c
clean :
    rm edit main.o kbd.o command.o display.o \
        insert.o search.o files.o utils.o

反斜杠( \ )是换行符的意思。这样比较便于makefile的阅读。我们可以把这个内容保存在名字为“makefile”或“Makefile”的文件中,然后在该目录下直接输入命令 make 就可以生成执行文件edit。如果要删除执行文件和所有的中间目标文件,那么,只要简单地执行一下 make clean 就可以了。

在这个makefile中,目标文件(target)包含:执行文件edit和中间目标文件( *.o ),依赖文件(prerequisites)就是冒号后面的那些 .c 文件和 .h 文件。每一个 .o 文件都有一组依赖文件,而这些 .o 文件又是执行文件 edit 的依赖文件。依赖关系的实质就是说明了目标文件是由哪些文件生成的,换言之,目标文件是哪些文件更新的。

在定义好依赖关系后,后续的那一行定义了如何生成目标文件的操作系统命令,一定要以一个 Tab 键作为开头。记住,make并不管命令是怎么工作的,他只管执行所定义的命令。make会比较targets文件和prerequisites文件的修改日期,如果prerequisites文件的日期要比targets文件的日期要新,或者target不存在的话,那么,make就会执行后续定义的命令。

这里要说明一点的是, clean 不是一个文件,它只不过是一个动作名字,有点像c语言中的label一样,其冒号后什么也没有,那么,make就不会自动去找它的依赖性,也就不会自动执行其后所定义的命令。要执行其后的命令,就要在make命令后明显得指出这个label的名字。这样的方法非常有用,我们可以在一个makefile中定义不用的编译或是和编译无关的命令,比如程序的打包,程序的备份,等等。

五.编译打包原理

1.make是如何工作的

在默认的方式下,也就是我们只输入 make 命令。那么,

  • make会在当前目录下找名字叫“Makefile”或“makefile”的文件。

  • 如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“edit”这个文件,并把这个文件作为最终的目标文件。

  • 如果edit文件不存在,或是edit所依赖的后面的 .o 文件的文件修改时间要比 edit 这个文件新,那么,他就会执行后面所定义的命令来生成 edit 这个文件。

  • 如果 edit 所依赖的 .o 文件也不存在,那么make会在当前文件中找目标为 .o 文件的依赖性,如果找到则再根据那一个规则生成 .o 文件。(这有点像一个堆栈的过程)

  • 当然,你的C文件和H文件是存在的啦,于是make会生成 .o 文件,然后再用 .o 文件生成make的终极任务,也就是执行文件 edit 了。

​ 这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理。make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起,我就不工作啦。

​ 通过上述分析,我们知道,像clean这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,不过,我们可以显示要make执行。即命令—— make clean ,以此来清除所有的目标文件,以便重编译。

​ 于是在我们编程中,如果这个工程已被编译过了,当我们修改了其中一个源文件,比如 file.c ,那么根据我们的依赖性,我们的目标 file.o 会被重编译(也就是在这个依性关系后面所定义的命令),于是 file.o 的文件也是最新的啦,于是 file.o 的文件修改时间要比 edit 要新,所以 edit 也会被重新链接了(详见 edit 目标文件后定义的命令)。

​ 而如果我们改变了 command.h ,那么, kdb.ocommand.ofiles.o 都会被重编译,并且, edit 会被重链接。

2.makefile中使用变量

我们在makefile一开始就这样定义:

1
2
objects = main.o kbd.o command.o display.o \
     insert.o search.o files.o utils.o

于是,我们就可以很方便地在我们的makefile中以 $(objects) 的方式来使用这个变量了 :

1
2
3
4
5
objects = main.o kbd.o command.o display.o \
    insert.o search.o files.o utils.o

edit : $(objects)
    cc -o edit $(objects)

3.让make自动推导

GNU的make很强大,它可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个 .o 文件后都写上类似的命令,因为,我们的make会自动识别,并自己推导命令。

只要make看到一个 .o 文件,它就会自动的把 .c 文件加在依赖关系中,如果make找到一个 whatever.o ,那么 whatever.c 就会是 whatever.o 的依赖文件。并且 cc -c whatever.c 也会被推导出来,于是,我们的makefile再也不用写得这么复杂。我们的新makefile又出炉了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
objects = main.o kbd.o command.o display.o \
    insert.o search.o files.o utils.o

edit : $(objects)
    cc -o edit $(objects)

main.o : defs.h
kbd.o : defs.h command.h
command.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h

.PHONY : clean
clean :
    rm edit $(objects)

这种方法,也就是make的“隐晦规则”。上面文件内容中, .PHONY 表示 clean 是个伪目标文件

4.另类风格的makefiles

既然我们的make可以自动推导命令,那么我看到那堆 .o 和 .h 的依赖就有点不爽,那么多的重复的 .h ,能不能把其收拢起来,好吧,没有问题,这个对于make来说很容易,谁叫它提供了自动推导命令和文件的功能呢?来看看最新风格的makefile吧。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
objects = main.o kbd.o command.o display.o \
    insert.o search.o files.o utils.o

edit : $(objects)
    cc -o edit $(objects)

$(objects) : defs.h
kbd.o command.o files.o : command.h
display.o insert.o search.o files.o : buffer.h

.PHONY : clean
clean :
    rm edit $(objects)

这种风格,让我们的makefile变得很简单,但我们的文件依赖关系就显得有点凌乱了。鱼和熊掌不可兼得。还看你的喜好了。我是不喜欢这种风格的,一是文件的依赖关系看不清楚,二是如果文件一多,要加入几个新的 .o 文件,那就理不清楚了。

5.清空目标文件的规则

每个Makefile中都应该写一个清空目标文件( .o 和执行文件)的规则,这不仅便于重编译,也很利于保持文件的清洁。这是一个“修养”(呵呵,还记得我的《编程修养》吗)。一般的风格都是

1
2
clean:
    rm edit $(objects)

更为稳健的做法是:

1
2
3
.PHONY : clean
clean :
    -rm edit $(objects)

前面说过, .PHONY 表示 clean 是一个“伪目标”。而在 rm 命令前面加了一个小减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。当然, clean 的规则不要放在文件的开头,不然,这就会变成make的默认目标,相信谁也不愿意这样。不成文的规矩是——“clean从来都是放在文件的最后”。

6.Makefile里有什么?

Makefile里主要包含了五个东西:显式规则、隐晦规则、变量定义、文件指示和注释。

  • 显式规则。显式规则说明了如何生成一个或多个目标文件。这是由Makefile的书写者明显指出要生成的文件、文件的依赖文件和生成的命令。
  • 隐晦规则。由于我们的make有自动推导的功能,所以隐晦的规则可以让我们比较简略地书写 Makefile,这是由make所支持的。
  • 变量的定义。在Makefile中我们要定义一系列的变量,变量一般都是字符串,这个有点像你C语言中的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。
  • 文件指示。其包括了三个部分,一个是在一个Makefile中引用另一个Makefile,就像C语言中的include一样;另一个是指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样;还有就是定义一个多行的命令。有关这一部分的内容,我会在后续的部分中讲述。
  • 注释。Makefile中只有行注释,和UNIX的Shell脚本一样,其注释是用 # 字符,这个就像C/C++中的 // 一样。如果你要在你的Makefile中使用 # 字符,可以用反斜杠进行转义,如: \#

最后,还值得一提的是,在Makefile中的命令,必须要以 Tab 键开始。

7.Makefile的文件名

​ 默认的情况下,make命令会在当前目录下按顺序找寻文件名为“GNUmakefile”、“makefile”、“Makefile”的文件,找到了解释这个文件。在这三个文件名中,最好使用“Makefile”这个文件名,因为,这个文件名第一个字符为大写,这样有一种显目的感觉。最好不要用“GNUmakefile”,这个文件是GNU的make识别的。有另外一些make只对全小写的“makefile”文件名敏感,但是基本上来说,大多数的make都支持“makefile”和“Makefile”这两种默认文件名。

​ 当然,你可以使用别的文件名来书写Makefile,比如:“Make.Linux”,“Make.Solaris”,“Make.AIX”等,如果要指定特定的Makefile,你可以使用make的 -f--file 参数,如: make -f Make.Linuxmake --file Make.AIX

8.引用其它的Makefile

在Makefile使用 include 关键字可以把别的Makefile包含进来,这很像C语言的 #include ,被包含的文件会原模原样的放在当前文件的包含位置。 include 的语法是:

1
include <filename>

filename 可以是当前操作系统Shell的文件模式(可以包含路径和通配符)。

include 前面可以有一些空字符,但是绝不能是 Tab 键开始。 include<filename> 可以用一个或多个空格隔开。举个例子,你有这样几个Makefile: a.mkb.mkc.mk ,还有一个文件叫 foo.make ,以及一个变量 $(bar) ,其包含了 e.mkf.mk ,那么,下面的语句:

1
2
3
include foo.make *.mk $(bar)
#等价于
include foo.make a.mk b.mk c.mk e.mk f.mk

make命令开始时,会找寻 include 所指出的其它Makefile,并把其内容安置在当前的位置。就好像C/C++的 #include 指令一样。如果文件都没有指定绝对路径或是相对路径的话,make会在当前目录下首先寻找,如果当前目录下没有找到,那么,make还会在下面的几个目录下找:

  1. 如果make执行时,有 -I--include-dir 参数,那么make就会在这个参数所指定的目录下去寻找。
  2. 如果目录 <prefix>/include (一般是: /usr/local/bin/usr/include )存在的话,make也会去找。

如果有文件没有找到的话,make会生成一条警告信息,但不会马上出现致命错误。它会继续载入其它的文件,一旦完成makefile的读取,make会再重试这些没有找到,或是不能读取的文件,如果还是不行,make才会出现一条致命信息。如果你想让make不理那些无法读取的文件,而继续执行,你可以在include前加一个减号“-”。如:

1
-include <filename>

其表示,无论include过程中出现什么错误,都不要报错继续执行。和其它版本make兼容的相关命令是sinclude,其作用和这一个是一样的。

9.环境变量MAKEFILES

如果你的当前环境中定义了环境变量 MAKEFILES ,那么,make会把这个变量中的值做一个类似于 include 的动作。这个变量中的值是其它的Makefile,用空格分隔。只是,它和 include 不同的是,从这个环境变量中引入的Makefile的“目标”不会起作用,如果环境变量中定义的文件发现错误,make也会不理。

但是在这里我还是建议不要使用这个环境变量,因为只要这个变量一被定义,那么当你使用make时,所有的Makefile都会受到它的影响,这绝不是你想看到的。在这里提这个事,只是为了告诉大家,也许有时候你的Makefile出现了怪事,那么你可以看看当前环境中有没有定义这个变量。

10.make的工作方式

GNU的make工作时的执行步骤如下:(想来其它的make也是类似)

  • 读入所有的Makefile。
  • 读入被include的其它Makefile。
  • 初始化文件中的变量。
  • 推导隐晦规则,并分析所有规则。
  • 为所有的目标文件创建依赖关系链。
  • 根据依赖关系,决定哪些目标要重新生成。
  • 执行生成命令。

1-5步为第一个阶段,6-7为第二个阶段。第一个阶段中,如果定义的变量被使用了,那么,make会把其展开在使用的位置。但make并不会完全马上展开,make使用的是拖延战术,如果变量出现在依赖关系的规则中,那么仅当这条依赖被决定要使用了,变量才会在其内部展开。

六.项目打包示例

1.几个项目打包的Makefile示例

1).codegen项目makefile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
PKG = $(shell cat go.mod | grep "^module " | sed -e "s/module //g")
VERSION = v$(shell cat .version)
COMMIT_SHA ?= $(shell git describe --always)-devel

GOOS ?= $(shell go env GOOS)
GOARCH ?= $(shell go env GOARCH)
GOBUILD=CGO_ENABLED=0 go build -ldflags "-X ${PKG}/version.Version=${VERSION}+sha.${COMMIT_SHA}"
GOINSTALL=CGO_ENABLED=0 go install -ldflags "-X ${PKG}/version.Version=${VERSION}+sha.${COMMIT_SHA}"

MAIN_ROOT ?= ./cmd/codegen

install:
	cd $(MAIN_ROOT) && $(GOINSTALL)

build:
	cd $(MAIN_ROOT) && $(GOBUILD) -o codegen

release:
	git push
	git push origin $(VERSION)

2).srv-gw-infinity项目makefile

1
2
3
4
5
6
7
all: srv-gw-infinity

srv-gw-infinity: ./*
	swag init && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build .

clean:
	rm srv-gw-infinity

3).infinitygw项目makefile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
prefix=/opt/infinitygw

.PHONY: install

install:
	\mkdir -p $(DESTDIR)${prefix}

	\install -p -D -m 755 $(CURDIR)/bin/infi-srv-gw $(DESTDIR)${prefix}
	\mkdir -p $(DESTDIR)/etc/i18n/srv-gw-infinity
	\cp -r $(CURDIR)/i18n/* $(DESTDIR)/etc/i18n/srv-gw-infinity/
	\mkdir -p $(DESTDIR)/usr/lib/systemd/system/
	\cp -f $(CURDIR)/conf/*.service $(DESTDIR)/usr/lib/systemd/system/
	\mkdir -p $(DESTDIR)/var/log
	\mkdir -p $(DESTDIR)/etc/logrotate.d/
	\cp -r $(CURDIR)/conf/infi-srv-gw.logrotate $(DESTDIR)/etc/logrotate.d/

4).go项目标准makefile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
BUILD_NAME:=goappname
BUILD_VERSION:=1.0
SOURCE:=*.go
LDFLAGS:=-ldflags "-X main.Version=${BUILD_VERSION}"

all: deps build install

deps:
	#安装依赖
	[ -x glide ] && glide install || yum install glide

test: 
	go test

build: test
	go build -o ${BUILD_NAME} ${SOURCE}

build_linux: test
	CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ${BUILD_NAME} ${SOURCE}

build_win: test
	CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o ${BUILD_NAME} ${SOURCE}

install: deps build
	go install
	#生成配置文件等
	#cp app.conf.example /etc/app.conf

clean:
	go clean

.PHONY: all deps test build build_linux build_win install clean

2.制作Go项目二进制软件包安装包

1).gitlab新建项目hello-world

dysoso/gocode-build 我新建的项目,如果需要请下载源码

项目目录:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[root@dy-workspace /workspace/gocode-build/SOURCES]# tree
.
├── bin
│   ├── hello-world
│   └── tips.md
├── build
│   ├── configure
│   ├── Makefile
│   └── README.md
├── conf
│   ├── global.conf
│   ├── hello-world.logrotate
│   └── hello-world.service
├── go.mod
├── go.sum
├── hello_world.go
├── hello_world_test.go
├── hello-world-v1.0.0.tar.gz
├── Makefile
└── README.md

3 directories, 15 files
[root@dy-workspace /workspace/gocode-build/SOURCES]#

目录说明

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
├── SOURCES                         二进制源码包制作文件夹
│   ├── bin                         构建后的二进制文件
│   ├── build                       存放构建脚本
│   ├── build                       存放构建脚本
│   │   ├── configure               用来打包二进制源码包
│   │   ├── Makefile                用来生成项目二进制文件
│   │   └── README.md               build文件夹说明
│   ├── go.mod                      使用Go Module包管理的依赖描述文件
│   ├── go.sum                      包管理依赖内容校验文件
│   ├── helle-world.gz              制作好的二进制源码安装包
│   ├── helle-world.go              应用主程序
│   ├── helle-world_test.go         应用程序测试文件
│   ├── Makefile                    二进制源码安装包中的Makefile,用户应用安装
│   └── README.md                   构建说明及制作流程

2).应用主要代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package main

import (
	"fmt"
	"io"
	"log"
	"net/http"
	"os"

	"github.com/Unknwon/goconfig"
	"github.com/gin-gonic/gin"
)

var (
	StartupLogPath     = "/var/log/hello-world/"
	StartupLogFileName = "go-gin-startup.log"
	GlobalConfPath     = "/etc/hello-world/"
	GlobalConfFileName = "global.conf"
	Port               = "80"
	Version            = ""
)

func main() {
	fmt.Printf("App Version:%s\n", Version)

	//1.控制台及启动日志配置
	if yes := IsPathExist(StartupLogPath); !yes {
		os.MkdirAll(StartupLogPath, os.ModePerm)
	}
	logFile, err := os.OpenFile(StartupLogPath+StartupLogFileName, os.O_APPEND|os.O_WRONLY|os.O_CREATE, os.ModePerm)
	if err != nil {
		log.Fatal("OpenFile[err]: ", err.Error())
	}
	gin.DefaultWriter = io.MultiWriter(logFile, os.Stdout)

	// 2.创建路由
	Router := gin.Default()

	// 3.绑定路由规则,执行的函数
	Router.GET("/", func(c *gin.Context) {
		c.String(http.StatusOK, "hello Go-Gin!")
	})
	Router.GET("/favicon.ico", func(c *gin.Context) {
		c.String(http.StatusNoContent, "")
	})

	// 4.从配置获取端口号
	port, _ := GetConfPort(GlobalConfPath, GlobalConfFileName)
	if port != "" {
		Port = port
	}

	// 5.监听端口
	err = Router.Run(":" + Port)
	if err != nil {
		log.Fatal("ListenAndServe[err]: ", err.Error())
	}
}

// GetConfPort 获取配置文件中端口号
func GetConfPort(confPath string, confFileName string) (string, error) {
	if yes := IsPathExist(confPath); !yes {
		os.MkdirAll(confPath, os.ModePerm)
	}

	confFile := confPath + confFileName

	cfg, err := goconfig.LoadConfigFile(confFile)
	if err != nil {
		return "", err
	}

	val, err := cfg.GetValue("global", "port")
	if err != nil {
		return "", err
	}

	return val, nil
}

// IsPathExist 判断path是否存在
func IsPathExist(path string) bool {
	_, err := os.Stat(path)
	if err == nil {
		return true
	}
	if os.IsNotExist(err) {
		return false
	}
	return false
}

3).应用makefile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#hello world源码安装
#安装hello world 程序
#edit by Zkq
#edit in 2022-03-06

all: deps install

deps:
	@#安装依赖
	git version || yum install -y git
	go version || yum install -y git

install: deps
	@#安装软件
	@#生成日志文件夹
	mkdir -p $(DESTDIR)/var/log/hello-world/
	@#生成日志分割配置文件夹,并安装配置文件
	mkdir -p $(DESTDIR)/etc/logrotate.d/
	cp -r $(CURDIR)/conf/*.logrotate $(DESTDIR)/etc/logrotate.d/
	@#生成项目配置文件,并安装配置文件
	mkdir -p $(DESTDIR)/etc/hello-world/
	cp -r $(CURDIR)/conf/*.conf $(DESTDIR)/etc/hello-world/
	@#生成项目服务文件夹,并安装配置文件
	mkdir -p $(DESTDIR)/usr/lib/systemd/system/
	cp -f $(CURDIR)/conf/*.service $(DESTDIR)/usr/lib/systemd/system/
	@#安装项目二进制文件
	mkdir -p $(DESTDIR)/opt/hello-world/
	install -p -D -m 755 $(CURDIR)/bin/hello-world $(DESTDIR)/opt/hello-world/
	@#启动服务
	systemctl start hello-world
	systemctl enable hello-world
	systemctl status hello-world

clean:
	@#停止服务
	systemctl disable hello-world
	systemctl stop hello-world
	@#清理项目
	rm -rf $(DESTDIR)/var/log/hello-world
	rm -rf $(DESTDIR)/etc/logrotate.d/hello-world.logrotate
	rm -rf $(DESTDIR)/etc/hello-world/
	rm -rf $(DESTDIR)/opt/hello-world/
	rm -rf $(DESTDIR)/usr/lib/systemd/system/hello-world.service

.PHONY: all deps install clean

4).构建二进制安装包

1
2
3
4
#进入项目build文件夹
cd build && make
cd .. && sh build/configure `pwd`
ll -h hello-world-v1.0.0.tar.gz

5).使用打包好的安装包安装应用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#先获取hello-world-v1.0.0.tar.gz
#解压
tar -zxvf hello-world-v1.0.0.tar.gz
cd hello-world-v1.0.0 && make

[root@dy-workspace /workspace/gocode-build/SOURCES]# cd hello-world-v1.0.0 && make
git version || yum install -y git
git version 1.8.3.1
go version || yum install -y git
go version go1.17.6 linux/amd64
mkdir -p /var/log/hello-world/
mkdir -p /etc/logrotate.d/
cp -r /workspace/gocode-build/SOURCES/hello-world-v1.0.0/conf/*.logrotate /etc/logrotate.d/
mkdir -p /etc/hello-world/
cp -r /workspace/gocode-build/SOURCES/hello-world-v1.0.0/conf/*.conf /etc/hello-world/
mkdir -p /usr/lib/systemd/system/
cp -f /workspace/gocode-build/SOURCES/hello-world-v1.0.0/conf/*.service /usr/lib/systemd/system/
mkdir -p /opt/hello-world/
install -p -D -m 755 /workspace/gocode-build/SOURCES/hello-world-v1.0.0/bin/hello-world /opt/hello-world/
systemctl start hello-world
systemctl enable hello-world
Created symlink from /etc/systemd/system/multi-user.target.wants/hello-world.service to /usr/lib/systemd/system/hello-world.service.
systemctl status hello-world
● hello-world.service - Hello-World service
   Loaded: loaded (/usr/lib/systemd/system/hello-world.service; enabled; vendor preset: disabled)
   Active: active (running) since 二 2022-04-26 17:32:24 CST; 95ms ago
 Main PID: 21754 (hello-world)
   CGroup: /system.slice/hello-world.service
           └─21754 /opt/hello-world/hello-world

4月 26 17:32:24 dy-workspace hello-world[21754]: App Version:
4月 26 17:32:24 dy-workspace hello-world[21754]: [GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware ...ttached.
4月 26 17:32:24 dy-workspace hello-world[21754]: [GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
4月 26 17:32:24 dy-workspace hello-world[21754]: - using env:        export GIN_MODE=release
4月 26 17:32:24 dy-workspace hello-world[21754]: - using code:        gin.SetMode(gin.ReleaseMode)
4月 26 17:32:24 dy-workspace hello-world[21754]: [GIN-debug] GET    /                         --> main.main.func1 (3 handlers)
4月 26 17:32:24 dy-workspace hello-world[21754]: [GIN-debug] GET    /favicon.ico              --> main.main.func2 (3 handlers)
4月 26 17:32:24 dy-workspace hello-world[21754]: [GIN-debug] [WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.
4月 26 17:32:24 dy-workspace hello-world[21754]: Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.
4月 26 17:32:24 dy-workspace hello-world[21754]: [GIN-debug] Listening and serving HTTP on :9782
Hint: Some lines were ellipsized, use -l to show in full.
[root@dy-workspace /workspace/gocode-build/SOURCES/hello-world-v1.0.0]#

#卸载软件,在hello-world-v1.0.0目录中
make clean

七、参考文献