前言
看懂这篇文章需要一点使用waf的经验,不过也不费事,看看例子也够了。
构建系统简谈
软件构建系统不像是个很多人在研究的东西,所以在网络上很少能找到剖析某个构建系统原理、或者阐述构建系统principle的文章。看ns3的过程中接触到了waf,发现其文档waf book[https://waf.io/book/]很好的阐述了构建系统的一些基础知识,个人认为比cmake的文档好一些。因为其核心只有十几个文件,这个构建系统只需要一个10k+的waf文件,所以可以放到版本库里(像对python的评价一样,batteries included),唯一要求就是环境中有python,而这对一个开发人员来说显然不是一件困难的事情。
|-- Build.py
|-- ConfigSet.py
|-- Configure.py
|-- Context.py
|-- Errors.py
|-- Logs.py
|-- Node.py
|-- Options.py
|-- Runner.py
|-- Scripting.py
|-- Task.py
|-- TaskGen.py
|-- Tools [directory]
|-- Utils.py
|-- ansiterm.py
|-- extras
|-- fixpy2.py
`-- processor.py
以上便是所有waf的内容,可以看到涉及到的文件不算多。Tools下包含了很多语言的构建工具,比如c/c++/java/qt/ruby/tex等等,如果自己有能力定制,可以只保留自己项目里需要的tool,可以做到更小。(虽然个人认为没有必要)
核心抽象
如果是写编译语言的(c/c++/rust/go/fc/d),那么构建系统是每天都在用的。在敲击make<cr>之后,屏幕上出现了一系列的自动运行的命令,然后就是漫长的等待。用waf也一样,一般是./waf configure build clean dist...再等机器的轰鸣停止后继续工作流。waf提供了一些核心的抽象,从而能够表达出构建这个活动的几个关键方面:
- 像make clean dist类似,可以在构建命令后面自行添加指令,这种capibility由Context提供
- 构建系统最重要的功能就是按需构建,要判断出哪些文件要编译而哪些是不用的,这用到了TaskGen与Task的抽象
- 并行构建提升速度,由Runner来提供。
这3个抽象几乎相互独立,个人认为是很好的一个抽象。
Context
每一个跟在./waf后面的指令,都对应一个Context。如果是build/configure/list/step/install/uninstall,waf自行提供了对应的Context的子类用于执行这些命令,如果是其他的自定义函数,那么就会依托于Context本身,可以在自定义函数里用Context自定义的函数,比如recurse来遍历子目录执行子目录里的同名自定义函数。
如果项目根目录下的wscript
有do_sth,就可以./waf do_sth
def do_sth(ctx):
ctx.load('compiler_cxx') # 加载工具
ctx.recurse(['src','dep']) # 遍历子目录,执行子目录下wscript里的do_sth
ctx.exec_command('touch foo.txt')
ctx.msg('hello')
这里函数参数ctx就是指向了Context的一个实例,而do_sth是作为Context上的一个方法而存在的,可以直观的理解为,我们为Context增加了一个自定义的do_sth方法,所以可以自由调用Context里本来提供的方法。
./waf build执行时绑定的Context是BuildConetxt,在Build.py里被定义,在waf build的时候,执行的是wscript里def build(bld)
这个方法。举一个例子
def configure(conf):
conf.load('compiler_cxx')
def build(bld):
bld.shlib(source='a.cpp', target='mylib3')
bld.program(source='main.cpp', target='app', use='mylib')
bld.stlib(target='foo', source='b.cpp')
# 直接调用bld
bld(features = 'c cprogram glib2',
use = 'GLIB GIO GOBJECT',
source = 'main.c org.glib2.test.gresource.xml',
target = 'gsettings-test')
这里bld指向了BuildContext的一个实例,这意味着BuildContext里所有的方法都在这个函数里都是可用的,可以通过bld.xxx
来调用。
值得注意的是,在Build.py中,可是找不到shlib/probram/stlib
这3个方法的,但是在这里却调用成功没有报错,这全部依赖于conf.load('compiler_cxx')
这一句。执行这句话后,就给bld指向的BuildContext实例绑定了shlib/program/stlib
这3个方法。
那直接调用bld()
呢?这个就要看Build.py里的BuildContex():__call__
方法了。从这里开始,就涉及到TaskGen
这个抽象了。
TaskGen & Task
最终需要执行的编译指令、中间代码生成等,每一条都对应一个task,我们不可能去一个一个的写task,而是希望以一种声明式的方法表达想要做的事情,这就是task_gen所完成的任务。从声明式表达到生成task的这项任务,由waf build完成。在执行的过程中,会对搜集到的每个task_gen执行一下post(),然后这个task_gen就生成了自己所有的task。作为一个灵活的构建系统,waf提供了很多方法来让我们hook到post()的过程中。对于每个task,到底该不该执行需不需要执行,它自己会追踪自己的依赖,职责分离,我很喜欢这个设计思路。
以前一小节为例,共在build(bld)里一共进行了4次调用,这意味着生成了4个task_gen的实例,在真正执行构建过程之前,会有一个地方对这4个实例各自调用一下post(),把所有的task_gen都消灭掉,变成task。至于怎么hook,这是个比较关键的点,如果理解了,就能很好的自定义waf了。
首先看看写好的wscript,它的声明式体现在什么地方呢?体现在函数参数里。得益于python的语言特点,可以随便加参数,然后在函数实现里用**kw来取这些值。这意味着可以随便加自己想要的key=value进去,这些加进去的参数是可以在自定义的hook过程中取到的,这算是可自定义的一个基础。(ruby自定义的能力更强,毕竟dsl是其强项,但可能限于ruby的流行程度以及发行版是否默认安装,让作者最后选择了python,不过也已经够用了)
在post()的过程中,会从task_gen.meths[]里依次取出方法来执行,hook的方式就是把自定义的方法塞到这个task_gen.meths[]之中。这只要在自定义的方法上加一个@TaskGen.taskgen_method的注解就能实现,还是挺简洁的吧?声明式中写的key=val,都能通过taskgen.key取到,这样一来,几乎就获得了无限的能力来自定义构建过程了。
在taskgen.meths[]里有几项预定义的方法,waf也提供了指令来让我们定制自己方法执行的位置。总而言之,想要什么内容,直接在wscript里以key=val的方式指定,然后在自己的方法里用getattr来取就行了。
这也只是个支持性框架,具体到某个语言(c/c++)是怎么做的,到后面再看。
Runner
waf自己会默认起和cpu core相同数量的进程来执行构建认任务,而且构建过程的输出也很清晰漂亮。waf也提供了lazy的模式,不是一下子把所有的task_gen都转化,所以也是用了一些技巧来达成这个目的。在看waf代码的过程中,能看到很多pythonic和近乎炫技的技法,可见作者真是把python语言玩弄于股掌之中。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。