第4章:BitBake深度解析:语法基础、任务依赖、变量展开机制、Python函数嵌入
好,咱们进入BitBake的世界。说实话,很多做Yocto的朋友一开始都被BitBake吓住了——觉得它像个黑盒子,不知道里面在干什么。我刚开始接触时也差不多,对着一个bb文件发呆,心想这玩意儿到底怎么工作的?
BitBake本质上就是个构建调度器。它不负责编译本身,而是告诉你「先做A,再做B,最后做C」。嗯,听起来简单,但里面的门道可不少。
4.1 BitBake语法基础:别被吓到
先看一个最简单的bb文件长什么样:
DESCRIPTION = "这是一个简单的例子"
LICENSE = "MIT"
SRC_URI = "file://hello.c"
do_compile() {
${CC} ${CFLAGS} hello.c -o hello
}
do_install() {
install -d ${D}${bindir}
install -m 0755 hello ${D}${bindir}
}
你看,其实挺直观的。变量赋值用等号,任务用函数定义。我个人习惯把变量定义放在文件头部,任务函数放在后面,这样结构清晰。
BitBake支持几种赋值方式,我列个表给你看:
| 操作符 | 含义 | 示例 |
|---|---|---|
| = | 标准赋值 | A = "hello" |
| += | 追加(带空格) | A += "world" |
| .= | 追加(不带空格) | A .= "world" |
| ?= | 条件赋值(未定义时才赋值) | A ?= "default" |
| ??= | 弱条件赋值 | A ??= "weak" |
4.2 任务依赖:谁先谁后?
BitBake里任务之间是有依赖关系的。你想想看,编译之前肯定要先解压源码吧?安装之前肯定要先编译吧?
常见的任务依赖链是这样的:
do_fetch -> do_unpack -> do_patch -> do_configure -> do_compile -> do_install -> do_package
但有时候你需要自定义依赖。比如我有个项目,需要在编译前生成一个头文件:
addtask generate_header before do_compile after do_patch
do_generate_header() {
echo "#define VERSION \"1.0\"" > ${S}/version.h
}
这里的关键词是addtask,它告诉BitBake:我这个任务要插在do_patch之后、do_compile之前执行。
任务依赖还可以跨配方。比如你的应用依赖某个库:
do_configure[depends] += "libfoo:do_populate_sysroot"
这行代码的意思是:我配置之前,libfoo必须先完成sysroot的填充。嗯,这种跨配方的依赖在实际项目中非常常见。
4.3 变量展开机制:BitBake的魔法
BitBake的变量展开,说白了就是「变量里面套变量」。你定义一个变量,里面引用另一个变量,BitBake会在使用时把它们全部展开。
看个例子:
PV = "1.0"
PR = "r0"
PN = "mylib"
WORKDIR = "${TMPDIR}/work/${PN}-${PV}-${PR}"
最终WORKDIR会被展开成类似/build/tmp/work/mylib-1.0-r0这样的路径。BitBake的展开是延迟的——它不会在赋值时展开,而是在使用时才展开。这个特性很重要。
为什么会这样?因为变量可能在后面被覆盖。比如:
A = "hello ${B}"
B = "world"
# 此时A的值是 "hello world"
B = "bitbake"
# 此时A的值是 "hello bitbake"
注意,A的值会随着B的变化而变化。这就是延迟展开的效果。
还有一种特殊情况——立即展开。用:=操作符:
A := "hello ${B}"
B = "world"
# 此时A的值是 "hello "(B还没定义,展开为空)
我个人很少用立即展开,除非有特殊需求。大多数情况下,延迟展开更符合直觉。
4.4 Python函数嵌入:让BitBake更强大
纯Shell脚本有时候不够灵活。比如你要处理字符串、解析文件内容、做条件判断——这些用Shell写起来很痛苦。BitBake允许你在bb文件中嵌入Python函数,这就舒服多了。
看个例子:
python do_print_version() {
import os
version = d.getVar('PV')
major, minor = version.split('.')
bb.note(f"Major version: {major}, Minor version: {minor}")
}
注意这里的d对象——它是BitBake的数据存储,你可以通过d.getVar()获取变量,通过d.setVar()设置变量。这个d对象是Python函数的标配参数。
Python函数还可以返回数据给Shell任务:
python do_get_source_date() {
import subprocess
result = subprocess.run(['git', 'log', '-1', '--format=%ct'],
capture_output=True, text=True)
return result.stdout.strip()
}
do_compile[prefuncs] += "do_get_source_date"
Python函数还可以定义类:
python() {
class VersionHandler:
def __init__(self, version_str):
self.version = version_str
self.parts = version_str.split('.')
def major(self):
return self.parts[0] if len(self.parts) > 0 else '0'
handler = VersionHandler(d.getVar('PV'))
d.setVar('MAJOR_VER', handler.major())
}
嗯,这个用法稍微高级一点。你可以在匿名Python函数里定义类、处理数据,然后把结果写回BitBake的变量空间。这样后续的Shell任务就能直接用这些变量了。
4.5 实战:一个完整的bb文件
最后,我给你看一个我实际项目中用过的bb文件片段,把今天讲的知识点串起来:
DESCRIPTION = "我的嵌入式应用"
LICENSE = "GPL-2.0-only"
LIC_FILES_CHKSUM = "file://LICENSE;md5=xxx"
SRC_URI = "git://github.com/myproject/app.git;branch=main"
SRCREV = "${AUTOREV}"
PV = "1.0+git${SRCPV}"
# Python函数:获取Git提交信息
python do_get_git_info() {
import subprocess
try:
result = subprocess.run(['git', 'rev-parse', '--short', 'HEAD'],
capture_output=True, text=True, cwd=d.getVar('S'))
d.setVar('GIT_SHORT_HASH', result.stdout.strip())
except:
d.setVar('GIT_SHORT_HASH', 'unknown')
}
addtask get_git_info before do_compile after do_patch
do_compile() {
# 使用Python设置的变量
echo "Building version ${PV} (${GIT_SHORT_HASH})"
${CC} ${CFLAGS} -DGIT_HASH=\"${GIT_SHORT_HASH}\" main.c -o myapp
}
do_install() {
install -d ${D}${bindir}
install -m 0755 myapp ${D}${bindir}
}
# 依赖关系
do_compile[depends] += "libfoo:do_populate_sysroot"
这个例子包含了变量定义、Python函数嵌入、任务依赖、addtask声明——基本上覆盖了今天讲的所有内容。你可以把它当作一个模板,以后写bb文件时参考。
好了,BitBake的核心机制就讲到这里。下一章我们会深入剖析Layer的层次结构和优先级管理,到时候你会看到BitBake的变量覆盖机制在Layer层面是如何发挥作用的。