import argparse
import configparser
from dataclasses import dataclass
import getpass
import glob
import logging
from multiprocessing import Process
import os
from pathlib import Path
import re
import shlex
import shutil
import subprocess
import sys
import sysconfig
import tempfile
from typing import Tuple
from exceptions import ManifestException
HTTPS_PROXY_STR = 'https_proxy'
BINGO_DIST_NAME = 'openubmc-bingo'
HW_PYPI_INDEX = 'https://mirrors.huaweicloud.com/repository/pypi/simple'
DEFAULT_COMMAND_TIMEOUT = 600
@dataclass(frozen=True)
class PlatformSpec:
"""系统分支描述:init 仅识别系统(os-release ID),不校验版本/SP/架构。
定位为 bootstrap 冷启动:把全新环境带到"bingo 自身可用",此后 bingo 已
覆盖的流程(deps/SDK/升级)由 bingo 全权管理,init 不假设任何 bingo
版本行为(兼容性不依赖版本号门槛拦截用户手动回退)。各阶段按本表分派:
- 'legacy-full'(Ubuntu):维持既有完整链——deb 源配置(apt_config)、
bingo deb 包安装、Conan/SDK/QEMU 等原行为全部保留,仅 Studio 移出;
- 'minimal'(openEuler):冷启动最小集——dnf Bootstrap + pip 平台
Wheel 安装 bingo,不执行 deps install 与 install_sdk(归 bingo 管理)。
支持的验证基线(Ubuntu 24.04 x86_64 / openEuler 24.03 LTS-SP4 aarch64)
在 README 声明;版本/SP/架构差异不在此拦截,交给下游(pip 选 Wheel、
制品校验)自然暴露,避免 init 耦合架构。
"""
os_id: str
package_manager: str
bootstrap_pkgs: Tuple[str, ...]
bootstrap_mode: str
UBUNTU = PlatformSpec(
os_id='ubuntu',
package_manager='apt',
bootstrap_pkgs=(
'python3-pip',
'python3-dev',
'ca-certificates',
'gcc',
'g++',
'pkgconf',
'libdbus-1-dev',
'libglib2.0-dev',
'git',
),
bootstrap_mode='legacy-full',
)
OPENEULER = PlatformSpec(
os_id='openeuler',
package_manager='dnf',
bootstrap_pkgs=(
'python3-pip',
'python3-devel',
'ca-certificates',
'gcc',
'gcc-c++',
'pkgconfig',
'dbus-devel',
'glib2-devel',
'sudo',
'git',
'wget',
'nodejs',
'npm',
'lua',
'lua-devel',
'luarocks',
),
bootstrap_mode='minimal',
)
PLATFORM_SPECS = (UBUNTU, OPENEULER)
class Task(Process):
@staticmethod
def log_init():
logger = logging.getLogger()
logger.setLevel(logging.INFO)
loglevel = os.environ.get('LOG')
if loglevel is not None:
formatter = logging.Formatter(
fmt='[{asctime} {levelname} {filename}:{lineno} {funcName:4}] {message}', style='{'
)
if loglevel == 'debug':
logger.setLevel(logging.DEBUG)
elif loglevel == 'warn':
logger.setLevel(logging.WARNING)
elif loglevel == 'error':
logger.setLevel(logging.ERROR)
else:
formatter = logging.Formatter(fmt='[{levelname}] {message}', style='{')
handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(formatter)
logger.handlers = []
logger.addHandler(handler)
@staticmethod
def format_command(command, sudo=False):
cmd_args: list = []
if isinstance(command, list):
cmd_args = command
elif isinstance(command, str):
cmd_args = shlex.split(command)
else:
raise ManifestException('Command with invalid instance')
cmd_args[0] = shutil.which(cmd_args[0])
if cmd_args[0] is None:
raise ManifestException(f'{cmd_args}不存在,请检查命令或环境配置')
sudo_cmd = shutil.which('sudo')
if sudo and sudo_cmd and cmd_args[0] != sudo_cmd:
cmd_args.insert(0, sudo_cmd)
return cmd_args
def chdir(self, path):
logging.info(f'chdir: {path}')
os.chdir(path)
def run_command(self, command, stdout='', check=True, sudo=False, timeout=DEFAULT_COMMAND_TIMEOUT, **kwargs):
"""执行子进程命令。
timeout=None 表示同步等待直至命令自然结束(供 SDK 安装等长时任务
使用,避免 600 秒默认值误杀);其余调用点不传该参数,保持原默认行为。
"""
command_echo = kwargs.get('command_echo', True)
capture_output = kwargs.get('capture_output', False)
if command_echo:
logging.info(f'>>>> {command}')
command = self.format_command(command, sudo)
if stdout:
return subprocess.run(command, check=check, timeout=timeout, stdout=stdout)
else:
return subprocess.run(command, check=check, timeout=timeout, capture_output=capture_output)
class WorkPrepare(Task):
def __init__(self):
super().__init__()
parser = argparse.ArgumentParser(
prog='openUBMC Development environment initialization', formatter_class=argparse.RawTextHelpFormatter
)
parser.add_argument(
'-b', '--board_name', help='构建类型,支持openUBMC或oee,默认为openUBMC', required=False, default='openUBMC'
)
parser.add_argument('-user', '--openubmc_user', help='openUBMC社区用户名', required=False, default='')
parser.add_argument(
'-psw',
'--openubmc_password',
help='openUBMC社区用户密码,推荐使用交互式输入密码',
required=False,
default='',
)
parser.add_argument('--no_build', action='store_true', help='不进行sdk初始化')
parsed_args, _ = parser.parse_known_args(sys.argv)
self.home = os.environ['HOME']
self.cwd = os.getcwd()
path = os.environ.get('PATH')
user_bin_path = f'{self.home}/.local/bin'
self.need_add_user_bin_path = False
if self.home.startswith('/home') and user_bin_path not in path:
self.need_add_user_bin_path = True
path = f'{user_bin_path}:' + path
os.environ['PATH'] = path
self.board_name = parsed_args.board_name
self.user = parsed_args.openubmc_user
self.password = parsed_args.openubmc_password
self.no_build = parsed_args.no_build
self.spec = None
self.stage = 'start'
self.summary = {'installed': [], 'skipped': []}
def enter_stage(self, stage):
"""登记当前执行阶段,供失败摘要定位。"""
self.stage = stage
def record_installed(self, item):
self.summary['installed'].append(item)
def record_skipped(self, item):
self.summary['skipped'].append(item)
@staticmethod
def read_os_release(path='/etc/os-release'):
"""解析 os-release 的机器可读字段,值去除单双引号。
openEuler 的 ID 实测为驼峰 "openEuler",统一转小写后再参与匹配。
"""
info = {}
try:
with open(path, encoding='utf-8') as fq:
for line in fq:
if '=' not in line:
continue
key, value = line.strip().split('=', 1)
info[key] = value.strip('\'"')
except OSError:
pass
return info
def detect_system(self):
"""在修改系统前识别系统类型(仅 os-release ID),命中分支即继续。
不校验版本/SP/架构:init 只做系统分支选择(不同阶段进入不同分支),
架构与版本适配交给下游(pip 按宿主架构选 Wheel、制品校验)自然暴露;
支持基线在 README 声明(openEuler 当前验证 24.03 LTS-SP4 aarch64)。
"""
if os.environ.get('SUDO_USER') is not None:
raise ManifestException('请直接执行,不支持以sudo环境执行脚本')
os_info = self.read_os_release()
if not os_info:
raise ManifestException('无法从 /etc/os-release 获取系统信息,无法识别操作系统')
os_id = os_info.get('ID', '').lower()
for spec in PLATFORM_SPECS:
if os_id == spec.os_id:
self.spec = spec
logging.info(f'检测到支持的系统: {spec.os_id}(包管理器: {spec.package_manager})')
return spec
supported = '、'.join(spec.os_id for spec in PLATFORM_SPECS)
raise ManifestException(f'不支持的操作系统: {os_id!r};当前支持: {supported}(支持基线见 README)')
def bootstrap_packages(self):
"""安装最小 Bootstrap 依赖。
legacy-full 复用既有全量安装链保持 Ubuntu 存量场景行为不变;minimal 使用
清单 + 平台包管理器,并在安装前做 sudo 可用性预检(openEuler 基础
系统实测不带 sudo)。
"""
if self.spec.bootstrap_mode == 'legacy-full':
self.ubuntu_init()
self.install_dependencies()
self.record_installed('bootstrap: apt-get(既有依赖集)')
return
if os.getuid() != 0 and shutil.which('sudo') is None:
raise ManifestException('当前非 root 且系统未安装 sudo,请先执行: dnf install -y sudo(或使用 root 运行)')
packages = ' '.join(self.spec.bootstrap_pkgs)
self.run_command(f'{self.spec.package_manager} install -y {packages}', sudo=(os.getuid() != 0))
self.record_installed(f'bootstrap: {self.spec.package_manager}({len(self.spec.bootstrap_pkgs)} 个最小依赖)')
def install_bingo(self):
"""冷启动安装 bingo,按系统分支:
- Ubuntu(legacy-full):既有 deb 链原样——apt 源探测与配置
(apt_config)、`apt install openubmc-bingo` deb 包安装、pip 兜底
升级,与 pip 安装 conan 的原顺序一致(存量场景行为不变);
- openEuler(minimal):pip 平台 Wheel(归一 PEP 668 探测)。
"""
if self.spec.bootstrap_mode == 'legacy-full':
self.install_bingo_legacy_apt()
return
self.pip_install_bingo()
def install_bingo_legacy_apt(self):
"""Ubuntu 既有 bingo 安装链(行为原样保留:deb 源 + deb 包 + pip)。"""
stop_ret = self.run_command('apt policy openubmc-bingo', capture_output=True)
if len(stop_ret.stdout) == 0:
self.apt_config()
try:
self.run_command('pip install conan>=2.0.0')
self.run_command('apt install -y openubmc-bingo --upgrade', sudo=True)
self.run_command('pip install openubmc-bingo --upgrade')
except Exception:
logging.info('openubmc-bingo下载失败,请检查apt源是否配置成功')
self.record_installed('bingo(apt deb 源 + deb 包 + pip 升级,既有链)')
def bingo_requirement(self):
"""从 .bmcgo/config 的 [bingo] version 解析 pip requirement。
该配置是 manifest 运行兼容约束的唯一来源,不在 init.py 中维护第二份版本。
"""
cp = configparser.ConfigParser()
cp.read(os.path.join(self.cwd, '.bmcgo', 'config'), encoding='utf-8')
raw = cp.get('bingo', 'version', fallback='').strip()
requirement = raw.strip('[]').strip()
if not requirement:
raise ManifestException(f'无法从 .bmcgo/config 解析 [bingo] version,读取值: {raw!r}')
return requirement
def pip_install_bingo(self):
"""openEuler 冷启动:pip 安装 Bingo 平台 Wheel(归一方式)。
是否追加 --break-system-packages 由运行时探测 EXTERNALLY-MANAGED
标记决定(实测 Ubuntu 24.04 存在、openEuler 24.03 不存在),不按
发行版硬编码;平台 Wheel 的架构选择交给 pip 按宿主架构完成——架构
不匹配时 pip 直接失败,即冷启动的自然边界。
"""
requirement = f'{BINGO_DIST_NAME}{self.bingo_requirement()}'
command = [sys.executable, '-m', 'pip', 'install', '--upgrade', requirement, '--index-url', HW_PYPI_INDEX]
pep668 = Path(sysconfig.get_path('stdlib')) / 'EXTERNALLY-MANAGED'
if pep668.exists():
command.append('--break-system-packages')
logging.info('检测到 PEP 668 外部托管环境,pip 追加 --break-system-packages')
self.run_command(command)
self.record_installed(f'bingo(pip 安装,{requirement})')
def verify_bingo(self):
"""冷启动完成判据(轻量):bingo CLI 可运行。
深度安装完整性(payload/packages marker、signer 架构)属 Bingo 自身
范畴且涉及其内部布局,init 不硬编码复核;端到端环境可用性由 bingo
覆盖的流程(deps/SDK/构建)自行验证。子进程执行 bingo CLI——其
元数据可见性与安装进程无关。
"""
self.run_command('bingo --version')
logging.info('bingo CLI 可运行(冷启动完成判据)')
def install_sdk(self):
"""调用 Bingo 安装 bmc_sdk:不设外层超时,失败上抛(不降级为 warning)。"""
logging.info('Start installing sdk:')
self.chdir(self.cwd)
self.run_command(f'bingo build -t install_sdk -b {self.board_name} -ucc', timeout=None)
self.record_installed(f'bmc_sdk: board={self.board_name}')
def install_qemu_chain(self):
"""Ubuntu 既有 QEMU 安装链(原行为保留)。"""
logging.info('开始安装qemu的deb包和qemu依赖环境: ')
self.install_qemu_compile_dependencies()
self.install_qemu_runtime_dependencies()
self.install_qemu()
self.record_installed('QEMU 环境')
def install_dev_tools(self):
"""pre-commit 工具(node/stylua/luacheck)两平台必装。
node/lua 工具是提交代码 pre-commit 钩子的依赖,属开发环境必备
(bootstrap 性质);Studio 两平台均不自动安装(README 手工指引)。
"""
self.install_node()
self.install_lua_tools()
self.record_installed('pre-commit 工具: node/stylua/luacheck')
self.record_skipped('BMC Studio: 已移出默认初始化,请参考 README 手工安装指引')
def print_summary(self):
installed = '、'.join(self.summary['installed']) or '无'
skipped = '、'.join(self.summary['skipped']) or '无'
logging.info('========== 初始化摘要 ==========')
logging.info(f'系统: {self.spec.os_id}({self.spec.bootstrap_mode})')
logging.info(f'board: {self.board_name}')
logging.info(f'installed: {installed}')
logging.info(f'skipped: {skipped}')
logging.info('================================')
def print_failure_summary(self, exc):
"""失败摘要:失败阶段、已完成项与重试建议,不输出笼统的失败信息。"""
installed = '、'.join(self.summary['installed']) or '无'
logging.error('========== 初始化失败 ==========')
logging.error(f'失败阶段: {self.stage}')
logging.error(f'已完成: {installed}')
logging.error(f'原因: {exc}')
logging.error('建议: 修复上述问题后重新执行 python3 init.py(各阶段可安全重跑)')
logging.error('================================')
def proxy_check(self):
https_proxy = os.environ.get(HTTPS_PROXY_STR)
if https_proxy:
logging.warning('检测到环境设置了https_proxy代理,可能导致初始化失败,请确认是否关闭代理运行?(Y/N)')
if input().upper() == 'Y':
os.environ[HTTPS_PROXY_STR] = ''
os.environ[HTTPS_PROXY_STR.upper()] = ''
def pip_config(self):
"""pip 镜像统一华为云;两平台共用(openEuler 无 PEP 668 标记,相关配置无副作用)。
pip config set 恒要求恰好 name/value 两个位置参数(值不能内嵌引号,
否则 pip 报 unexpected number of arguments)。
"""
logging.info('Start configuring pip:')
pip = [sys.executable, '-m', 'pip', 'config', 'set']
self.run_command(pip + ['global.index-url', HW_PYPI_INDEX])
self.run_command(pip + ['global.trusted-host', 'mirrors.huaweicloud.com'])
pep668 = Path(sysconfig.get_path('stdlib')) / 'EXTERNALLY-MANAGED'
if pep668.exists():
self.run_command(pip + ['global.break-system-packages', 'true'])
def ubuntu_init(self):
self.run_command('ln -snf /bin/bash /bin/sh', sudo=True)
logging.info('Initial ubuntu env')
self.run_command(
f"sed -i 's@/archive.ubuntu.com@/mirrors.huaweicloud.com@g' /etc/apt/sources.list.d/ubuntu.sources",
sudo=True,
check=False,
)
self.run_command(
f"sed -i 's@/security.ubuntu.com@/mirrors.huaweicloud.com@g' /etc/apt/sources.list.d/ubuntu.sources",
sudo=True,
check=False,
)
self.run_command(
f"sed -i 's@/azure.mirrors.huaweicloud.com@/azure.archive.ubuntu.com@g' "
f'/etc/apt/sources.list.d/ubuntu.sources',
sudo=True,
check=False,
)
self.run_command('apt-get update', sudo=True)
def install_dependencies(self):
logging.info('Start installing the dependencies:')
self.run_command(
f'apt-get install -y python3 python3-pip python3-dev git wget curl ca-certificates \
ipmitool dbus-x11 libdbus-1-dev dbus \
pkgconf libglib2.0-dev gcc-9 g++-9 unzip npm',
sudo=True,
)
self.run_command(
f'update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-9 20 \
--slave /usr/bin/g++ g++ /usr/bin/g++-9 --slave /usr/bin/gcov gcov /usr/bin/gcov-9',
sudo=True,
)
self.run_command('apt remove -y python3-urllib3 python3-distro', sudo=True)
self.run_command('apt autoremove -y', sudo=True)
def node_version(self):
"""返回已装 node 主次版本元组(如 (20, 18));未装返回 None。"""
if shutil.which('node') is None:
return None
ret = self.run_command('node -v', capture_output=True, check=False)
matched = re.match(r'v(\d+)\.(\d+)', ret.stdout.decode('utf-8', errors='ignore'))
return (int(matched.group(1)), int(matched.group(2))) if matched else None
def install_node(self):
logging.info('Start installing node:')
_ = self.run_command('npm config set registry https://registry.npmmirror.com', sudo=True)
version = self.node_version()
if version is not None and version >= (20, 18):
logging.info(f'node v{version[0]}.{version[1]} 已满足要求,跳过 n 版本管理链')
return
self.run_command('npm install -g n', sudo=True)
self.run_command('n 20.18.0', sudo=True)
self.run_command('npm install -g npm@10.8.2', sudo=True)
def install_lua_tools(self):
logging.info('Start installing pre-commit tools (stylua/luacheck):')
if shutil.which('stylua') is None:
logging.info('stylua not found, starting installation:')
self.run_command(
'npm install -g @johnnymorganz/stylua-bin --registry=https://mirrors.huaweicloud.com/repository/npm/',
sudo=True,
)
self.run_command('stylua --version')
else:
logging.info('stylua is already installed, skipping.')
if shutil.which('luacheck') is None:
logging.info('luacheck not found, starting installation:')
if self.spec.bootstrap_mode == 'legacy-full':
self.run_command('apt update', sudo=True)
self.run_command('apt install -y luarocks lua5.1 liblua5.1-dev', sudo=True)
rocks = ['argparse-0.7.2-1', 'luafilesystem-1.9.0-1', 'luacheck-1.2.0-1']
with tempfile.TemporaryDirectory() as tmpdir:
self.chdir(tmpdir)
try:
logging.info('Downloading luacheck dependencies from dailybuild.openubmc.cn:')
download_urls = ' '.join(
f'https://dailybuild.openubmc.cn/pre-commit/{rock}.src.rock' for rock in rocks
)
self.run_command(f'wget {download_urls}')
for rock in rocks:
self.run_command(f'luarocks install {rock}.src.rock', sudo=True)
finally:
self.chdir(self.cwd)
else:
logging.info('luacheck is already installed, skipping.')
logging.info('Pre-commit tools (stylua/luacheck) installation completed.')
def apt_config(self):
key_id = 'BF3B2D407FDEDEA0DC91CC9412640FBCB175287A'
keyrings_dir = '/usr/share/keyrings'
gpg_path = f'{keyrings_dir}/openubmc.gpg'
apt_source = (
f'deb [arch=amd64 signed-by={gpg_path}] '
f' https://openubmc-apt-repo.obs.cn-north-4.myhuaweicloud.com/Ubuntu/ noble main'
)
sources_list = '/etc/apt/sources.list.d/openubmc.list'
try:
self.run_command(f'mkdir -p {keyrings_dir}', sudo=True)
self.run_command(f'gpg --keyserver keyserver.ubuntu.com --recv-keys {key_id}')
with tempfile.NamedTemporaryFile() as tmp_f:
tmp_gpg_path = tmp_f.name
with open(tmp_gpg_path, 'wb') as f:
self.run_command(f'gpg --export {key_id}', check=True, stdout=f)
self.run_command(f'cp {tmp_gpg_path} {gpg_path}', sudo=True)
self.run_command(f'chmod 644 {gpg_path}', sudo=True)
logging.info(f'Writing APT source to {sources_list}...')
with tempfile.NamedTemporaryFile() as tmp_f:
tmp_list_path = tmp_f.name
with open(tmp_list_path, 'w') as f:
f.write(apt_source + '\n')
self.run_command(f'cp {tmp_list_path} {sources_list}', sudo=True)
self.run_command(f'chmod 644 {sources_list}', sudo=True)
self.run_command('apt update', sudo=True)
except subprocess.CalledProcessError as e:
logging.error(f'Command failed: {" ".join(e.cmd)}\nError: {e.stderr}')
except Exception as e:
logging.error(f'Error: {str(e)}')
def conan_config(self):
logging.info('Start configuring conan:')
self.run_command('conan profile detect --force')
_ = self.run_command("conan remote remove '*'", check=False)
_ = self.run_command(
"conan remote add openubmc_opensource 'https://conan.openubmc.cn/openubmc_opensource' --insecure --force"
)
if not self.user:
conan_usr = input('请输入conan仓的用户名: ').strip()
else:
conan_usr = self.user
if not self.password:
conan_psw = getpass.getpass(prompt='请输入conan仓的密码: ')
else:
conan_psw = self.password
_ = self.run_command(f'conan remote login openubmc_opensource {conan_usr} -p {conan_psw}', command_echo=False)
def install_qemu(self):
stop_ret = self.run_command('apt policy openubmc-qemu', capture_output=True)
if len(stop_ret.stdout) == 0:
self.apt_config()
try:
self.run_command('apt install -y openubmc-qemu --upgrade', sudo=True)
except Exception:
logging.info('openubmc-qemu下载失败, 请检查apt源是否配置成功')
def install_qemu_compile_dependencies(self):
self.run_command(
f'apt-get install -y make bzip2 pkg-config libslirp-dev libpixman-1-dev ninja-build \
libcap-ng-dev libfdt-dev libattr1-dev',
sudo=True,
)
def install_qemu_runtime_dependencies(self):
self.run_command(
'cp /etc/apt/sources.list.d/ubuntu.sources /etc/apt/sources.list.d/ubuntu.sources.bak', sudo=True
)
self.run_command("sed -i 's/Suites:/Suites: plucky/g' /etc/apt/sources.list.d/ubuntu.sources", sudo=True)
self.run_command('apt-get update', sudo=True)
self.run_command('apt-get install -y libcapstone5', sudo=True)
self.run_command('rm /etc/apt/sources.list.d/ubuntu.sources', sudo=True)
self.run_command(
'mv /etc/apt/sources.list.d/ubuntu.sources.bak /etc/apt/sources.list.d/ubuntu.sources', sudo=True
)
self.run_command('apt-get update', sudo=True)
def check_path(self):
if self.need_add_user_bin_path:
logging.warning(
'检测到当前用户为非root用户,且未配置环境变量,请配置环境变量,'
'如不配置环境变量可能导致bingo命令无法运行!'
)
logging.warning('参考命令:')
logging.warning('echo \'export PATH="$HOME/.local/bin:$PATH"\' >> ~/.bashrc')
logging.warning('source ~/.bashrc')
logging.warning('请确认已知悉,输入Y:')
if input().upper() == 'Y':
return
def run(self):
try:
self.enter_stage('proxy-check')
self.proxy_check()
self.enter_stage('detect-system')
self.detect_system()
self.enter_stage('bootstrap-packages')
self.bootstrap_packages()
self.enter_stage('pip-config')
self.pip_config()
self.enter_stage('install-bingo')
self.install_bingo()
self.enter_stage('verify-bingo')
self.verify_bingo()
self.enter_stage('conan-config')
self.conan_config()
self.enter_stage('dev-tools')
self.install_dev_tools()
if self.spec.bootstrap_mode == 'legacy-full':
if not self.no_build:
self.enter_stage('install-sdk')
self.install_sdk()
self.enter_stage('qemu')
self.install_qemu_chain()
else:
self.record_skipped('bingo deps install / install_sdk: openEuler 冷启动不执行,由 bingo 管理')
self.check_path()
self.print_summary()
logging.info('openUBMC开发者环境初始化完成!')
except Exception as exc:
if os.environ.get('LOG'):
import traceback
logging.error(traceback.format_exc())
self.print_failure_summary(exc)
return -1
return 0
if __name__ == '__main__':
Task.log_init()
wk = WorkPrepare()
sys.exit(wk.run())