plugins/withReleaseSigning.js
Rama main · réplica de lectura del repositorio original.
const fs = require('fs');
const path = require('path');
const { withAppBuildGradle } = require("expo/config-plugins");
// ===== 私有 release 签名注入(v0.11.0,审查 P0)=====
// 背景:prebuild 生成的 build.gradle 默认让 release 用 debug keystore 签名,
// 而 debug key 是公开已知的——他人可用同包名恶意 APK 覆盖安装用户设备。
// 本插件在 prebuild 时把 release 签名改为读取根目录 keystore.properties(不入库)。
//
// v0.11.8 加固(审查结论:插件本身是「静默失败」的):
// 1. 三处文本注入都断言「确实改动了内容」。此前只要 Expo 模板挪动一下缩进,
// .replace() 就匹配不到、静默不注入,release 包照常构建成功但仍是 debug 签名——
// 只有跑发布脚本时那道 apksigner 校验能发现,手动按 README 构建的路径完全没有拦截。
// 2. keystore.properties 缺失时:开发默认沿用 debug 并大声告警;
// 设了 TAPLEDGER_REQUIRE_RELEASE_SIGNING=1(发布脚本会设)则直接报错终止构建。
const MUST = (contents, before, pattern, replacement, what) => {
const next = contents.replace(pattern, replacement);
if (next === before) {
throw new Error(
`withReleaseSigning: ${what} 注入失败——build.gradle 模板与本插件预期不一致,` +
`请同步更新插件里的匹配规则(绝不能带着 debug 签名发 release)。`
);
}
return next;
};
const PROPS_LOADER = `
// TapLedger: release 签名配置(由 plugins/withReleaseSigning.js 注入,勿手改)
def tapledgerKeystorePropsFile = rootProject.file('../keystore.properties')
def tapledgerKeystoreProps = new Properties()
if (tapledgerKeystorePropsFile.exists()) {
tapledgerKeystorePropsFile.withInputStream { tapledgerKeystoreProps.load(it) }
}
`;
const RELEASE_SIGNING_CONFIG = `
if (tapledgerKeystorePropsFile.exists()) {
release {
storeFile file(tapledgerKeystoreProps.getProperty('storeFile'))
storePassword tapledgerKeystoreProps.getProperty('storePassword')
keyAlias tapledgerKeystoreProps.getProperty('keyAlias')
keyPassword tapledgerKeystoreProps.getProperty('keyPassword')
}
}`;
// 读取并校验 keystore.properties 的四个必填键(路径相对项目根目录)
function readSigningProps(projectRoot) {
const propsFile = path.join(projectRoot, 'keystore.properties');
if (!fs.existsSync(propsFile)) return null;
const text = fs.readFileSync(propsFile, 'utf8');
const props = {};
for (const line of text.split(/\r?\n/)) {
const m = line.match(/^\s*([A-Za-z]+)\s*=\s*(.*)$/);
if (m) props[m[1]] = m[2].trim();
}
const missing = ['storeFile', 'storePassword', 'keyAlias', 'keyPassword'].filter((k) => !props[k]);
if (missing.length > 0) {
throw new Error(`withReleaseSigning: keystore.properties 缺少 ${missing.join(', ')}`);
}
// storeFile 是相对 android/app 的路径,解析后确认密钥库文件真的存在
const storePath = path.resolve(projectRoot, 'android', 'app', props.storeFile);
if (!fs.existsSync(storePath)) {
throw new Error(
`withReleaseSigning: keystore.properties 指向的密钥库不存在:${storePath}` +
`(丢失 keystore 将无法给老用户推送升级,请先从备份恢复)`
);
}
return props;
}
module.exports = function withReleaseSigning(config) {
const projectRoot = path.resolve(__dirname, '..');
const requireSigning = process.env.TAPLEDGER_REQUIRE_RELEASE_SIGNING === '1';
const props = readSigningProps(projectRoot);
if (!props) {
const message =
'withReleaseSigning: 未找到 keystore.properties —— release 将回落到 debug 签名。' +
' 开发可以这样,但这个包绝对不能发布(同包名可被覆盖安装)。' +
' 发布请设 TAPLEDGER_REQUIRE_RELEASE_SIGNING=1(scripts/release-app.ps1 会自动设置)。';
if (requireSigning) throw new Error(message);
console.warn(`\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n!! ${message}\n!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n`);
}
return withAppBuildGradle(config, (cfg) => {
let contents = cfg.modResults.contents;
// 幂等:已注入过则跳过(prebuild --clean 重跑时防重复)
if (!contents.includes("tapledgerKeystorePropsFile")) {
// 1) android { 之前插入 properties 加载器
contents = MUST(
contents, contents,
/\r?\nandroid \{/,
`${PROPS_LOADER}\nandroid {`,
'keystore.properties 加载器',
);
// 2) signingConfigs 块内追加 release 配置(debug 块之后、闭合括号之前)
const before2 = contents;
contents = MUST(
contents, before2,
/(signingConfigs \{)([\s\S]*?)(\r?\n \})/,
`$1$2${RELEASE_SIGNING_CONFIG}$3`,
'signingConfigs.release 块',
);
// 3) release buildType 的签名从 debug 切到私有 key(缺配置时回落 debug)
contents = MUST(
contents, contents,
/signingConfig signingConfigs\.debug(\s*\r?\n\s*def enableShrinkResources)/,
"signingConfig tapledgerKeystorePropsFile.exists() ? signingConfigs.release : signingConfigs.debug$1",
'release buildType 签名切换',
);
}
cfg.modResults.contents = contents;
return cfg;
});
};