编写代码只是软件开发的一小部分,更多的时间往往花在构建(build)和测试(test)。

为了提高软件开发的效率,构建和测试的自动化工具层出不穷。Travis CI 就是这类工具之中,市场份额最大的一个。

本文介绍 Travis CI 的基本用法。用好这个工具不仅可以提高效率,还能使开发流程更可靠和专业化,从而提高软件的价值。而且,它对于开源项目是免费的,不花一分钱,就能帮你做掉很多事情。

一、什么是持续集成?

Travis CI 提供的是持续集成服务(Continuous Integration,简称 CI)。它绑定 Github 上面的项目,只要有新的代码,就会自动抓取。然后,提供一个运行环境,执行测试,完成构建,还能部署到服务器。

持续集成指的是只要代码有变更,就自动运行构建和测试,反馈运行结果。确保符合预期以后,再将新代码"集成"到主干。

持续集成的好处在于,每次代码的小幅变更,就能看到运行结果,从而不断累积小的变更,而不是在开发周期结束时,一下子合并一大块代码。

二、使用准备

Travis CI 只支持 Github,不支持其他代码托管服务。这意味着,你必须满足以下条件,才能使用 Travis CI。

  • 拥有 GitHub 帐号
  • 该帐号下面有一个项目
  • 该项目里面有可运行的代码
  • 该项目还包含构建或测试脚本

如果这些条件都没问题,就可以开始使用 Travis CI 了。

首先,访问官方网站 travis-ci.org,点击右上角的个人头像,使用 Github 账户登入 Travis CI。

Travis 会列出 Github 上面你的所有仓库,以及你所属于的组织。此时,选择你需要 Travis 帮你构建的仓库,打开仓库旁边的开关。一旦激活了一个仓库,Travis 会监听这个仓库的所有变化。

三、.travis.yml

Travis 要求项目的根目录下面,必须有一个.travis.yml文件。这是配置文件,指定了 Travis 的行为。该文件必须保存在 Github 仓库里面,一旦代码仓库有新的 Commit,Travis 就会去找这个文件,执行里面的命令。

这个文件采用 YAML 格式。下面是一个最简单的 Python 项目的.travis.yml文件。

1
2
3

language: python
script: true

上面代码中,设置了两个字段。language字段指定了默认运行环境,这里设定使用 Python 环境。script字段指定要运行的脚本,script: true表示不执行任何脚本,状态直接设为成功。

Travis 默认提供的运行环境,请参考官方文档 。目前一共支持31种语言,以后还会不断增加。

下面是一个稍微复杂一点的.travis.yml

1
2
3
4
5

language: python
sudo: required
before_install: sudo pip install foo
script: py.test

上面代码中,设置了四个字段:运行环境是 Python,需要sudo权限,在安装依赖之前需要安装foo模块,然后执行脚本py.test

四、运行流程

Travis 的运行流程很简单,任何项目都会经过两个阶段。

  • install 阶段:安装依赖
  • script 阶段:运行脚本

4.1 install 字段

install字段用来指定安装脚本。

1
2

install: ./install-dependencies.sh

如果有多个脚本,可以写成下面的形式。

1
2
3
4

install:
- command1
- command2

上面代码中,如果command1失败了,整个构建就会停下来,不再往下进行。

如果不需要安装,即跳过安装阶段,就直接设为true

1
2

install: true

4.2、script 字段

script字段用来指定构建或测试脚本。

1
2

script: bundle exec thor build

如果有多个脚本,可以写成下面的形式。

1
2
3
4

script:
- command1
- command2

注意,scriptinstall不一样,如果command1失败,command2会继续执行。但是,整个构建阶段的状态是失败。

如果command2只有在command1成功后才能执行,就要写成下面这样。

1
2

script: command1 && command2

4.3 实例:Node 项目

Node 项目的环境需要写成下面这样。

1
2
3
4

language: node_js
node_js:
- "8"

上面代码中,node_js字段用来指定 Node 版本。

Node 项目的installscript阶段都有默认脚本,可以省略。

  • install默认值:npm install
  • script默认值:npm test

更多设置请看官方文档

4.4 部署

script阶段结束以后,还可以设置通知步骤(notification)和部署步骤(deployment),它们不是必须的。

部署的脚本可以在script阶段执行,也可以使用 Travis 为几十种常见服务提供的快捷部署功能。比如,要部署到 Github Pages,可以写成下面这样。

1
2
3
4
5
6
7

deploy:
provider: pages
skip_cleanup: true
github_token: $GITHUB_TOKEN # Set in travis-ci.org dashboard
on:
branch: master

其他部署方式,请看官方文档

4.5 钩子方法

Travis 为上面这些阶段提供了7个钩子。

  • before_install:install 阶段之前执行
  • before_script:script 阶段之前执行
  • after_failure:script 阶段失败时执行
  • after_success:script 阶段成功时执行
  • before_deploy:deploy 步骤之前执行
  • after_deploy:deploy 步骤之后执行
  • after_script:script 阶段之后执行

完整的生命周期,从开始到结束是下面的流程。

  1. before_install
  2. install
  3. before_script
  4. script
  5. aftersuccess or afterfailure
  6. [OPTIONAL] before_deploy
  7. [OPTIONAL] deploy
  8. [OPTIONAL] after_deploy
  9. after_script

下面是一个before_install钩子的例子。

1
2
3
4

before_install:
- sudo apt-get -qq update
- sudo apt-get install -y libxml2-dev

上面代码表示before_install阶段要做两件事,第一件事是要更新依赖,第二件事是安装libxml2-dev。用到的几个参数的含义如下:-qq表示减少中间步骤的输出,-y表示如果需要用户输入,总是输入yes

4.6 运行状态

最后,Travis 每次运行,可能会返回四种状态。

  • passed:运行成功,所有步骤的退出码都是0
  • canceled:用户取消执行
  • errored:before_installinstallbefore_script有非零退出码,运行会立即停止
  • failed :script有非零状态码 ,会继续运行

五、使用技巧

5.1 环境变量

.travis.ymlenv字段可以定义环境变量。

1
2
3
4
5

env:
- DB=postgres
- SH=bash
- PACKAGE_VERSION="1.0.*"

然后,脚本内部就使用这些变量了。

有些环境变量(比如用户名和密码)不能公开,这时可以通过 Travis 网站,写在每个仓库的设置页里面,Travis 会自动把它们加入环境变量。这样一来,脚本内部依然可以使用这些环境变量,但是只有管理员才能看到变量的值。具体操作请看官方文档

5.2 加密信息

如果不放心保密信息明文存在 Travis 的网站,可以使用 Travis 提供的加密功能。

首先,安装 Ruby 的包travis

1
2

$ gem install travis

然后,就可以用travis encrypt命令加密信息。

在项目的根目录下,执行下面的命令。

1
2

$ travis encrypt SOMEVAR=secretvalue

上面命令中,SOMEVAR是要加密的变量名,secretvalue是要加密的变量值。执行以后,屏幕上会输出如下信息。

1
2

secure: ".... encrypted data ...."

现在,就可以把这一行加入.travis.yml

1
2
3
4

env:
global:
- secure: ".... encrypted data ...."

然后,脚本里面就可以使用环境变量$SOMEVAR了,Travis 会在运行时自动对它解密。

travis encrypt命令的--add参数会把输出自动写入.travis.yml,省掉了修改env字段的步骤。

1
2

$ travis encrypt SOMEVAR=secretvalue --add

详细信息请看官方文档

5.3 加密文件

如果要加密的是文件(比如私钥),Travis 提供了加密文件功能。

安装命令行客户端以后,使用下面的命令登入 Travis CI。

1
2

$ travis login

然后,进入项目的根目录,使用travis encrypt-file命令加密那些想要加密的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

$ travis encrypt-file bacon.txt

encrypting bacon.txt for rkh/travis-encrypt-file-example
storing result as bacon.txt.enc
storing secure env variables for decryption

Please add the following to your build script (before_install stage in your .travis.yml, for instance):

openssl aes-256-cbc -K $encrypted_0a6446eb3ae3_key -iv $encrypted_0a6446eb3ae3_key -in bacon.txt.enc -out bacon.txt -d

Pro Tip: You can add it automatically by running with --add.

Make sure to add bacon.txt.enc to the git repository.
Make sure not to add bacon.txt to the git repository.
Commit all changes to your .travis.yml.

上面的代码对文件bacon.txt进行加密,加密后会生成bacon.txt.enc,该文件需要提交到代码库。此外,该命令还会生成一个环境变量$encrypted_0a6446eb3ae3_key,保存密钥,储存在 Travis CI,文件解密时需要这个环境变量。你需要把解密所需的openssl命令,写在.travis.ymlbefore_install字段里面。这些都写在上面的命令行提示里面。

--add参数可以自动把环境变量写入.travis.yml

1
2
3
4
5
6
7
8
9
10

$ travis encrypt-file bacon.txt --add

encrypting bacon.txt for rkh/travis-encrypt-file-example
storing result as bacon.txt.enc
storing secure env variables for decryption

Make sure to add bacon.txt.enc to the git repository.
Make sure not to add bacon.txt to the git repository.
Commit all changes to your .travis.yml.

详细信息请看官方文档,实际的例子可以参考下面两篇文章。

(完)

留言與分享

makefile教程

分類 devops, make

简介

Linux的make程序用来自动化编译大型源码,很多时候,我们在Linux下编译安装软件,只需要敲一个make就可以全自动完成,非常方便。

make能自动化完成这些工作,是因为项目提供了一个Makefile文件,它负责告诉make,应该如何编译和链接程序。

Makefile相当于Java项目的pom.xml,Node工程的package.json,Rust项目的Cargo.toml,不同之处在于,make虽然最初是针对C语言开发,但它实际上并不限定C语言,而是可以应用到任意项目,甚至不是编程语言。此外,make主要用于Unix/Linux环境的自动化开发,掌握Makefile的写法,可以更好地在Linux环境下做开发,也可以为后续开发Linux内核做好准备。

在本教程中,我们将由浅入深,一步一步学习如何编写Makefile,完全针对零基础小白,只需要提前掌握如何使用Linux命令。


在Linux环境下,当我们输入make命令时,它就在当前目录查找一个名为Makefile的文件,然后,根据这个文件定义的规则,自动化地执行任意命令,包括编译命令。

Makefile这个单词,顾名思义,就是指如何生成文件。

我们举个例子:在当前目录下,有3个文本文件:a.txtb.txtc.txt

现在,我们要合并a.txtb.txt,生成中间文件m.txt,再用中间文件m.txtc.txt合并,生成最终的目标文件x.txt,整个逻辑如下图所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌─────┐ ┌─────┐ ┌─────┐
│a.txt│ │b.txt│ │c.txt│
└─────┘ └─────┘ └─────┘
│ │ │
└───┬───┘ │
│ │
▼ │
┌─────┐ │
│m.txt│ │
└─────┘ │
│ │
└─────┬─────┘


┌─────┐
│x.txt│
└─────┘

根据上述逻辑,我们来编写Makefile

规则

Makefile由若干条规则(Rule)构成,每一条规则指出一个目标文件(Target),若干依赖文件(prerequisites),以及生成目标文件的命令。

例如,要生成m.txt,依赖a.txtb.txt,规则如下:

1
2
3
# 目标文件: 依赖文件1 依赖文件2
m.txt: a.txt b.txt
cat a.txt b.txt > m.txt

一条规则的格式为目标文件: 依赖文件1 依赖文件2 ...,紧接着,以Tab开头的是命令,用来生成目标文件。上述规则使用cat命令合并了a.txtb.txt,并写入到m.txt。用什么方式生成目标文件make并不关心,因为命令完全是我们自己写的,可以是编译命令,也可以是cpmv等任何命令。

#开头的是注释,会被make命令忽略。

1
注意:Makefile的规则中,命令必须以Tab开头,不能是空格。

类似的,我们写出生成x.txt的规则如下:

1
2
x.txt: m.txt c.txt
cat m.txt c.txt > x.txt

由于make执行时,默认执行第一条规则,所以,我们把规则x.txt放到前面。完整的Makefile如下:

1
2
3
4
5
x.txt: m.txt c.txt
cat m.txt c.txt > x.txt

m.txt: a.txt b.txt
cat a.txt b.txt > m.txt

在当前目录创建a.txtb.txtc.txt,输入一些内容,执行make

1
2
3
$ make
cat a.txt b.txt > m.txt
cat m.txt c.txt > x.txt

make默认执行第一条规则,也就是创建x.txt,但是由于x.txt依赖的文件m.txt不存在(另一个依赖c.txt已存在),故需要先执行规则m.txt创建出m.txt文件,再执行规则x.txt。执行完成后,当前目录下生成了两个文件m.txtx.txt

可见,Makefile定义了一系列规则,每个规则在满足依赖文件的前提下执行命令,就能创建出一个目标文件,这就是英文Make file的意思。

把默认执行的规则放第一条,其他规则的顺序是无关紧要的,因为make执行时自动判断依赖。

此外,make会打印出执行的每一条命令,便于我们观察执行顺序以便调试。

如果我们再次运行make,输出如下:

1
2
$ make
make: `x.txt' is up to date.

make检测到x.txt已经是最新版本,无需再次执行,因为x.txt的创建时间晚于它依赖的m.txtc.txt的最后修改时间。

1
make使用文件的创建和修改时间来判断是否应该更新一个目标文件。

修改c.txt后,运行make,会触发x.txt的更新:

1
2
$ make
cat m.txt c.txt > x.txt

但并不会触发m.txt的更新,原因是m.txt的依赖a.txtb.txt并未更新,所以,make只会根据Makefile去执行那些必要的规则,并不会把所有规则都无脑执行一遍。

在编译大型程序时,全量编译往往需要几十分钟甚至几个小时。全量编译完成后,如果仅修改了几个文件,再全部重新编译完全没有必要,用Makefile实现增量编译就十分节省时间。

当然,是否能正确地实现增量更新,取决于我们的规则写得对不对,make本身并不会检查规则逻辑是否正确。

伪目标

因为m.txtx.txt都是自动生成的文件,所以,可以安全地删除。

删除时,我们也不希望手动删除,而是编写一个clean规则来删除它们:

1
2
3
clean:
rm -f m.txt
rm -f x.txt

clean规则与我们前面编写的规则有所不同,它没有依赖文件,因此,要执行clean,必须用命令make clean

1
2
3
$ make clean
rm -f m.txt
rm -f x.txt

然而,在执行clean时,我们并没有创建一个名为clean的文件,所以,因为目标文件clean不存在,每次运行make clean,都会执行这个规则的命令。

如果我们手动创建一个clean的文件,这个clean规则就不会执行了!

如果我们希望makeclean不要视为文件,可以添加一个标识:

1
2
3
4
.PHONY: clean
clean:
rm -f m.txt
rm -f x.txt

此时,clean就不被视为一个文件,而是伪目标(Phony Target)。

大型项目通常会提供cleaninstall这些约定俗成的伪目标名称,方便用户快速执行特定任务。

一般来说,并不需要用.PHONY标识clean等约定俗成的伪目标名称,除非有人故意搞破坏,手动创建名字叫clean的文件。

执行多条命令

一个规则可以有多条命令,例如:

1
2
3
4
cd:
pwd
cd ..
pwd

执行cd规则:

1
2
3
4
5
6
$ make cd
pwd
/home/ubuntu/makefile-tutorial/v1
cd ..
pwd
/home/ubuntu/makefile-tutorial/v1

观察输出,发现cd ..命令执行后,并未改变当前目录,两次输出的pwd是一样的,这是因为make针对每条命令,都会创建一个独立的Shell环境,类似cd ..这样的命令,并不会影响当前目录。

解决办法是把多条命令以;分隔,写到一行:

1
2
cd_ok:
pwd; cd ..; pwd;

再执行cd_ok目标就得到了预期结果:

1
2
3
4
$ make cd_ok
pwd; cd ..; pwd
/home/ubuntu/makefile-tutorial/v1
/home/ubuntu/makefile-tutorial

可以使用\把一行语句拆成多行,便于浏览:

1
2
3
4
cd_ok:
pwd; \
cd ..; \
pwd

另一种执行多条命令的语法是用&&,它的好处是当某条命令失败时,后续命令不会继续执行:

1
2
cd_ok:
cd .. && pwd

控制打印

默认情况下,make会打印出它执行的每一条命令。如果我们不想打印某一条命令,可以在命令前加上@,表示不打印命令(但是仍然会执行):

1
2
3
no_output:
@echo 'not display'
echo 'will display'

执行结果如下:

1
2
3
4
$ make no_output
not display
echo 'will display'
will display

注意命令echo 'not display'本身没有打印,但命令仍然会执行,并且执行的结果仍然正常打印。

控制错误

make在执行命令时,会检查每一条命令的返回值,如果返回错误(非0值),就会中断执行。

例如,不使用-f删除一个不存在的文件会报错:

1
2
3
has_error:
rm zzz.txt
echo 'ok'

执行上述目标,输出如下:

1
2
3
4
$ make has_error
rm zzz.txt
rm: zzz.txt: No such file or directory
make: *** [has_error] Error 1

由于命令rm zzz.txt报错,导致后面的命令echo 'ok'并不会执行,make打印出错误,然后退出。

有些时候,我们想忽略错误,继续执行后续命令,可以在需要忽略错误的命令前加上-

1
2
3
ignore_error:
-rm zzz.txt
echo 'ok'

执行上述目标,输出如下:

1
2
3
4
5
6
$ make ignore_error
rm zzz.txt
rm: zzz.txt: No such file or directory
make: [ignore_error] Error 1 (ignored)
echo 'ok'
ok

make检测到rm zzz.txt报错,并打印错误,但显示(ignored),然后继续执行后续命令。

对于执行可能出错,但不影响逻辑的命令,可以用-忽略。

参考源码

可以从GitHub下载源码。

小结

编写Makefile就是编写一系列规则,用来告诉make如何执行这些规则,最终生成我们期望的目标文件。

查看官方手册:

编译C程序

C程序的编译通常分两步:

  1. 将每个.c文件编译为.o文件;
  2. 将所有.o文件链接为最终的可执行文件。

我们假设如下的一个C项目,包含hello.chello.hmain.c

hello.c内容如下:

1
2
3
4
5
6
7
#include <stdio.h>

int hello()
{
printf("hello, world!\n");
return 0;
}

hello.h内容如下:

1
int hello();

main.c内容如下:

1
2
3
4
5
6
7
8
9
10
#include <stdio.h>
#include "hello.h"

int main()
{
printf("start...\n");
hello();
printf("exit.\n");
return 0;
}

注意到main.c引用了头文件hello.h。我们很容易梳理出需要生成的文件,逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌───────┐ ┌───────┐ ┌───────┐
│hello.c│ │main.c │ │hello.h│
└───────┘ └───────┘ └───────┘
│ │ │
│ └────┬────┘
│ │
▼ ▼
┌───────┐ ┌───────┐
│hello.o│ │main.o │
└───────┘ └───────┘
│ │
└───────┬──────┘


┌─────────┐
│world.out│
└─────────┘

假定最终生成的可执行文件是world.out,中间步骤还需要生成hello.omain.o两个文件。根据上述依赖关系,我们可以很容易地写出Makefile如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 生成可执行文件:
world.out: hello.o main.o
cc -o world.out hello.o main.o

# 编译 hello.c:
hello.o: hello.c
cc -c hello.c

# 编译 main.c:
main.o: main.c hello.h
cc -c main.c

clean:
rm -f *.o world.out

执行make,输出如下:

1
2
3
4
$ make
cc -c hello.c
cc -c main.c
cc -o world.out hello.o main.o

在当前目录下可以看到hello.omain.o以及最终的可执行程序world.out。执行world.out

1
2
3
4
$ ./world.out 
start...
hello, world!
exit.

与我们预期相符。

修改hello.c,把输出改为"hello, bob!\n",再执行make,观察输出:

1
2
3
$ make
cc -c hello.c
cc -o world.out hello.o main.o

仅重新编译了hello.c,并未编译main.c。由于hello.o已更新,所以,仍然要重新生成world.out。执行world.out

1
2
3
4
$ ./world.out 
start...
hello, bob!
exit.

与我们预期相符。

修改hello.h

1
2
// int 变为 void:
void hello();

以及hello.c,再次执行make

1
2
3
4
$ make
cc -c hello.c
cc -c main.c
cc -o world.out hello.o main.o

会触发main.c的编译,因为main.c依赖hello.h

执行make clean会删除所有的.o文件,以及可执行文件world.out,再次执行make就会强制全量编译:

1
2
3
4
5
$ make clean && make
rm -f *.o world.out
cc -c hello.c
cc -c main.c
cc -o world.out hello.o main.o

这个简单的Makefile使我们能自动化编译C程序,十分方便。

不过,随着越来越多的.c文件被添加进来,如何高效维护Makefile的规则?我们后面继续讲解。

参考源码

可以从GitHub下载源码。

小结

Makefile正确定义规则后,我们就能用make自动化编译C程序。



使用隐式规则

我们仍然以上一节的C项目为例,当我们添加越来越多的.c文件时,就需要编写越来越多的规则来生成.o文件。

实际上,有的同学可能发现了,即使我们把.o的规则删掉,也能正常编译:

1
2
3
4
5
6
# 只保留生成 world.out 的规则:
world.out: hello.o main.o
cc -o world.out hello.o main.o

clean:
rm -f *.o world.out

执行make,输出如下:

1
2
3
4
$ make
cc -c -o hello.o hello.c
cc -c -o main.o main.c
cc -o world.out hello.o main.o

我们没有定义hello.omain.o的规则,为什么make也能正常创建这两个文件?

因为make最初就是为了编译C程序而设计的,为了免去重复创建编译.o文件的规则,make内置了隐式规则(Implicit Rule),即遇到一个xyz.o时,如果没有找到对应的规则,就自动应用一个隐式规则:

1
2
xyz.o: xyz.c
cc -c -o xyz.o xyz.c

make针对C、C++、ASM、Fortran等程序内置了一系列隐式规则,可以参考官方手册查看。

对于C程序来说,使用隐式规则有一个潜在问题,那就是无法跟踪.h文件的修改。如果我们修改了hello.h的定义,由于隐式规则main.o: main.c并不会跟踪hello.h的修改,导致main.c不会被重新编译,这个问题我们放到后面解决。

参考源码

可以从GitHub下载源码。

小结

针对C、C++、ASM、Fortran等程序,make内置了一系列隐式规则,使用隐式规则可减少大量重复的通用编译规则。

查看官方手册:



当我们在Makefile中重复写很多文件名时,一来容易写错,二来如果要改名,要全部替换,费时费力。

编程语言使用变量(Variable)来解决反复引用的问题,类似的,在Makefile中,也可以使用变量来解决重复问题。

以上一节的Makefile为例:

1
2
3
4
5
world.out: hello.o main.o
cc -o world.out hello.o main.o

clean:
rm -f *.o world.out

编译的最终文件world.out重复出现了3次,因此,完全可以定义一个变量来替换它:

1
2
3
4
5
6
7
TARGET = world.out

$(TARGET): hello.o main.o
cc -o $(TARGET) hello.o main.o

clean:
rm -f *.o $(TARGET)

变量定义用变量名 = 值或者变量名 := 值,通常变量名全大写。引用变量用$(变量名),非常简单。

注意到hello.o main.o这个“列表”也重复了,我们也可以用变量来替换:

1
2
3
4
5
6
7
8
OBJS = hello.o main.o
TARGET = world.out

$(TARGET): $(OBJS)
cc -o $(TARGET) $(OBJS)

clean:
rm -f *.o $(TARGET)

如果有一种方式能让make自动生成hello.o main.o这个“列表”,就更好了。注意到每个.o文件是由对应的.c文件编译产生的,因此,可以让make先获取.c文件列表,再替换,得到.o文件列表:

1
2
3
4
5
6
7
8
9
10
# $(wildcard *.c) 列出当前目录下的所有 .c 文件: hello.c main.c
# 用函数 patsubst 进行模式替换得到: hello.o main.o
OBJS = $(patsubst %.c,%.o,$(wildcard *.c))
TARGET = world.out

$(TARGET): $(OBJS)
cc -o $(TARGET) $(OBJS)

clean:
rm -f *.o $(TARGET)

这样,我们每添加一个.c文件,不需要修改Makefile,变量OBJS会自动更新。

思考:为什么我们不能直接定义OBJS = $(wildcard *.o)make列出所有.o文件?

内置变量

我们还可以用变量$(CC)替换命令cc

1
2
$(TARGET): $(OBJS)
$(CC) -o $(TARGET) $(OBJS)

没有定义变量CC也可以引用它,因为它是make的内置变量(Builtin Variables),表示C编译器的名字,默认值是cc,我们也可以修改它,例如使用交叉编译时,指定编译器:

1
2
CC = riscv64-linux-gnu-gcc
...

自动变量

Makefile中,经常可以看到$@$<这样的变量,这种变量称为自动变量(Automatic Variable),它们在一个规则中自动指向某个值。

例如,$@表示目标文件,$^表示所有依赖文件,因此,我们可以这么写:

1
2
world.out: hello.o main.o
cc -o $@ $^

在没有歧义时可以写$@,也可以写$(@),有歧义时必须用括号,例如$(@D)

为了更好地调试,我们还可以把变量打印出来:

1
2
3
4
5
world.out: hello.o main.o
@echo '$$@ = $@' # 变量 $@ 表示target
@echo '$$< = $<' # 变量 $< 表示第一个依赖项
@echo '$$^ = $^' # 变量 $^ 表示所有依赖项
cc -o $@ $^

执行结果输出如下:

1
2
3
4
$@ = world.out
$< = hello.o
$^ = hello.o main.o
cc -o world.out hello.o main.o

参考源码

可以从GitHub下载源码。

小结

使用变量可以让Makefile更加容易维护。

查看官方手册:

使用模式规则

前面我们讲了使用隐式规则可以让make在必要时自动创建.o文件的规则,但make的隐式规则的命令是固定的,对于xyz.o: xyz.c,它实际上是:

1
$(CC) $(CFLAGS) -c -o $@ $<

能修改的只有变量$(CC)$(CFLAGS)。如果要执行多条命令,使用隐式规则就不行了。

这时,我们可以自定义模式规则(Pattern Rules),它允许make匹配模式规则,如果匹配上了,就自动创建一条模式规则。

我们修改上一节的Makefile如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
OBJS = $(patsubst %.c,%.o,$(wildcard *.c))
TARGET = world.out

$(TARGET): $(OBJS)
cc -o $(TARGET) $(OBJS)

# 模式匹配规则:当make需要目标 xyz.o 时,自动生成一条 xyz.o: xyz.c 规则:
%.o: %.c
@echo 'compiling $<...'
cc -c -o $@ $<

clean:
rm -f *.o $(TARGET)

make执行world.out: hello.o main.o时,发现没有hello.o文件,于是需要查找以hello.o为目标的规则,结果匹配到模式规则%.o: %.c,于是make自动根据模式规则为我们动态创建了如下规则:

1
2
3
hello.o: hello.c
@echo 'compiling $<...'
cc -c -o $@ $<

查找main.o也是类似的匹配过程,于是我们执行make,就可以用模式规则完成编译:

1
2
3
4
5
6
$ make
compiling hello.c...
cc -c -o hello.o hello.c
compiling main.c...
cc -c -o main.o main.c
cc -o world.out hello.o main.o

模式规则的命令完全由我们自己定义,因此,它比隐式规则更灵活。

但是,模式规则仍然没有解决修改hello.h头文件不会触发main.c重新编译的问题,这个依赖问题我们继续放到后面解决。

最后注意,模式规则是按需生成,如果我们在当前目录创建一个zzz.o文件,因为make并不会在执行过程中用到它,所以并不会自动生成zzz.o: zzz.c这个规则。

参考源码

可以从GitHub下载源码。

小结

使用模式规则可以灵活地按需动态创建规则,它比隐式规则更灵活。

查看官方手册:



前面我们讲了隐式规则和模式规则,这两种规则都可以解决自动把.c文件编译成.o文件,但都无法解决.c文件依赖.h文件的问题。

因为一个.c文件依赖哪个.h文件必须要分析文件内容才能确定,没有一个简单的文件名映射规则。

但是,要识别出.c文件的头文件依赖,可以用GCC提供的-MM参数:

1
2
$ cc -MM main.c
main.o: main.c hello.h

上述输出告诉我们,编译main.o依赖main.chello.h两个文件。

因此,我们可以利用GCC的这个功能,对每个.c文件都生成一个依赖项,通常我们把它保存到.d文件中,再用include引入到Makefile,就相当于自动化完成了每个.c文件的精准依赖。

我们改写上一节的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
# 列出所有 .c 文件:
SRCS = $(wildcard *.c)

# 根据SRCS生成 .o 文件列表:
OBJS = $(SRCS:.c=.o)

# 根据SRCS生成 .d 文件列表:
DEPS = $(SRCS:.c=.d)

TARGET = world.out

# 默认目标:
$(TARGET): $(OBJS)
$(CC) -o $@ $^

# xyz.d 的规则由 xyz.c 生成:
%.d: %.c
rm -f $@; \
$(CC) -MM $< >$@.tmp; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.tmp > $@; \
rm -f $@.tmp

# 模式规则:
%.o: %.c
$(CC) -c -o $@ $<

clean:
rm -rf *.o *.d $(TARGET)

# 引入所有 .d 文件:
include $(DEPS)

变量$(SRCS)通过扫描目录可以确定为hello.c main.c,因此,变量$(OBJS)赋值为hello.o main.o,变量$(DEPS)赋值为hello.d main.d

通过include $(DEPS)我们引入hello.dmain.d文件,但是这两个文件一开始并不存在,不过,make通过模式规则匹配到%.d: %.c,这就给了我们一个机会,在这个模式规则内部,用cc -MM命令外加sed.d文件创建出来。

运行make,首次输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ make
Makefile:31: hello.d: No such file or directory
Makefile:31: main.d: No such file or directory
rm -f main.d; \
cc -MM main.c >main.d.tmp; \
sed 's,\(main\)\.o[ :]*,\1.o main.d : ,g' < main.d.tmp > main.d; \
rm -f main.d.tmp
rm -f hello.d; \
cc -MM hello.c >hello.d.tmp; \
sed 's,\(hello\)\.o[ :]*,\1.o hello.d : ,g' < hello.d.tmp > hello.d; \
rm -f hello.d.tmp
cc -c -o hello.o hello.c
cc -c -o main.o main.c
cc -o world.out hello.o main.o

make会提示找不到hello.dmain.d,不过随后自动创建出hello.dmain.dhello.d内容如下:

1
hello.o hello.d : hello.c

上述规则有两个目标文件,实际上相当于如下两条规则:

1
2
hello.o : hello.c
hello.d : hello.c

main.d内容如下:

1
main.o main.d : main.c hello.h

因此,main.o依赖于main.chello.h,这个依赖关系就和我们手动指定的一致。

改动hello.h,再次运行make,可以触发main.c的编译:

1
2
3
4
5
6
7
$ make
rm -f main.d; \
cc -MM main.c >main.d.tmp; \
sed 's,\(main\)\.o[ :]*,\1.o main.d : ,g' < main.d.tmp > main.d; \
rm -f main.d.tmp
cc -c -o main.o main.c
cc -o world.out hello.o main.o

在实际项目中,对每个.c文件都可以生成一个对应的.d文件表示依赖关系,再通过include引入到Makefile,同时又能让make自动更新.d文件,有点蛋生鸡和鸡生蛋的关系,不过,这种机制能正常工作,除了.d文件不存在时会打印错误,有强迫症的同学肯定感觉不满意,这个问题我们后面解决。

参考源码

可以从GitHub下载源码。

小结

利用GCC生成.d文件,再用include引入Makefile,可解决一个.c文件应该如何正确触发编译的问题。

查看官方手册:

完善Makefile

上一节我们解决了自动生成依赖的问题,这一节我们对项目目录进行整理,把所有源码放入src目录,所有编译生成的文件放入build目录:

1
2
3
4
5
6
7
<project>
├── Makefile
├── build
└── src
├── hello.c
├── hello.h
└── main.c

整理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
SRC_DIR = ./src
BUILD_DIR = ./build
TARGET = $(BUILD_DIR)/world.out

CC = cc
CFLAGS = -Wall

# ./src/*.c
SRCS = $(shell find $(SRC_DIR) -name '*.c')
# ./src/*.c => ./build/*.o
OBJS = $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SRCS))
# ./src/*.c => ./build/*.d
DEPS = $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.d,$(SRCS))

# 默认目标:
all: $(TARGET)

# build/xyz.d 的规则由 src/xyz.c 生成:
$(BUILD_DIR)/%.d: $(SRC_DIR)/%.c
@mkdir -p $(dir $@); \
rm -f $@; \
$(CC) -MM $< >$@.tmp; \
sed 's,\($*\)\.o[ :]*,$(BUILD_DIR)/\1.o $@ : ,g' < $@.tmp > $@; \
rm -f $@.tmp

# build/xyz.o 的规则由 src/xyz.c 生成:
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
@mkdir -p $(dir $@)
$(CC) $(CFLAGS) -c -o $@ $<

# 链接:
$(TARGET): $(OBJS)
@echo "buiding $@..."
@mkdir -p $(dir $@)
$(CC) -o $(TARGET) $(OBJS)

# 清理 build 目录:
clean:
@echo "clean..."
rm -rf $(BUILD_DIR)

# 引入所有 .d 文件:
-include $(DEPS)

这个Makefile定义了源码目录SRC_DIR、生成目录BUILD_DIR,以及其他变量,同时用-include消除了.d文件不存在的错误。执行make,输出如下:

1
2
3
4
5
$ make
cc -Wall -c -o build/hello.o src/hello.c
cc -Wall -c -o build/main.o src/main.c
buiding build/world.out...
cc -o ./build/world.out ./build/hello.o ./build/main.o

可以说基本满足编译需求,收工!

参考源码

可以从GitHub下载源码。

小结

除了基础的用法外,Makefile还支持条件判断,环境变量,嵌套执行,变量展开等各种功能,需要用到时可以查询官方手册



留言與分享

vim教程

分類 devops, vim

Vim 编辑器使用笔记整理

1. 退出 Vim

命令行模式退出方式

命令 说明
:wq 保存并退出
:q! 强制退出,不保存
:q 退出(未修改时)
:wq! 强制保存并退出
:w <文件路径> 另存为指定文件
:saveas 文件路径 另存为指定文件
:x 保存并退出(类似:wq)

普通模式退出方式

  • 输入 Shift+zz 即可保存退出

2. 删除文本

普通模式删除命令

命令 说明
x 删除游标所在字符
X 删除游标前一个字符
Delete x
dd 删除整行
dw 删除一个单词(不适用中文)
d$D 删除至行尾
d^ 删除至行首
dG 删除到文档结尾
d1G 删除至文档开头
:%d 清空整个文档内容(删除所有行)
:1,$d :%d,删除从第一行到最后一行
ggdG 普通模式下清空文档的快捷方式(先跳转到首行,然后删除到末尾)

注意:这些命令会立即生效且不可撤销,使用前请确保已保存重要内容

数字前缀用法

  • 2dd 表示一次删除2行
  • 3dw 表示删除3个单词

3. 重复执行命令

  • 普通模式下 . (小数点)表示重复上一次命令
  • 数字前缀:10x 删除10个连续字符

4. 游标跳转

行间跳转

命令 说明
nG 跳转到第n行(需先:set nu显示行号)
gg 跳转到第一行
G 跳转到最后一行

行内跳转

命令 说明
w 到下一个单词开头
e 到当前单词结尾
b 到前一个单词开头
ge 到前一个单词结尾
0^ 到行头
$ 到行尾
f<字母> 向后搜索字母并跳转
F<字母> 向前搜索字母并跳转
t<字母> 向后搜索字母并跳转到匹配前
T<字母> 向前搜索字母并跳转到匹配后

5. 复制粘贴和剪切

复制命令(yank)

命令 说明
yy 复制整行(3yy复制3行)
y^ 复制至行首
y$ 复制至行尾
yw 复制一个单词
yG 复制至文本末
y1G 复制至文本开头

粘贴命令

命令 说明
p 粘贴至光标后
P 粘贴至光标前

6. 替换和撤销

命令 说明
r+<字母> 替换游标所在字母
R 连续替换(按Esc结束)
cc 替换整行
cw 替换一个单词
C 替换至行末
~ 反转字母大小写
u 撤销操作
U 撤销当前行所有修改
Ctrl+r 重做(redo)

7. 缩进调整

缩进命令

命令 说明
>> 整行向右缩进
<< 整行向左回退
:set shiftwidth=n 设置缩进字符数

文本位置调整

命令 说明
:ce 本行内容居中
:ri 本行文本靠右
:le 本行内容靠左

8. 查找功能

基本查找

命令 说明
/字符串 向下查找
?字符串 向上查找
n 继续查找
N 反向查找

高级查找

命令 说明
* 向后查找当前单词
# 向前查找当前单词
g* 向后查找部分匹配单词
g# 向前查找部分匹配单词

9. 多文件编辑

多文件操作

命令 说明
:n 编辑下一个文件
:N 编辑上一个文件
:e 文件名 打开新文件
:e# 回到前一个文件
:ls 列出编辑过的文档
:b 文件名/编号 切换到指定文件
:bd 文件名/编号 从列表删除文件
:f 显示当前文件名

文件恢复

1
2
vim -r 文件名
:ewcover 文件名

10. 可视模式

进入可视模式

命令 说明
v 字符选择模式
V 行选择模式
Ctrl+v 区域选择模式

可视模式操作

  • d 删除选中区域
  • y 复制选中区域

11. 视窗操作

窗口分割

命令 说明
:new 新建窗口
:sp 文件名 水平分割窗口
:vsp 文件名 垂直分割窗口
Ctrl+w s 水平分割当前窗口
Ctrl+w v 垂直分割当前窗口

窗口切换

命令 说明
Ctrl+w j/k/h/l 向下/上/左/右切换窗口
Ctrl+w q 关闭当前窗口
Ctrl+w o 只保留当前窗口

12. 其他功能

文档加密

1
vim -x 文件名

执行外部命令

1
2
:!命令
:w 文件名 # 另存为

帮助系统

1
2
3
:F1        # 打开帮助
:h 主题 # 查看特定帮助
:ver # 显示版本

功能设定

1
2
3
4
:set nu    # 显示行号
:set ai # 自动缩进
:set aw # 自动保存
:set cin # C语言风格缩进

提示:所有设置可通过修改 ~/.vimrc 文件永久保存

留言與分享

pytest基础

分類 devops, pytest

1. 概述

pytest 是一个功能强大的 Python 测试框架,具有以下特点:

  • 简单灵活,文档丰富
  • 支持参数化测试
  • 可用于单元测试、功能测试、自动化测试(如 Selenium/Appium)和接口测试
  • 丰富的插件生态系统
  • 支持测试跳过(skip)和预期失败(xfail)
  • 良好的 CI 工具集成(如 Jenkins)

主要插件

插件名称 功能描述
pytest-selenium Selenium 集成
pytest-html 生成 HTML 测试报告
pytest-rerunfailures 失败用例重试
pytest-xdist 多 CPU 并行测试

2. 使用介绍

2.1 安装

1
pip install pytest

2.2 编写规则

  • 测试文件:test_*.py*_test.py
  • 测试类:以 Test 开头,不能有 __init__ 方法
  • 测试函数:以 test_ 开头
  • 断言:使用 Python 原生 assert

2.3 示例代码

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
# pytest1.py
import pytest

@pytest.fixture(scope='function')
def setup_function(request):
def teardown_function():
print("teardown_function called.")
request.addfinalizer(teardown_function)
print('setup_function called.')

@pytest.fixture(scope='module')
def setup_module(request):
def teardown_module():
print("teardown_module called.")
request.addfinalizer(teardown_module)
print('setup_module called.')

@pytest.mark.website
def test_1(setup_function):
print('Test_1 called.')

def test_2(setup_module):
print('Test_2 called.')

def test_3(setup_module):
print('Test_3 called.')

2.4 fixture 的 scope 参数

参数值 作用范围
function 每个测试函数运行一次(默认)
class 每个测试类运行一次
module 每个模块运行一次
session 每次会话运行一次

2.5 setup 和 teardown

  • setup:测试前的准备工作
  • teardown:测试后的清理工作
  • 替代方案:使用 yield 实现 setup/teardown

3. 测试执行

3.1 基本执行命令

命令 功能
pytest 运行当前目录下所有测试
pytest test_mod.py 运行指定模块的测试
pytest somepath/ 运行指定路径下的测试
pytest -k "stringexpr" 运行名称匹配表达式的测试
pytest test_mod.py::test_func 运行指定模块的特定测试函数

3.2 标记执行

1
2
3
4
5
# 执行标记为 website 的测试
pytest -v -m "website" pytest1.py

# 执行未标记为 website 的测试
pytest -v -m "not website" pytest1.py

3.3 常用命令行参数

参数 功能
-v 显示详细测试结果
-q 只显示简要结果
-s 显示 print 输出
-x 遇到第一个失败立即停止
--exitfirst -x
-h 显示帮助信息

4. 扩展插件

4.1 测试覆盖率 (pytest-cov)

1
2
3
4
pip install pytest-cov

# 生成 HTML 覆盖率报告
pytest --cov-report=html --cov=./ test_code_target_dir

覆盖率参数

参数 说明
--cov=[path] 指定计算覆盖率的路径
--cov-report=type 报告类型:term, html, xml 等
--cov-fail-under=MIN 设置覆盖率最低阈值

4.2 其他实用插件

插件 功能 安装命令
pytest-randomly 随机执行测试 pip install pytest-randomly
pytest-xdist 分布式测试 pip install pytest-xdist
pytest-instafail 立即显示失败 pip install pytest-instafail

5. 示例输出

5.1 详细模式 (-v)

1
2
3
4
5
6
7
8
$ pytest -v pytest1.py
==============================================================================
platform linux2 -- Python 2.7.14, pytest-3.0.0
collected 3 items

pytest1.py::test_1 PASSED
pytest1.py::test_3 PASSED
pytest1.py::test_2 PASSED

5.2 显示输出 (-s)

1
2
3
4
5
6
7
8
9
$ pytest -s pytest1.py
==============================================================================
setup_function called.
Test_1 called.
.teardown_function called.
setup_module called.
Test_2 called.
.Test_3 called.
.teardown_module called.

5.3 覆盖率报告

1
2
3
4
---------------------------------------------------------------- coverage: platform linux2, python 2.7.14-final-0 ----------------------------------------------------------------
Name Stmts Miss Cover
--------------------------------
pytest1.py 18 0 100%

6. 参考资源

  1. Python 测试工具大全
  2. 主流测试工具比较
  3. pytest 官方播客
  4. pytest 简介

留言與分享

  • 第 1 頁 共 1 頁
作者的圖片

Kein Chan

這是獨立全棧工程師Kein Chan的技術博客
分享一些技術教程,命令備忘(cheat-sheet)等


全棧工程師
資深技術顧問
數據科學家
Hit廣島觀光大使


Tokyo/Macau