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.
146 lines
8.0 KiB
146 lines
8.0 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(推荐,省得每次记着手动跑):
|
|
# Product → Scheme → Edit Scheme → Build → Pre-actions → + New Run Script Action
|
|
# Provide build settings from: Runner
|
|
# 脚本内容: "$SRCROOT/sync_spm_platform.sh"
|
|
#
|
|
# ⚠️ 不依赖 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"
|
|
MANIFEST="$IOS_DIR/Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage/Package.swift"
|
|
|
|
CHECK_ONLY=0
|
|
[[ "${1:-}" == "--check" ]] && CHECK_ONLY=1
|
|
|
|
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) 对齐生成包 ───────────────────────────────────────────────────────────
|
|
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
|
|
|
|
CURRENT_RAW="$(grep -o '\.iOS([^)]*)' "$MANIFEST" | head -1 | sed 's/^\.iOS(//; s/)$//')"
|
|
CURRENT="$(normalize "$CURRENT_RAW")"
|
|
|
|
if [[ "$CURRENT" == "$CEILING" ]]; then
|
|
green "✓ 生成包已是 iOS ${CURRENT},无需改动"
|
|
exit 0
|
|
fi
|
|
|
|
if [[ $CHECK_ONLY -eq 1 ]]; then
|
|
red "✗ 生成包是 iOS ${CURRENT},应为 ${CEILING}(跑 ./ios/sync_spm_platform.sh 修)"
|
|
exit 1
|
|
fi
|
|
|
|
# 只替换 platforms 段里的第一个 .iOS(...),不碰文件其余内容
|
|
/usr/bin/sed -i '' "s/\.iOS($(printf '%s' "$CURRENT_RAW" | sed 's/[.[\*^$\/]/\\&/g'))/.iOS(\"$CEILING\")/" "$MANIFEST"
|
|
|
|
NEW="$(normalize "$(grep -o '\.iOS([^)]*)' "$MANIFEST" | head -1 | sed 's/^\.iOS(//; s/)$//')")"
|
|
if [[ "$NEW" != "$CEILING" ]]; then
|
|
red "✗ 改写失败,现在是 ${NEW}"; exit 1
|
|
fi
|
|
green "✓ 生成包平台版本 ${CURRENT} → ${CEILING},可以回 Xcode 构建了"
|
|
|