UNPKG

cadb

Version:

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

687 lines (651 loc) 22 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, 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; }); }