前言
从 2.1.2 版开始,MuJoCo 附带使用 pybind11 以 C++ 开发的本地 Python 绑定。Python API 与底层的 C API 保持一致。这导致了一些非 Python 代码结构(如函数参数的顺序),但其好处是 API 文档适用于两种语言。
Python 绑定作为 mujoco 包发布在 PyPI 上。这些都是底层绑定,旨在尽可能直接访问 MuJoCo 库。不过,为了提供开发人员期望在典型 Python 库中使用的 API 和语义,这些绑定在许多地方故意与原始 MuJoCo API 有所不同,本页将对此进行详细记录。
Google DeepMind 的 dm_control 强化学习库(在 1.0.0 版本之前,该库基于 ctypes 实现了自己的 MuJoCo 绑定)已更新为依赖于 mujoco 包,并继续得到 Google DeepMind 的支持。dm_control 中的更改对以前版本的用户来说应该基本透明,但直接依赖于其底层 API 的代码可能需要更新。详情请查阅迁移指南。
对于 mujoco-py 用户,我们在下文中提供了帮助迁移的说明。
一、教程笔记本
这里有使用 Python 绑定的 MuJoCo 教程:colab
二、安装
建议通过 PyPI 安装此软件包:
pip install mujoco
MuJoCo 库的副本作为软件包的一部分提供,无需单独下载或安装。
三、交互式查看器
作为 Python 软件包的一部分,mujoco.viewer 模块提供了一个交互式图形用户界面查看器。它与随 MuJoCo 二进制版本一起发布的仿真应用程序基于相同的代码库。支持三种不同的使用情况:
3.1 独立应用程序
python -m mujoco.viewer
- 会启动一个空的可视化会话,可通过拖放方式加载模型。
python -m mujoco.viewer --mjcf=/path/to/some/mjcf.xml
- 启动指定模型文件的可视化会话。
3.2 托管查看器
通过函数 viewer.launch 从 Python 程序/脚本中调用。该函数会阻塞用户代码,以支持物理循环的精确计时。如果用户代码是作为引擎插件或物理回调实现的,并在 mj_step 期间被 MuJoCo 调用,则应使用此模式。
viewer.launch()
- 会启动一个空的可视化会话,可通过拖放方式加载模型。
viewer.launch(model)
- 为给定的 mjModel 启动一个可视化会话,在该会话中,可视化器会在内部创建自己的 mjData 实例
viewer.launch(model, data)
- 与上述操作相同,但可视化器会直接对给定的 mjData 实例进行操作,退出时数据对象将被修改。
3.3 被动查看器
调用 viewer.launch_passive(model,data)。该函数不会阻塞,允许用户代码继续执行。在此模式下,用户脚本负责计时和推进物理状态,除非用户明确同步传入事件,否则鼠标拖动扰动将不起作用。
警告
在 MacOS 上,launch_passive 要求通过特殊的 mjpython 启动器执行用户脚本。mjpython 命令是作为 mujoco 软件包的一部分安装的,可直接用于替代常用的 python 命令,并支持相同的命令行标志和参数集。例如,脚本可以通过 mjpython my_script.py 执行,IPython shell 可以通过 mjpython -m IPython 启动。
launch_passive 函数返回一个句柄,可用于与查看器交互。它具有以下属性:
- cam、opt 和 pert 属性:分别对应 mjvCamera、mjvOption 和 mjvPerturb 结构。
- lock():为作为上下文管理器的查看器提供一个互斥锁。由于查看器运行自己的线程,用户代码必须确保在修改任何物理或可视化状态之前持有查看器锁。这些属性包括传递给 launch_passive 的 mjModel 和 mjData 实例,以及查看器句柄的 cam、opt 和 pert 属性。
- sync():同步 mjModel、mjData 和 GUI 用户输入自上次调用 sync 后的状态。为了允许用户脚本在无需锁定查看器的情况下对 mjModel 和 mjData 进行任意修改,被动查看器不会在同步调用之外访问或修改这些结构。
- 用户脚本必须调用同步才能使查看器反映物理状态的变化。同步函数还会将用户输入从图形用户界面传回 mjOption(位于 mjModel 内部)和 mjData,包括启用/禁用标志、控制输入和鼠标扰动。
- update_hfield(hfieldid):更新指定 hfieldid 处的高度字段数据,以便后续渲染。
- update_mesh(meshid): 更新指定网格 ID 上的网格数据,以便后续渲染使用。
- update_texture(texid): 更新指定 texid 处的纹理数据,以便后续渲染使用。
- close():以编程方式关闭查看器窗口。该方法可以在不锁定的情况下安全调用。
- is_running():如果查看器窗口正在运行,则返回 True;如果已关闭,则返回 False。该方法可在不加锁的情况下安全调用。
- user_scn:一个 mjvScene 对象,允许用户在渲染场景中添加更改渲染标志和添加自定义可视化地形。它与查看器内部用于渲染最终场景的 mjvScene 是分开的,完全由用户控制。用户脚本可以调用 mjv_initGeom 或 mjv_makeConnector 等脚本向 user_scn 添加可视化几何图形,在下一次调用 sync() 时,查看器就会将这些几何图形添加到未来渲染的图像中。同样,用户脚本也可以更改 user_scn.flags,这些更改将在下一次调用 sync() 时被接收。同步()调用还会将通过图形用户界面对渲染标志所做的更改复制到 user_scn 中,以保持一致性。例如
with mujoco.viewer.launch_passive(m, d, key_callback=key_callback) as viewer: # Enable wireframe rendering of the entire scene. viewer.user_scn.flags[mujoco.mjtRndFlag.mjRND_WIREFRAME] = 1 viewer.sync() while viewer.is_running(): ... # Step the physics. mujoco.mj_step(m, d) # Add a 3x3x3 grid of variously colored spheres to the middle of the scene. viewer.user_scn.ngeom = 0 i = 0 for x, y, z in itertools.product(*((range(-1, 2),) * 3)): mujoco.mjv_initGeom( viewer.user_scn.geoms[i], type=mujoco.mjtGeom.mjGEOM_SPHERE, size=[0.02, 0, 0], pos=0.1*np.array([x, y, z]), mat=np.eye(3).flatten(), rgba=0.5*np.array([x + 1, y + 1, z + 1, 2]) ) i += 1 viewer.user_scn.ngeom = i viewer.sync() ...
查看器句柄还可用作上下文管理器,在退出时自动调用 close()。使用 launch_passive 的用户脚本的最小示例如下。(请注意,该示例只是一个简单的说明性示例,并不一定能使物理程序以正确的挂钟速度运行)。
import time import mujoco import mujoco.viewer m = mujoco.MjModel.from_xml_path('/path/to/mjcf.xml') d = mujoco.MjData(m) with mujoco.viewer.launch_passive(m, d) as viewer: # Close the viewer automatically after 30 wall-seconds. start = time.time() while viewer.is_running() and time.time() - start < 30: step_start = time.time() # mj_step can be replaced with code that also evaluates # a policy and applies a control signal before stepping the physics. mujoco.mj_step(m, d) # Example modification of a viewer option: toggle contact points every two seconds. with viewer.lock(): viewer.opt.flags[mujoco.mjtVisFlag.mjVIS_CONTACTPOINT] = int(d.time % 2) # Pick up changes to the physics state, apply perturbations, update options from GUI. viewer.sync() # Rudimentary time keeping, will drift relative to wall clock. time_until_next_step = m.opt.timestep - (time.time() - step_start) if time_until_next_step > 0: time.sleep(time_until_next_step)
作为选项,viewer.launch_passive 接受以下关键字参数。
- key_callback: 每次查看器窗口中发生键盘事件时都会被调用的可调用函数。这允许用户脚本对各种按键做出反应,例如,按下空格键时暂停或恢复运行循环。
paused = False def key_callback(keycode): if chr(keycode) == ' ': nonlocal paused paused = not paused ... with mujoco.viewer.launch_passive(m, d, key_callback=key_callback) as viewer: while viewer.is_running(): ... if not paused: mujoco.mj_step(m, d) viewer.sync() ...
- show_left_ui 和 show_right_ui: 布尔参数,用于指示启动查看器时用户界面面板是可见还是隐藏。请注意,无论指定的值是多少,用户仍可在启动后通过按 Tab 或 Shift+Tab 键切换这些面板的可见性。
四、基本用法
安装后,可通过 import mujoco 导入软件包。结构体、函数、常量和枚举可直接从顶层的 mujoco 模块中获取。
4.1 结构体
绑定包括暴露 MuJoCo 数据结构的 Python 类。为了获得最佳性能,这些类提供了对 MuJoCo 所用原始内存的访问,而无需复制或缓冲。这意味着某些 MuJoCo 函数(如 mj_step)会在原处更改字段的内容。因此,建议用户在需要时创建副本。例如,在记录正文的位置时,可以写入 positions.append(data.body('my_body').xpos.copy())。如果没有 .copy(),列表中将包含完全相同的元素,所有元素都指向最近的值。
为了符合 PEP 8 命名指南,结构体名称以大写字母开头,例如 mjData 在 Python 中就变成了 mujoco.MjData。
除 mjModel 之外的所有结构体在 Python 中都有构造函数。对于有 mj_defaultFoo 样式初始化函数的结构体,Python 构造函数会自动调用默认初始化函数,例如 mujoco.MjOption() 会创建一个新的 mjOption 实例,该实例已预先用 mj_defaultOption 初始化。否则,Python 构造函数将零初始化底层 C 结构。
具有 mj_makeFoo 风格初始化函数的结构体在 Python 中有相应的构造函数重载,例如 Python 中的 mujoco.MjvScene(model, maxgeom=10) 会创建一个新的 mjvScene 实例,在 C 语言中用 mjv_makeScene(model, [the new mjvScene instance], 10) 进行初始化。使用这种形式的初始化时,删除 Python 对象时会自动调用相应的去分配函数 mj_freeFoo/mj_deleteFoo。用户无需手动释放资源。
mujoco.MjModel 类没有 Python 构造函数。相反,我们提供了三个静态工厂函数来创建新的 mjModel 实例:mujoco.MjModel.from_xml_string、mujoco.MjModel.from_xml_path 和 mujoco.MjModel.from_binary_path。第一个函数接受 XML 模型字符串,后两个函数接受 XML 或 MJB 模型文件的路径。所有这三个函数都可选择接受一个 Python 字典,该字典会被转换成一个 MuJoCo 虚拟文件系统,供模型编译时使用。
4.2 函数
MuJoCo 函数与 Python 函数同名。与结构体不同,我们并不试图使函数名符合 PEP 8 标准,因为 MuJoCo 同时使用下划线和 CamelCases。在大多数情况下,函数参数与 C 语言中的参数完全相同,关键字参数也支持与 mujoco.h 中声明的相同名称。接受数组输入参数的 C 函数的 Python 绑定需要 NumPy 数组或可转换为 NumPy 数组的迭代对象(如列表)。输出参数(即 MuJoCo 希望将值写回给调用者的数组参数)必须始终是可写的 NumPy 数组。
在 C API 中,将动态大小数组作为输入的函数需要一个指向数组的指针参数和一个指定数组大小的整数参数。在 Python 中,由于我们可以从 NumPy 数组中自动(而且更安全地)推导出大小参数,因此省略了大小参数。调用这些函数时,除了数组大小之外的所有参数都要按照 mujoco.h 中的顺序传递,或者使用关键字参数。例如,mj_jac 在 Python 中应调用为 mujoco.mj_jac(m,d,jacp,jacr,point,body)。
绑定会在调用底层 MuJoCo 函数之前释放 Python 全局解释器锁(GIL)。这允许一些基于线程的并行性,但用户应注意,GIL 只在 MuJoCo C 函数本身的持续时间内释放,而不会在任何其他 Python 代码执行期间释放。
注意事项
绑定提供附加功能的一个地方是顶层的 mj_step 函数。由于它经常在循环中被调用,我们增加了一个额外的 nstep 参数,指示底层 mj_step 应被调用多少次。如果没有指定,nstep 的默认值为 1。以下两个代码片段执行了相同的计算,但第一个代码片段在后续物理步骤之间没有获取 GIL:
mj_step(model, data, nstep=20)
for _ in range(20): mj_step(model, data)
4.3 枚举和常量
MuJoCo 枚举以 mujoco.mjtEnumType.ENUM_VALUE 的形式提供,例如 mujoco.mjtObj.mjOBJ_SITE。MuJoCo 常量可直接在 mujoco 模块下以相同名称使用,例如 mujoco.mjVISSTRING。
4.4 最小示例
import mujoco XML=r""" <mujoco> <asset> <mesh file="gizmo.stl"/> </asset> <worldbody> <body> <freejoint/> <geom type="mesh" name="gizmo" mesh="gizmo"/> </body> </worldbody> </mujoco> """ ASSETS=dict() with open('/path/to/gizmo.stl', 'rb') as f: ASSETS['gizmo.stl'] = f.read() model = mujoco.MjModel.from_xml_string(XML, ASSETS) data = mujoco.MjData(model) while data.time < 1: mujoco.mj_step(model, data) print(data.geom_xpos)
4.5 命名访问
大多数设计良好的 MuJoCo 模型都会为感兴趣的对象(关节、几何体、主体等)指定名称。当模型被编译成 mjModel 实例时,这些名称就会与数字 ID 相关联,这些数字 ID 用于索引各种数组成员。为了方便和提高代码可读性,Python 绑定为 MjModel 和 MjData 提供了 "命名访问 "API。mjModel 结构中的每个 name_fooadr 字段都定义了一个名称类别 foo。
对于每个名称类别 foo,mujoco.MjModel 和 mujoco.MjData 对象都提供了一个方法 foo,该方法接收一个字符串参数,并返回与给定名称的实体 foo 相对应的所有数组的访问器对象。访问器对象包含的属性名称与 mujoco.MjModel 或 mujoco.MjData 的字段相对应,但去掉了下划线前的部分。此外,访问器对象还提供 id 和 name 属性,可分别用于替换 mj_name2id 和 mj_id2name。例如
- m.geom('gizmo') 返回 MjModel 对象 m 中与名为 "gizmo "的 geom 相关联的数组的访问器。
- m.geom('gizmo').rgba 是一个长度为 4 的 NumPy 数组视图,用于指定 geom 的 RGBA 颜色。具体来说,它对应 m.geom_rgba[4*i:4*i+4] 中的部分,其中 i = mujoco.mj_name2id(m,mujoco.mjtObj.mjOBJ_GEOM,'gizmo')。
- m.geom('gizmo').id 与 mujoco.mj_name2id(m, mujoco.mjtObj.mjOBJ_GEOM, 'gizmo')返回的数字相同。
- m.geom(i).name 为 "gizmo",其中 i = mujoco.mj_name2id(m,mujoco.mjtObj.mjOBJ_GEOM,'gizmo')。
此外,Python API 还为某些名称类别定义了一些别名,这些别名与 MJCF 模式中定义该类别实体的 XML 元素名称相对应。例如,m.joint('foo') 与 m.jnt('foo') 相同。这些别名的完整列表如下。
关节的访问器与其他类别的访问器有些不同。一些 mjModel 和 mjData 字段(大小为 nq 或 nv 的字段)与自由度 (DoF) 而不是关节相关联。这是因为不同类型的关节具有不同数量的自由度。尽管如此,我们还是会通过 d.joint('foo').qpos 和 d.joint('foo').qvel 等方式,将这些字段与相应的关节关联起来,但这些数组的大小会因关节类型的不同而在不同的访问器中有所不同。
命名访问保证与模型中实体的数量成 O(1)关系。换句话说,通过名称访问实体所需的时间不会随着模型中名称或实体数量的增加而增加。
为完整起见,我们在此提供了 MuJoCo 中所有名称类别的完整列表,以及它们在 Python API 中定义的相应别名。
body
jnt
orjoint
geom
site
cam
orcamera
light
mesh
skin
hfield
tex
ortexture
mat
ormaterial
pair
exclude
eq
orequality
tendon
orten
actuator
sensor
numeric
text
tuple
key
orkeyframe
4.6 渲染
MuJoCo 本身希望用户在调用其 mjr_ 渲染例程之前,先设置一个可用的 OpenGL 上下文。Python 绑定提供了一个基本类 mujoco.GLContext,帮助用户为离屏渲染设置这样一个上下文。要创建上下文,请调用 ctx = mujoco.GLContext(max_width,max_height)。创建上下文后,必须在调用 MuJoCo 渲染函数前将其设置为当前,可以通过 ctx.make_current() 来实现。请注意,在任何时候都只能在一个线程上将上下文设置为当前状态,而且所有后续的渲染调用都必须在同一线程上进行。
删除 ctx 对象时,上下文会被自动释放,但在某些多线程情况下,可能需要显式释放底层 OpenGL 上下文。为此,请调用 ctx.free(),之后用户有责任确保不再对上下文进行渲染调用。
一旦创建了上下文,用户就可以按照 MuJoCo 的标准进行渲染,例如可视化部分所记录的渲染。
4.7 错误处理
MuJoCo 通过 mju_error 机制报告不可恢复的错误,并立即终止整个进程。允许用户通过 mju_user_error 回调安装自定义错误处理程序,但它也应终止进程,否则回调返回后 MuJoCo 的行为将是未定义的。实际上,只要确保错误回调不返回 MuJoCo 就足够了,但允许使用 longjmp 跳过 MuJoCo 的调用栈返回外部调用站点。
Python 绑定利用 longjmp 允许它将不可恢复的 MuJoCo 错误转换为 mujoco.FatalError 类型的 Python 异常,这种异常可以用通常的 Pythonic 方式捕获和处理。此外,它还使用当前私有的 API 以线程本地的方式安装了错误回调,从而允许从多个线程并发调用 MuJoCo。
4.8 回调函数
MuJoCo 允许用户安装自定义回调函数,以修改其计算管道的某些部分。例如,mjcb_sensor 可用来实现自定义传感器,mjcb_control 可用来实现自定义执行器。回调通过 mujoco.h 中以 mjcb_ 为前缀的函数指针进行公开。
对于每个回调 mjcb_foo,用户可以通过 mujoco.set_mjcb_foo(some_callable) 将其设置为 Python 可调用函数。要重置它,请调用 mujoco.set_mjcb_foo(None)。要检索当前已安装的回调,请调用 mujoco.get_mjcb_foo()(如果没有通过 Python 绑定安装回调,则不应使用 getter)。每次进入回调时,绑定都会自动获取 GIL,并在重新进入 MuJoCo 之前释放它。这可能会对性能造成严重影响,因为回调会在整个 MuJoCo 的计算管道中多次触发,因此不太适合 "生产 "用例。不过,预计这一功能将有助于复杂模型的原型设计。
另外,如果回调是在本地动态库中实现的,用户可以使用 ctypes 获取指向 C 函数指针的 Python 句柄,并将其传递给 mujoco.set_mjcb_foo。然后,绑定将检索底层函数指针,并将其直接赋值给原始回调指针,而且每次输入回调时都不会获取 GIL。
五、开环滚动
我们提供了一个代码示例,展示如何通过 pybind11 以 Python 模块的形式添加额外的 C/C++ 功能。该示例在 rollout.cc 中实现,并封装在 rollout.py 中,实现了一个在 Python 之外实现紧密循环的常见用例:在给定初始状态和控制序列的情况下,推出轨迹(即在循环中调用 mj_step()),并返回后续状态和传感器值。基本使用形式为
state, sensordata = rollout.rollout(model, data, initial_state, control)
initial_state 是一个 nroll x nstate 数组,包含 nroll 个大小为 nstate 的初始状态,其中 nstate = mj_stateSize(model,mjtState.mjSTATE_FULLPHYSICS)是完整物理状态的大小。 control 是一个 nroll x nstep x ncontrol 的控制数组。默认情况下,控制是 mjModel.nu 标准执行器,但也可以通过传递可选的 control_spec bitflag 来指定用户输入数组的任意组合。
如果滚动出现偏离,当前状态和传感器值将用于填充轨迹的剩余部分。因此,非递增时间值可用于检测发散滚动。
滚动功能设计为完全无状态,因此步进流水线的所有输入都已设置,给定 MjData 实例中已存在的任何值都不会对输出产生影响。
由于全局解释器锁可以释放,因此该函数可以使用 Python 线程进行高效线程化。请参阅 rollout_test.py 中的 test_threading 函数,了解线程操作的示例(以及更广泛的使用示例)。
六、从 mujoco-py 移植
在 mujoco-py 中,主要入口点是 MjSim 类。用户通过 MJCF 模型(类似于 dm_control.Physics)构建一个有状态的 MjSim 实例,该实例持有对 mjModel 实例及其相关 mjData 的引用。相比之下,如上所述,MuJoCo Python 绑定(mujoco)采用了更底层的方法:遵循 C 库的设计原则,mujoco 模块本身是无状态的,只是封装了底层的本地结构和函数。
本文档无法对 mujoco-py 进行全面介绍,但我们在下文中提供了一些实现说明,这些说明并不是 mujoco-py 具体功能的详尽列表:
- mujoco_py.load_model_from_xml(bstring):该工厂函数构建了一个有状态的 MjSim 实例。使用 mujoco 时,用户应调用上述工厂函数 mujoco.MjModel.from_xml_*。然后,用户负责保存生成的 MjModel 结构实例,并通过调用 mujoco.MjData(model) 明确生成相应的 MjData。
- sim.reset(), sim.forward(), sim.step():同上,mujoco 用户需要调用底层库函数,并传递 MjModel 和 MjData 实例:mujoco.mj_resetData(model, data)、mujoco.mj_forward(model, data) 和 mujoco.mj_step(model, data)。
- sim.get_state()、sim.set_state(state)、sim.get_flattened_state()、sim.set_state_from_flattened(state):mujoco-py 实现了获取和设置某些相关字段的方法(类似地,dm_control.Physics 提供了与扁平化情况相对应的方法)。Mujoco 没有提供这种抽象,用户需要明确地获取/设置相关字段的值。
- sim.model.get_joint_qvel_addr(joint_name):这是 mujoco-py 中的一个便利方法,用于返回与此关节相对应的连续索引列表。该列表从 model.jnt_qposadr[joint_index] 开始,其长度取决于关节类型。mujoco 不提供此功能,但可以使用 model.jnt_qposadr[joint_index] 和 xrange 轻松构建此列表。
- sim.model.*_name2id(name):mujoco-py 在 MjSim 中创建了 dicts,可以高效查找不同类型对象的索引:site_name2id、body_name2id 等。这些函数取代了 mujoco.mj_name2id(model, type_enum, name) 函数。mujoco 为使用实体名称提供了一种不同的方法--命名访问以及本地 mj_name2id 访问。
- sim.save(fstream, format_name):在这种情况下,MuJoCo 库(因此也包括 mujoco)是有状态的:它在内存中保存了最后编译的 XML 的副本,该副本在 mujoco.mj_saveLastXML(fname) 中使用。请注意,mujoco-py 的实现有一个方便的额外功能,即姿势(由 sim.data 的状态决定)会转换为关键帧,在保存前添加到模型中。而 mujoco 目前还没有这项额外功能。
七、从源构建
注意
只有在修改 Python 绑定(或试图在特别老的 Linux 系统上运行)时,才有必要从源代码构建。如果不是这种情况,我们建议从 PyPI 安装预编译的二进制文件。
- 确保已安装 CMake 和 C++17 编译器。
- 从 GitHub 下载最新的二进制版本。在 macOS 上,下载对应的是一个 DMG 文件,你可以将 MuJoCo.app 拖入你的 /Applications 文件夹。
- 从 GitHub 克隆整个 mujoco 代码库,然后 cd 进入 python 目录:
git clone https://github.com/google-deepmind/mujoco.git cd mujoco/python
- 创建虚拟环境:
python3 -m venv /tmp/mujoco source /tmp/mujoco/bin/activate
- 使用 make_sdist.sh 脚本生成源代码发布压缩包。
cd python bash make_sdist.sh
- make_sdist.sh 脚本会生成构建绑定所需的其他 C++ 头文件,并将 Python 目录之外的其他版本库中的所需文件拉入 sdist。完成后,脚本将创建一个包含 mujoco-x.y.z.tar.gz 文件(其中 x.y.z 是版本号)的 dist 目录。
- 使用生成的源代码分发版构建并安装绑定。你需要在 MUJOCO_PATH 环境变量中指定之前下载的 MuJoCo 库的路径。
注释
对于 macOS,这可以是包含 mujoco.framework 的目录路径。特别是,如果按照步骤 1 中的建议安装了 MuJoCo,可以设置 MUJOCO_PATH=/Applications/MuJoCo.app。
cd dist MUJOCO_PATH=/PATH/TO/MUJOCO MUJOCO_PLUGIN_PATH=/PATH/TO/MUJOCO_PLUGIN pip install mujoco-x.y.z.tar.gz
- 现在应该已经安装了 Python 绑定!要检查它们是否已成功安装,请跳出 mujoco 目录并运行 python -c "import mujoco"。
提示
作为参考,可在 GitHub 上的 MuJoCo 持续集成设置中找到可行的构建配置。