第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之前执行。

我曾经踩过的坑:有一次我写了个任务,忘了加addtask声明,结果BitBake根本不执行它。排查了半天才发现——任务定义好了,但没告诉BitBake什么时候跑。记住:任务定义 + addtask声明,缺一不可。

任务依赖还可以跨配方。比如你的应用依赖某个库:

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的变化而变化。这就是延迟展开的效果。

重点:BitBake的变量展开是「按需展开」的。它不会在赋值时就把所有变量替换掉,而是保留引用关系,等到真正用到这个变量时才去展开。这个机制让变量覆盖变得非常灵活。

还有一种特殊情况——立即展开。用:=操作符:

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,简单操作用Shell。比如文件复制、简单编译用Shell就够了;但如果你要做版本号解析、条件分支判断、网络请求——果断上Python。我在一个项目中用Python写了自动检测工具链版本的功能,比用Shell写省了一半代码。

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层面是如何发挥作用的。