#!/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 -- " 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*]*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 =[\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' 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'' ) if entry in content: sys.exit(0) content = content.replace(' ', f'{entry}\n ') 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"