当前位置: 首页 > 后端技术 > Python

Waf——一个基于Python的构建系统

时间:2023-03-26 01:51:29 Python

前言要看懂这篇文章,你需要有一点使用waf的经验,不过也不算太麻烦,看例子就好了。浅谈构建系统软件构建系统并不是很多人研究的东西,所以在网上很少能找到分析某个构建系统原理或者讲解构建系统原理的文章。在看ns3的过程中接触到了waf,发现它的文档wafbook[https://waf.io/book/]对构建系统的一些基础知识讲解的很好,个人认为比cmake文档好。因为核心只有十几个文件,所以这个构建系统只需要一个10k+的waf文件,所以可以放到repository中(像python的评价一样,包括电池),唯一的要求就是里面有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[目录]|--Utils.py|--ansiterm.py|--extras|--fixpy2。py`--processor.py上面就是waf的全部内容,可以看到涉及的文件不多。Tools包含很多语言构建工具,比如c/c++/java/qt/ruby/tex等,如果你有能力自定义,你可以只保留你项目中需要的工具,你可以把它做小。(虽然我个人认为没有必要)如果核心抽象是用编译型语言(c/c++/rust/go/fc/d)写的,那么每天都要用到构建系统。输入make后,屏幕上出现了一系列自动运行的命令,然后就是漫长的等待。waf也一样,一般都是./wafconfigurebuildcleandist...等机器的轰鸣声停止后继续工作流程。Waf提供了一些核心抽象,因此它可以表达构建这个活动的几个关键方面:像makecleandist一样,你可以在构建命令之后添加指令。此功能由Context提供。构建系统最重要的功能就是按需构建,需要判断哪些文件需要编译,哪些不用。这里使用了TaskGen和Task的抽象并行构造来提高速度,由Runner提供。这三个抽象几乎是相互独立的,个人认为是一个非常好的抽象。Context./waf后面的每条指令对应一个Context。如果是build/configure/list/step/install/uninstall,waf提供了相应的Context子类来执行这些命令。如果是其他自定义函数,会依赖于Context本身,在自定义函数中,使用Context自定义的函数,比如recurse,遍历子目录,执行子目录下的同名自定义函数。如果项目根目录下的wscript有do_sth,可以./wafdo_sthdefdo_sth(ctx):ctx.load('compiler_cxx')#加载工具ctx.recurse(['src','dep'])#遍历子目录,在子目录的wscript中执行do_sthctx.exec_command('touchfoo.txt')ctx.msg('hello'),这里函数参数ctx指向一个Context的实例,do_sth是一个关于Context存在的方法,可以直观的理解为我们在Context中添加了一个自定义的do_sth方法,这样我们就可以自由调用Context中原本提供的方法了。./wafbuild执行时绑定的Context是BuildCoretxt,定义在Build.py中。执行wafbuild时,会执行wscript中的defbuild(bld)方法。举个例子defconfigure(conf):conf.load('compiler_cxx')defbuild(bld):bld.shlib(source='a.cpp',target='mylib3')bld.program(source='main.cpp',target='app',use='mylib')bld.stlib(target='foo',source='b.cpp')#直接调用bldbld(features='ccprogramglib2',use='GLIBGIOGOBJECT',source='main.corg.glib2.test.gresource.xml',target='gsettings-test')其中bld指向一个BuildContext的实例,也就是说BuildContext中的所有方法都在这个它在函数中可用,可以被bld.xxx调用。值得注意的是,在Build.py中,找不到shlib/probram/stlib这三个方法,但是这里调用成功,没有报错,全看conf.load('compiler_cxx')这句了。执行完这句话后,shlib/program/stlib这三个方法就绑定到bld指向的BuildContext实例上了。直接调用bld()怎么样?这取决于Build.py中的BuildContex():__call__方法。从这里开始,就涉及到TaskGen的抽象了。TaskGen&Task最后需要执行编译指令、中间代码生成等,每一个都对应一个任务。我们不可能一个一个的写任务,但是我们希望用声明的方式来表达我们要做的事情。它是由task_gen完成的任务。从声明式表达到任务生成,都是通过wafbuild来完成的。在执行过程中,会对每个收集到的task_gen执行post(),然后这个task_gen会生成自己的所有任务。waf作为一个灵活的构建系统,提供了很多方法让我们可以hook到post()的进程中。对于每个任务,无论是否应该执行,它都会跟踪自己的依赖关系和单独的职责。我非常喜欢这个设计理念。以上一节为例,在build(bld)中一共调用了4次,也就是说生成了4个task_gen实例。在实际构建过程执行之前,会有一个地方分别调用这4个实例post(),销毁所有task_gen,成为task。至于怎么钩,这是一个重点。如果你了解它,你可以很好地定制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++)如何实现,后面会看到。Runnerwaf本身默认会启动与CPU核心相同数量的进程来执行构建任务,构建过程的输出也非常清晰漂亮。waf还提供了惰性模式,而不是一次性转换所有的task_gen,所以使用了一些技巧来达到这个目的。在看waf代码的过程中,看到了很多pythonic的,近乎眼花缭乱的技巧,可见作者真的把python语言玩在了手心。