// Expo 配置插件:在 prebuild 阶段注入 Android 正式签名配置 const { withAppBuildGradle, withDangerousMod, createRunOncePlugin } = require('@expo/config-plugins'); const fs = require('fs'); const path = require('path'); const PLUGIN_NAME = 'signing-config-plugin'; const PLUGIN_VERSION = '1.0.0'; function ensureSigningConfig(contents) { // 在 app/build.gradle 的 signingConfigs 中确保存在 release 配置(仅注入一次,具备幂等性) const startMarker = '// [signing-config-plugin] signingConfigs.release start'; const endMarker = '// [signing-config-plugin] signingConfigs.release end'; if (contents.includes(startMarker)) return contents; // 已注入,直接返回(幂等) const releaseConfig = `\n ${startMarker}\n release {\n def keystorePropsFile = rootProject.file("keystore.properties")\n if (!keystorePropsFile.exists()) {\n throw new GradleException("Missing keystore.properties at project root. Cannot sign release.")\n }\n def props = new Properties()\n keystorePropsFile.withInputStream { stream -> props.load(stream) }\n ["STORE_FILE", "STORE_PASSWORD", "KEY_ALIAS", "KEY_PASSWORD"].each { key ->\n if (!props.containsKey(key) || props.getProperty(key).trim().isEmpty()) {\n throw new GradleException("Missing required property '" + key + "' in keystore.properties")\n }\n }\n storeFile file(props.getProperty("STORE_FILE"))\n storePassword props.getProperty("STORE_PASSWORD")\n keyAlias props.getProperty("KEY_ALIAS")\n keyPassword props.getProperty("KEY_PASSWORD")\n }\n ${endMarker}\n`; // 精确定位 signingConfigs 区块并判断其中是否已有 release 子块,避免跨越到 buildTypes.release 的误判 const signingBlockMatch = contents.match(/signingConfigs\s*\{/); if (signingBlockMatch) { const openBraceIdx = signingBlockMatch.index + signingBlockMatch[0].length - 1; // '{' 的位置 // 通过括号计数找到对应的收尾 '}' 位置 let level = 0; let endIdx = -1; for (let i = openBraceIdx; i < contents.length; i++) { const ch = contents[i]; if (ch === '{') level++; else if (ch === '}') { level--; if (level === 0) { endIdx = i; break; } } } if (endIdx > -1) { const block = contents.slice(openBraceIdx, endIdx + 1); // 若 signingConfigs 区块内已包含 release 子块,则不再注入 if (/\brelease\s*\{/.test(block)) { return contents; } // 在 signingConfigs 区块起始处插入 release 配置 const insertIdx = signingBlockMatch.index + signingBlockMatch[0].length; return contents.slice(0, insertIdx) + releaseConfig + contents.slice(insertIdx); } } // 否则在 buildTypes 之前新建 signingConfigs 区块并插入 const buildTypesMatch = contents.match(/\n\s*buildTypes\s*\{/); if (buildTypesMatch) { const insert = `\n signingConfigs {\n${releaseConfig} }\n\n`; const idx = buildTypesMatch.index; return contents.slice(0, idx) + insert + contents.slice(idx); } return contents; // 兜底:不做任何修改 } function ensureBuildTypesReleasePointsToSigning(contents) { // 确保 buildTypes.release 使用 signingConfigs.release(幂等,不重复注入) const startMarker = '// [signing-config-plugin] buildTypes.release start'; const endMarker = '// [signing-config-plugin] buildTypes.release end'; if (contents.includes(startMarker)) return contents; // 已注入,直接返回(幂等) // 若已存在 signingConfig 指向 signingConfigs.release,则无需再处理 if (/buildTypes\s*\{[\s\S]*?release\s*\{[\s\S]*?signingConfig\s+signingConfigs\.release[\s\S]*?\}/m.test(contents)) { return contents; } // 若 release 区块存在: // - 已存在其它 signingConfig,则替换为指向 signingConfigs.release // - 不存在 signingConfig,则插入一条 const releaseBlockRegex = /(buildTypes\s*\{[\s\S]*?release\s*\{)([\s\S]*?)(\})/m; if (releaseBlockRegex.test(contents)) { return contents.replace(releaseBlockRegex, (m, a, mid, c) => { const hasAnySigning = /\bsigningConfig\s+signingConfigs\.[^\s]+/.test(mid); if (hasAnySigning) { // 替换首个 signingConfig 为 release,并用标记包裹,便于幂等 const replacedMid = mid.replace(/\bsigningConfig\s+signingConfigs\.[^\s]+/, `${startMarker}\n signingConfig signingConfigs.release\n ${endMarker}`); return a + replacedMid + c; } const inject = `\n ${startMarker}\n signingConfig signingConfigs.release\n ${endMarker}\n`; // 尽量靠前插入,避免与其他配置混在一起 return a + inject + mid + c; }); } // 若缺少 release 区块且存在 buildTypes,则新建 release 区块并注入 const buildTypesBlockRegex = /(buildTypes\s*\{)([\s\S]*?)(\})/m; if (buildTypesBlockRegex.test(contents)) { return contents.replace(buildTypesBlockRegex, (m, a, mid, c) => { const injectBlock = `\n release {\n ${startMarker}\n signingConfig signingConfigs.release\n ${endMarker}\n }\n`; return a + mid + injectBlock + c; }); } return contents; // 兜底:不做任何修改 } function ensureBuildTypesDebugPointsToSigning(contents) { // 确保 buildTypes.debug 使用 signingConfigs.release(幂等,不重复注入) const startMarker = '// [signing-config-plugin] buildTypes.debug start'; const endMarker = '// [signing-config-plugin] buildTypes.debug end'; if (contents.includes(startMarker)) return contents; // 已注入,直接返回(幂等) // 若 debug 已指向 signingConfigs.release,则无需处理 if (/buildTypes\s*\{[\s\S]*?debug\s*\{[\s\S]*?signingConfig\s+signingConfigs\.release[\s\S]*?\}/m.test(contents)) { return contents; } // 若 debug 区块存在: // - 已存在其它 signingConfig,则替换为指向 signingConfigs.release // - 不存在 signingConfig,则插入一条 const debugBlockRegex = /(buildTypes\s*\{[\s\S]*?debug\s*\{)([\s\S]*?)(\})/m; if (debugBlockRegex.test(contents)) { return contents.replace(debugBlockRegex, (m, a, mid, c) => { const hasAnySigning = /\bsigningConfig\s+signingConfigs\.[^\s]+/.test(mid); if (hasAnySigning) { const replacedMid = mid.replace(/\bsigningConfig\s+signingConfigs\.[^\s]+/, `${startMarker}\n signingConfig signingConfigs.release\n ${endMarker}`); return a + replacedMid + c; } const inject = `\n ${startMarker}\n signingConfig signingConfigs.release\n ${endMarker}\n`; return a + inject + mid + c; }); } // 若缺少 debug 区块且存在 buildTypes,则新建 debug 区块并注入 const buildTypesBlockRegex = /(buildTypes\s*\{)([\s\S]*?)(\})/m; if (buildTypesBlockRegex.test(contents)) { return contents.replace(buildTypesBlockRegex, (m, a, mid, c) => { const injectBlock = `\n debug {\n ${startMarker}\n signingConfig signingConfigs.release\n ${endMarker}\n }\n`; return a + mid + injectBlock + c; }); } return contents; // 兜底:不做任何修改 } function ensureHuaweiProguardRules(proguardPath) { // 向 proguard-rules.pro 追加 HMS 相关 keep 规则(幂等,多次追加会被拦截) const start = '// [signing-config-plugin] huawei proguard start'; const end = '// [signing-config-plugin] huawei proguard end'; let content = ''; try { content = fs.existsSync(proguardPath) ? fs.readFileSync(proguardPath, 'utf8') : ''; } catch {} if (content.includes(start)) return; // 已写入则不重复写 const rules = `\n${start}\n-ignorewarnings\n-keepattributes *Annotation*\n-keepattributes Exceptions\n-keepattributes InnerClasses\n-keepattributes Signature\n-keepattributes SourceFile,LineNumberTable\n-keep class com.hianalytics.android.** { *; }\n-keep class com.huawei.updatesdk.** { *; }\n-keep class com.huawei.hms.** { *; }\n${end}\n`; fs.writeFileSync(proguardPath, content + rules, 'utf8'); } const withSigningAssets = (config) => withDangerousMod(config, ['android', async (config) => { const projectRoot = config.modRequest.projectRoot; const pluginDir = path.join(projectRoot, 'NativePlugins', 'signing-config-plugin'); const srcProps = path.join(pluginDir, 'keystore.properties'); const srcKeystore = path.join(pluginDir, 'FLink.keystore'); const androidRoot = path.join(projectRoot, 'android'); const appDir = path.join(androidRoot, 'app'); const destProps = path.join(androidRoot, 'keystore.properties'); const destKeystore = path.join(appDir, 'FLink.keystore'); try { if (fs.existsSync(srcProps)) { fs.copyFileSync(srcProps, destProps); console.log(`[${PLUGIN_NAME}] 已复制 keystore.properties 到 android/`); } else { console.log(`[${PLUGIN_NAME}] 未找到 ${srcProps},跳过复制 keystore.properties`); } if (fs.existsSync(srcKeystore)) { fs.copyFileSync(srcKeystore, destKeystore); console.log(`[${PLUGIN_NAME}] 已复制 FLink.keystore 到 android/app/`); } else { console.log(`[${PLUGIN_NAME}] 未找到 ${srcKeystore},跳过复制 FLink.keystore`); } // 始终确保追加 HMS 相关 Proguard 规则(即使未启用 Proguard 也无影响) ensureHuaweiProguardRules(path.join(appDir, 'proguard-rules.pro')); } catch (e) { console.warn(`[${PLUGIN_NAME}] 复制签名文件或写入 Proguard 规则失败:`, e); } return config; }]); const withSigningConfig = (config) => { // 第一步:复制证书文件并追加 HMS Proguard 规则 config = withSigningAssets(config); // 第二步:仅在证书与属性文件同时存在时,才对 app/build.gradle 注入签名及 buildTypes 配置 return withAppBuildGradle(config, (config) => { const projectRoot = config.modRequest.projectRoot; const keystorePropsPath = path.join(projectRoot, 'android', 'keystore.properties'); const keystoreFilePath = path.join(projectRoot, 'android', 'app', 'FLink.keystore'); const hasProps = fs.existsSync(keystorePropsPath); const hasKeystore = fs.existsSync(keystoreFilePath); if (!hasProps || !hasKeystore) { console.log(`[${PLUGIN_NAME}] 未找到签名文件(keystore.properties 或 FLink.keystore),跳过 Gradle 注入。`); return config; // 保护性兜底:缺少文件则不修改 Gradle } let contents = config.modResults.contents; contents = ensureSigningConfig(contents); contents = ensureBuildTypesReleasePointsToSigning(contents); contents = ensureBuildTypesDebugPointsToSigning(contents); config.modResults.contents = contents; return config; }); }; module.exports = createRunOncePlugin(withSigningConfig, PLUGIN_NAME, PLUGIN_VERSION);