BitBake使用攻略--BitBake的语法知识二


写在前面

这是《BitBake使用攻略》系列文章的第三篇,主要讲解BitBake的基本语法。由于此篇的实验依赖于第一篇的项目,建议先将HelloWorld项目完成之后再食用此篇为好。
第一篇的链接在这:BitBake使用攻略--从HelloWorld讲起


1. BitBake中的任务

对于BitBake,运行一个菜谱(recipe)其实运行的就是这个菜谱中的任务。任务可以说是BitBake的执行单元,它仅仅出现在菜谱和类文件中,同时其名称通常会以do_开头。
为了在菜谱中添加任务,我们可以使用addtask将一个shell函数或者BitBake风格的Python函数定义为任务。与此同时,由于一个菜谱中可能存在多个任务,因此BitBake提供了afterbefore定义了这些任务的执行顺序,也就是依赖关系,这主要是为了避免并行构建导致的顺序错误,下面有一个定义任务的例子(通过修改前面的printhello.bb)文件得到的。

DESCRIPTION = "Prints Hello World"
PN = 'printhello'
PV = '1'

python do_printdate() {
    import time
    bb.plain("Date: " + time.strftime('%Y%m%d', time.gmtime()))
}
addtask printdate

python do_printendmsg() {
    bb.plain("Build End")
}
addtask printendmsg

python do_build() {
    bb.plain("********************");
    bb.plain("*                  *");
    bb.plain("*  Hello, World!   *");
    bb.plain("*                  *");
    bb.plain("********************");
}

addtask build before do_printendmsg after do_printdate

在这个例子中,我们使用addtask将三个函数添加到了任务列表中,并在最后定义了三个任务的执行顺序,即build任务需要在printdate任务前和printendmsg任务后完成。另外,可以看到我们在添加任务时省略掉了do_,这是合法的,因为BitBake会自动地为其添加。此时切换到主目录运行bitbake printhello -c printendmsg(bitbake不指定任务的情况下会默认运行build任务,-c可以指定我们要执行的任务)会看到如下内容:

Date: 20230219
********************
*                  *
*  Hello, World!   *
*                  *
********************
Build End

显然,我们运行printendmsg直接导致了buildprintdate任务的运行,这是依赖关系造成的。对于afterbefore,它们并不是仅限于定义串行关系,其还可以定义树形关系,也就是说关键字后面可以有一个或多个任务,但要注意不能出现圈形关系。由于每个菜谱中可能会有很多任务,因此它通常会提供do_listtasks去查看当前菜谱中存在的任务。
BitBake可以添加任务,同样地就可以删除任务。在菜谱中,通过使用deltask可以删除任务,同时还会删除任务相关的依赖。例如,我们删除上面的build任务(不是很恰当的例子,build任务不该删除),其会造成printdateprintendmsg任务没有任何关联,也就是说,运行printendmsg任务不会造成printdate任务的执行。为了保留依赖关系,我们应该使用下面的方法:

do_build[noexec] = "1"

这样,在运行printendmsg的时候,仍然会运行printdate,但不再运行build任务。另外,在运行任务的时候会发现bitbake报告没有需要执行的任务,这是因为每次bitbake执行完一个任务后会生成stamp文件,下次再运行相同任务时会比对stamp,如果发现任务内容无变化将不再执行该任务。因此,为了再次执行运行过的任务,可以删除xx/tmp/stamps.do_xx文件或者在命令行中加-f选项。
如果想要在bitbake构建过程中使用外部环境变量,有一种方法可以实现:

  • 先将该环境变量的名称添加到BB_ENV_PASSTHROUGH_ADDITIONS,此时该环境变量被允许添加到BitBake的数据仓库中,例如:export BB_ENV_PASSTHROUGH_ADDITIONS="$BB_ENV_PASSTHROUGH_ADDITIONS TEST_ENV"
  • 然后导出该环境变量,例如:export TEST_ENV="123"

另外值得注意的是,每次修改该环境变量都会导致与该变量相关的任务校验和不一致,因此这些任务每次都会重新构建。
BitBake为了保证每次能够用干净的环境去执行任务,通常会清除掉外部环境导出的或者PassThrough列表(BB_ENV_PASSTHROUGHBB_ENV_PASSTHROUGH_ADDITIONS)中列出的变量,但我们能够通过设置BB_PRESERVE_ENV来阻止这种清除,例如我们在运行bitbake命令前先在命令行执行如下命令:

export BB_PRESERVE_ENV="1"

这样,在本次bitbake运行时就会使用和上一次一样的环境。因此,如果我们将TEST_ENV传进了数据仓库,同时设置了BB_PRESERVE_ENV,那么我们在下次运行bitbake前,即使清除了BB_ENV_PASSTHROUGH_ADDITIONS中的TEST_ENV,在环境中也仍然能用TEST_ENV。但如果我们此时又想要原始的环境变量,那么我们可以通过BB_ORIGENV(原始数据仓库)得到,例如我们想获得BAR的原始值:

origenv = d.getVar("BB_ORIGENV", False)
bar = origenv.getVar("BAR", False)

2. 任务配置

除了上一节中提及的依赖关系,任务还有其他的一些属性,这些都可以通过标志(Flag,语法详见前一节)来控制。下面介绍一组BitBake提供的任务属性,其用法与之前的do_build[noexec] = "1"一样:

  • [cleandirs] : 指定任务运行前需要创建的空目录,如果这些目录原本存在将会被删除重建
  • [depends] : 控制任务间的依赖关系
  • [deptask] : 控制任务间构建时的依赖关系
  • [dirs] : 指定任务运行前需要创建的目录,如果这些目录原本存在将保留,另外列出的最后一个目录将作为本任务运行的当前工作目录
  • [lockfiles] : 指定一个或多个锁文件(lockfile),只有拿到指定锁文件的权限后才能执行该任务,否则会阻塞,这是BitBake提供的一种互斥机制
  • [noexec] : 当设置为"1"时,该任务不再执行,但仍作为类似占位符一样存在
  • [nostamp] : 当设置为"1"时,不再生成该任务的stamp文件,该任务(包括依赖该任务的任务)将每次都会执行
  • [number_threads] : 指定同时可以执行该任务的最大线程数,以限制某些资源的使用。另外需要注意,该属性需要在全局设置,而不能单独的设置到某个菜谱文件中,例如可以在base.bbclass文件中定义do_fetch[number_threads] = "2",同时,当number_threads超过BB_NUMBER_THREADS时,该属性将无效
  • [postfuncs] : 任务完成后要执行的函数列表
  • [prefuncs] : 任务执行前要执行的函数列表
  • [rdepends] : 控制内部任务运行时的依赖关系
  • [rdeptask] : 控制任务运行时的依赖关系
  • [recideptask] : 当与[recrdeptask]一起设置时,为额外的依赖指定一个要被检查的任务
  • [recrdeptask] : 控制任务递归运行时的依赖
  • [stamp-extra-info] : 追加到任务stamp的额外stamp信息
  • [umask] : 运行任务的umask

2.1 依赖

由于BitBake支持多线程构建代码,所以依赖对于它来说也是必须的,因为所有任务之间都有先后顺序,例如线程1在执行一个软件包的配置任务,线程2在执行一个软件包的编译任务,由于配置必须在编译之前(否则编译会报错),因此我们必须声明两个任务的依赖关系,即编译任务依赖于配置任务。对于BitBake,你可以声明一个菜谱中两个任务的依赖关系,也可以声明不同菜谱中的依赖关系,前提是这些任务是存在的(即addtask声明过的)。
定义依赖的方法比较多,接下来我们一个一个的进行讲解,并尽可能的提供一些例子帮助理解。

2.1.1 内部任务间的依赖

这个在第一节中已经进行了说明,它是通过afterbefore指定的,其作用范围仅限于同一个菜谱中的任务。由于在先前已经有相关的例子了,这里就不再进行赘述。

2.1.2 不同菜谱下的任务间依赖

对于两个不同菜谱文件中的任务,如果要指定它们间的依赖关系,其需要先使用DEPENDS指定依赖的菜谱,然后通过任务的[deptask]指定要依赖的DEPENDS列出全部菜谱的该依赖任务。这可能有点不太容易理解,举个例子,现在有个菜谱A,里面有一个build任务,还有两个菜谱BC,它们里面都有一个configure任务,对于A的build任务来讲,其需要在完成其他菜谱文件中的configure任务后才能执行,因此此时我们可以有以下方法来指定该依赖关系:

  1. A中定义DEPENDS变量,其内容为DEPENDS = "B C"
  2. A中定义依赖:do_build[deptask] = "do_configure"

此时,运行Abuild任务时将会先运行BC中的configure任务。下面有两个菜谱文件:

printhello.bb: 

DESCRIPTION = "Prints Hello World"
PN = 'printhello'
PV = '1'
DEPENDS = "printelse"

do_build[deptask] = "do_configure"

python do_build() {
    bb.plain("********************");
    bb.plain("*                  *");
    bb.plain("*  Hello, World!   *");
    bb.plain("*                  *");
    bb.plain("********************");
}

printelse.bb:

DESCRIPTION = "Prints Else Info"
PN = 'printelse'
PV = '1'

python do_configure() {
    bb.plain("********************");
    bb.plain("*                  *");
    bb.plain("*   Pre Configure! *");
    bb.plain("*                  *");
    bb.plain("********************");
}

addtask do_configure

此时,运行bitbake -f printhello,你可以得到如下内容:

********************
*                  *
*   Pre Configure! *
*                  *
********************
********************
*                  *
*  Hello, World!   *
*                  *
********************
2.1.3 运行时态下的依赖

前面描述的几种依赖定义方法仅限于构建时态下,相反,如果想要定义运行时态下的依赖就需要借助[rdeptask]属性。在BitBake构建代码的过程中会生成许多包,这些都列在PACKAGES变量中。对于每个包,BitBake都提供了RDEPENDS变量用于表示对应包依赖的其他包,这类似于DEPENDS变量。另外,类似[deptask]属性,对于每个任务同样有[rdeptask]属性用于表示包内任务依赖的其他任务,一旦指定该依赖关系,那么在运行该包内的此任务时,一定会先运行其RDEPENDS列出的全部包中的[rdeptask]依赖任务。
这里,你可能有点混淆,但你只需要知道一点,这些归根结底也只是任务A依赖任务B这种简单关系。

2.1.4 递归依赖

BitBake还提供了一种任务属性[recrdeptask],其提供了递归依赖方式,运行机制如下:

  1. 寻找当前任务的构建时依赖和运行时依赖
  2. 添加这些依赖到该任务的依赖列表中
  3. 递归到依赖列表中继续1,直到不存在依赖关系

我的理解是,[recrdeptask]提供了一种更为普遍的方法去管理不同菜谱间任务的依赖,其本质上是构建依赖(2.1.2)和运行时依赖(2.1.3)的结合,因此,这个属性通常用在高级别的菜谱中。
另外,BitBake会忽略掉循环依赖,例如do_a[recrdeptask] = "do_a"

2.1.5 任务间的依赖

对于2.1.2和2.1.3中提到的依赖关系定义,其都由DEPNEDS或者RDEPENDS和任务属性共同决定,为了能够用更为一般的定义依赖关系的方法,BitBake提供了[depends]属性。对于该属性,其指定了一个任务依赖的任务列表,这些依赖任务不同于[deptask]或者[rdeptask],它需要在依赖任务前指定任务所属的目标,格式如下:

do_a = "target1:do_b target2:do_b"

在这个例子中,执行任务a之前必须要先执行target1的任务b和target2的任务b。你可以尝试下面的例子:

printhello.bb:

DESCRIPTION = "Prints Hello World"
PN = 'printhello'
PV = '1'

python do_configure() {
    bb.plain("************************");
    bb.plain("*                      *");
    bb.plain("* Pre Configure self ! *");
    bb.plain("*                      *");
    bb.plain("************************");
}
do_configure[nostamp] = "1"
addtask do_configure

do_build[depends] = "printhello:do_configure printbeef:do_configure printbird:do_configure"

python do_build() {
    bb.plain("********************");
    bb.plain("*                  *");
    bb.plain("*  Hello, World!   *");
    bb.plain("*                  *");
    bb.plain("********************");
}

printbird.bb

DESCRIPTION = "Prints Bird Info"
PN = 'printbird'
PV = '1'

python do_configure() {
    bb.plain("************************");
    bb.plain("*                      *");
    bb.plain("* Pre Configure bird ! *");
    bb.plain("*                      *");
    bb.plain("************************");
}

do_configure[nostamp] = "1"
addtask do_configure

printbeef.bb:

DESCRIPTION = "Prints Beef Info"
PN = 'printbeef'
PV = '1'

python do_configure() {
    bb.plain("************************");
    bb.plain("*                      *");
    bb.plain("* Pre Configure Beef ! *");
    bb.plain("*                      *");
    bb.plain("************************");
}

do_configure[nostamp] = "1"
addtask do_configure

运行结果如下:

************************
*                      *
* Pre Configure bird ! *
*                      *
************************
************************
*                      *
* Pre Configure Beef ! *
*                      *
************************
************************
*                      *
* Pre Configure self ! *
*                      *
************************
********************
*                  *
*  Hello, World!   *
*                  *
********************

可见,通过这种方法,我们可以实现前面2.1.1,2.1.2,2.1.3提到的所有依赖定义方法。

2.2 事件

BitBake提供了事件处理函数机制用于在一些特定场景下执行某个指定函数,例如在某个任务失败后触发事件处理函数,然后发送邮件,另外,事件只能定义在菜谱文件或者类文件中,下面是一个事件的例子,在printhello.bb文件中添加如下内容:

addhandler myclass_eventhandler
python myclass_eventhandler() {
    from bb.event import getName
    bb.plain("The name of the Event is %s" % getName(e))
    bb.plain("The file we run for is %s" % d.getVar('FILE'))
}
myclass_eventhandler[eventmask] = "bb.event.RecipeParsed"

在这个例子中,我们使用addhandler将一个python函数定义为事件处理函数,然后通过[eventmask]属性指定了哪些事件可以触发该函数(若不指定,任何事件都将触发该函数),例子中指定了在解析完菜谱后触发myclass_eventhandler函数。另外在这个例子中,e是一个全局变量,指代的是当前发生的事件,通过getName函数我们可以得到该事件的名称。
下面列出了在标准构建过程中,最经常使用的一些事件:

  • bb.event.ConfigParsed(): 基本配置(bitbake.conf,base.bbclass以及继承的其他全局变量)解析后触发。通过在该事件中设置BB_INVALIDCONF可以重解析基本配置。
  • bb.event.HeartbeatEvent(): 定时触发,默认一秒,可以通过配置BB_HEARTBEAT_EVENT配置触发间隔。
  • bb.event.ParseStarted(): 开始解析菜谱时触发,事件有total属性表示计划解析菜谱的数量。
  • bb.event.ParseProgress(): 解析菜谱触发,事件有current属性表示已经解析菜谱的数量。
  • bb.event.ParseCompleted(): 解析菜谱完成时触发,事件有cached, parsed, skipped, virtuals, masked, errors属性,可以用来统计解析情况。
  • bb.event.BuildStarted(): 一个新的构建开始时触发。
  • bb.build.TaskStarted(): 一个任务开始时触发。taskfile属性表示任务是哪个配方发起的,taskname表示任务名,logfile表示任务的输出路径,time表示任务开始时间。
  • bb.build.TaskInvalid(): 执行不存在的任务时触发。
  • bb.build.TaskFailedSilent(): setscene任务失败,输出过于冗长不给用户呈现时触发。
  • bb.build.TaskFailed(): 正常任务失败时触发。
  • bb.build.TaskSucceeded(): 一个任务成功完成时触发。
  • bb.event.BuildCompleted(): 一个构建完成时触发。
  • bb.cooker.CookerExit(): 构建服务器关机时触发。

2.3 校验和

BitBake在执行每个任务时都会默认生成一个stamp文件,其保存了该任务输入的校验和数据,通过它,BitBake可以避免去重复运行一些任务(输入未改变)。另外,BitBake提供了bitbake-dumpsigs命令去读取任务生成的签名数据。

3. Class Extension Mechanism

BitBake提供了一种在单个recipe文件下定义多个版本的机制,其通过BBCLASSEXTENDBBVERSIONS变量实现。


至此,基本的BitBake语法知识就算是学完了,在之后的时间里,我将继续介绍命令用法以及怎样使用它完成一个复杂的工程构建任务。当然啦,也希望大家能多多支持一下博主,码字不易,还望一键三连,再次谢谢大家能看到这个地方。
我是chegxy,欢迎关注!!!

热门相关:地球第一剑   网游之逆天飞扬   网游之逆天飞扬   巡狩万界   薄先生,情不由己