212 lines
11 KiB
JavaScript
212 lines
11 KiB
JavaScript
// 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); |