#!/usr/bin/env bash
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
BACKUP_DIR="$REPO_ROOT/sign-backup"
SIGN_SRC="$REPO_ROOT/ai-agent/sign"
BUILD_PROFILE_FRAG_DIR="$BACKUP_DIR/build-profile"
die() {
echo "错误: $*" >&2
exit 1
}
find_build_profiles() {
find "$REPO_ROOT" -name build-profile.json5 \
-not -path "$REPO_ROOT/.git/*" \
-not -path "*/node_modules/*" \
-not -path "*/oh_modules/*" \
-not -path "*/build/*" \
-not -path "$BACKUP_DIR/*" \
2>/dev/null |
while read -r f; do
if [ -f "$f" ] && grep -q '"signingConfigs"' "$f" 2>/dev/null; then
echo "$f"
fi
done
}
extract_block() {
awk '
/"signingConfigs"/ { inblock = 1; depth = 0 }
inblock {
line = $0
nb = gsub(/\[/, "[", line)
line = $0
nc = gsub(/\]/, "]", line)
depth += nb - nc
print
if (depth <= 0) { exit }
}
' "$1"
}
strip_block() {
awk '
/"signingConfigs"/ { inblock = 1; depth = 0 }
!inblock { print; next }
{
line = $0
nb = gsub(/\[/, "[", line)
line = $0
nc = gsub(/\]/, "]", line)
depth += nb - nc
if (depth <= 0) { inblock = 0 }
}
' "$1"
}
insert_block() {
awk -v frag="$1" '
BEGIN {
while ((getline l < frag) > 0) block = block l "\n"
close(frag)
}
/^[[:space:]]*"app"[[:space:]]*:[[:space:]]*\{/ && !inserted {
print
printf "%s", block
inserted = 1
next
}
{ print }
END { if (!inserted) exit 3 }
' "$2"
}
check_balanced() {
local f="$1" o c
o=$(tr -cd '{' < "$f" | wc -c)
c=$(tr -cd '}' < "$f" | wc -c)
[ "$o" = "$c" ]
}
has_cr() { tr -cd '\r' < "$1" | grep -q .; }
has_trailing_nl() {
tail -c 1 "$1" | od -An -tx1 | tr -d ' \n' | grep -q '0a$'
}
lf_to_crlf_file() { awk '{ printf "%s\r\n", $0 }' "$1"; }
strip_trailing_nl() {
awk '{ if (NR > 1) print prev; prev = $0 }
END { if (NR > 0) { gsub(/\r$/, "", prev); printf "%s", prev } }' "$1"
}
cmd_backup() {
mkdir -p "$BACKUP_DIR"
echo "==> 备份签名材料"
if [ -d "$SIGN_SRC" ]; then
mkdir -p "$BACKUP_DIR/sign"
cp -a "$SIGN_SRC/." "$BACKUP_DIR/sign/"
echo " 已备份: $SIGN_SRC -> $BACKUP_DIR/sign/"
else
echo " [警告] 签名材料目录不存在: $SIGN_SRC(跳过材料备份)"
fi
echo "==> 备份 signingConfigs 配置片段"
local n=0
while read -r f; do
[ -n "$f" ] || continue
local rel dest
rel="${f#"$REPO_ROOT"/}"
dest="$BUILD_PROFILE_FRAG_DIR/$rel"
mkdir -p "$(dirname "$dest")"
extract_block "$f" | tr -d '\r' > "$dest"
echo " 已备份: $rel"
n=$((n + 1))
done < <(find_build_profiles)
if [ "$n" -eq 0 ]; then
echo " [警告] 未发现任何含 signingConfigs 的 build-profile.json5"
fi
cat > "$BACKUP_DIR/MANIFEST.txt" <<EOF
FlexUI 签名配置备份
时间: $(date '+%Y-%m-%d %H:%M:%S')
签名材料: $SIGN_SRC
signingConfigs 片段数: $n
备份目录: $BACKUP_DIR
恢复方式: ./sign.sh restore
EOF
echo "==> 备份完成 -> $BACKUP_DIR"
echo " 注意: 该目录已 gitignore,请勿提交;需要带走时执行 ./sign.sh pack"
}
cmd_pack() {
[ -d "$BACKUP_DIR" ] || die "备份不存在: $BACKUP_DIR,请先执行 ./sign.sh backup"
local ts zip_name zip_path ok=0
ts=$(date +%Y%m%d-%H%M%S)
zip_name="flexui-sign-config-$ts.zip"
zip_path="$BACKUP_DIR/$zip_name"
if [ "$ok" -eq 0 ] && command -v zip >/dev/null 2>&1; then
(cd "$REPO_ROOT" && zip -qr "$zip_path" sign-backup sign.sh) && ok=1
fi
if [ "$ok" -eq 0 ] && command -v python >/dev/null 2>&1; then
(cd "$REPO_ROOT" && python - "$zip_path" <<'PY') && ok=1
import os, sys, zipfile
with zipfile.ZipFile(sys.argv[1], 'w', zipfile.ZIP_DEFLATED) as z:
for item in ('sign-backup', 'sign.sh'):
if os.path.isdir(item):
for dp, _dn, fn in os.walk(item):
for f in fn:
p = os.path.join(dp, f)
z.write(p, p)
else:
z.write(item, item)
PY
fi
if [ "$ok" -eq 0 ] && command -v python3 >/dev/null 2>&1; then
(cd "$REPO_ROOT" && python3 - "$zip_path" <<'PY') && ok=1
import os, sys, zipfile
with zipfile.ZipFile(sys.argv[1], 'w', zipfile.ZIP_DEFLATED) as z:
for item in ('sign-backup', 'sign.sh'):
if os.path.isdir(item):
for dp, _dn, fn in os.walk(item):
for f in fn:
p = os.path.join(dp, f)
z.write(p, p)
else:
z.write(item, item)
PY
fi
if [ "$ok" -eq 0 ] && command -v powershell >/dev/null 2>&1; then
(cd "$REPO_ROOT" && powershell -NoProfile -Command "Compress-Archive -Path 'sign-backup','sign.sh' -DestinationPath '$zip_path' -Force") && ok=1
fi
[ "$ok" -eq 1 ] || die "打包失败:请安装 zip / python / powershell 后重试"
echo "==> 打包完成: $zip_path"
echo " 其他机器恢复: 将 zip 拷贝到仓库根目录解压,然后执行 ./sign.sh restore"
}
cmd_restore() {
[ -d "$BACKUP_DIR" ] || die "备份不存在: $BACKUP_DIR(请先 ./sign.sh backup,或从 zip 解压)"
echo "==> 恢复签名材料"
if [ -d "$BACKUP_DIR/sign" ]; then
mkdir -p "$SIGN_SRC"
cp -a "$BACKUP_DIR/sign/." "$SIGN_SRC/"
echo " 已恢复: $BACKUP_DIR/sign/ -> $SIGN_SRC"
else
echo " [警告] 备份中没有签名材料目录,跳过"
fi
echo "==> 恢复 signingConfigs 配置"
local n=0
while read -r frag; do
[ -n "$frag" ] || continue
local rel target tmp1 tmp2
rel="${frag#"$BUILD_PROFILE_FRAG_DIR"/}"
target="$REPO_ROOT/$rel"
if [ ! -f "$target" ]; then
echo " [警告] 目标文件不存在,跳过: $rel"
continue
fi
tmp1="$target.sign-restore-tmp.$$"
tmp2="$target.sign-restore-tmp2.$$"
rm -f "$tmp1" "$tmp2"
local orig_crlf=false orig_nl=true
if has_cr "$target"; then orig_crlf=true; fi
if ! has_trailing_nl "$target"; then orig_nl=false; fi
strip_block "$target" | tr -d '\r' > "$tmp1"
if ! insert_block "$frag" "$tmp1" > "$tmp2"; then
echo " [错误] 未找到 \"app\": { 插入点,跳过: $rel"
rm -f "$tmp1" "$tmp2"
continue
fi
if [ "$orig_crlf" = true ]; then
lf_to_crlf_file "$tmp2" > "$tmp2.eol" && mv "$tmp2.eol" "$tmp2"
fi
if [ "$orig_nl" = false ]; then
strip_trailing_nl "$tmp2" > "$tmp2.eol" && mv "$tmp2.eol" "$tmp2"
fi
if ! check_balanced "$tmp2"; then
echo " [错误] 恢复后括号不平衡,跳过: $rel"
rm -f "$tmp1" "$tmp2"
continue
fi
cp "$tmp2" "$target"
rm -f "$tmp1" "$tmp2"
echo " 已恢复: $rel"
n=$((n + 1))
done < <(find "$BUILD_PROFILE_FRAG_DIR" -type f -name 'build-profile.json5' 2>/dev/null)
if [ "$n" -eq 0 ]; then
echo " [警告] 备份中没有 signingConfigs 片段"
fi
echo "==> 恢复完成。"
echo " 提示: 恢复后 build-profile.json5 的修改属于本地签名配置,请勿提交(见 CLAUDE.md)"
}
case "${1:-}" in
backup) cmd_backup ;;
pack) cmd_pack ;;
restore) cmd_restore ;;
*) echo "用法: $0 {backup|pack|restore}" >&2; exit 1 ;;
esac