UNPKG

cadb

Version:

安卓/鸿蒙系统截图/录屏工具

635 lines (598 loc) 20.4 kB
/** * 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); }