#!/usr/bin/env bash
set -euo pipefail
CONF="${EBS_CONFIG:-$(cd "$(dirname "$0")/.." && pwd)/ebs-build.conf}"
conf_get() {
local key="$1" fallback="$2"
local val="$fallback"
if [ -f "$CONF" ]; then
local fv
fv="$(sed -nE "s/^[[:space:]]*${key}[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$/\\1/p" "$CONF" | head -1)"
[ -n "$fv" ] && val="$fv"
fi
[ -n "${!key:-}" ] && val="${!key}"
printf '%s' "$val"
}
load_config() {
if [ ! -f "$CONF" ]; then
if [ -f "$CONF.example" ]; then
cp "$CONF.example" "$CONF"
info "未找到 $CONF,已从 $CONF.example 复制。"
info "请编辑 $CONF 里的版本/分支(当前为模板示例值),临时覆盖可用环境变量。"
else
die "未找到配置文件 $CONF,也没有模板 $CONF.example。请复制模板或设置 EBS_CONFIG。"
fi
fi
if [ -z "${PROJECT:-}" ]; then
local sfx
sfx="$(conf_get PROJECT_SUFFIX "")"
PROJECT="${USER}${sfx:+-${sfx}}"
fi
SPEC_URL="$(conf_get SPEC_URL "")"
SPEC_BRANCH="$(conf_get SPEC_BRANCH "")"
INHERIT_FROM="$(conf_get INHERIT_FROM "")"
PACKAGE="$(conf_get PACKAGE kernel)"
if [ -z "$SPEC_URL" ] || [ -z "$SPEC_BRANCH" ] || [ -z "$INHERIT_FROM" ]; then
die "配置文件 $CONF 缺少 SPEC_URL / SPEC_BRANCH / INHERIT_FROM,请补全后重试。"
fi
}
check_spec_repo_name() {
local repo
repo="$(basename "${SPEC_URL%.git}")"
[ "$repo" = "$PACKAGE" ] || die "SPEC_URL 仓库名 '$repo' 必须为 '$PACKAGE'(EBS 只对名为 kernel 的 spec 仓库自动生成 Source0 kernel.tar.gz)。fork 请用 gitcode.com/kernel-source/kernel 这类纯 kernel 名,不要加前缀。"
}
ARCHS="${ARCHS:-aarch64 x86_64}"
MAX_POLLS="${MAX_POLLS:-90}"
POLL_INTERVAL="${POLL_INTERVAL:-60}"
CCB_VERSION="${CCB_VERSION:-2.0.1}"
INSTALL_DIR="${CCB_INSTALL_DIR:-$HOME/.local/libexec/ccb-$CCB_VERSION}"
LOCAL_BIN="$HOME/.local/bin"
CONFIG_DIR="$HOME/.config/cli/defaults"
CONFIG_FILE="$CONFIG_DIR/config.yaml"
JWT_FILE="$HOME/.config/cli/jwt"
GATEWAY="https://eulermaker.openeuler.openatom.cn"
die() { echo "[ebs] $*" >&2; exit 1; }
info() { echo "[ebs] $*"; }
need_cmd() { command -v "$1" >/dev/null 2>&1 || die "缺少命令: $1"; }
find_ccb() {
if command -v ccb >/dev/null 2>&1; then echo ccb; return 0; fi
if [ -x "$LOCAL_BIN/ccb" ]; then echo "$LOCAL_BIN/ccb"; return 0; fi
if [ -x /usr/sbin/ccb ]; then echo /usr/sbin/ccb; return 0; fi
return 1
}
CCB="$(find_ccb 2>/dev/null || true)"
require_ccb() {
[ -n "$CCB" ] || die "未找到 ccb,先运行: $0 --install-ccb"
}
require_jwt() {
if [ ! -f "$JWT_FILE" ]; then
info "未找到已保存的登录令牌 (~/.config/cli/jwt),需要重新登录"
info "请确认已运行 --config 且已 export PASSWORD 环境变量,然后重试"
return 1
fi
}
jwt_password_hint() {
grep -qE "ENV\['PASSWORD'\]|undefined method .strip. for nil|PASSWORD" <<<"$1" \
&& die "登录令牌(JWT)已失效且未设置密码环境变量。
请先 export PASSWORD 后重试:
export PASSWORD='你的社区账号密码'
$0 build
脚本不会把密码写入任何文件;若尚未配置账号,先执行 $0 --config。"
return 1
}
cmd_install_ccb() {
info "检查依赖..."
need_cmd git
need_cmd wget
for c in ruby; do need_cmd "$c"; done
local miss=0
for gem in activesupport rest-client; do
if ! ruby -e "require '$gem'" >/dev/null 2>&1; then
echo " 缺少 ruby gem: $gem"
miss=1
fi
done
if [ "$miss" -eq 1 ]; then
echo " 请先安装: dnf install -y ruby rubygem-activesupport rubygem-rest-client wget"
echo " (docker 仅本地构建需要,此处不检查)"
exit 1
fi
info "安装 ccb v${CCB_VERSION} 到 $INSTALL_DIR"
if [ ! -d "$INSTALL_DIR/sbin/cli" ]; then
mkdir -p "$(dirname "$INSTALL_DIR")"
info " git clone https://gitcode.com/src-openeuler/ccb ..."
git clone --depth 1 "https://gitcode.com/src-openeuler/ccb" "$INSTALL_DIR" 2>&1 \
|| die "git clone 失败,请检查网络或改用官方 tarball"
else
info " 已存在,跳过下载"
fi
mkdir -p "$LOCAL_BIN"
ln -sfn "$INSTALL_DIR/sbin/cli/ccb" "$LOCAL_BIN/ccb"
info "已软链: $LOCAL_BIN/ccb -> $INSTALL_DIR/sbin/cli/ccb"
if ! find_ccb >/dev/null 2>&1; then
echo " 警告: ~/.local/bin 不在 PATH,可执行以下命令加入:"
echo " echo 'export PATH=\"\$HOME/.local/bin:\$PATH\"' >> ~/.bashrc && source ~/.bashrc"
fi
info "验证: $($LOCAL_BIN/ccb -h 2>&1 | head -1)"
info "下一步: $0 --config"
}
cmd_config() {
mkdir -p "$CONFIG_DIR"
if [ -f "$CONFIG_FILE" ]; then
info "已有 $CONFIG_FILE,将覆盖。Ctrl-C 可取消。"
fi
read -rp "社区账号 (ACCOUNT): " ACCOUNT
: "${ACCOUNT:?账号不能为空}"
cat > "$CONFIG_FILE" <<EOF
GATEWAY_IP: ${GATEWAY_IP:-eulermaker.openeuler.openatom.cn}
GATEWAY_PORT: ${GATEWAY_PORT:-443}
SRV_URL: ${SRV_URL:-https://eulermaker.openeuler.openatom.cn}
ACCOUNT: ${ACCOUNT}
# PASSWORD 不写入本文件,构建时通过环境变量 PASSWORD 提供:
# export PASSWORD='你的社区密码'
OAUTH_TOKEN_URL: ${OAUTH_TOKEN_URL:-https://omapi.osinfra.cn/oneid/oidc/token}
OAUTH_REDIRECT_URL: ${OAUTH_REDIRECT_URL:-https://eulermaker.openeuler.openatom.cn/oauth}
PUBLIC_KEY_URL: ${PUBLIC_KEY_URL:-https://omapi.osinfra.cn/oneid/public/key?community=openeuler}
EOF
chmod 600 "$CONFIG_FILE"
info "已生成 $CONFIG_FILE"
info "提示: 使用前请先 export PASSWORD 环境变量(不会写入任何文件)"
info "验证: $0 --setup 会检查登录令牌,必要时自动重新获取"
}
cmd_setup() {
require_ccb
load_config
check_spec_repo_name
info "准备工程: $PROJECT"
if "$CCB" select projects os_project="$PROJECT" --size 1 2>/dev/null | grep -q '"_id"'; then
info " 工程已存在,跳过创建"
else
info " 创建继承子工程: inherited_from=$INHERIT_FROM"
"$CCB" create projects "$PROJECT" inherited_from="$INHERIT_FROM"
fi
case "$SPEC_URL" in
*.git) : ;;
*) SPEC_URL="${SPEC_URL}.git" ;;
esac
local cur_json
cur_json="$("$CCB" select projects os_project="$PROJECT" --field package_repos --size 1 2>/dev/null)"
if printf '%s' "$cur_json" | grep -Fq "\"spec_name\": \"${PACKAGE}\"" \
&& printf '%s' "$cur_json" | grep -Fq "\"spec_url\": \"${SPEC_URL}\"" \
&& printf '%s' "$cur_json" | grep -Fq "\"spec_branch\": \"${SPEC_BRANCH}\""; then
info " package_repos 已与配置一致,跳过"
elif command -v python3 >/dev/null 2>&1; then
info " 配置 package_repos: $SPEC_URL ($SPEC_BRANCH)"
local tmp
tmp="$(mktemp)"
SPEC_URL="$SPEC_URL" SPEC_BRANCH="$SPEC_BRANCH" PACKAGE="$PACKAGE" python3 - "$cur_json" > "$tmp" <<'PYEOF'
import json, os, sys
doc = json.loads(sys.argv[1])
cur = doc[0]['_source'].get('package_repos', []) if doc else []
target = {"spec_name": os.environ["PACKAGE"], "spec_url": os.environ["SPEC_URL"], "spec_branch": os.environ["SPEC_BRANCH"]}
new = [e for e in cur if e.get("spec_name") != target["spec_name"]] + [target]
print(json.dumps({"package_repos": new}))
PYEOF
"$CCB" update projects "$PROJECT" -j "$tmp"
rm -f "$tmp"
elif printf '%s' "$cur_json" | grep -Fq "\"spec_name\": \"${PACKAGE}\""; then
info " WARN: 缺少 python3 且 package_repos 已有 ${PACKAGE},无法精确更新 spec_url"
info " 请手动执行: $CCB update projects $PROJECT -j '{\"package_repos\": [...]}'"
else
info " 追加 package_repos: $SPEC_URL ($SPEC_BRANCH)"
local tmp
tmp="$(mktemp)"
cat > "$tmp" <<EOF
{"package_repos+":[{"spec_name":"${PACKAGE}","spec_url":"${SPEC_URL}","spec_branch":"${SPEC_BRANCH}"}]}
EOF
"$CCB" update projects "$PROJECT" -j "$tmp"
rm -f "$tmp"
fi
if "$CCB" select snapshots os_project="$PROJECT" --size 1 2>/dev/null | grep -q '"_id"'; then
info " snapshot 已存在,跳过"
else
info " 创建 snapshot(固定依赖基线)"
"$CCB" create snapshots "$PROJECT"
fi
info "完成。下一步: $0 build"
}
cmd_build() {
require_ccb
load_config
check_spec_repo_name
require_jwt || { "$CCB" -h >/dev/null 2>&1 || true; exit 1; }
info "将触发真实云端构建: os_project=$PROJECT packages=$PACKAGE"
if [ "${FORCE:-0}" != "1" ]; then
read -rp "确认构建? [y/N] " ans
case "$ans" in
y|Y) : ;;
*) die "已取消" ;;
esac
fi
local out
out="$("$CCB" build-single os_project="$PROJECT" packages="$PACKAGE" 2>&1)" || {
jwt_password_hint "$out"
die "build-single 失败,输出如下:
$out"
}
echo "$out"
local ba
ba="$(parse_build_arch <<<"$out")"
if [ -z "$ba" ]; then
info "未能从响应中解析 build_id/架构,请在上方输出中查找 build_id 后用 'log' 查看进度"
exit 0
fi
info "触发成功,build_id -> arch:"
sed 's/^/ /' <<<"$ba"
poll_builds "$ba"
}
parse_build_arch() {
awk '
/"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"/ {
match($0, /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/)
bid = substr($0, RSTART, RLENGTH)
}
/"architecture"[[:space:]]*:[[:space:]]*"/ {
if (bid != "") {
arch = $0
sub(/.*"architecture"[[:space:]]*:[[:space:]]*"/, "", arch)
sub(/".*/, "", arch)
print bid, arch
bid = ""
}
}
'
}
status_label() {
case "$1" in
200) echo "构建中" ;;
201) echo "成功" ;;
202) echo "已发布" ;;
203) echo "失败" ;;
204) echo "准备中" ;;
*) echo "状态?" ;;
esac
}
fmt_elapsed() {
echo "$(( $1 / 60 ))m $(( $1 % 60 ))s"
}
get_build_status() {
"$CCB" select builds build_id="$1" --field status --size 1 2>/dev/null \
| grep -oE '"status":[[:space:]]*[0-9]+' | grep -oE '[0-9]+' | head -1
}
poll_builds() {
local ba="$1"
local i=0 start elapsed labels
start="$(date +%s)"
while [ "$i" -lt "$MAX_POLLS" ]; do
i=$((i + 1))
elapsed=$(( $(date +%s) - start ))
labels=""
local all_done=1
local bid arch status
while read -r bid arch; do
status="$(get_build_status "$bid")"
[ -n "$status" ] || status="?"
labels="${labels:+$labels, }${arch}: $(status_label "$status")"
case "$status" in
201|202) : ;;
203) info " [${arch}] 构建失败(status=203)"; exit 1 ;;
*) all_done=0 ;;
esac
done <<<"$ba"
if [ "$all_done" -eq 1 ]; then
info " 全部架构构建成功: ${labels}"
print_log_hint "$ba"
while read -r bid arch; do print_repo_url "$arch"; done <<<"$ba"
return 0
fi
info " [第 ${i}/${MAX_POLLS} 次 · 已用 $(fmt_elapsed "$elapsed")]${labels:+ ${labels}}(${POLL_INTERVAL}s 后重试)"
[ "$i" -lt "$MAX_POLLS" ] && sleep "$POLL_INTERVAL"
done
info "轮询超时(已等待 $(( MAX_POLLS * POLL_INTERVAL / 60 )) 分钟)。最近状态: ${labels}"
info "仍可能在后台上构建,可用 'log <job_id> --check' 或 Web 构建历史查看"
exit 1
}
print_log_hint() {
local ba="$1" bid arch
info "各架构 job 日志(可用 $0 log <job_id> 查看):"
while read -r bid arch; do
"$CCB" select jobs build_id="$bid" --field id,arch --size 20 2>/dev/null \
| grep -oE '"id": "[^"]+"|"arch": "[^"]+"' | paste - - | sed 's/^/ /'
done <<<"$ba"
}
get_rpm_path() {
local arch="$1"
"$CCB" select rpms "$PROJECT" repo_name="${PACKAGE%%:*}" architecture="$arch" \
--field 'rpm_path' --sort 'submit_time:desc' --size 1 2>/dev/null \
| python3 -c 'import sys,json
try:
d=json.load(sys.stdin)
print(d[0]["_source"]["rpm_path"] if d else "")
except Exception:
print("")'
}
get_emsx() {
"$CCB" select projects "$PROJECT" --field 'emsx' --size 1 2>/dev/null \
| python3 -c 'import sys,json
try:
d=json.load(sys.stdin)
print(d[0]["_source"].get("emsx","ems1") if d else "ems1")
except Exception:
print("ems1")'
}
ccb_cfg() {
sed -nE "s/^${1//./\\.}: *//p" "$HOME/.config/cli/defaults/config.yaml" | tr -d ' \r' | head -1
}
repo_prefix() {
local proto host port
proto="$(ccb_cfg SRV_HTTP_REPOSITORIES_PROTOCOL)"
host="$(ccb_cfg SRV_HTTP_REPOSITORIES_HOST)"
port="$(ccb_cfg SRV_HTTP_REPOSITORIES_PORT)"
printf '%s%s:%s' "${proto:-https://}" "${host}" "${port}"
}
print_repo_url() {
local arch="$1" path emsx
path="$(get_rpm_path "$arch")" || return 1
if [ -z "$path" ]; then
info " [${arch}] 未查到产物记录(rpms 索引无匹配)"
return 1
fi
emsx="$(get_emsx)"
info " [${arch}] 产物仓库: $(repo_prefix)/api/${emsx}${path}"
}
cmd_log() {
require_ccb
local job_id="${1:-}"
[ -n "$job_id" ] || die "用法: $0 log <job_id> [--check]"
local check="${2:-}"
local rr
rr="$("$CCB" select jobs id="$job_id" --field result_root --size 1 2>/dev/null \
| sed -nE 's/.*"result_root"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/p' | head -1)"
[ -n "$rr" ] || die "未找到 job: $job_id"
local url
url="${GATEWAY}/api/result${rr#/result}/dmesg"
echo "构建日志: $url"
if [ "$check" = "--check" ]; then
need_cmd curl
info "拉取日志并检查...(可能较大,请耐心)"
local log
log="$(curl -fsSL "$url" 2>/dev/null)" || die "拉取日志失败: $url"
info "--- 真实失败模式 ---"
if grep -E 'make: \*\*\*|undefined reference|No rule to make target|Out of memory|Killed|PANIC|internal compiler' <<<"$log"; then
exit 1
else
echo " (未发现上述失败模式)"
fi
info "--- 模块签名状态 ---"
grep -oE 'openEuler_has_sign_perm=[01]' <<<"$log" | sort -u || echo " (日志中未找到签名探测输出)"
fi
}
# ---------------- download ----------------
cmd_download() {
require_ccb
load_config
local arch=""
while [ "$#" -gt 0 ]; do
case "$1" in
-a|--arch) arch="$2"; shift 2 ;;
*) die "未知参数: $1(用法: $0 download [-a aarch64|x86_64])" ;;
esac
done
local args=(os_project="$PROJECT" packages="$PACKAGE")
if [ -n "$arch" ]; then args+=(architecture="$arch"); fi
info "下载 rpm: ${args[*]}"
"$CCB" download "${args[@]}"
}
# 显示最后一次成功构建的产物仓库 URL(不触发构建)
cmd_repo_url() {
require_ccb
load_config
local archs=("$@")
[ "${#archs[@]}" -eq 0 ] && archs=($ARCHS)
for arch in "${archs[@]}"; do print_repo_url "$arch" || true; done
}
# ---------------- help ----------------
cmd_help() {
sed -n '2,26p' "$0" | sed 's/^# \{0,1\}//'
}
# ---------------- 入口 ----------------
case "${1:-}" in
--install-ccb) cmd_install_ccb ;;
--config) cmd_config ;;
--setup) cmd_setup ;;
build) shift; cmd_build ;;
log) shift; cmd_log "$@" ;;
download) shift; cmd_download "$@" ;;
repo-url) shift; cmd_repo_url "$@" ;;
--help|-h|"") cmd_help ;;
*) die "未知命令: ${1:-}(--help 查看用法)" ;;
esac