cadb
Version:
安卓/鸿蒙系统截图/录屏工具
635 lines (598 loc) • 20.4 kB
JavaScript
/**
* Created by Niki on 2024/8/30 12:07.
* Email: m13296644326@163.com
*/
const {execSync, exec, execFile} = require('child_process');
const program = require('commander');
const fs = require('fs');
const {
shellConfigs,
logRed,
logGreen,
aliYunIp,
aliYunPort,
} = require('../common/constants');
// const aliYunIp = '192.168.31.130';
// const aliYunPort = '35404';
const path = require('path');
const {
getProjectRootPath,
collectUserInput,
getCurrModuleInfo,
createLocalId,
getHeadCommitId,
rcPath,
} = require('../common/utils');
const _ = require('lodash');
const {fetchStsConfig, upload2OSS} = require('../common/fileUpload');
const doTextFetch = require('../common/utils/doTextFetch');
const qrcode = require('qrcode-terminal');
const {
maybeParseXTaroName,
getValueIgnoreCase,
} = require('../common/utils/crnBundleUtil');
const {doPackPush} = require('./inner/packpush');
const bundleParentDir = path.join(process.cwd(), 'bundle_output');
const bundleDir = path.join(bundleParentDir, 'publish');
/**
* exModuleMap: 缓存已知映射之外的频道名
* packCommandInfoList: 缓存频道的pack命令
*/
let rcConfig = {};
let productName = '';
let buildID = '';
// 枚举范围: android ios harmony all
let platform = '';
let appId = '';
let isRN72 = false;
let isRN77 = false;
program
// 如果加了 -c 参数, 则不会使用rcConfig的缓存了
.option('-c --noCache', 'run command without rc cache')
.action(arg => {
run(arg);
})
.parse(process.argv);
// TODO: xTaro项目打包; 已验证可以直接打包; 建议更新文档
// TODO: 如何自定义配置和更改配置
// todo: 如果是harmony平台, 且原package.json不是harmony平台的, 则将package.harmony.json拷贝至package.json
// todo 依赖工具完全检测
// todo doRNPack内将exec改为span以解决日志转发问题
async function run(arg) {
// 检测脚手架依赖是否齐全
checkCli();
// 加载配置或要求用户写入配置
await loadRcConfig(arg);
optimizeCrnPackScript(false);
optimizeCrnPackScript(true);
parseRNVersion();
// 执行rn项目打包
const packSuccess = await doRNPack(arg);
console.log('packSuccess', packSuccess);
if (!packSuccess) {
return;
}
if (isRnCommon()) {
// const zipFilePath = await compressFile();
const compileSuccess = hermesCompile();
if (!compileSuccess) {
logRed('hermes编译失败');
return;
}
}
exePackPush();
}
async function loadRcConfig(arg) {
const {noCache} = arg;
if (!fs.existsSync(rcPath)) {
fs.writeFileSync(rcPath, JSON.stringify({}));
} else {
rcConfig = JSON.parse(fs.readFileSync(rcPath, 'utf-8'));
}
let {moduleName = ''} = getCurrModuleInfo();
// 如果是xTaro项目的moduleName, 则转换为crn频道名
moduleName = maybeParseXTaroName(moduleName);
/*if (moduleName === 'rn_taro_trainMain') {
moduleName = 'rn_taro_train_main';
}*/
console.log('moduleName', moduleName);
const {exModuleMap = {}} = rcConfig;
if (!moduleName.startsWith('rn_')) {
const moduleMaps = require('../config/module_map.json');
let prdName = getValueIgnoreCase(moduleMaps, moduleName);
// 如果配置了不使用缓存, 则不去exModuleMap查询了
if (!prdName && !noCache) {
prdName = getValueIgnoreCase(exModuleMap, moduleName);
if (!prdName) {
// 如果仍找不到, 则怀疑是xTaro项目
prdName = maybeParseXTaroName('', true);
}
}
if (!prdName) {
console.log('请输入crn频道名(比如rn_train、rn_setting): ');
prdName = await collectUserInput();
if (!prdName.startsWith('rn_')) {
logRed('crn频道名必须以rn_开头');
process.exit();
return;
}
exModuleMap[moduleName] = prdName;
rcConfig.exModuleMap = exModuleMap;
fs.writeFileSync(rcPath, JSON.stringify(rcConfig));
console.log(`${moduleName} --> ${prdName} 已写入缓存文件: ${rcPath}`);
}
productName = prdName;
} else {
productName = moduleName;
}
console.log(`crn频道名: ${productName}\r\n`);
}
function checkCli() {
console.log(`\r\ncrn: v${execSync('crn -v', shellConfigs)}`);
console.log(`crn-build: v${execSync('crn-build -v', shellConfigs)}`);
console.log(`crn-pack-v72: v${execSync('crn-pack-v72 -v', shellConfigs)}`);
console.log(`crn-pack-v77: v${execSync('crn-pack-v77 -v', shellConfigs)}`);
}
function isNoEndCallback() {
// 非rn_common必须监听end回调
if (productName !== 'rn_common') {
return false;
}
// 72版本, 鸿蒙可以监听end, 因为不会自动发起hermes编译
if (isRN72) {
return platform === 'android' || platform === 'ios';
} else {
// 77版本全平台都需要中断hermes编译
return true;
}
}
function getJsBundlePath() {
const lowName = platform.toLowerCase();
return path.join(bundleDir, `common_${lowName}.js`);
}
function getHbcBundlePath() {
const lowName = platform.toLowerCase();
return path.join(bundleDir, `common_${lowName}.hbc`);
}
async function doRNPack(arg) {
const {packCommand, buID, pf} = await collectPackCommand(arg);
buildID = buID;
platform = pf;
console.log('\r\n' + packCommand + '\r\n');
// 必须移除backup_bu、bundle_output、lockPackageJsonVersion和package-lock-before.json文件, 否则产生的bundle不对
// TODO: 弄清楚lockPackageJsonVersion文件的作用, 为何删除它会变慢; 能否比对package.json来决定是否删除?
const toDeleteFiles = [
'backup_bu',
'bundle_output',
'package-lock-before.json',
'package-lock.json',
'metro.config.js',
];
execSync('rm -rf ' + toDeleteFiles.join(' '), shellConfigs);
console.log('已删除: ', toDeleteFiles.join(' '));
console.log('platform:', platform);
console.log('\r\n');
detectAndExeTripHooks();
const childProcess = exec(packCommand, shellConfigs);
const {stdout, stderr} = childProcess;
// 如果是rn_common, 安卓和ios打包成功时不会触发end回调; hermes编译任务导致
const noEndCallback = isNoEndCallback();
return new Promise(resolve => {
stdout.on('data', d => {
console.log(d);
// 注意 编译rn_common时, 打包成功标志在这里
if (noEndCallback) {
const bundlePath = getJsBundlePath();
if (d.includes(bundlePath)) {
logGreen(platform + ' bundle打包成功');
resolve(true);
// 必须执行kill, 否则会被hermes编译任务卡住
childProcess.kill();
}
}
});
stdout.on('end', d => {
logGreen('执行end');
// 注意, 安卓/ios编译rn_common时, 不会触发end事件
if (!noEndCallback) {
const exists = fs.existsSync(bundleDir);
if (exists) {
logGreen(platform + ' bundle打包完毕');
resolve(true);
} else {
logRed(platform + ' bundle打包失败');
resolve(false);
}
}
});
stderr.on('error', err => {
logRed(err);
});
});
}
async function collectPackCommand(arg) {
const {noCache} = arg;
const {packCommandInfoList = []} = rcConfig;
let {moduleName = ''} = getCurrModuleInfo();
// 解析xTaro项目的moduleName
moduleName = maybeParseXTaroName(moduleName);
let packCommand;
const index = _.findIndex(packCommandInfoList, {moduleName});
// 获取项目的最新的commit作为打包命令的缓存键位; 若git仓库未创建则使用package.json的创建时间戳
const {headCommitId: localHeadCommit, isGit} = getHeadCommitId();
console.log(`localHeadCommit=${localHeadCommit}, isGit=${isGit}`);
const find = packCommandInfoList[index];
// 如果配置了不使用缓存, 则直接要求用户输入
if (!find || !find.command || noCache) {
console.log('\r\n');
logGreen('请输入crn频道的pack命令: ');
console.log(
'如何获取crn频道pack命令可参考: http://conf.ctripcorp.com/pages/viewpage.action?pageId=2813896385',
);
console.log('\r\n');
packCommand = await collectUserInput();
packCommand = formatPackCommand(packCommand);
if (!checkPackCommand(packCommand)) {
process.exit();
return {};
}
// 先删掉同名
_.remove(packCommandInfoList, {moduleName});
// 缓存起来
packCommandInfoList.push({
moduleName,
createTime: Date.now(),
command: packCommand,
headCommitId: localHeadCommit,
oldCommand: '',
});
rcConfig.packCommandInfoList = packCommandInfoList;
fs.writeFileSync(rcPath, JSON.stringify(rcConfig));
logGreen('pack命令已写入缓存');
} else {
const {command, headCommitId, oldCommand} = find;
// showCommand不可能为空
const showCommand = command || oldCommand;
console.log('\r\n上次使用的pack命令: ');
console.log(showCommand);
let commandNeedUpdate = false;
// 从来都没有进行过版本管理
if (!localHeadCommit) {
console.log('\r\n未检测到git仓库');
commandNeedUpdate = true;
} else if (!headCommitId) {
console.log('\r\n上次未缓存headCommitId');
commandNeedUpdate = true;
} else if (headCommitId !== localHeadCommit) {
console.log('\r\n当前git仓库headCommitId与缓存headCommitId不一致');
commandNeedUpdate = true;
}
// console.log('\r\n建议重新输入pack命令');
if (commandNeedUpdate) {
// 先将原有command置空
find.command = '';
find.oldCommand = showCommand;
const res = await collectPackCommand(arg);
packCommand = res.packCommand;
} else {
packCommand = showCommand;
}
}
// 再check一遍也无妨, 顺便拿到buID和pf
const checkResult = checkPackCommand(packCommand);
if (!checkResult) {
process.exit();
return {};
}
const {buID, pf, prdName} = checkResult;
productName = prdName;
console.log('修正后的crn频道名: ', productName);
logGreen(`\r\n\r\nok, 即将开始 ${pf} 平台的打包\r\n\r\n`);
return {packCommand, buID, pf};
}
/*
* 如果是sourceCode项目, 不执行trip_hooks命令
* 否则执行trip_hooks命令
* */
function detectAndExeTripHooks() {
const {headCommitId, isGit} = getHeadCommitId();
console.log(`localHeadCommit=${headCommitId}, isGit=${isGit}`);
if (!isGit) {
console.log('未检测到git仓库, 视为sourceCode项目, 跳过trip_hooks命令执行');
return;
}
const {moduleName = ''} = getCurrModuleInfo();
if (moduleName.endsWith('_sourcecode')) {
console.log('项目名检测为sourceCode项目, 跳过trip_hooks命令执行');
return;
}
const packageJsonPath = path.join(process.cwd(), 'package.json');
const jsonString = fs.readFileSync(packageJsonPath, 'utf-8');
const packageJson = JSON.parse(jsonString);
const {scripts} = packageJson;
const {trip_hooks} = scripts || {};
// android ios crn_all harmony
if (trip_hooks) {
console.log('检测到trip_hooks命令: ', trip_hooks);
const cmd = `export NODE_ENV_PLATFORM=${platform} && npm run trip_hooks`;
console.log(cmd);
const res = execSync(cmd, shellConfigs);
console.log(res);
}
}
// 支持以build,platform开头的命令
function formatPackCommand(packCommand) {
if (!packCommand) {
return '';
}
if (packCommand.startsWith('build,')) {
packCommand = `crn-build ${packCommand.replaceAll(',', ' ')}`;
}
// 必须携带--v5参数, 否则不会打包成.jsbundle
if (!packCommand.includes('--v5 ')) {
packCommand += ' --v5 true';
}
if (!packCommand.includes('--multipleVersion ')) {
packCommand += ' --multipleVersion false';
}
// 必须携带--ttf参数, 否则ttf icon不展示
if (!packCommand.includes('--ttf ')) {
packCommand += ' --ttf true';
}
if (!packCommand.includes('--hbc ')) {
packCommand += ' --hbc false';
}
if (!packCommand.includes('--lockfile ')) {
packCommand += ' --lockfile false';
}
return packCommand;
}
function checkPackCommand(packCommand) {
let buID = '';
let pf = '';
let prdName = '';
if (!packCommand.startsWith('crn-build build')) {
logRed('crn频道pack命令必须以 crn-build build 开头');
process.exit();
return null;
}
if (!/--buildID (\d+) /.test(packCommand)) {
logRed('crn频道pack命令必须包含 --buildID 参数');
process.exit();
return null;
}
buID = RegExp.$1;
if (!/--platform (.+?) /.test(packCommand)) {
logRed('crn频道pack命令必须包含 --platform 参数');
process.exit();
return null;
}
pf = RegExp.$1;
if (!/--product-name (.+?) /.test(packCommand)) {
// 进一步判断是否是rn_common打包
if (
packCommand.includes('--build-common true') &&
packCommand.includes('--entry-file crn_common_entry.js')
) {
prdName = 'rn_common';
} else {
logRed('crn频道pack命令必须包含 --product-name 参数');
process.exit();
return null;
}
} else {
prdName = RegExp.$1;
}
if (!/--appId (.+?) /.test(packCommand)) {
logRed('crn频道pack命令必须包含 --appId 参数');
process.exit();
return null;
}
appId = RegExp.$1;
return {buID, pf, prdName};
}
function exePackPush() {
doPackPush({
isCtrip: appId === '99999999',
isZhixing: appId === '1003',
isZhixingLight: appId === '5208',
productName,
platform,
});
}
function compressFile(force = false) {
if (!isRnCommon() && !force) {
return Promise.resolve('');
}
return new Promise(async (pRes, rej) => {
// 编译失败的话仅提示, 不阻断后续流程
const compileSuccess = hermesCompile();
if (!compileSuccess) {
console.log('hermes编译失败, 在安卓/ios工程中rn_common可能会加载失败!');
}
// 将publish目录改为频道名字, 否则bundle会加载失败
/*fs.renameSync(bundleDir, bundleDirWithProductName);
// 解析ctzstd的绝对路径
const ctzstdPath = path.resolve(getProjectRootPath(), 'libs', 'ctzstd');
execFile(
ctzstdPath,
['-c', bundleDirWithProductName, outputDirPath],
(error, stdout, stderr) => {
if (error) {
console.error(`Compression error: ${stderr}`, error);
pRes('');
return;
}
pRes(outputFilePath);
console.log(`File compressed successfully: ${stdout}`);
fs.renameSync(bundleDirWithProductName, bundleDir);
},
);*/
});
}
function isRnCommon() {
if (isRN72) {
return !(
productName !== 'rn_common' || !['android', 'ios'].includes(platform)
);
}
return productName === 'rn_common';
}
function parseRNVersion() {
const packageJsonPath = path.join(process.cwd(), 'package.json');
const jsonString = fs.readFileSync(packageJsonPath, 'utf-8');
const packageJson = JSON.parse(jsonString);
const {dependencies, devDependencies} = packageJson;
const rnVersion =
dependencies['react-native'] || devDependencies['react-native'];
console.log('react-native version: ', rnVersion);
isRN72 = rnVersion === '0.72.5';
isRN77 = rnVersion === '0.77.1';
}
function getHermesCliPath() {
if (!isRN72 && !isRN77) {
logRed('当前cadb脚本支持的rn版本为0.72.5和0.77.1');
process.exit();
return false;
}
console.log('npm root -g');
const rootNpmPath = execSync('npm root -g', shellConfigs);
console.log('rootNpmPath:', rootNpmPath);
if (isRN72) {
return path.join(
rootNpmPath.trim(),
'@ctrip/crn-build/hermesvm/hermes-cli-darwin-v0.72.5/hermes',
);
}
return path.join(
rootNpmPath.trim(),
'@ctrip/crn-build/hermesvm/hermes-cli-darwin-v0.77.1/hermes',
);
}
/**
* 手动发起hermes编译
*/
function hermesCompile() {
console.log(
`!!!!!hermesCompile, productName: ${productName}, platform: ${platform}`,
);
const hbcPath = getHbcBundlePath();
const bundlePath = getJsBundlePath();
const compileCommand = `${getHermesCliPath()} -emit-binary -out ${hbcPath} ${bundlePath}`;
console.log('hermes compile command:', compileCommand);
execSync(compileCommand, {
...shellConfigs,
maxBuffer: 1024 * 1024 * 20,
});
const exists = fs.existsSync(hbcPath);
if (exists) {
logGreen('\r\nhermes编译成功\r\n');
return true;
} else {
logRed('\r\nhermes编译失败\r\n');
return false;
}
}
async function uploadFile(filePath) {
if (!fs.existsSync(filePath)) {
logRed('路径不存在');
return '';
}
const stsConfig = await fetchStsConfig();
const url = await upload2OSS(filePath, stsConfig);
// console.log('上传成功, url:\r\n', url);
console.log('压缩包上传成功');
return url;
}
async function uploadPackageInfo(url) {
const content = {
productName,
increFlag: 0,
pkgURL: url,
packageType: 'ReactNative',
productCode: productName,
// 先使用固定值, 后面若有问题在打日志查看
packageID: '99035732',
// 先使用固定值, 后面若有问题再引导去mcd获取
hybridPackageInfoID: 98392232,
buildID,
size: 473244,
signCode:
'KGIi45CMNU7T8XABPdrNseBmKJa/wRqmpRG9O3iPIVyej0FVUrJBP5/oWSaG1aeMmr5CFG1mF+QX\\nN6pAt7NtGvzVh0eHasnMn1Nfh68zDDYp6ilGM8W/2lUojRKq60bXjhcYxxf/TC1+v3wnSG/VaTq8\\nZJXfawo65u+8CYHMfKs=\\n',
zstdSignCode:
'IfIuqcqsBMwUNyC+AaiRImm01GQBB0aWkgArlJwLDam3wQ+D3TWfEJ9LWgcXFAp3GKeUDQZ3AuxA\\nPdtpgjRDpZqCjkAL7cjuVeCsINm7372VGuuBX/lUSBIhPu3bKlYoWIFdWO1w8joFBkL3dmxajCm9\\nlV3lQQueyVWZUbq/ahI=\\n',
};
const info = {
result: JSON.stringify({data: [content], success: true}),
responseStatus: {
Ack: 'Success',
},
};
// 以 频道名_电脑mac地址 作为唯一id
const uniqueId = createLocalId(productName);
console.log('uniqueId:', uniqueId);
const map = encodeURIComponent(JSON.stringify(info));
console.log('\r\nmap参数', JSON.stringify(info, null, 2));
const fetchUrl = `http://${aliYunIp}:${aliYunPort}/set-cache?platform=1&uniqueId=${uniqueId}&map=${map}`;
try {
await doTextFetch(fetchUrl);
logGreen('package信息上传成功');
return true;
} catch (err) {
logRed('package信息上传失败:', err);
}
return false;
}
function printPkgUrlQRCode() {
const uniqueId = createLocalId(productName);
const fakeProp = 'platform=1&getMcdPublishPreviewData=1';
const pkdInfoUrl = `http://${aliYunIp}:${aliYunPort}/get-cache?uniqueId=${uniqueId}&${fakeProp}`;
console.log('package信息url:', pkdInfoUrl);
const qrText = `trip-dev://wireless/newMCD?url=${encodeURIComponent(
pkdInfoUrl,
)}&_scanPlatform=ReactNative&_moduleName=${productName}`;
console.log('\r\n二维码文本:');
console.log(qrText);
console.log(
`\r\n${platform}平台${productName} bundle已打包成功并上传完毕!\r\n`,
);
logGreen('\r\n请打开app扫码: \r\n');
// 在终端输出二维码
qrcode.generate(qrText, {small: true}, qrBinary => {
console.log(qrBinary);
});
}
/**
* 注释掉本地crn-pack-v72和crn-pack-v77包中, 关于mapping的console.log语句
* 否则会导致脚本执行失败
*/
function optimizeCrnPackScript(isRN077) {
// /Users/likai/.nvm/versions/node/v18.12.1/lib/node_modules/@ctrip/crn-pack-v72/patch/metro/crn/crn-as-assets.js
const nodeModuleDir = execSync('npm root -g', shellConfigs);
if (!nodeModuleDir) {
console.log('npm root -g 命令执行失败');
return;
}
let absPath = '@ctrip/crn-pack-v72/patch/metro/crn/crn-as-assets.js';
if (isRN077) {
absPath = '@ctrip/crn-pack-v77/patch/metro/crn/crn-as-assets.js';
}
const targetFilePath = path.join(nodeModuleDir.trim(), absPath);
if (!fs.existsSync(targetFilePath)) {
console.log(`未找到文件: ${targetFilePath}`);
return;
}
let crnPackScript = fs.readFileSync(targetFilePath, 'utf-8');
const code1 = 'console.log(JSON.stringify(buAllMapping))';
const code2 = 'console.log(JSON.stringify(buMapping))';
if (crnPackScript.includes(code1) && !crnPackScript.includes(`// ${code1}`)) {
crnPackScript = crnPackScript.replace(code1, `// ${code1}`);
console.log(`已注释掉 ${code1}`);
}
if (crnPackScript.includes(code2) && !crnPackScript.includes(`// ${code2}`)) {
crnPackScript = crnPackScript.replace(code2, `// ${code2}`);
console.log(`已注释掉 ${code2}`);
console.log(`${targetFilePath} 已优化`);
}
fs.writeFileSync(targetFilePath, crnPackScript);
}