bitbake 调试运行
bitbake 的多进程架构导致调试变的不是很方便,从而对于理解 bitbake 的运行原理不是很友好。

bitbake 是典型的 C/S 架构,bitbake 客户端与 bitbake-server 之间通过 unix socket 进行通信,bitbake-server 没有提供简单的方式在前台运行(通过 bitbake --server-only 或者 bitbake 客户端启动的 bitbake-server 都运行在后台),因此调试运行(或者前台运行)并不是很方便。

standalone bitbake server

根据 bitbake-server 代码(bitbake/bin/bitbake-server),可以通过自己实现 bitbake-server 逻辑进行规避:

https://github.com/runsisi/obmc-utils/blob/master/obmc-server.py

#!/usr/bin/python3
# coding: utf-8

import argparse
import os
import sys
import logging


class Tee:
    def __init__(self, *files):
        self._files = files

    def __getattr__(self, attr, *args):
        return self._wrap(attr, *args)

    def _wrap(self, attr, *args):
        def g(*a, **kw):
            for f in self._files:
                r = getattr(f, attr, *args)(*a, **kw)
            return r
        return g


def parse_args():
    parser = argparse.ArgumentParser('obmc-server')
    parser.add_argument(
        '-b', '--build',
        dest='build',
        required=True,
        help='openbmc build dir'
    )
    return parser.parse_args()


def run(build):
    libpath = os.path.abspath(os.path.join(build, '../../bitbake/lib'))
    sys.path.insert(0, libpath)

    import bb.event
    import bb.utils
    import bb.server.process

    bb.utils.check_system_locale()

    logfile = os.path.join(build, 'bitbake-cookerdaemon.log')
    lockname = os.path.join(build, 'bitbake.lock')
    sockname = os.path.join(build, 'bitbake.sock')

    lock = bb.utils.lockfile(lockname, False, False)
    if not lock:
        print('bitbake server is already running, please run `bitbake -m` to kill')
        sys.exit(1)
    lockfd = lock.fileno()
    readypipe, readypipeinfd = os.pipe()

    timeout = -1
    profile = False
    xmlrpcinterface = (None, '0')

    # Replace standard fds with our own
    with open('/dev/null', 'r') as si:
        os.dup2(si.fileno(), sys.stdin.fileno())

    so = open(logfile, 'a+')
    sys.stdout = Tee(sys.stdout, so)

    # Have stdout and stderr be the same so log output matches chronologically
    # and there aren't two seperate buffers
    sys.stderr = sys.stdout

    logger = logging.getLogger("BitBake")
    # Ensure logging messages get sent to the UI as events
    handler = bb.event.LogHandler()
    logger.addHandler(handler)

    bb.server.process.execServer(lockfd, readypipeinfd, lockname, sockname, timeout, xmlrpcinterface, profile)
    os.close(readypipe)


if __name__ == '__main__':
    args = parse_args()

    build = os.path.abspath(os.path.expanduser(args.build))
    if not os.path.exists(build):
        print("build dir does not exist", file=sys.stderr)
        sys.exit(1)

    run(build)

运行之后效果如下(客户端执行了 bitbake bmcweb -c listtasks 命令):

❯ ./obmc-server.py -b ~/working/bmc/openbmc/build/romulus
89324 23:09:24.415613 --- Starting bitbake server pid 89324 at 2023-12-29 23:09:24.415595 ---
89324 23:09:24.416181 Started bitbake server pid 89324
89324 23:09:24.416343 Entering server connection loop
89324 23:09:24.416386 Lockfile is: /home/runsisi/working/bmc/openbmc/build/romulus/bitbake.lock
Socket is /home/runsisi/working/bmc/openbmc/build/romulus/bitbake.sock (True)
89324 23:09:40.267400 Accepting [<socket.socket fd=7, family=1, type=1, proto=0, laddr=bitbake.sock>] ([])
89324 23:09:40.268237 Processing Client
89324 23:09:40.268299 Connecting Client
89324 23:09:40.268572 Running command ['setFeatures', [2, 1]]
89324 23:09:40.268619 Sending reply (None, None)
89324 23:09:40.268681 Command Completed (socket: True)
89324 23:09:40.269027 Running command ['updateConfig', {'halt': True, 'force': False, 'invalidate_stamp': None, 'dry_run': False, 'dump_signatures': [], 'extra_assume_provided': [], 'profile': False, 'prefile': [], 'postfile': [], 'server_timeout': None, 'nosetscene': False, 'setsceneonly': False, 'skipsetscene': False, 'runall': [], 'runonly': None, 'writeeventlog': None, 'build_verbose_shell': False, 'build_verbose_stdout': False, 'default_loglevel': 20, 'debug_domains': {}}, {'HOME': '/home/runsisi', 'LOGNAME': 'runsisi', 'PATH': '/home/runsisi/working/bmc/openbmc/scripts:/home/runsisi/working/bmc/openbmc/poky/bitbake/bin:/home/runsisi/working/cxx/depot_tools:/home/runsisi/.bun/bin:/opt/cni/bin:/home/runsisi/go/bin:/home/runsisi/.npm/bin:/usr/local/bin:/usr/bin:/usr/local/sbin:/home/runsisi/working/cxx/depot_tools:/home/runsisi/.dotnet/tools:/opt/flutter/bin:/usr/lib/jvm/default/bin:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl:/usr/lib/rustup/bin:/opt/flutter/bin:/home/runsisi/.local/bin', 'PWD': '/home/runsisi/working/bmc/openbmc/build/romulus', 'SHELL': '/bin/zsh', 'SSH_AUTH_SOCK': '/run/user/1000/keyring/ssh', 'USER': 'runsisi', 'BBPATH': '/home/runsisi/working/bmc/openbmc/build/romulus', 'KUBECONFIG': '/etc/rancher/k3s/k3s.yaml', 'BUN_INSTALL': '/home/runsisi/.bun', 'EDITOR': 'vim', 'PYTHONPATH': '/home/runsisi/working/bmc/openbmc/poky/bitbake/lib:', 'OE_ADDED_PATHS': '/home/runsisi/working/bmc/openbmc/scripts:/home/runsisi/working/bmc/openbmc/poky/bitbake/bin:', 'BUILDDIR': '/home/runsisi/working/bmc/openbmc/build/romulus', '_': '/home/runsisi/working/bmc/openbmc/poky/bitbake/bin/bitbake'}, ['/home/runsisi/working/bmc/openbmc/poky/bitbake/bin/bitbake', 'bmcweb', '-c', 'listtasks']]
89324 23:09:40.493238 Base config valid
89324 23:09:40.494288 Sending reply (None, None)
89324 23:09:40.494367 Command Completed (socket: True)
89324 23:09:40.494531 Running command ['getVariable', 'BBINCLUDELOGS']
89324 23:09:40.494585 Sending reply ('yes', None)
89324 23:09:40.494627 Command Completed (socket: True)
89324 23:09:40.494719 Running command ['getVariable', 'BBINCLUDELOGS_LINES']
89324 23:09:40.494933 Sending reply (None, None)
89324 23:09:40.494973 Command Completed (socket: True)
89324 23:09:40.495044 Running command ['getSetVariable', 'BB_CONSOLELOG']
89324 23:09:40.495104 Sending reply ('/home/runsisi/working/bmc/openbmc/build/romulus/tmp/log/cooker/romulus/20231229150940.log', None)
89324 23:09:40.495142 Command Completed (socket: True)
89324 23:09:40.495214 Running command ['getSetVariable', 'BB_LOGCONFIG']
89324 23:09:40.495257 Sending reply (None, None)
89324 23:09:40.495303 Command Completed (socket: True)
89324 23:09:40.496214 Running command ['getUIHandlerNum']
89324 23:09:40.496247 Sending reply (1, None)
89324 23:09:40.496287 Command Completed (socket: True)
89324 23:09:40.496373 Running command ['setEventMask', 1, 20, {'BitBake.SigGen.HashEquiv': 19, 'BitBake.RunQueue.HashEquiv': 19}, ['bb.runqueue.runQueueExitWait', 'bb.event.LogExecTTY', 'logging.LogRecord', 'bb.build.TaskFailed', 'bb.build.TaskBase', 'bb.event.ParseStarted', 'bb.event.ParseProgress', 'bb.event.ParseCompleted', 'bb.event.CacheLoadStarted', 'bb.event.CacheLoadProgress', 'bb.event.CacheLoadCompleted', 'bb.command.CommandFailed', 'bb.command.CommandExit', 'bb.command.CommandCompleted', 'bb.cooker.CookerExit', 'bb.event.MultipleProviders', 'bb.event.NoProvider', 'bb.runqueue.sceneQueueTaskStarted', 'bb.runqueue.runQueueTaskStarted', 'bb.runqueue.runQueueTaskFailed', 'bb.runqueue.sceneQueueTaskFailed', 'bb.event.BuildBase', 'bb.build.TaskStarted', 'bb.build.TaskSucceeded', 'bb.build.TaskFailedSilent', 'bb.build.TaskProgress', 'bb.event.ProcessStarted', 'bb.event.ProcessProgress', 'bb.event.ProcessFinished']]
89324 23:09:40.496410 Sending reply (True, None)
89324 23:09:40.496445 Command Completed (socket: True)
89324 23:09:40.496509 Running command ['setConfig', 'cmd', 'listtasks']
89324 23:09:40.496534 Sending reply (None, None)
89324 23:09:40.496566 Command Completed (socket: True)
89324 23:09:40.496630 Running command ['buildTargets', ['bmcweb'], 'listtasks']
89324 23:09:40.496675 Registering idle function <bound method Command.runAsyncCommand of <bb.command.Command object at 0x7f4eeca2f7d0>>
89324 23:09:40.496691 Sending reply (True, None)
89324 23:09:40.496724 Command Completed (socket: True)
89324 23:09:40.558883 Parsing started
89324 23:09:43.279924 Parse cache valid
89324 23:09:44.949872 Registering idle function <function BBCooker.buildTargets.<locals>.buildTargetsIdle at 0x7f4ed6536fc0>
89324 23:09:44.950021 Removing idle function <bound method Command.runAsyncCommand of <bb.command.Command object at 0x7f4eeca2f7d0>>
89324 23:09:46.747136 Removing idle function <function BBCooker.buildTargets.<locals>.buildTargetsIdle at 0x7f4ed6536fc0> at idleFinish
89324 23:09:46.997835 Processing Client
89324 23:09:46.998027 Disconnecting Client (socket: True)
89324 23:09:46.998373 Parse cache invalidated

需要注意的是,bitbake-server 内部显式忽略了 CTRL-C 的处理:

// bitbake/lib/bb/server/process.py

def idle_commands(self, delay, fds=None):
    try:
        return select.select(fds,[],[],nextsleep)[0]
    except InterruptedError:
        # Ignore EINTR
        return []

如需退出需要按两次 CTRL-C

PyCharm pydevd

bitbake-server 其实也不是真正最终干事的,在它后面还有 bitbake-worker 工作进程,由于 Pycharm 和 VSCode 支持自动 attach 子进程,因此在 bitbake-worker 上设置的断点可以自动触发,从而实现 bitbake-server 的全流程调试。

需要注意的是如果系统的 Python 版本是 3.11+ 则稍微有点麻烦,Pycharm 的调试后端 pydevd 需要更新如下补丁(相比 pydevd 上游的补丁稍有修改,因为 pydevd-pycharm 用的是更老一些的版本),否则无法触发子进程的断点:

--- plugins/python/helpers/pydev/_pydev_bundle/pydev_monkey.py.orig     2023-12-30 23:06:12.368331536 +0800
+++ plugins/python/helpers/pydev/_pydev_bundle/pydev_monkey.py  2023-12-30 23:09:39.446791415 +0800
@@ -582,6 +582,37 @@
     return new_warn_fork_exec
 
 
+def create_subprocess_fork_exec(original_name):
+    """
+    subprocess._fork_exec(args, executable_list, close_fds, ... (13 more))
+    """
+
+    def new_fork_exec(args, *other_args):
+        import subprocess
+        args = patch_args(args)
+        send_process_created_message()
+
+        return getattr(subprocess, original_name)(args, *other_args)
+
+    return new_fork_exec
+
+
+def create_subprocess_warn_fork_exec(original_name):
+    """
+    subprocess._fork_exec(args, executable_list, close_fds, ... (13 more))
+    """
+
+    def new_warn_fork_exec(*args):
+        try:
+            import subprocess
+            warn_multiproc()
+            return getattr(subprocess, original_name)(*args)
+        except:
+            pass
+
+    return new_warn_fork_exec
+
+
 def create_CreateProcess(original_name):
     """
     CreateProcess(*args, **kwargs)
@@ -728,6 +759,12 @@
                 monkey_patch_module(_posixsubprocess, 'fork_exec', create_fork_exec)
             except ImportError:
                 pass
+
+            try:
+                import subprocess
+                monkey_patch_module(subprocess, '_fork_exec', create_subprocess_fork_exec)
+            except AttributeError:
+                pass
         else:
             # Windows
             try:
@@ -767,6 +804,12 @@
                 monkey_patch_module(_posixsubprocess, 'fork_exec', create_warn_fork_exec)
             except ImportError:
                 pass
+
+            try:
+                import subprocess
+                monkey_patch_module(subprocess, '_fork_exec', create_subprocess_warn_fork_exec)
+            except AttributeError:
+                pass
         else:
             # Windows
             try:

On Python 3.11 _fork_exec now needs to be monkey-patched for subproceses
https://github.com/fabioz/PyDev.Debugger/commit/2e02c252af89c2cd1056de432f791403f7a00c26

Cannot attach to subprocess when debugging with Python 3.11, 3.12
https://youtrack.jetbrains.com/issue/PY-61217

pydevd-pycharm
https://pypi.org/project/pydevd-pycharm/

bitbake debug

设置 PATH 环境变量加上 bitbake/bin 目录,则不会有如下的警告(不过不加也不影响调试,因为服务端是我们自己手工启动的):

WARNING: bitbake binary is not found in PATH, did you source the script?

OpenBMC 源代码下面的 bitbake, meta 等多个文件夹和文件实际上都是 poky 下文件夹的符号链接:

❯ ll | grep -- '->'
lrwxrwxrwx  1 runsisi runsisi   13 Nov  3 21:21 bitbake -> poky/bitbake/
lrwxrwxrwx  1 runsisi runsisi   12 Nov  3 21:21 LICENSE -> poky/LICENSE
lrwxrwxrwx  1 runsisi runsisi    9 Nov  3 21:21 meta -> poky/meta
lrwxrwxrwx  1 runsisi runsisi   15 Nov  3 21:21 meta-poky -> poky/meta-poky/
lrwxrwxrwx  1 runsisi runsisi   18 Nov  3 21:21 meta-skeleton -> poky/meta-skeleton
lrwxrwxrwx  1 runsisi runsisi   22 Nov  3 21:21 oe-init-build-env -> poky/oe-init-build-env
lrwxrwxrwx  1 runsisi runsisi   13 Nov  3 21:21 scripts -> poky/scripts/

在 PyCharm 中增加 bitbake/bin 以及 bitbake/lib, meta/lib 目录时最好添加 poky 目录下的文件夹,这样断点和调试时的源代码对应关系会显示的更明确一些:

bitbake-server debug

exec task

bb, bbclass 等文件中定义的 python 函数和 shell 函数的执行逻辑如下:

// bitbake/lib/bb/buid.py

def exec_task(fn, task, d, profile = False):
    return _exec_task(fn, task, d, quieterr)

def _exec_task(fn, task, d, quieterr):
    exec_func(task, localdata)

def exec_func(func, d, dirs = None):
    // runfile  temp 目录下的 run.do_XXX.PID 文件
    if ispython:
        exec_func_python(func, d, runfile, cwd=adir)
    else:
        exec_func_shell(func, d, runfile, cwd=adir)

def exec_func_python(func, d, runfile, cwd=None):
    code = _functionfmt.format(function=func)
    with open(runfile, 'w') as script:
        bb.data.emit_func_python(func, script, d)
    //  exec_func_shell 不同生成的 run.do_XXX.PID python 脚本并不是直接运行    // 而是通过 python 的内置函数 `exec` 执行
    comp = utils.better_compile(code, func, "exec_func_python() autogenerated")
    utils.better_exec(comp, {"d": d}, code, "exec_func_python() autogenerated")

def exec_func_shell(func, d, runfile, cwd=None):
    with open(runfile, 'w') as script:
        script.write(shell_trap_code())
        //  func 代码写入之前会将 meta/conf/bitbake.conf 中定义的 export 变量
        //  shell 脚本中 export 出来典型的如 `export PATH`)
        bb.data.emit_func(func, script, d)

    // 直接执行生成的 shell 脚本
    cmd = runfile
    bb.process.run(cmd, shell=False, stdin=stdin, log=logfile, extrafiles=[(fifo,readfifo)])

appendix

测试过程中倒是发现了 bitbake-server 的一个 bug:

diff --git a/poky/bitbake/lib/bb/server/process.py b/poky/bitbake/lib/bb/server/process.py
index d495ac6245..38c86fb2ce 100644
--- a/poky/bitbake/lib/bb/server/process.py
+++ b/poky/bitbake/lib/bb/server/process.py
@@ -373,7 +373,7 @@ class ProcessServer():
         while not lock:
             i = 0
             lock = None
-            if not os.path.exists(os.path.basename(lockfile)):
+            if not os.path.exists(os.path.dirname(lockfile)):
                 serverlog("Lockfile directory gone, exiting.")
                 return

最后修改于 2024-01-02