为何子Make在Python调用时出现jobserver警告,Bash调用时无此问题?
问题场景
我有一个基于CMake ExternalProject的项目,通过BUILD_COMMAND执行脚本调用GNU Make并行构建时遇到了奇怪的警告差异:
- 用Bash脚本调用Make时,在
BUILD_COMMAND末尾添加$(MAKE)参数就能消除jobserver unavailable: using -j1的警告 - 改用Python脚本后,无论是否添加
$(MAKE)参数,警告都会重现;甚至在Bash→Python→Make的调用链中,只要加入Python层就会触发警告
相关代码示例如下:
CMake配置
# demo/CMakeLists.txt cmake_minimum_required(VERSION 3.14.1) project(demo) include(ExternalProject) ExternalProject_Add(demo SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}" CONFIGURE_COMMAND "" BUILD_COMMAND python3 "${CMAKE_CURRENT_SOURCE_DIR}/test.py" $(MAKE) INSTALL_COMMAND "" BUILD_ALWAYS TRUE )
Python脚本
# demo/test.py from pathlib import Path import subprocess as sp SCRIPT_DIR = Path(__file__).resolve().parent sp.run(['make'], cwd=SCRIPT_DIR)
Makefile
# demo/Makefile demo: echo demo
不一致行为的核心原因
这个差异本质是GNU Make的jobserver机制传递逻辑,以及Bash和Python在处理进程继承时的区别:
- GNU Make的jobserver传递规则:
父Make在执行包含$(MAKE)的命令时,会自动将jobserver的控制文件描述符(通常是3、4号)和MAKEFLAGS环境变量传递给子进程,子Make通过这些信息复用父进程的jobserver,实现并行资源协调。 - Bash的默认继承特性:
Bash作为shell进程,会默认继承父进程的所有文件描述符和环境变量。即使Bash脚本里没有用到传递的$(MAKE)参数,父Make因为识别到命令里的$(MAKE),会主动传递jobserver信息,Bash再把这些信息传给内部的Make进程,因此警告消失。 - Python的默认限制:
Python的subprocess.run()默认只继承标准输入、输出、错误三个文件描述符,不会传递jobserver用的额外描述符。哪怕父Make已经把jobserver信息传递给了Python进程,Python也不会自动把这些描述符传给子Make进程,导致子Make无法找到jobserver,触发警告。
添加
$(MAKE)参数的可行性 - 对Bash脚本:完全可行。父Make识别到
$(MAKE)后会自动传递jobserver信息,Bash的继承特性让子Make能拿到这些信息,哪怕脚本里没用到该参数。 - 对Python脚本:不可行。因为Python默认不传递额外文件描述符,父Make传递的jobserver描述符无法到达子Make,加了
$(MAKE)参数也没用。
消除Python脚本调用Make警告的解决方案
方案1:让Python传递jobserver文件描述符
修改Python脚本,解析MAKEFLAGS获取jobserver的文件描述符,在调用Make时通过pass_fds参数传递这些描述符:
from pathlib import Path import subprocess as sp import os SCRIPT_DIR = Path(__file__).resolve().parent # 从MAKEFLAGS中提取jobserver的文件描述符 makeflags = os.environ.get('MAKEFLAGS', '') pass_fds = [] for flag in makeflags.split(): if flag.startswith('--jobserver-fds='): fds = flag.split('=')[1].split(',') pass_fds = [int(fd) for fd in fds] break # 调用Make时传递指定的文件描述符 sp.run(['make'], cwd=SCRIPT_DIR, pass_fds=pass_fds)
同时保留CMakeBUILD_COMMAND中的$(MAKE)参数,确保父Make传递jobserver信息给Python进程。
方案2:在Python中使用$(MAKE)路径调用Make
将Python脚本中的make替换为父Make传递的MAKE环境变量值(即$(MAKE)对应的路径),同时传递jobserver描述符:
from pathlib import Path import subprocess as sp import os SCRIPT_DIR = Path(__file__).resolve().parent # 使用父进程传递的MAKE命令路径 make_cmd = os.environ.get('MAKE', 'make') # 解析jobserver描述符(同方案1) makeflags = os.environ.get('MAKEFLAGS', '') pass_fds = [] for flag in makeflags.split(): if flag.startswith('--jobserver-fds='): fds = flag.split('=')[1].split(',') pass_fds = [int(fd) for fd in fds] break sp.run([make_cmd], cwd=SCRIPT_DIR, pass_fds=pass_fds)
这种方式更贴合GNU Make的子进程处理逻辑,子Make会自动识别jobserver信息。
不推荐方案:强制指定-j参数
如果直接在Python中调用make -j8,会触发-jN forced in submake: disabling jobserver mode警告,这会关闭jobserver机制,导致嵌套构建无法共享并行资源,降低构建效率,仅适用于简单场景。
内容的提问来源于stack exchange,提问作者clpgr
相关产品推荐
相关产品推荐

