You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
214 lines
10 KiB
214 lines
10 KiB
#!/usr/bin/env bash
|
|
#
|
|
# 把 Flutter 生成的 SPM 中间包的 iOS 平台版本,对齐到本工程的 deployment target。
|
|
#
|
|
# ── 解决什么问题 ────────────────────────────────────────────────────────────
|
|
# Flutter 3.29+ 的 Swift Package Manager 集成会生成
|
|
# ios/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage/Package.swift
|
|
# 所有插件都挂在这个包下面。**它生成时的平台版本写死是 Flutter 自己的最低版本
|
|
# (iOS 13.0)**,而本项目有十几个本地插件声明了 `.iOS("16.0")`。SPM 不允许依赖的
|
|
# 平台版本高于上层包,于是 Xcode 报:
|
|
#
|
|
# The package product 'xxx' requires minimum platform version 16.0
|
|
# for the iOS platform, but this target supports 13.0
|
|
#
|
|
# Flutter 自己有补救(SwiftPackageManager.updateMinimumDeployment:读 Runner 的
|
|
# IPHONEOS_DEPLOYMENT_TARGET 把生成包改写上去),但**只在 `flutter build/run` 里跑**。
|
|
# 而 Xcode 直接构建时,SPM 解析发生在所有 Flutter 步骤之前,看到的还是 13.0 —— 所以
|
|
# `flutter build ios` 能过、Xcode 里点构建就炸。
|
|
#
|
|
# ⚠️ 任何重新生成插件清单的操作都会把它打回 13.0:flutter pub get / clean / install、
|
|
# 改完 pubspec 的自动 pub get。也就是说这不是一次性问题,会反复出现。
|
|
#
|
|
# ── 怎么用 ──────────────────────────────────────────────────────────────────
|
|
# ./ios/sync_spm_platform.sh # 在 apps/client 下随时手动跑,幂等
|
|
# ./ios/sync_spm_platform.sh --check # 只检查不改写,不一致时退出码 1(给 CI 用)
|
|
#
|
|
# Xcode 在构建进程启动时就读完 Package.swift,scheme 里的 pre-action 改文件
|
|
# 已经晚了(当前 Xcode 还会把 Flutter 写的 13.0 抬成 15.0 再去比插件的 16.0)。
|
|
# 所以除了构建前再跑一遍,脚本还会装一个只盯这份生成包的 LaunchAgent:
|
|
# flutter pub get 一把文件写回 13.0,它就立刻改回工程的 deployment target。
|
|
# 标题必须保持 "Run Prepare Flutter Framework Script",Flutter 靠这串判断
|
|
# scheme 已迁移;改掉的话下次 flutter pub get 会再插一条、把这里冲乱。
|
|
#
|
|
# ⚠️ 不依赖 flutter 命令:Xcode 的 pre-action 环境 PATH 很干净,调 flutter 经常找不到。
|
|
# 这里只做纯文本改写,几毫秒完成。代价是生成包必须已经存在(跑过一次
|
|
# flutter pub get 即可),不存在时脚本会明确告诉你先跑什么。
|
|
#
|
|
# ⚠️ 别改 Package.swift 本身来"修好"这个问题——它头上写着 Generated file. Do not edit.,
|
|
# 下次 pub get 就被覆盖,属于假修好。本脚本做的是同一件事,但可重复执行。
|
|
|
|
# ⚠️ macOS 自带 bash 3.2 不认多字节字符边界:`$VAR,` 会被当成变量名 `VAR,`,
|
|
# 在 set -u 下报 unbound variable。所以本文件里凡是变量后面紧跟中文标点的,
|
|
# 一律写成 ${VAR}。
|
|
set -euo pipefail
|
|
|
|
IOS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
CLIENT_DIR="$(cd "$IOS_DIR/.." && pwd)"
|
|
PBXPROJ="$IOS_DIR/Runner.xcodeproj/project.pbxproj"
|
|
MANIFESTS=(
|
|
"$IOS_DIR/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage/Package.swift"
|
|
"$IOS_DIR/Flutter/ephemeral/Packages/.packages/FlutterFramework/Package.swift"
|
|
)
|
|
|
|
CHECK_ONLY=0
|
|
[[ "${1:-}" == "--check" ]] && CHECK_ONLY=1
|
|
|
|
# flutter pub get 会把生成包写回 13.0。Xcode 读这份文件的时机早于任何构建脚本,
|
|
# 所以要在写回的那一下就改回来,而不是等下一次点构建。
|
|
install_watcher() {
|
|
[[ $CHECK_ONLY -eq 1 ]] && return 0
|
|
local uid dir label plist tmp
|
|
uid="$(id -u)"
|
|
dir="$IOS_DIR/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage"
|
|
[[ -d "$dir" ]] || return 0
|
|
label="com.yimai.eaimar.sync-spm-platform"
|
|
plist="$HOME/Library/LaunchAgents/${label}.plist"
|
|
mkdir -p "$HOME/Library/LaunchAgents"
|
|
tmp="$(mktemp)"
|
|
cat > "$tmp" <<EOF
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
<plist version="1.0">
|
|
<dict>
|
|
<key>Label</key>
|
|
<string>${label}</string>
|
|
<key>ProgramArguments</key>
|
|
<array>
|
|
<string>${IOS_DIR}/sync_spm_platform.sh</string>
|
|
<string>--from-watcher</string>
|
|
</array>
|
|
<key>WatchPaths</key>
|
|
<array>
|
|
<string>${dir}</string>
|
|
</array>
|
|
<key>ThrottleInterval</key>
|
|
<integer>1</integer>
|
|
</dict>
|
|
</plist>
|
|
EOF
|
|
if [[ -f "$plist" ]] && cmp -s "$tmp" "$plist"; then
|
|
rm -f "$tmp"
|
|
else
|
|
mv "$tmp" "$plist"
|
|
launchctl bootout "gui/${uid}/${label}" >/dev/null 2>&1 || true
|
|
launchctl bootstrap "gui/${uid}" "$plist" >/dev/null 2>&1 || true
|
|
fi
|
|
# 已经在跑就别再 bootstrap;没加载过则补一次
|
|
if ! launchctl print "gui/${uid}/${label}" >/dev/null 2>&1; then
|
|
launchctl bootstrap "gui/${uid}" "$plist" >/dev/null 2>&1 || true
|
|
fi
|
|
}
|
|
|
|
# 监视器回调自己时不要再去装卸监视器,否则写文件会把自己叫醒。
|
|
if [[ "${1:-}" != "--from-watcher" ]]; then
|
|
install_watcher || true
|
|
fi
|
|
|
|
red() { printf "\033[31m%s\033[0m\n" "$*"; }
|
|
green() { printf "\033[32m%s\033[0m\n" "$*"; }
|
|
blue() { printf "\033[34m%s\033[0m\n" "$*"; }
|
|
|
|
# 把 .iOS(.v16) / .iOS("16.0") / 16 统一成可比较的 x.y 形式
|
|
normalize() {
|
|
local v="$1"
|
|
v="${v#.v}" # .v16 → 16
|
|
v="${v//\"/}" # 去引号
|
|
[[ "$v" == *.* ]] || v="$v.0" # 16 → 16.0
|
|
printf '%s' "$v"
|
|
}
|
|
|
|
# 版本比较:$1 <= $2 ?
|
|
le() { [[ "$(printf '%s\n%s\n' "$1" "$2" | sort -V | head -1)" == "$1" ]]; }
|
|
|
|
[[ -f "$PBXPROJ" ]] || { red "✗ 找不到 $PBXPROJ"; exit 1; }
|
|
|
|
# ── 1) 工程能接受的上限 ─────────────────────────────────────────────────────
|
|
# 生成包的平台版本必须 ≤ 每个链接它的 target,所以取全工程最低的那个 deployment target。
|
|
CEILING="$(grep -o 'IPHONEOS_DEPLOYMENT_TARGET = [0-9.]*' "$PBXPROJ" \
|
|
| awk '{print $3}' | sed 's/;$//' | sort -V | head -1)"
|
|
[[ -n "$CEILING" ]] || { red "✗ 没能从 project.pbxproj 读出 IPHONEOS_DEPLOYMENT_TARGET"; exit 1; }
|
|
CEILING="$(normalize "$CEILING")"
|
|
|
|
# ── 2) 插件要求的下限 ───────────────────────────────────────────────────────
|
|
# 扫本地插件的 Package.swift,取声明得最高的那个。
|
|
FLOOR="0.0"; FLOOR_OWNER=""
|
|
while IFS= read -r f; do
|
|
raw="$(grep -o '\.iOS([^)]*)' "$f" | head -1 | sed 's/^\.iOS(//; s/)$//')" || true
|
|
[[ -n "$raw" ]] || continue
|
|
v="$(normalize "$raw")"
|
|
if ! le "$v" "$FLOOR"; then FLOOR="$v"; FLOOR_OWNER="${f#"$CLIENT_DIR"/}"; fi
|
|
done < <(find "$CLIENT_DIR/local_plugins" -name Package.swift -not -path '*/.*' 2>/dev/null | sort)
|
|
|
|
blue "工程 deployment target(取最低) = ${CEILING} 本地插件最高要求 = ${FLOOR}${FLOOR_OWNER:+ (${FLOOR_OWNER})}"
|
|
|
|
# ── 3) 插件要求超过工程上限 = 真问题,脚本救不了,直接点名 ───────────────────
|
|
if ! le "$FLOOR" "$CEILING"; then
|
|
red "✗ 插件要求 iOS ${FLOOR},但工程最低 target 只有 ${CEILING} —— 这个脚本改不了。"
|
|
red " 要么把工程的 IPHONEOS_DEPLOYMENT_TARGET 抬到 ${FLOOR}(Runner + Podfile 都要),"
|
|
red " 要么把 ${FLOOR_OWNER} 的平台声明降下来。"
|
|
exit 1
|
|
fi
|
|
|
|
# ── 4) 对齐生成包 ───────────────────────────────────────────────────────────
|
|
# 插件包必须存在。FlutterFramework 是可选的,在才改。
|
|
MANIFEST="${MANIFESTS[0]}"
|
|
if [[ ! -f "$MANIFEST" ]]; then
|
|
red "✗ 生成包还不存在:${MANIFEST#"$CLIENT_DIR"/}"
|
|
red " 先在 apps/client 下跑一次 \`flutter pub get\`(或 \`flutter build ios --config-only\`)再执行本脚本。"
|
|
exit 1
|
|
fi
|
|
|
|
# ── 本地插件之间的相对路径依赖 ────────────────────────────────────────────
|
|
# 若干本地插件的 Package.swift 用 `../../../<plugin>/ios/<plugin>` 引用兄弟插件
|
|
# (agent_service→test、azure_speech→classic_bluetooth、recorder_holder→device_jieli、
|
|
# chat_api→swift_sdk_local)。这些路径是按 `local_plugins/<name>/ios/<name>` 的真实
|
|
# 目录写的,但 Flutter 把插件以符号链接挂在 `ephemeral/Packages/.packages/<name>` 下,
|
|
# SPM 不解析符号链接,`../../..` 就落到了 `ephemeral/`——于是报
|
|
# "the package at 'ephemeral/<plugin>/ios/<plugin>' cannot be accessed"。
|
|
# 这里在 ephemeral/ 下给每个本地插件目录补一个同名链接,让那些相对路径能落地。
|
|
# 幂等;`flutter pub get` 不会清掉它们。
|
|
ensure_local_plugin_links() {
|
|
local eph="$IOS_DIR/Flutter/ephemeral"
|
|
local plugins_dir="$CLIENT_DIR/local_plugins"
|
|
[ -d "$eph" ] || return 0
|
|
for d in "$plugins_dir"/*/; do
|
|
local name
|
|
name="$(basename "$d")"
|
|
[ -e "$eph/$name" ] && continue
|
|
ln -s "../../../local_plugins/$name" "$eph/$name"
|
|
done
|
|
}
|
|
ensure_local_plugin_links
|
|
|
|
align_one() {
|
|
local manifest="$1"
|
|
[[ -f "$manifest" ]] || return 0
|
|
local current_raw current
|
|
current_raw="$(grep -o '\.iOS([^)]*)' "$manifest" | head -1 | sed 's/^\.iOS(//; s/)$//')"
|
|
current="$(normalize "$current_raw")"
|
|
local label="${manifest#"$CLIENT_DIR"/}"
|
|
if [[ "$current" == "$CEILING" ]]; then
|
|
green "✓ ${label} 已是 iOS ${current}"
|
|
return 0
|
|
fi
|
|
if [[ $CHECK_ONLY -eq 1 ]]; then
|
|
red "✗ ${label} 是 iOS ${current},应为 ${CEILING}(跑 ./ios/sync_spm_platform.sh 修)"
|
|
return 1
|
|
fi
|
|
# 只替换 platforms 段里的第一个 .iOS(...),不碰文件其余内容
|
|
/usr/bin/sed -i '' "s/\.iOS($(printf '%s' "$current_raw" | sed 's/[.[\*^$\/]/\\&/g'))/.iOS(\"$CEILING\")/" "$manifest"
|
|
local new
|
|
new="$(normalize "$(grep -o '\.iOS([^)]*)' "$manifest" | head -1 | sed 's/^\.iOS(//; s/)$//')")"
|
|
if [[ "$new" != "$CEILING" ]]; then
|
|
red "✗ 改写失败 ${label},现在是 ${new}"
|
|
return 1
|
|
fi
|
|
green "✓ ${label} 平台版本 ${current} → ${CEILING}"
|
|
}
|
|
|
|
fail=0
|
|
for m in "${MANIFESTS[@]}"; do
|
|
align_one "$m" || fail=1
|
|
done
|
|
exit "$fail"
|
|
|