Files
qingjian/apps/macos/scripts/bundle.sh
T
Joelee 2db9677a4c feat: 学习语言新增越南语 (#480)
Language 加 Vietnamese,三端设置与装配接线,云释义补越南语提示词;随包越南语释义表由 gloss-gen 生成(GPL-3.0-or-later)。

Closes #47
2026-10-10 10:42:44 +08:00

222 lines
14 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# 把 qingjian-macos 打包成 Qingjian.app。
#
# scripts/bundle.sh # 只打包到 target/Qingjian.app
# scripts/bundle.sh --install # 打包并安装到 ~/Library/Input Methods/,杀掉旧进程(开发用)
# scripts/bundle.sh --pkg # 打包并做成 target/pkg/qingjian-<版本>-macos-<arm64|x86_64>.pkg(分发给测试者)
#
# 架构:缺省编译本机架构;QINGJIAN_TARGET=x86_64-apple-darwin(或 aarch64-apple-darwin)交叉编译另一种,
# 先 `rustup target add` 一次。CI 在 Apple Silicon runner 上两个都打(.github/workflows/release.yml)。
# 产品数据:data/generated/ 里有 dict.qj 就用自建词库(TSV 比 .qj 新会重打),没有就退回 assets/sample/ 样例。
#
# 签名与公证都由环境变量决定,没设就 ad-hoc 签名、pkg 不签(本机自用够了,分发给别人会被 Gatekeeper 拦,
# 对方要在「系统设置 → 隐私与安全性」里点「仍要打开」):
# QINGJIAN_SIGN_IDENTITY "Developer ID Application: …" 给 .app 签名(hardened runtime)
# QINGJIAN_INSTALLER_IDENTITY "Developer ID Installer: …" 给 .pkg 签名
# QINGJIAN_NOTARY_PROFILE notarytool store-credentials 存的 keychain profile 名,设了就公证并钉票据
#
# 首次 --install 后要在「系统设置 → 键盘 → 输入法」里添加「青简」;输入法列表不刷新就注销再登录。
# pkg 装的不用:postinstall 会以登录用户身份跑 `qingjian-macos --register` 注册并启用。
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
APP_NAME="Qingjian"
BIN_NAME="qingjian-macos"
PROFILE="${PROFILE:-release}"
APP="$ROOT/target/$APP_NAME.app"
INSTALL_DIR="$HOME/Library/Input Methods"
# 目标三元组为空就是本机;架构名按 pkg 文件名与 distribution.xml 的 hostArchitectures 用的写法(arm64 / x86_64)
TARGET="${QINGJIAN_TARGET:-}"
case "${TARGET:-$(uname -m)}" in
aarch64-apple-darwin|arm64) ARCH="arm64" ;;
x86_64-apple-darwin|x86_64) ARCH="x86_64" ;;
*) echo "不认识的架构: ${TARGET:-$(uname -m)}" >&2; exit 1 ;;
esac
cd "$ROOT"
# 构建标识进「关于」页与诊断信息:git 短哈希(工作区有改动加 +)与日期;编译期 option_env! 读
GIT_REV="$(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
if [[ -n "$(git status --porcelain 2>/dev/null)" ]]; then GIT_REV="${GIT_REV}+"; fi
export QINGJIAN_BUILD="${GIT_REV} · $(date +%Y-%m-%d)"
BUILD_ARGS=(-p "$BIN_NAME" --locked)
[[ "$PROFILE" == "release" ]] && BUILD_ARGS+=(--release)
BIN_DIR="target/$PROFILE"
if [[ -n "$TARGET" ]]; then
BUILD_ARGS+=(--target "$TARGET")
BIN_DIR="target/$TARGET/$PROFILE"
fi
cargo build "${BUILD_ARGS[@]}"
rm -rf "$APP"
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
cp "$BIN_DIR/$BIN_NAME" "$APP/Contents/MacOS/$BIN_NAME"
cp apps/macos/Info.plist "$APP/Contents/Info.plist"
# 版本号来自 apps/macos/Cargo.toml(各平台壳版本号独立,不跟 workspace 走),构建号用提交数(单调递增,pkg 升级判断靠它)。
# 发版之间版本号带 -dev(0.1.2-dev):本地与 CI 中间构建一眼能与线上包区分;发版提交去掉 -dev 再打标签(docs/notes/release.md)。
# 开发版再接上 git 短哈希(0.1.3-dev-1a2b3c4,工作区有改动加 +),测试时一眼知道装的是哪个提交;Cargo.toml 里仍只写 -dev。
# pkgbuild / distribution 的 version 只认数字点号,去掉预发布后缀;Info.plist 与 pkg 文件名保留完整版本
VERSION="$(sed -n 's/^version = "\(.*\)"/\1/p' apps/macos/Cargo.toml | head -1)"
if [[ "$VERSION" == *-dev ]]; then VERSION="${VERSION}-${GIT_REV}"; fi
PKG_VERSION="${VERSION%%-*}"
BUILD_NUMBER="$(git rev-list --count HEAD 2>/dev/null || echo 1)"
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" \
-c "Set :CFBundleVersion $BUILD_NUMBER" "$APP/Contents/Info.plist"
# 卸载脚本随包,装了 pkg 的用户从 Resources 里运行
cp apps/macos/scripts/uninstall.sh "$APP/Contents/Resources/uninstall.sh"
# 输入源名字按系统语言本地化(中文系统显示「青简」,其他显示 Qingjian)
cp -R apps/macos/resources/*.lproj "$APP/Contents/Resources/"
# 词库与释义表打进 Resources。data/generated/ 里有生成好的产品数据(自建词库 + 语言模型 + LLM 释义表)就用它,
# 否则用 assets/sample/ 的样例。没有数据管道的机器跑 tools/release/data-fetch.sh 按 tools/release/data.lock 下载。
cp assets/sample/*.tsv "$APP/Contents/Resources/"
# emoji 表(Unicode CLDR,可发布)
cp assets/emoji/*.tsv "$APP/Contents/Resources/"
# 词汇等级表(CEFR-J / Octanove / JLPT,见 assets/levels/README.md),「统计」页按级数词汇
cp assets/levels/levels-*.tsv "$APP/Contents/Resources/"
# 五笔码表(输入方案选五笔时用,见 assets/wubi/README.md;极点 86 码表,Apache-2.0)
mkdir -p "$APP/Contents/Resources/wubi"
cp assets/wubi/wubi86.tsv "$APP/Contents/Resources/wubi/"
if [[ -f data/generated/dict.tsv || -f data/generated/dict.qj ]]; then
# 词库与语言模型打成 .qj(mmap 直接用),TSV 比 .qj 新时重新打包;只有 .qj(CI 从数据包解出来的)就直接用
if [[ -f data/generated/dict.tsv && ( ! -f data/generated/dict.qj || data/generated/dict.tsv -nt data/generated/dict.qj ) ]]; then
cargo run --release -q -p qingjian-dict-convert -- pack dict --name "青简基础词库" \
--license "MIT AND Unicode-3.0" --attribution "通用规范汉字表;现代汉语常用词表(liuxilu 校对版);THUOCL(清华大学自然语言处理实验室,MIT);读音 Unihan(Unicode)" \
--source https://github.com/qingjian-team/qingjian/tree/main/assets/lexicon
fi
if [[ -f data/generated/lm-bigram.tsv && ( ! -f data/generated/lm.qj || data/generated/lm-bigram.tsv -nt data/generated/lm.qj ) ]]; then
cargo run --release -q -p qingjian-dict-convert -- pack lm --name "青简语言模型(中文维基 + LCCC,青简词库分词)" \
--license "CC-BY-SA-4.0 AND MIT" --attribution "中文维基百科(CC BY-SA 4.0);LCCC(清华大学 CoAI,MIT)"
fi
cp data/generated/dict.qj "$APP/Contents/Resources/"
# 领域词库(lexicon 拆出的 dicts/*.qj)随包放 Resources/dicts/,缺省只开成语,偏好设置「词库」页可勾选
if ls data/generated/dicts/*.qj >/dev/null 2>&1; then
mkdir -p "$APP/Contents/Resources/dicts"
cp data/generated/dicts/*.qj "$APP/Contents/Resources/dicts/"
fi
[[ -f data/generated/lm.qj ]] && cp data/generated/lm.qj "$APP/Contents/Resources/"
# 含章·知微(字级 Transformer):三件套与单文件放 data/models/hanzhang-zhiwei/。
model_dir="${QINGJIAN_MODEL_DIR:-data/models/hanzhang-zhiwei}"
if [[ -f "$model_dir/model.safetensors" ]]; then
QINGJIAN_MODEL_DIR="$model_dir" tools/release/pack-model.sh
fi
if [[ -f "$model_dir/hanzhang-zhiwei-small.qjm" ]]; then
mkdir -p "$APP/Contents/Resources/models/hanzhang-zhiwei"
cp "$model_dir/hanzhang-zhiwei-small.qjm" "$APP/Contents/Resources/models/hanzhang-zhiwei/"
chmod 644 "$APP/Contents/Resources/models/hanzhang-zhiwei/hanzhang-zhiwei-small.qjm"
echo "打包含章·知微:$model_dir/hanzhang-zhiwei-small.qjm"
fi
# 含章·通变(P2C,带噪拼音 → 汉字):放 Resources/models/hanzhang-tongbian/。
p2c_dir="${QINGJIAN_P2C_MODEL_DIR:-data/models/hanzhang-tongbian}"
if [[ -f "$p2c_dir/model.safetensors" ]]; then
QINGJIAN_MODEL_DIR="$p2c_dir" tools/release/pack-model.sh
fi
if [[ -f "$p2c_dir/hanzhang-tongbian-small.qjm" ]]; then
mkdir -p "$APP/Contents/Resources/models/hanzhang-tongbian"
cp "$p2c_dir/hanzhang-tongbian-small.qjm" "$APP/Contents/Resources/models/hanzhang-tongbian/"
chmod 644 "$APP/Contents/Resources/models/hanzhang-tongbian/hanzhang-tongbian-small.qjm"
echo "打包含章·通变:$p2c_dir/hanzhang-tongbian-small.qjm"
fi
# 释义表打成 .qj(TSV 比 .qj 新时重打),英文词表仍是 TSV。各表来源不同,元数据按表写(见 assets/glossary/README.md)
for lang in en ja zh es vi; do
src="assets/glossary/glossary-$lang.tsv"
out="data/generated/glossary-$lang.qj"
[[ -f "$src" ]] || continue
if [[ "$lang" == es ]]; then
license="GPL-3.0-or-later"
attribution="Azure Translator 机器翻译(Tofuzhu,tools/corpus/glossary_es.py)"
elif [[ "$lang" == vi ]]; then
license="GPL-3.0-or-later"
attribution="LLM 生成(gpt-5.5),qingjian-gloss-gen"
else
license="MIT"
attribution="LLM 生成(DeepSeek),qingjian-gloss-gen"
fi
if [[ ! -f "$out" || "$src" -nt "$out" ]]; then
cargo run --release -q -p qingjian-dict-convert -- pack glossary --language "$lang" --input "$src" \
--name "青简释义表(${lang})" --license "$license" --attribution "$attribution"
fi
cp "$out" "$APP/Contents/Resources/"
done
for f in assets/lexicon/english.tsv data/generated/english.tsv; do
[[ -f "$f" ]] && cp "$f" "$APP/Contents/Resources/"
done
echo "使用 data/generated/ 的产品数据(自建词库)"
fi
printf 'APPL????' > "$APP/Contents/PkgInfo"
# 图标:应用图标从 assets/icon/logo.png 生成 .icns;输入法菜单图标直接用 assets/icon/menu.pdf(矢量,随 Info.plist 的 TISIconIsTemplate 按深浅色反色)
ICONSET="$ROOT/target/Qingjian.iconset"
rm -rf "$ICONSET" && mkdir -p "$ICONSET"
for size in 16 32 128 256 512; do
sips -z $size $size assets/icon/logo.png --out "$ICONSET/icon_${size}x${size}.png" >/dev/null
double=$((size * 2))
sips -z $double $double assets/icon/logo.png --out "$ICONSET/icon_${size}x${size}@2x.png" >/dev/null
done
iconutil -c icns "$ICONSET" -o "$APP/Contents/Resources/Qingjian.icns"
cp assets/icon/menu.pdf "$APP/Contents/Resources/qingjian-menu.pdf"
# 仓库放在 iCloud 同步的目录(Documents)时新建的 .app 会带上 Finder 扩展属性,codesign 会拒(detritus not allowed):签名前清掉
xattr -cr "$APP"
# Apple Silicon 上未签名的二进制不会被系统加载。有 Developer ID 证书就正式签(开 hardened runtime,公证要求),
# 没有就 ad-hoc 签名,本机自用够了
if [[ -n "${QINGJIAN_SIGN_IDENTITY:-}" ]]; then
codesign --force --deep --options runtime --timestamp --sign "$QINGJIAN_SIGN_IDENTITY" "$APP"
echo "已用 Developer ID 签名: $QINGJIAN_SIGN_IDENTITY"
else
codesign --force --deep --sign - "$APP"
fi
echo "打包完成: ${APP}(版本 ${VERSION},构建 ${BUILD_NUMBER},${ARCH})"
if [[ "${1:-}" == "--pkg" ]]; then
# 每个架构一个工作目录,成品都放 target/pkg/,两个架构接着打互不覆盖
PKG="$ROOT/target/pkg/qingjian-$VERSION-macos-$ARCH.pkg"
PKG_DIR="$ROOT/target/pkg/$ARCH"
rm -rf "$PKG_DIR"
mkdir -p "$PKG_DIR/root" "$PKG_DIR/resources"
# 不带扩展属性复制,否则载荷里全是 ._ 元数据文件
ditto --noextattr --norsrc --noacl "$APP" "$PKG_DIR/root/$APP_NAME.app"
# 组件描述里关掉 bundle 重定位:否则机器上别处已有同 bundle id 的 .app(比如 ~/Library 下的开发副本)时,
# 安装器会把新版装到那里而不是 /Library/Input Methods
pkgbuild --analyze --root "$PKG_DIR/root" "$PKG_DIR/component.plist" >/dev/null
/usr/libexec/PlistBuddy -c "Set :0:BundleIsRelocatable false" "$PKG_DIR/component.plist" 2>/dev/null || \
/usr/libexec/PlistBuddy -c "Add :0:BundleIsRelocatable bool false" "$PKG_DIR/component.plist"
pkgbuild --root "$PKG_DIR/root" --component-plist "$PKG_DIR/component.plist" \
--install-location "/Library/Input Methods" --scripts apps/macos/pkg/scripts \
--identifier app.qingjian.inputmethod --version "$PKG_VERSION" "$PKG_DIR/$APP_NAME-component.pkg" >/dev/null
cp apps/macos/pkg/resources/*.html "$PKG_DIR/resources/"
cp LICENSE "$PKG_DIR/resources/license.txt"
# 二进制只有一种架构,hostArchitectures 限定只在对应机器上装;另一种架构用 QINGJIAN_TARGET 再打一份
sed -e "s/@VERSION@/$VERSION/g" -e "s/@PKG_VERSION@/$PKG_VERSION/g" -e "s/@ARCH@/$ARCH/g" apps/macos/pkg/distribution.xml > "$PKG_DIR/distribution.xml"
SIGN_ARGS=()
if [[ -n "${QINGJIAN_INSTALLER_IDENTITY:-}" ]]; then
SIGN_ARGS=(--sign "$QINGJIAN_INSTALLER_IDENTITY" --timestamp)
fi
# bash 3.2 下空数组展开会撞 set -u,用 ${arr[@]+"${arr[@]}"} 写法
productbuild --distribution "$PKG_DIR/distribution.xml" --package-path "$PKG_DIR" \
--resources "$PKG_DIR/resources" ${SIGN_ARGS[@]+"${SIGN_ARGS[@]}"} "$PKG" >/dev/null
if [[ -n "${QINGJIAN_NOTARY_PROFILE:-}" ]]; then
xcrun notarytool submit "$PKG" --keychain-profile "$QINGJIAN_NOTARY_PROFILE" --wait
xcrun stapler staple "$PKG"
echo "已公证并钉上票据"
elif [[ -z "${QINGJIAN_INSTALLER_IDENTITY:-}" ]]; then
echo "注意: pkg 未签名未公证,测试者首次打开要在「系统设置 → 隐私与安全性」里点「仍要打开」"
fi
echo "pkg: $PKG"
shasum -a 256 "$PKG"
fi
if [[ "${1:-}" == "--install" ]]; then
if [[ -d "/Library/Input Methods/$APP_NAME.app" ]]; then
echo "注意: /Library/Input Methods/$APP_NAME.app 也装着一份(pkg 装的),两份同 id 会互相顶;先跑 scripts/uninstall.sh"
fi
mkdir -p "$INSTALL_DIR"
# 先整个拷到旁边再换上去:直接删了再拷,拷到一半中断会留下残缺的包,旧进程还在用它
STAGING="$INSTALL_DIR/.$APP_NAME.app.new"
rm -rf "$STAGING"
cp -R "$APP" "$STAGING"
# 系统会在下次切换到该输入法时重新拉起进程
pkill -x "$BIN_NAME" 2>/dev/null || true
rm -rf "$INSTALL_DIR/$APP_NAME.app"
mv "$STAGING" "$INSTALL_DIR/$APP_NAME.app"
echo "已安装到: $INSTALL_DIR/$APP_NAME.app"
echo "日志: ~/Library/Logs/Qingjian/"
fi