brands-monorepo/NativePlugins/signing-config-plugin/app.plugin.js
2025-12-30 19:46:48 +08:00

212 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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);