cadb
Version:
安卓/鸿蒙系统截图/录屏工具
687 lines (651 loc) • 22 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,
processFile,
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 bundleParentDir = path.join(process.cwd(), 'bundle_output');
const bundleDir = path.join(bundleParentDir, 'publish');
const androidBundlePath = path.join(bundleDir, 'common_android.js');
const androidHbcPath = path.join(bundleDir, 'common_android.hbc');
const iosBundlePath = path.join(bundleDir, 'common_ios.js');
const iosHbcPath = path.join(bundleDir, 'common_ios.hbc');
const expireDuration = 7200000;
/**
* exModuleMap: 缓存已知映射之外的频道名
* packCommandInfoList: 缓存频道的pack命令
*/
let rcConfig = {};
let productName = '';
let buildID = '';
// 枚举范围: android ios harmony all
let platform = '';
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();
// 执行rn项目打包
const packSuccess = await doRNPack(arg);
console.log('packSuccess', packSuccess);
if (!packSuccess) {
return;
}
// 仅鸿蒙平台压缩和上传
if (platform === 'harmony') {
// 压缩bundle文件
const zipFilePath = await compressFile();
if (!zipFilePath) {
return;
}
const url = await uploadFile(zipFilePath);
if (!url) {
logRed('压缩包上传失败');
return;
}
const pkgInfoSuccess = await uploadPackageInfo(url);
if (!pkgInfoSuccess) {
return;
}
printPkgUrlQRCode();
} else {
// 业务包不会压缩
if (isRnCommon()) {
const zipFilePath = await compressFile();
}
doPackPush();
// 安卓平台直接塞入app的app_ctripwebapp5目录
// do nothing
}
// ios什么也不干
}
function doPackPush() {
let cmd = 'cadb packpush';
console.log(`执行: ${cmd}`);
const childProcess = exec(cmd, shellConfigs);
const {stdout, stderr} = childProcess;
return new Promise(resolve => {
stdout.on('data', d => {
console.log(d);
});
stdout.on('end', d => {
logGreen('执行end');
resolve(true);
});
stderr.on('error', err => {
logRed(err);
resolve(false);
});
});
}
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);
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)}`);
}
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 =
productName === 'rn_common' &&
(platform === 'android' || platform === 'ios');
return new Promise(resolve => {
stdout.on('data', d => {
console.log(d);
// 注意 安卓/ios编译rn_common时, 打包成功标志在这里
if (noEndCallback) {
const bundlePath =
platform === 'android' ? androidBundlePath : iosBundlePath;
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});
const localHeadCommit = getHeadCommitId();
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} = checkResult;
logGreen(`\r\n\r\nok, 即将开始 ${pf} 平台的打包\r\n\r\n`);
return {packCommand, buID, pf};
}
// 检测并执行trip_hooks命令
function detectAndExeTripHooks() {
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 = '';
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;
return {buID, pf};
}
function compressFile() {
if (!isRnCommon()) {
return Promise.resolve('');
}
const outputDirPath = path.join(bundleParentDir, 'temp');
const bundleDirWithProductName = path.join(bundleParentDir, productName);
const outputFilePath = path.join(
bundleParentDir,
'temp',
`${productName}.ctz`,
);
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() {
return !(
productName !== 'rn_common' || !['android', 'ios'].includes(platform)
);
}
/**
* 暂时仅支持安卓/iOS平台的rn_common编译hbc
* 鸿蒙不编译hbc也能玩, 所以不做处理
*/
function hermesCompile() {
console.log(
`!!!!!hermesCompile, productName: ${productName}, platform: ${platform}`,
);
if (!isRnCommon()) {
return true;
}
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);
if (rnVersion !== '0.72.5') {
logRed('当前cadb脚本支持的rn版本为0.72.5');
process.exit();
return false;
}
console.log('npm root -g');
const rootNpmPath = execSync('npm root -g', shellConfigs);
console.log('rootNpmPath:', rootNpmPath);
const hermesPath = path.join(
rootNpmPath.trim(),
'@ctrip/crn-build/hermesvm/hermes-cli-darwin-v0.72.5/hermes',
);
const hbcPath = platform === 'android' ? androidHbcPath : iosHbcPath;
const bundlePath = platform === 'android' ? androidBundlePath : iosBundlePath;
const compileCommand = `${hermesPath} -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: '1035732',
// 先使用固定值, 后面若有问题再引导去mcd获取
hybridPackageInfoID: 28392232,
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包中, 关于mapping的console.log语句
* 否则会导致脚本执行失败
*/
function optimizeCrnPackScript() {
// /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;
}
const targetFilePath = path.join(
nodeModuleDir.trim(),
'@ctrip/crn-pack-v72/patch/metro/crn/crn-as-assets.js',
);
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);
}
/**
* 将zip文件移动到native工程中
*/
function moveZipFile2NativeProject(zipFilePath) {
const {nativeProjectPath} = rcConfig;
const webappPath = path.join(
nativeProjectPath,
'/entry/Phone/src/main/resources/rawfile/webapp/',
);
const files = fs.readdirSync(webappPath);
if (!files || files.length <= 0) {
logRed('webapp目录为空, 请至native工程执行clear_and_sync_project.sh脚本');
process.exit();
return;
}
const find = _.find(files, f => {
return f.startsWith('rn_common-');
});
if (!find) {
logRed(
'未找到rn_common文件, 请至native工程执行clear_and_sync_project.sh脚本',
);
process.exit();
return;
}
let bundleName = _.find(files, f => {
return f.toLowerCase().startsWith(productName.toLowerCase());
});
if (bundleName) {
fs.unlinkSync(path.join(webappPath, bundleName));
console.log(`\r\n已删除原有${bundleName}`);
}
// 全部使用较高的版本号, 保证不会被覆盖
let newName = `${productName}-82346193-89286710.ctz`;
if (/.+-(823.+)-(892.+).ctz/.test(bundleName)) {
// 版本号必须升一级, 否则覆盖安装时无法生效
const v1 = +RegExp.$1 + 1;
const v2 = +RegExp.$2 + 1;
newName = `${productName}-${v1}-${v2}.ctz`;
}
const newPath = path.join(webappPath, newName);
fs.renameSync(zipFilePath, newPath);
logGreen(`\r\n${zipFilePath} ----> ${newPath} 完成\r\n`);
}
async function loadNativeProjectPath() {
const {nativeProjectPath} = rcConfig;
if (!nativeProjectPath) {
console.log('\r\n请输入ztrip_harmony工程路径: ');
const projPath = await collectUserInput();
if (!fs.existsSync(projPath)) {
logRed('路径不存在');
process.exit();
return;
}
rcConfig.nativeProjectPath = projPath;
fs.writeFileSync(rcPath, JSON.stringify(rcConfig));
console.log(`nativeProjectPath --> ${projPath} 已写入缓存文件: ${rcPath}`);
}
console.log('native工程路径为: ', rcConfig.nativeProjectPath);
}
function modifyConfig() {
const {nativeProjectPath} = rcConfig;
const hvigorfile = path.join(nativeProjectPath, 'hvigorfile.ts');
// 1.注释掉PackageDownloadPlugin逻辑
processFile(hvigorfile, line => {
if (line.includes('PackageDownloadPlugin()') && !line.includes('//')) {
return `// ${line}`;
}
return line;
});
const download_hybrid_packages = path.join(
nativeProjectPath,
'/script/PackageDownload/download_hybrid_packages.py',
);
// 2.添加crn频道名
processFile(download_hybrid_packages, line => {
if (
line.includes('kRNWhiteListPackages=[') &&
!line.includes(productName)
) {
console.log('matched');
let newLine = line.replace('];', '');
let comma = ',';
if (line.endsWith(',')) {
comma = '';
}
newLine += `${comma}"${productName}"];`;
return newLine;
}
return line;
});
}