420 lines
13 KiB
Bash
Executable File
420 lines
13 KiB
Bash
Executable File
#!/bin/bash
|
||
# 创建新的 SDK 包
|
||
#
|
||
# 流程:
|
||
# 1. flutter create --template=plugin 生成完整 Flutter 插件骨架(含原生代码)
|
||
# 2. mason make clean_plugin_sdk 覆盖写入 Facade+Wiring 架构
|
||
# 3. 修正 pubspec.yaml 注入 resolution: workspace + 统一版本约束
|
||
# 4. 更新 IDE 配置 .iml + modules.xml
|
||
# 5. dart pub get
|
||
#
|
||
# 用法:
|
||
# melos run new:sdk -- push # 创建 packages/push_sdk/
|
||
# melos run new:sdk -- push_sdk # 同上,_sdk 后缀可省略
|
||
|
||
set -euo pipefail
|
||
|
||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||
|
||
if [[ $# -eq 0 ]]; then
|
||
echo "Usage: melos run new:sdk -- <name>"
|
||
echo "Example: melos run new:sdk -- push"
|
||
exit 1
|
||
fi
|
||
|
||
INPUT="$1"
|
||
BASE_NAME="${INPUT%_sdk}"
|
||
PKG_NAME="${BASE_NAME}_sdk"
|
||
|
||
PASCAL_NAME=$(python3 -c "
|
||
parts = '${BASE_NAME}'.split('_')
|
||
print(''.join(p.capitalize() for p in parts))
|
||
")
|
||
|
||
PKG_DIR="$ROOT_DIR/packages/$PKG_NAME"
|
||
|
||
if [[ -d "$PKG_DIR" ]]; then
|
||
echo "Error: packages/$PKG_NAME already exists"
|
||
exit 1
|
||
fi
|
||
|
||
# mason 路径解析:PATH 里找不到时 fallback 到 pub-cache 绝对路径
|
||
# setup.sh 把 mason 装到 ~/.pub-cache/bin,需要重启终端 PATH 才生效
|
||
if command -v mason &> /dev/null; then
|
||
MASON_CMD="mason"
|
||
elif [[ -x "$HOME/.pub-cache/bin/mason" ]]; then
|
||
MASON_CMD="$HOME/.pub-cache/bin/mason"
|
||
else
|
||
echo "Error: mason not found."
|
||
echo "Run 'bash scripts/setup.sh' to install required tools."
|
||
exit 1
|
||
fi
|
||
|
||
# ---- 失败回滚 ----
|
||
# set -e 触发退出时自动还原:删目录、撤 workspace 条目、撤 modules.xml 条目
|
||
_cleanup_on_error() {
|
||
local code=$?
|
||
[[ $code -eq 0 ]] && return 0
|
||
trap - EXIT # 防止函数内部错误触发递归
|
||
echo ""
|
||
echo "Error (exit $code). Rolling back..."
|
||
[[ -d "$PKG_DIR" ]] && { rm -rf "$PKG_DIR"; echo " Removed packages/$PKG_NAME"; }
|
||
python3 - "$ROOT_DIR/pubspec.yaml" "$PKG_NAME" << 'PY_ROLLBACK'
|
||
import sys
|
||
path, pkg = sys.argv[1], sys.argv[2]
|
||
with open(path) as f: lines = f.readlines()
|
||
lines = [l for l in lines if l != f' - packages/{pkg}\n']
|
||
with open(path, 'w') as f: f.writelines(lines)
|
||
PY_ROLLBACK
|
||
python3 - "$ROOT_DIR/.idea/modules.xml" "$PKG_NAME" << 'PY_ROLLBACK'
|
||
import sys, re
|
||
path, pkg = sys.argv[1], sys.argv[2]
|
||
with open(path) as f: content = f.read()
|
||
content = re.sub(rf'\s*<module[^>]*packages/{re.escape(pkg)}/[^>]*/>\n?', '', content)
|
||
with open(path, 'w') as f: f.write(content)
|
||
PY_ROLLBACK
|
||
echo " Rollback complete. Re-run after fixing the issue."
|
||
}
|
||
trap _cleanup_on_error EXIT
|
||
|
||
echo "Creating packages/$PKG_NAME (${PASCAL_NAME}SdkApi)..."
|
||
echo ""
|
||
|
||
# ---- Step 1: flutter create ----
|
||
echo "[1/5] flutter create --template=plugin..."
|
||
cd "$ROOT_DIR"
|
||
flutter create \
|
||
--template=plugin \
|
||
--platforms=ios,android,macos,windows \
|
||
--org com.example \
|
||
--project-name "$PKG_NAME" \
|
||
"packages/$PKG_NAME"
|
||
echo ""
|
||
|
||
# ---- Step 2: mason make ----
|
||
echo "[2/5] mason make clean_plugin_sdk (Facade+Wiring)..."
|
||
cd "$ROOT_DIR"
|
||
$MASON_CMD make clean_plugin_sdk \
|
||
--name "$PKG_NAME" \
|
||
--sdk_class "${PASCAL_NAME}Sdk" \
|
||
--output-dir "packages/$PKG_NAME" \
|
||
--on-conflict overwrite
|
||
echo ""
|
||
|
||
# ---- Step 3: 修正 pubspec.yaml ----
|
||
echo "[3/5] Patching pubspec.yaml..."
|
||
python3 - "$PKG_DIR/pubspec.yaml" "$ROOT_DIR" << 'PY'
|
||
import sys, re, os, glob
|
||
|
||
path, root_dir = sys.argv[1], sys.argv[2]
|
||
with open(path) as f:
|
||
content = f.read()
|
||
|
||
# 从根 pubspec.yaml 读取当前 Dart 版本约束
|
||
dart_version = '3.11.0'
|
||
root_pubspec = os.path.join(root_dir, 'pubspec.yaml')
|
||
with open(root_pubspec) as f:
|
||
root_content = f.read()
|
||
m = re.search(r'^\s*sdk:\s*\^([\d.]+)', root_content, re.MULTILINE)
|
||
if m:
|
||
dart_version = m.group(1)
|
||
|
||
# 从已有 plugin 包读取当前 Flutter 最低版本(取最大值,避免旧包的低版本干扰)
|
||
def _ver(s):
|
||
try:
|
||
return tuple(int(x) for x in s.split('.'))
|
||
except Exception:
|
||
return (0,)
|
||
|
||
flutter_version = '3.38.4'
|
||
for pkg_pubspec in glob.glob(os.path.join(root_dir, 'packages', '*', 'pubspec.yaml')):
|
||
with open(pkg_pubspec) as f:
|
||
pkg_content = f.read()
|
||
m = re.search(r"flutter:\s*'>=([^']+)'", pkg_content)
|
||
if m and _ver(m.group(1)) > _ver(flutter_version):
|
||
flutter_version = m.group(1)
|
||
|
||
# 1. 注入 resolution: workspace
|
||
# flutter create 新版不含 publish_to,按 publish_to → version → name 顺序降级匹配
|
||
if 'resolution: workspace' not in content:
|
||
if re.search(r'publish_to:[^\n]*\n', content):
|
||
content = re.sub(
|
||
r'(publish_to:[^\n]*\n)',
|
||
r'\1resolution: workspace\n',
|
||
content,
|
||
)
|
||
elif re.search(r'^version:[^\n]*\n', content, re.MULTILINE):
|
||
content = re.sub(
|
||
r'(^version:[^\n]*\n)',
|
||
r'\1resolution: workspace\n',
|
||
content,
|
||
flags=re.MULTILINE,
|
||
)
|
||
else:
|
||
content = re.sub(
|
||
r'(^name:[^\n]*\n)',
|
||
r'\1resolution: workspace\n',
|
||
content,
|
||
flags=re.MULTILINE,
|
||
)
|
||
|
||
# 2. 统一 Dart SDK 约束:'>=x.x.x <4.0.0' 或 ^x.x.x → 与主工程一致
|
||
# flutter create 新版直接生成 ^x.x.x,旧版生成 '>=x <y',两种格式都处理
|
||
content = re.sub(r"sdk:\s*(?:['\"]>=[\d.]+ <[\d.]+['\"]|\^[\d.]+)", f"sdk: ^{dart_version}", content)
|
||
|
||
# 3. 统一 Flutter 最低版本:与主工程已有包一致
|
||
content = re.sub(r'flutter:\s*["\']>=[\d.]+["\']', f"flutter: '>={flutter_version}'", content)
|
||
|
||
# 4. 统一 flutter_lints 版本
|
||
content = re.sub(r"flutter_lints:\s*\^[\d.]+", "flutter_lints: ^6.0.0", content)
|
||
|
||
# 5. 注入代码生成依赖(gen / gen:watch 靠 --depends-on="build_runner" 过滤,
|
||
# 没有 build_runner 的包会被跳过,@JsonSerializable / @freezed 等注解不会生成)
|
||
code_gen_deps = {
|
||
'build_runner': '^2.4.6',
|
||
'json_serializable': '^6.7.1',
|
||
'freezed': '^3.0.0',
|
||
}
|
||
# 从根 pubspec 读取实际版本(保持全局一致)
|
||
for dep, default_ver in code_gen_deps.items():
|
||
m = re.search(rf'^\s+{re.escape(dep)}:\s*(\^[\d.]+)', root_content, re.MULTILINE)
|
||
if m:
|
||
code_gen_deps[dep] = m.group(1)
|
||
|
||
for dep, ver in code_gen_deps.items():
|
||
if dep not in content:
|
||
content = re.sub(
|
||
r'(dev_dependencies:\n)',
|
||
rf'\1 {dep}: {ver}\n',
|
||
content,
|
||
)
|
||
|
||
with open(path, 'w') as f:
|
||
f.write(content)
|
||
PY
|
||
|
||
# example/pubspec.yaml 同步统一 Dart 版本(flutter create 生成的 example 有相同问题)
|
||
python3 - "$PKG_DIR/example/pubspec.yaml" "$ROOT_DIR" << 'PY'
|
||
import sys, re, os
|
||
|
||
path, root_dir = sys.argv[1], sys.argv[2]
|
||
if not os.path.exists(path):
|
||
sys.exit(0)
|
||
|
||
dart_version = '3.11.0'
|
||
root_pubspec = os.path.join(root_dir, 'pubspec.yaml')
|
||
with open(root_pubspec) as f:
|
||
root_content = f.read()
|
||
m = re.search(r'^\s*sdk:\s*\^([\d.]+)', root_content, re.MULTILINE)
|
||
if m:
|
||
dart_version = m.group(1)
|
||
|
||
with open(path) as f:
|
||
content = f.read()
|
||
content = re.sub(r"sdk:\s*(?:['\"]>=[\d.]+ <[\d.]+['\"]|\^[\d.]+)", f"sdk: ^{dart_version}", content)
|
||
with open(path, 'w') as f:
|
||
f.write(content)
|
||
PY
|
||
|
||
# ---- Step 3.5: 修正 podspec swift_version ----
|
||
# flutter create 默认生成 swift_version = '5.0',统一改为项目标准 6.2
|
||
for podspec in \
|
||
"$PKG_DIR/ios/${PKG_NAME}.podspec" \
|
||
"$PKG_DIR/macos/${PKG_NAME}.podspec"; do
|
||
if [[ -f "$podspec" ]]; then
|
||
sed -i '' "s/s.swift_version = '5.0'/s.swift_version = '6.2'/" "$podspec"
|
||
fi
|
||
done
|
||
|
||
# Flutter SDK 尚未完整标注 Swift 6 并发属性,用 @preconcurrency import 将
|
||
# FlutterMethodNotImplemented 等全局变量的并发警告降级,避免编译失败。
|
||
# 注意:不在类上加 @MainActor,否则与 FlutterPlugin 协议的 nonisolated 要求冲突,
|
||
# 导致 ConformanceIsolation 编译错误。
|
||
for swift_file in \
|
||
"$PKG_DIR/ios/Classes/${PASCAL_NAME}SdkPlugin.swift" \
|
||
"$PKG_DIR/macos/Classes/${PASCAL_NAME}SdkPlugin.swift"; do
|
||
if [[ -f "$swift_file" ]]; then
|
||
sed -i '' \
|
||
's/^import Flutter$/@preconcurrency import Flutter/' \
|
||
"$swift_file"
|
||
fi
|
||
done
|
||
|
||
# ---- Step 3.6: 替换 Android build.gradle → build.gradle.kts(Kotlin DSL + Compose) ----
|
||
# flutter create 默认生成 Groovy build.gradle,统一替换为 Kotlin DSL
|
||
ANDROID_DIR="$PKG_DIR/android"
|
||
[[ -f "$ANDROID_DIR/build.gradle" ]] && rm "$ANDROID_DIR/build.gradle"
|
||
|
||
cat > "$ANDROID_DIR/build.gradle.kts" << GRADLE_EOF
|
||
group = "com.example.${PKG_NAME}"
|
||
version = "1.0-SNAPSHOT"
|
||
|
||
buildscript {
|
||
val kotlinVersion = "2.2.20"
|
||
repositories {
|
||
google()
|
||
mavenCentral()
|
||
}
|
||
dependencies {
|
||
classpath("com.android.tools.build:gradle:8.11.1")
|
||
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:\$kotlinVersion")
|
||
}
|
||
}
|
||
|
||
allprojects {
|
||
repositories {
|
||
google()
|
||
mavenCentral()
|
||
}
|
||
}
|
||
|
||
plugins {
|
||
id("com.android.library")
|
||
id("org.jetbrains.kotlin.android")
|
||
// 若该 SDK 需要 Compose,取消注释下面两行,并在 dependencies 中加 compose runtime
|
||
// id("org.jetbrains.kotlin.plugin.compose")
|
||
}
|
||
|
||
android {
|
||
namespace = "com.example.${PKG_NAME}"
|
||
compileSdk = 36
|
||
|
||
compileOptions {
|
||
sourceCompatibility = JavaVersion.VERSION_17
|
||
targetCompatibility = JavaVersion.VERSION_17
|
||
}
|
||
|
||
sourceSets {
|
||
getByName("main") { java.srcDirs("src/main/kotlin") }
|
||
getByName("test") { java.srcDirs("src/test/kotlin") }
|
||
}
|
||
|
||
defaultConfig {
|
||
minSdk = 24
|
||
}
|
||
|
||
// 若该 SDK 需要 Compose,取消注释
|
||
// buildFeatures { compose = true }
|
||
|
||
testOptions {
|
||
unitTests {
|
||
isIncludeAndroidResources = true
|
||
all {
|
||
it.useJUnitPlatform()
|
||
it.outputs.upToDateWhen { false }
|
||
it.testLogging {
|
||
events("passed", "skipped", "failed", "standardOut", "standardError")
|
||
showStandardStreams = true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
kotlin {
|
||
compilerOptions {
|
||
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
|
||
}
|
||
}
|
||
|
||
dependencies {
|
||
// 若该 SDK 需要 Compose,取消注释以下两行并按需引入具体组件:
|
||
// val composeBom = platform("androidx.compose:compose-bom:2025.05.01")
|
||
// implementation(composeBom)
|
||
// implementation("androidx.compose.runtime:runtime")
|
||
testImplementation("org.jetbrains.kotlin:kotlin-test")
|
||
testImplementation("org.mockito:mockito-core:5.0.0")
|
||
}
|
||
GRADLE_EOF
|
||
|
||
# ---- Step 4: IDE 配置 ----
|
||
echo "[4/5] Updating IDE config..."
|
||
|
||
# .iml 文件
|
||
cat > "$PKG_DIR/melos_${PKG_NAME}.iml" << 'IML_EOF'
|
||
<?xml version="1.0" encoding="UTF-8"?>
|
||
<module type="WEB_MODULE" version="4">
|
||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||
<exclude-output />
|
||
<content url="file://$MODULE_DIR$">
|
||
<sourceFolder url="file://$MODULE_DIR$" isTestSource="false" />
|
||
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
|
||
<excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
|
||
<excludeFolder url="file://$MODULE_DIR$/.pub" />
|
||
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||
<excludeFolder url="file://$MODULE_DIR$/example/.dart_tool" />
|
||
<excludeFolder url="file://$MODULE_DIR$/example/.pub" />
|
||
<excludeFolder url="file://$MODULE_DIR$/example/build" />
|
||
</content>
|
||
<orderEntry type="sourceFolder" forTests="false" />
|
||
<orderEntry type="library" name="Dart SDK" level="project" />
|
||
<orderEntry type="library" name="Dart Packages" level="project" />
|
||
<orderEntry type="library" name="Flutter Plugins" level="project" />
|
||
</component>
|
||
</module>
|
||
IML_EOF
|
||
|
||
# 根 pubspec.yaml workspace
|
||
python3 - "$ROOT_DIR/pubspec.yaml" "$PKG_NAME" << 'PY'
|
||
import sys
|
||
|
||
pubspec_path, pkg_name = sys.argv[1], sys.argv[2]
|
||
with open(pubspec_path) as f:
|
||
lines = f.readlines()
|
||
|
||
new_entry = f' - packages/{pkg_name}\n'
|
||
if new_entry in lines:
|
||
sys.exit(0)
|
||
|
||
last_pkg_idx = -1
|
||
in_workspace = False
|
||
for i, line in enumerate(lines):
|
||
stripped = line.strip()
|
||
if stripped == 'workspace:':
|
||
in_workspace = True
|
||
elif in_workspace and stripped.startswith('- packages/'):
|
||
last_pkg_idx = i
|
||
elif in_workspace and stripped and not stripped.startswith('-'):
|
||
break
|
||
|
||
if last_pkg_idx >= 0:
|
||
lines.insert(last_pkg_idx + 1, new_entry)
|
||
|
||
with open(pubspec_path, 'w') as f:
|
||
f.writelines(lines)
|
||
PY
|
||
|
||
# .idea/modules.xml
|
||
python3 - "$ROOT_DIR/.idea/modules.xml" "$PKG_NAME" << 'PY'
|
||
import sys
|
||
|
||
modules_path, pkg_name = sys.argv[1], sys.argv[2]
|
||
with open(modules_path) as f:
|
||
content = f.read()
|
||
|
||
entry = (
|
||
f'<module fileurl="file://$PROJECT_DIR$/packages/{pkg_name}/melos_{pkg_name}.iml"'
|
||
f' filepath="$PROJECT_DIR$/packages/{pkg_name}/melos_{pkg_name}.iml"/>'
|
||
)
|
||
if entry in content:
|
||
sys.exit(0)
|
||
|
||
content = content.replace(' </modules>', f'{entry}\n </modules>')
|
||
with open(modules_path, 'w') as f:
|
||
f.write(content)
|
||
PY
|
||
|
||
# ---- Step 5: dart pub get ----
|
||
echo ""
|
||
echo "[5/5] dart pub get..."
|
||
cd "$ROOT_DIR" && dart pub get
|
||
|
||
echo ""
|
||
echo "Done. packages/$PKG_NAME is ready."
|
||
echo ""
|
||
echo "Next steps:"
|
||
echo " 1. Define API in lib/src/presentation/facade/${PKG_NAME}_api.dart"
|
||
echo " 2. Add domain entities under lib/src/domain/entities/"
|
||
echo " 3. Implement in lib/src/presentation/wiring/${PKG_NAME}_api_impl.dart"
|