2024 lines
103 KiB
JavaScript
2024 lines
103 KiB
JavaScript
"use strict";
|
||
var __importDefault = (this && this.__importDefault) || function (mod) {
|
||
return (mod && mod.__esModule) ? mod : { "default": mod };
|
||
};
|
||
Object.defineProperty(exports, "__esModule", { value: true });
|
||
const child_process_1 = require("child_process");
|
||
const util_1 = require("util");
|
||
const path_1 = __importDefault(require("path"));
|
||
const fs_1 = __importDefault(require("fs"));
|
||
const Logger_1 = __importDefault(require("../utils/Logger"));
|
||
const CloudflareShareService_1 = __importDefault(require("./CloudflareShareService"));
|
||
const os_1 = require("os");
|
||
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
||
/**
|
||
* APK构建服务
|
||
*/
|
||
class APKBuildService {
|
||
constructor() {
|
||
this.isBuilding = false;
|
||
this.buildProgress = '';
|
||
this.buildStatus = {
|
||
isBuilding: false,
|
||
progress: 0,
|
||
message: '未开始构建',
|
||
success: false
|
||
};
|
||
// 构建日志记录
|
||
this.buildLogs = [];
|
||
this.MAX_LOG_ENTRIES = 1000; // 最多保存1000条日志
|
||
this.logger = new Logger_1.default('APKBuildService');
|
||
this.cloudflareService = new CloudflareShareService_1.default();
|
||
}
|
||
/**
|
||
* 添加构建日志
|
||
*/
|
||
addBuildLog(level, message) {
|
||
const logEntry = {
|
||
timestamp: Date.now(),
|
||
level,
|
||
message
|
||
};
|
||
this.buildLogs.push(logEntry);
|
||
// 限制日志数量,保留最新的
|
||
if (this.buildLogs.length > this.MAX_LOG_ENTRIES) {
|
||
this.buildLogs = this.buildLogs.slice(-this.MAX_LOG_ENTRIES);
|
||
}
|
||
// 同时输出到控制台
|
||
switch (level) {
|
||
case 'info':
|
||
this.logger.info(`[构建日志] ${message}`);
|
||
break;
|
||
case 'warn':
|
||
this.logger.warn(`[构建日志] ${message}`);
|
||
break;
|
||
case 'error':
|
||
this.logger.error(`[构建日志] ${message}`);
|
||
break;
|
||
case 'success':
|
||
this.logger.info(`[构建日志] ✅ ${message}`);
|
||
break;
|
||
}
|
||
}
|
||
/**
|
||
* 获取构建日志
|
||
*/
|
||
getBuildLogs(limit) {
|
||
let logs = [...this.buildLogs];
|
||
if (limit && limit > 0) {
|
||
logs = logs.slice(-limit);
|
||
}
|
||
return logs.map(log => ({
|
||
...log,
|
||
timeString: new Date(log.timestamp).toLocaleString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
second: '2-digit',
|
||
hour12: false
|
||
})
|
||
}));
|
||
}
|
||
/**
|
||
* 清空构建日志
|
||
*/
|
||
clearBuildLogs() {
|
||
this.buildLogs = [];
|
||
this.addBuildLog('info', '构建日志已清空');
|
||
}
|
||
/**
|
||
* 检查是否有可用的APK
|
||
*/
|
||
async checkExistingAPK(enableEncryption, encryptionLevel, customFileName) {
|
||
try {
|
||
// 查找 build_output 目录(apktool打包的输出目录)
|
||
const buildOutputDir = path_1.default.join(process.cwd(), 'android/build_output');
|
||
if (!fs_1.default.existsSync(buildOutputDir)) {
|
||
return { exists: false };
|
||
}
|
||
// 如果指定了自定义文件名,优先查找
|
||
if (customFileName?.trim()) {
|
||
const customApkName = `${customFileName.trim()}.apk`;
|
||
const customApkPath = path_1.default.join(buildOutputDir, customApkName);
|
||
if (fs_1.default.existsSync(customApkPath)) {
|
||
const stats = fs_1.default.statSync(customApkPath);
|
||
this.logger.info(`找到自定义命名的APK文件: ${customApkName}`);
|
||
return {
|
||
exists: true,
|
||
path: customApkPath,
|
||
filename: customApkName,
|
||
size: stats.size,
|
||
buildTime: stats.mtime
|
||
};
|
||
}
|
||
}
|
||
// 查找所有APK文件
|
||
const files = fs_1.default.readdirSync(buildOutputDir);
|
||
const apkFiles = files.filter(f => f.endsWith('.apk'));
|
||
if (apkFiles.length > 0) {
|
||
// 按修改时间排序,返回最新的
|
||
const apkFilesWithStats = apkFiles.map(f => {
|
||
const apkPath = path_1.default.join(buildOutputDir, f);
|
||
return {
|
||
filename: f,
|
||
path: apkPath,
|
||
stats: fs_1.default.statSync(apkPath)
|
||
};
|
||
}).sort((a, b) => b.stats.mtime.getTime() - a.stats.mtime.getTime());
|
||
const latestApk = apkFilesWithStats[0];
|
||
this.logger.info(`找到APK文件: ${latestApk.filename}`);
|
||
return {
|
||
exists: true,
|
||
path: latestApk.path,
|
||
filename: latestApk.filename,
|
||
size: latestApk.stats.size,
|
||
buildTime: latestApk.stats.mtime
|
||
};
|
||
}
|
||
return { exists: false };
|
||
}
|
||
catch (error) {
|
||
this.logger.error('检查APK失败:', error);
|
||
return { exists: false };
|
||
}
|
||
}
|
||
/**
|
||
* 构建APK(使用apktool重新打包反编译目录)
|
||
*/
|
||
async buildAPK(serverUrl, options) {
|
||
if (this.buildStatus.isBuilding) {
|
||
return {
|
||
success: false,
|
||
message: '正在构建中,请稍候...'
|
||
};
|
||
}
|
||
// 使用setImmediate确保异步执行,避免阻塞
|
||
return new Promise((resolve, reject) => {
|
||
setImmediate(async () => {
|
||
try {
|
||
await this._buildAPKInternal(serverUrl, options, resolve, reject);
|
||
}
|
||
catch (error) {
|
||
this.logger.error('构建APK内部错误:', error);
|
||
this.addBuildLog('error', `构建过程发生未捕获的错误: ${error.message}`);
|
||
this.addBuildLog('error', `错误堆栈: ${error.stack}`);
|
||
this.buildStatus.isBuilding = false;
|
||
reject(error);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
/**
|
||
* 内部构建方法
|
||
*/
|
||
async _buildAPKInternal(serverUrl, options, resolve, reject) {
|
||
try {
|
||
// 清空之前的日志,开始新的构建
|
||
this.buildLogs = [];
|
||
// 记录构建开始
|
||
this.addBuildLog('info', '========== 开始构建APK ==========');
|
||
this.addBuildLog('info', `服务器地址: ${serverUrl}`);
|
||
if (options?.webUrl) {
|
||
this.addBuildLog('info', `Web地址: ${options.webUrl}`);
|
||
}
|
||
this.addBuildLog('info', `配置遮盖: ${options?.enableConfigMask ? '启用' : '禁用'}`);
|
||
this.addBuildLog('info', `进度条: ${options?.enableProgressBar ? '启用' : '禁用'}`);
|
||
if (options?.pageStyleConfig?.appName) {
|
||
this.addBuildLog('info', `应用名称: ${options.pageStyleConfig.appName}`);
|
||
}
|
||
if (options?.pageStyleConfig?.apkFileName) {
|
||
this.addBuildLog('info', `APK文件名: ${options.pageStyleConfig.apkFileName}`);
|
||
}
|
||
this.buildStatus = {
|
||
isBuilding: true,
|
||
progress: 0,
|
||
message: '开始构建APK...',
|
||
success: false
|
||
};
|
||
this.addBuildLog('info', '开始构建APK...');
|
||
// 检查构建环境(只需要Java,不需要Gradle和Android项目)
|
||
this.addBuildLog('info', '检查构建环境...');
|
||
const envCheck = await this.checkBuildEnvironment();
|
||
if (!envCheck.hasJava) {
|
||
this.addBuildLog('error', '构建环境检查失败: Java未安装或未在PATH中');
|
||
this.buildStatus.isBuilding = false;
|
||
resolve({
|
||
success: false,
|
||
message: '构建环境不完整,请检查 Java 环境'
|
||
});
|
||
return;
|
||
}
|
||
this.addBuildLog('success', `Java环境检查通过: ${envCheck.javaVersion || '已安装'}`);
|
||
// 检查apktool和source.apk
|
||
const apktoolPath = path_1.default.join(process.cwd(), 'android/apktool.jar');
|
||
const sourceApkFile = path_1.default.join(process.cwd(), 'android/source.apk');
|
||
const sourceApkPath = path_1.default.join(process.cwd(), 'android/source_apk');
|
||
if (!fs_1.default.existsSync(apktoolPath)) {
|
||
this.addBuildLog('error', 'apktool不存在: android/apktool.jar');
|
||
this.buildStatus.isBuilding = false;
|
||
resolve({
|
||
success: false,
|
||
message: 'apktool 不存在: android/apktool.jar'
|
||
});
|
||
return;
|
||
}
|
||
this.addBuildLog('success', 'apktool检查通过');
|
||
if (!fs_1.default.existsSync(sourceApkFile)) {
|
||
this.addBuildLog('error', 'source.apk文件不存在: android/source.apk');
|
||
this.buildStatus.isBuilding = false;
|
||
resolve({
|
||
success: false,
|
||
message: 'source.apk 文件不存在: android/source.apk'
|
||
});
|
||
return;
|
||
}
|
||
this.addBuildLog('success', 'source.apk文件检查通过');
|
||
// 清理并反编译source.apk
|
||
this.buildStatus.progress = 5;
|
||
this.buildStatus.message = '清理并反编译source.apk...';
|
||
this.addBuildLog('info', '开始清理并反编译source.apk...');
|
||
// 删除source_apk目录(如果存在)
|
||
if (fs_1.default.existsSync(sourceApkPath)) {
|
||
this.addBuildLog('info', '删除旧的source_apk目录...');
|
||
await this.deleteDirectoryWithRetry(sourceApkPath, 3);
|
||
this.addBuildLog('success', '旧的source_apk目录已删除');
|
||
}
|
||
// 反编译source.apk到source_apk目录
|
||
this.addBuildLog('info', '开始反编译source.apk...');
|
||
const decompileResult = await this.decompileAPK(sourceApkFile, sourceApkPath, apktoolPath);
|
||
if (!decompileResult.success) {
|
||
this.addBuildLog('error', `反编译失败: ${decompileResult.message}`);
|
||
this.buildStatus.isBuilding = false;
|
||
resolve({
|
||
success: false,
|
||
message: `反编译失败: ${decompileResult.message}`
|
||
});
|
||
return;
|
||
}
|
||
this.addBuildLog('success', 'source.apk反编译完成');
|
||
this.buildStatus.progress = 10;
|
||
this.buildStatus.message = '更新服务器配置...';
|
||
this.addBuildLog('info', '更新服务器配置...');
|
||
// 更新反编译目录中的服务器配置
|
||
await this.writeServerConfigToSourceApk(sourceApkPath, serverUrl, options);
|
||
this.addBuildLog('success', '服务器配置更新完成');
|
||
this.buildStatus.progress = 20;
|
||
this.buildStatus.message = '处理应用图标...';
|
||
// 处理应用图标(如果有上传)
|
||
if (options?.pageStyleConfig?.appIconFile) {
|
||
this.addBuildLog('info', '处理应用图标...');
|
||
await this.updateAppIconInSourceApk(sourceApkPath, options.pageStyleConfig.appIconFile);
|
||
this.addBuildLog('success', '应用图标更新完成');
|
||
}
|
||
else {
|
||
this.addBuildLog('info', '未上传应用图标,跳过图标更新');
|
||
}
|
||
this.buildStatus.progress = 30;
|
||
this.buildStatus.message = '更新应用名称...';
|
||
// 更新应用名称(如果有配置)
|
||
if (options?.pageStyleConfig?.appName) {
|
||
this.addBuildLog('info', `更新应用名称为: ${options.pageStyleConfig.appName}`);
|
||
await this.updateAppNameInSourceApk(sourceApkPath, options.pageStyleConfig.appName);
|
||
this.addBuildLog('success', '应用名称更新完成');
|
||
}
|
||
else {
|
||
this.addBuildLog('info', '未配置应用名称,跳过名称更新');
|
||
}
|
||
this.buildStatus.progress = 40;
|
||
this.buildStatus.message = '更新页面样式配置...';
|
||
// 更新页面样式配置
|
||
if (options?.pageStyleConfig) {
|
||
this.addBuildLog('info', '更新页面样式配置...');
|
||
await this.updatePageStyleConfigInSourceApk(sourceApkPath, options.pageStyleConfig);
|
||
this.addBuildLog('success', '页面样式配置更新完成');
|
||
}
|
||
this.buildStatus.progress = 45;
|
||
this.buildStatus.message = '生成随机包名...';
|
||
this.addBuildLog('info', '生成随机包名...');
|
||
// 生成随机包名并修改
|
||
const randomPackageName = this.generateRandomPackageName();
|
||
this.addBuildLog('info', `随机包名: ${randomPackageName}`);
|
||
await this.changePackageName(sourceApkPath, 'com.hikoncont', randomPackageName);
|
||
this.addBuildLog('success', `包名已修改为: ${randomPackageName}`);
|
||
this.buildStatus.progress = 47;
|
||
this.buildStatus.message = '生成随机版本号...';
|
||
this.addBuildLog('info', '生成随机版本号...');
|
||
// 生成随机版本号并修改
|
||
const randomVersion = this.generateRandomVersion();
|
||
this.addBuildLog('info', `随机版本号: versionCode=${randomVersion.versionCode}, versionName=${randomVersion.versionName}`);
|
||
await this.changeVersion(sourceApkPath, randomVersion.versionCode, randomVersion.versionName);
|
||
this.addBuildLog('success', `版本号已修改为: ${randomVersion.versionName} (${randomVersion.versionCode})`);
|
||
this.buildStatus.progress = 50;
|
||
this.buildStatus.message = '使用apktool重新打包APK...';
|
||
this.addBuildLog('info', '开始使用apktool重新打包APK...');
|
||
// 使用apktool重新打包
|
||
const buildResult = await this.rebuildAPKWithApktool(sourceApkPath, apktoolPath, options?.pageStyleConfig?.apkFileName);
|
||
if (buildResult.success) {
|
||
this.addBuildLog('success', `APK打包成功: ${buildResult.filename}`);
|
||
this.buildStatus.progress = 80;
|
||
this.buildStatus.message = '签名APK...';
|
||
this.addBuildLog('info', '开始签名APK...');
|
||
// 签名APK
|
||
const signedApkPath = await this.signAPK(buildResult.apkPath, buildResult.filename);
|
||
if (!signedApkPath) {
|
||
this.addBuildLog('error', 'APK签名失败');
|
||
this.buildStatus.isBuilding = false;
|
||
resolve({
|
||
success: false,
|
||
message: 'APK签名失败'
|
||
});
|
||
return;
|
||
}
|
||
this.buildStatus.progress = 90;
|
||
this.buildStatus.message = '生成分享链接...';
|
||
this.addBuildLog('info', '生成分享链接...');
|
||
// 🚀 自动生成Cloudflare分享链接
|
||
const apkPath = signedApkPath;
|
||
const filename = buildResult.filename;
|
||
const shareResult = await this.cloudflareService.createShareLink(apkPath, filename, 10 // 10分钟有效期
|
||
);
|
||
this.buildStatus.progress = 100;
|
||
if (shareResult.success) {
|
||
this.addBuildLog('success', `分享链接生成成功: ${shareResult.shareUrl}`);
|
||
this.addBuildLog('success', '========== 构建完成 ==========');
|
||
this.buildStatus.message = `构建完成!分享链接已生成,有效期10分钟`;
|
||
this.buildStatus.success = true;
|
||
this.buildStatus.shareUrl = shareResult.shareUrl;
|
||
this.buildStatus.shareSessionId = shareResult.sessionId;
|
||
this.buildStatus.shareExpiresAt = shareResult.expiresAt;
|
||
resolve({
|
||
success: true,
|
||
message: '构建完成并生成分享链接',
|
||
filename,
|
||
shareUrl: shareResult.shareUrl,
|
||
shareExpiresAt: shareResult.expiresAt,
|
||
sessionId: shareResult.sessionId
|
||
});
|
||
}
|
||
else {
|
||
this.addBuildLog('warn', `分享链接生成失败: ${shareResult.error}`);
|
||
this.addBuildLog('success', '========== 构建完成(分享链接生成失败)==========');
|
||
this.buildStatus.message = `构建完成,但生成分享链接失败: ${shareResult.error}`;
|
||
this.buildStatus.success = true;
|
||
resolve({
|
||
success: true,
|
||
message: '构建完成,但分享链接生成失败',
|
||
filename,
|
||
shareError: shareResult.error
|
||
});
|
||
}
|
||
}
|
||
else {
|
||
this.addBuildLog('error', `APK打包失败: ${buildResult.message}`);
|
||
this.addBuildLog('error', '========== 构建失败 ==========');
|
||
this.buildStatus.isBuilding = false;
|
||
resolve(buildResult);
|
||
}
|
||
}
|
||
catch (error) {
|
||
this.addBuildLog('error', `构建过程发生异常: ${error.message}`);
|
||
this.addBuildLog('error', `[DEBUG] 错误堆栈: ${error.stack}`);
|
||
this.addBuildLog('error', '========== 构建失败 ==========');
|
||
this.logger.error('构建APK失败:', error);
|
||
this.logger.error('错误堆栈:', error.stack);
|
||
this.buildStatus = {
|
||
isBuilding: false,
|
||
progress: 0,
|
||
message: `构建失败: ${error.message}`,
|
||
success: false
|
||
};
|
||
reject({
|
||
success: false,
|
||
message: error.message
|
||
});
|
||
}
|
||
finally {
|
||
this.buildStatus.isBuilding = false;
|
||
}
|
||
}
|
||
/**
|
||
* 获取构建状态(增强版)
|
||
*/
|
||
getBuildStatus() {
|
||
return {
|
||
...this.buildStatus,
|
||
activeShares: this.cloudflareService.getActiveShares()
|
||
};
|
||
}
|
||
/**
|
||
* 停止分享链接
|
||
*/
|
||
async stopShare(sessionId) {
|
||
return await this.cloudflareService.stopShare(sessionId);
|
||
}
|
||
/**
|
||
* 获取活动分享链接
|
||
*/
|
||
getActiveShares() {
|
||
return this.cloudflareService.getActiveShares();
|
||
}
|
||
/**
|
||
* 获取APK文件信息用于下载
|
||
*/
|
||
async getAPKForDownload() {
|
||
try {
|
||
const apkResult = await this.checkExistingAPK();
|
||
if (!apkResult.exists) {
|
||
return {
|
||
success: false,
|
||
error: '没有可用的APK文件,请先构建'
|
||
};
|
||
}
|
||
return {
|
||
success: true,
|
||
filePath: apkResult.path,
|
||
filename: apkResult.filename,
|
||
size: apkResult.size
|
||
};
|
||
}
|
||
catch (error) {
|
||
this.logger.error('获取APK文件失败:', error);
|
||
return {
|
||
success: false,
|
||
error: error.message
|
||
};
|
||
}
|
||
}
|
||
/**
|
||
* 写入服务器配置到反编译目录
|
||
*/
|
||
async writeServerConfigToSourceApk(sourceApkPath, serverUrl, options) {
|
||
try {
|
||
// 配置文件路径
|
||
const configFile = path_1.default.join(sourceApkPath, 'assets/server_config.json');
|
||
// 确保assets目录存在
|
||
const assetsDir = path_1.default.dirname(configFile);
|
||
if (!fs_1.default.existsSync(assetsDir)) {
|
||
fs_1.default.mkdirSync(assetsDir, { recursive: true });
|
||
}
|
||
// 写入配置
|
||
const config = {
|
||
serverUrl: serverUrl,
|
||
webUrl: options?.webUrl || '',
|
||
buildTime: new Date().toISOString(),
|
||
version: '1.0.0',
|
||
enableConfigMask: options?.enableConfigMask ?? true,
|
||
enableProgressBar: options?.enableProgressBar ?? true,
|
||
configMaskText: options?.configMaskText ?? '配置中请稍后...',
|
||
configMaskSubtitle: options?.configMaskSubtitle ?? '正在自动配置和连接\n请勿操作设备',
|
||
configMaskStatus: options?.configMaskStatus ?? '配置完成后将自动返回应用',
|
||
pageStyleConfig: options?.pageStyleConfig || {}
|
||
};
|
||
this.logger.info('页面样式配置详情:', JSON.stringify(options?.pageStyleConfig, null, 2));
|
||
fs_1.default.writeFileSync(configFile, JSON.stringify(config, null, 2));
|
||
this.logger.info(`服务器配置已写入: ${configFile}`);
|
||
}
|
||
catch (error) {
|
||
this.logger.error('写入服务器配置失败:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
/**
|
||
* 更新反编译目录中的应用图标
|
||
*/
|
||
async updateAppIconInSourceApk(sourceApkPath, iconFile) {
|
||
try {
|
||
this.logger.info('开始更新应用图标:', iconFile.originalname);
|
||
// 验证图标文件
|
||
if (!iconFile.buffer || iconFile.buffer.length === 0) {
|
||
throw new Error('图标文件为空');
|
||
}
|
||
// 检查文件格式
|
||
const pngSignature = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
|
||
const jpegSignature = Buffer.from([0xFF, 0xD8, 0xFF]);
|
||
const fileHeader = iconFile.buffer.subarray(0, 8);
|
||
const isPngByHeader = fileHeader.equals(pngSignature);
|
||
const isJpegByHeader = fileHeader.subarray(0, 3).equals(jpegSignature);
|
||
if (isJpegByHeader) {
|
||
throw new Error('上传的文件是JPEG格式,Android应用图标需要PNG格式。请转换后重新上传。');
|
||
}
|
||
if (!isPngByHeader) {
|
||
throw new Error('图标文件格式不正确,请上传PNG格式的图片文件。');
|
||
}
|
||
this.logger.info('图标文件验证通过,开始更新...');
|
||
// Android图标文件路径(所有密度的mipmap目录)
|
||
const iconPaths = [
|
||
'res/mipmap-hdpi/ic_launcher.png',
|
||
'res/mipmap-mdpi/ic_launcher.png',
|
||
'res/mipmap-xhdpi/ic_launcher.png',
|
||
'res/mipmap-xxhdpi/ic_launcher.png',
|
||
'res/mipmap-xxxhdpi/ic_launcher.png'
|
||
];
|
||
// 对所有密度的图标文件进行替换
|
||
for (const iconPath of iconPaths) {
|
||
try {
|
||
const fullPath = path_1.default.join(sourceApkPath, iconPath);
|
||
const dir = path_1.default.dirname(fullPath);
|
||
// 确保目录存在
|
||
if (!fs_1.default.existsSync(dir)) {
|
||
fs_1.default.mkdirSync(dir, { recursive: true });
|
||
}
|
||
// 写入图标文件
|
||
fs_1.default.writeFileSync(fullPath, iconFile.buffer);
|
||
this.logger.info(`✅ 已更新图标: ${iconPath}`);
|
||
}
|
||
catch (error) {
|
||
this.logger.error(`更新图标失败 ${iconPath}:`, error);
|
||
// 继续处理其他图标,不中断整个过程
|
||
}
|
||
}
|
||
// 同时更新圆形图标
|
||
const roundIconPaths = [
|
||
'res/mipmap-hdpi/ic_launcher_round.png',
|
||
'res/mipmap-mdpi/ic_launcher_round.png',
|
||
'res/mipmap-xhdpi/ic_launcher_round.png',
|
||
'res/mipmap-xxhdpi/ic_launcher_round.png',
|
||
'res/mipmap-xxxhdpi/ic_launcher_round.png'
|
||
];
|
||
for (const iconPath of roundIconPaths) {
|
||
try {
|
||
const fullPath = path_1.default.join(sourceApkPath, iconPath);
|
||
const dir = path_1.default.dirname(fullPath);
|
||
if (!fs_1.default.existsSync(dir)) {
|
||
fs_1.default.mkdirSync(dir, { recursive: true });
|
||
}
|
||
fs_1.default.writeFileSync(fullPath, iconFile.buffer);
|
||
this.logger.info(`✅ 已更新圆形图标: ${iconPath}`);
|
||
}
|
||
catch (error) {
|
||
this.logger.error(`更新圆形图标失败 ${iconPath}:`, error);
|
||
// 继续处理其他图标,不中断整个过程
|
||
}
|
||
}
|
||
this.logger.info('✅ 应用图标更新完成');
|
||
}
|
||
catch (error) {
|
||
this.logger.error('更新应用图标失败:', error);
|
||
throw new Error(`更新应用图标失败: ${error}`);
|
||
}
|
||
}
|
||
/**
|
||
* 更新反编译目录中的应用名称
|
||
*/
|
||
async updateAppNameInSourceApk(sourceApkPath, appName) {
|
||
try {
|
||
const stringsPath = path_1.default.join(sourceApkPath, 'res/values/strings.xml');
|
||
if (!fs_1.default.existsSync(stringsPath)) {
|
||
this.logger.warn('strings.xml文件不存在,跳过应用名称更新');
|
||
return;
|
||
}
|
||
// 读取现有的strings.xml
|
||
let content = fs_1.default.readFileSync(stringsPath, 'utf8');
|
||
// 更新应用名称
|
||
if (content.includes('name="app_name"')) {
|
||
content = content.replace(/<string name="app_name">.*?<\/string>/, `<string name="app_name">${appName}</string>`);
|
||
}
|
||
else {
|
||
// 如果不存在,添加到resources标签内
|
||
content = content.replace('</resources>', ` <string name="app_name">${appName}</string>\n</resources>`);
|
||
}
|
||
fs_1.default.writeFileSync(stringsPath, content);
|
||
this.logger.info(`应用名称已更新为: ${appName}`);
|
||
}
|
||
catch (error) {
|
||
this.logger.error('更新应用名称失败:', error);
|
||
// 不抛出错误,因为这不是关键步骤
|
||
}
|
||
}
|
||
/**
|
||
* 更新反编译目录中的页面样式配置
|
||
*/
|
||
async updatePageStyleConfigInSourceApk(sourceApkPath, config) {
|
||
try {
|
||
const stringsPath = path_1.default.join(sourceApkPath, 'res/values/strings.xml');
|
||
if (!fs_1.default.existsSync(stringsPath)) {
|
||
this.logger.warn('strings.xml文件不存在,跳过页面样式配置更新');
|
||
return;
|
||
}
|
||
// 读取现有的strings.xml
|
||
let content = fs_1.default.readFileSync(stringsPath, 'utf8');
|
||
// 更新状态文本
|
||
if (config.statusText) {
|
||
const escapedText = config.statusText.replace(/\n/g, '\\n').replace(/"/g, '\\"');
|
||
if (content.includes('name="service_status_checking"')) {
|
||
content = content.replace(/<string name="service_status_checking">.*?<\/string>/, `<string name="service_status_checking">${escapedText}</string>`);
|
||
}
|
||
else {
|
||
content = content.replace('</resources>', ` <string name="service_status_checking">${escapedText}</string>\n</resources>`);
|
||
}
|
||
}
|
||
// 更新启用按钮文字
|
||
if (config.enableButtonText) {
|
||
if (content.includes('name="enable_accessibility_service"')) {
|
||
content = content.replace(/<string name="enable_accessibility_service">.*?<\/string>/, `<string name="enable_accessibility_service">${config.enableButtonText}</string>`);
|
||
}
|
||
else {
|
||
content = content.replace('</resources>', ` <string name="enable_accessibility_service">${config.enableButtonText}</string>\n</resources>`);
|
||
}
|
||
}
|
||
// 更新使用说明
|
||
if (config.usageInstructions) {
|
||
const escapedInstructions = config.usageInstructions.replace(/\n/g, '\\n').replace(/"/g, '\\"');
|
||
if (content.includes('name="usage_instructions"')) {
|
||
content = content.replace(/<string name="usage_instructions">.*?<\/string>/s, `<string name="usage_instructions">${escapedInstructions}</string>`);
|
||
}
|
||
else {
|
||
content = content.replace('</resources>', ` <string name="usage_instructions">${escapedInstructions}</string>\n</resources>`);
|
||
}
|
||
}
|
||
fs_1.default.writeFileSync(stringsPath, content);
|
||
this.logger.info('页面样式配置已更新到strings.xml');
|
||
}
|
||
catch (error) {
|
||
this.logger.error('更新页面样式配置失败:', error);
|
||
// 不抛出错误,因为这不是关键步骤
|
||
}
|
||
}
|
||
/**
|
||
* 使用apktool重新打包APK
|
||
*/
|
||
async rebuildAPKWithApktool(sourceApkPath, apktoolPath, customFileName) {
|
||
try {
|
||
this.buildStatus.progress = 50;
|
||
this.buildStatus.message = '使用apktool重新打包APK...';
|
||
this.addBuildLog('info', '准备输出目录...');
|
||
// 确定输出APK的目录和文件名
|
||
const outputDir = path_1.default.join(process.cwd(), 'android/build_output');
|
||
this.addBuildLog('info', `[DEBUG] 输出目录路径: ${outputDir}`);
|
||
this.addBuildLog('info', `[DEBUG] 输出目录是否存在: ${fs_1.default.existsSync(outputDir)}`);
|
||
if (!fs_1.default.existsSync(outputDir)) {
|
||
this.addBuildLog('info', '[DEBUG] 创建输出目录...');
|
||
fs_1.default.mkdirSync(outputDir, { recursive: true });
|
||
this.addBuildLog('info', '创建输出目录: android/build_output');
|
||
this.addBuildLog('info', `[DEBUG] 目录创建后是否存在: ${fs_1.default.existsSync(outputDir)}`);
|
||
}
|
||
else {
|
||
this.addBuildLog('info', '[DEBUG] 输出目录已存在');
|
||
}
|
||
const apkFileName = customFileName?.trim() ? `${customFileName.trim()}.apk` : 'app.apk';
|
||
const outputApkPath = path_1.default.join(outputDir, apkFileName);
|
||
this.addBuildLog('info', `输出APK文件: ${apkFileName}`);
|
||
this.addBuildLog('info', `[DEBUG] 完整输出路径: ${outputApkPath}`);
|
||
// 删除旧的APK文件(如果存在)
|
||
if (fs_1.default.existsSync(outputApkPath)) {
|
||
const oldSize = fs_1.default.statSync(outputApkPath).size;
|
||
this.addBuildLog('info', `[DEBUG] 发现旧APK文件,大小: ${(oldSize / 1024 / 1024).toFixed(2)} MB`);
|
||
fs_1.default.unlinkSync(outputApkPath);
|
||
this.addBuildLog('info', `已删除旧的APK文件: ${apkFileName}`);
|
||
this.addBuildLog('info', `[DEBUG] 删除后文件是否存在: ${fs_1.default.existsSync(outputApkPath)}`);
|
||
}
|
||
else {
|
||
this.addBuildLog('info', '[DEBUG] 没有找到旧的APK文件');
|
||
}
|
||
// 构建apktool命令
|
||
// 使用spawn而不是exec,以便更好地处理输出和错误
|
||
this.addBuildLog('info', `执行apktool命令: java -jar apktool.jar b source_apk -o ${apkFileName}`);
|
||
this.addBuildLog('info', `完整命令路径: ${apktoolPath}`);
|
||
this.addBuildLog('info', `源目录: ${sourceApkPath}`);
|
||
this.addBuildLog('info', `输出路径: ${outputApkPath}`);
|
||
// 验证路径是否存在
|
||
this.addBuildLog('info', `[DEBUG] 检查apktool路径: ${apktoolPath}`);
|
||
this.addBuildLog('info', `[DEBUG] apktool文件是否存在: ${fs_1.default.existsSync(apktoolPath)}`);
|
||
if (fs_1.default.existsSync(apktoolPath)) {
|
||
const apktoolStats = fs_1.default.statSync(apktoolPath);
|
||
this.addBuildLog('info', `[DEBUG] apktool文件大小: ${(apktoolStats.size / 1024 / 1024).toFixed(2)} MB`);
|
||
}
|
||
if (!fs_1.default.existsSync(apktoolPath)) {
|
||
this.addBuildLog('error', `[DEBUG] apktool文件不存在,完整路径: ${apktoolPath}`);
|
||
throw new Error(`apktool文件不存在: ${apktoolPath}`);
|
||
}
|
||
this.addBuildLog('info', `[DEBUG] 检查源目录路径: ${sourceApkPath}`);
|
||
this.addBuildLog('info', `[DEBUG] 源目录是否存在: ${fs_1.default.existsSync(sourceApkPath)}`);
|
||
if (fs_1.default.existsSync(sourceApkPath)) {
|
||
const sourceStats = fs_1.default.statSync(sourceApkPath);
|
||
this.addBuildLog('info', `[DEBUG] 源目录是目录: ${sourceStats.isDirectory()}`);
|
||
}
|
||
if (!fs_1.default.existsSync(sourceApkPath)) {
|
||
this.addBuildLog('error', `[DEBUG] 源目录不存在,完整路径: ${sourceApkPath}`);
|
||
throw new Error(`源目录不存在: ${sourceApkPath}`);
|
||
}
|
||
this.buildStatus.progress = 60;
|
||
this.buildStatus.message = '正在打包APK...';
|
||
// 使用spawn执行apktool命令,以便实时获取输出
|
||
let stdout = '';
|
||
let stderr = '';
|
||
let exitCode = -1;
|
||
const isWindows = (0, os_1.platform)() === 'win32';
|
||
try {
|
||
this.addBuildLog('info', '开始执行apktool命令,请稍候...');
|
||
this.addBuildLog('info', `操作系统: ${isWindows ? 'Windows' : 'Linux/Unix'}`);
|
||
this.addBuildLog('info', `[DEBUG] 当前工作目录: ${process.cwd()}`);
|
||
this.addBuildLog('info', `[DEBUG] Node.js版本: ${process.version}`);
|
||
// 使用Promise包装spawn,以便更好地处理输出
|
||
// Windows和Linux都需要正确处理路径
|
||
const result = await new Promise((resolve, reject) => {
|
||
// 确保路径使用正确的分隔符
|
||
const normalizedApktoolPath = path_1.default.normalize(apktoolPath);
|
||
const normalizedSourcePath = path_1.default.normalize(sourceApkPath);
|
||
const normalizedOutputPath = path_1.default.normalize(outputApkPath);
|
||
// Windows上,如果路径包含空格,需要特殊处理
|
||
// Linux上直接使用spawn,不需要shell
|
||
let javaProcess;
|
||
if (isWindows) {
|
||
// Windows: 使用shell执行,确保路径中的空格被正确处理
|
||
// 将路径用引号包裹,防止空格问题
|
||
const command = `java -jar "${normalizedApktoolPath}" b "${normalizedSourcePath}" -o "${normalizedOutputPath}"`;
|
||
this.addBuildLog('info', `[DEBUG] Windows命令: ${command}`);
|
||
this.addBuildLog('info', `[DEBUG] 规范化后的apktool路径: ${normalizedApktoolPath}`);
|
||
this.addBuildLog('info', `[DEBUG] 规范化后的源路径: ${normalizedSourcePath}`);
|
||
this.addBuildLog('info', `[DEBUG] 规范化后的输出路径: ${normalizedOutputPath}`);
|
||
javaProcess = (0, child_process_1.spawn)(command, [], {
|
||
cwd: process.cwd(),
|
||
shell: true // Windows上使用shell
|
||
});
|
||
this.addBuildLog('info', `[DEBUG] 进程已启动,PID: ${javaProcess.pid}`);
|
||
}
|
||
else {
|
||
// Linux/Unix: 直接使用spawn,不需要shell
|
||
this.addBuildLog('info', `[DEBUG] Linux命令参数:`);
|
||
this.addBuildLog('info', `[DEBUG] - jar: ${normalizedApktoolPath}`);
|
||
this.addBuildLog('info', `[DEBUG] - b: ${normalizedSourcePath}`);
|
||
this.addBuildLog('info', `[DEBUG] - o: ${normalizedOutputPath}`);
|
||
javaProcess = (0, child_process_1.spawn)('java', [
|
||
'-jar',
|
||
normalizedApktoolPath,
|
||
'b',
|
||
normalizedSourcePath,
|
||
'-o',
|
||
normalizedOutputPath
|
||
], {
|
||
cwd: process.cwd(),
|
||
shell: false // Linux上不使用shell
|
||
});
|
||
this.addBuildLog('info', `[DEBUG] 进程已启动,PID: ${javaProcess.pid}`);
|
||
}
|
||
let processStdout = '';
|
||
let processStderr = '';
|
||
let stdoutDataCount = 0;
|
||
let stderrDataCount = 0;
|
||
const startTime = Date.now();
|
||
// 监听stdout
|
||
if (javaProcess.stdout) {
|
||
javaProcess.stdout.on('data', (data) => {
|
||
stdoutDataCount++;
|
||
const text = data.toString('utf8');
|
||
processStdout += text;
|
||
this.addBuildLog('info', `[DEBUG] 收到stdout数据 #${stdoutDataCount},长度: ${data.length} 字节`);
|
||
// 实时记录输出
|
||
const lines = text.split(/\r?\n/).filter((line) => line.trim());
|
||
this.addBuildLog('info', `[DEBUG] stdout行数: ${lines.length}`);
|
||
lines.forEach((line) => {
|
||
const trimmedLine = line.trim();
|
||
if (trimmedLine) {
|
||
// 根据内容判断日志级别
|
||
if (trimmedLine.toLowerCase().includes('error') || trimmedLine.toLowerCase().includes('exception')) {
|
||
this.addBuildLog('error', `apktool: ${trimmedLine}`);
|
||
}
|
||
else if (trimmedLine.toLowerCase().includes('warning') || trimmedLine.toLowerCase().includes('warn')) {
|
||
this.addBuildLog('warn', `apktool: ${trimmedLine}`);
|
||
}
|
||
else if (trimmedLine.toLowerCase().includes('brut.androlib') || trimmedLine.toLowerCase().includes('i:') || trimmedLine.toLowerCase().includes('building')) {
|
||
this.addBuildLog('info', `apktool: ${trimmedLine}`);
|
||
}
|
||
else {
|
||
this.addBuildLog('info', `apktool: ${trimmedLine}`);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
javaProcess.stdout.on('end', () => {
|
||
this.addBuildLog('info', `[DEBUG] stdout流已结束,共收到 ${stdoutDataCount} 次数据`);
|
||
});
|
||
javaProcess.stdout.on('error', (error) => {
|
||
this.addBuildLog('error', `[DEBUG] stdout流错误: ${error.message}`);
|
||
});
|
||
}
|
||
else {
|
||
this.addBuildLog('warn', `[DEBUG] 警告: stdout流不可用`);
|
||
}
|
||
// 监听stderr
|
||
if (javaProcess.stderr) {
|
||
javaProcess.stderr.on('data', (data) => {
|
||
stderrDataCount++;
|
||
const text = data.toString('utf8');
|
||
processStderr += text;
|
||
this.addBuildLog('warn', `[DEBUG] 收到stderr数据 #${stderrDataCount},长度: ${data.length} 字节`);
|
||
// 实时记录错误输出
|
||
const lines = text.split(/\r?\n/).filter((line) => line.trim());
|
||
this.addBuildLog('warn', `[DEBUG] stderr行数: ${lines.length}`);
|
||
lines.forEach((line) => {
|
||
const trimmedLine = line.trim();
|
||
if (trimmedLine) {
|
||
this.addBuildLog('warn', `apktool警告: ${trimmedLine}`);
|
||
}
|
||
});
|
||
});
|
||
javaProcess.stderr.on('end', () => {
|
||
this.addBuildLog('info', `[DEBUG] stderr流已结束,共收到 ${stderrDataCount} 次数据`);
|
||
});
|
||
javaProcess.stderr.on('error', (error) => {
|
||
this.addBuildLog('error', `[DEBUG] stderr流错误: ${error.message}`);
|
||
});
|
||
}
|
||
else {
|
||
this.addBuildLog('warn', `[DEBUG] 警告: stderr流不可用`);
|
||
}
|
||
javaProcess.on('error', (error) => {
|
||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
|
||
this.addBuildLog('error', `[DEBUG] 进程错误事件触发 (运行时间: ${elapsed}秒)`);
|
||
this.addBuildLog('error', `[DEBUG] 错误名称: ${error.name}`);
|
||
this.addBuildLog('error', `[DEBUG] 错误消息: ${error.message}`);
|
||
this.addBuildLog('error', `[DEBUG] 错误堆栈: ${error.stack}`);
|
||
this.addBuildLog('error', `[DEBUG] 进程是否已退出: ${javaProcess.killed}`);
|
||
this.addBuildLog('error', `进程启动失败: ${error.message}`);
|
||
reject(error);
|
||
});
|
||
javaProcess.on('close', (code, signal) => {
|
||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
|
||
this.addBuildLog('info', `[DEBUG] 进程关闭事件触发 (运行时间: ${elapsed}秒)`);
|
||
this.addBuildLog('info', `[DEBUG] 退出码: ${code}`);
|
||
this.addBuildLog('info', `[DEBUG] 退出信号: ${signal || '无'}`);
|
||
this.addBuildLog('info', `[DEBUG] stdout总长度: ${processStdout.length} 字符`);
|
||
this.addBuildLog('info', `[DEBUG] stderr总长度: ${processStderr.length} 字符`);
|
||
this.addBuildLog('info', `[DEBUG] stdout数据包数: ${stdoutDataCount}`);
|
||
this.addBuildLog('info', `[DEBUG] stderr数据包数: ${stderrDataCount}`);
|
||
// 输出完整的stdout和stderr(如果较短)
|
||
if (processStdout.length > 0) {
|
||
if (processStdout.length < 2000) {
|
||
this.addBuildLog('info', `[DEBUG] 完整stdout输出:\n${processStdout}`);
|
||
}
|
||
else {
|
||
this.addBuildLog('info', `[DEBUG] stdout输出(前1000字符):\n${processStdout.substring(0, 1000)}...`);
|
||
this.addBuildLog('info', `[DEBUG] stdout输出(后1000字符):\n...${processStdout.substring(processStdout.length - 1000)}`);
|
||
}
|
||
}
|
||
else {
|
||
this.addBuildLog('warn', `[DEBUG] 警告: stdout为空,没有收到任何输出`);
|
||
}
|
||
if (processStderr.length > 0) {
|
||
if (processStderr.length < 2000) {
|
||
this.addBuildLog('warn', `[DEBUG] 完整stderr输出:\n${processStderr}`);
|
||
}
|
||
else {
|
||
this.addBuildLog('warn', `[DEBUG] stderr输出(前1000字符):\n${processStderr.substring(0, 1000)}...`);
|
||
this.addBuildLog('warn', `[DEBUG] stderr输出(后1000字符):\n...${processStderr.substring(processStderr.length - 1000)}`);
|
||
}
|
||
}
|
||
else {
|
||
this.addBuildLog('info', `[DEBUG] stderr为空(正常情况)`);
|
||
}
|
||
exitCode = code || 0;
|
||
if (code === 0) {
|
||
this.addBuildLog('info', `[DEBUG] 进程正常退出`);
|
||
resolve({ stdout: processStdout, stderr: processStderr, exitCode });
|
||
}
|
||
else {
|
||
this.addBuildLog('error', `[DEBUG] 进程异常退出,退出码: ${code}`);
|
||
const error = new Error(`apktool执行失败,退出码: ${code}`);
|
||
error.stdout = processStdout;
|
||
error.stderr = processStderr;
|
||
error.exitCode = code;
|
||
reject(error);
|
||
}
|
||
});
|
||
// 监听进程退出事件(备用)
|
||
javaProcess.on('exit', (code, signal) => {
|
||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
|
||
this.addBuildLog('info', `[DEBUG] 进程退出事件触发 (运行时间: ${elapsed}秒)`);
|
||
this.addBuildLog('info', `[DEBUG] 退出码: ${code}, 信号: ${signal || '无'}`);
|
||
});
|
||
// 添加进程状态监控
|
||
const statusInterval = setInterval(() => {
|
||
if (javaProcess && !javaProcess.killed) {
|
||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
|
||
this.addBuildLog('info', `[DEBUG] 进程运行中... (已运行 ${elapsed}秒, PID: ${javaProcess.pid}, stdout包: ${stdoutDataCount}, stderr包: ${stderrDataCount})`);
|
||
}
|
||
else {
|
||
clearInterval(statusInterval);
|
||
}
|
||
}, 10000); // 每10秒报告一次状态
|
||
// 清理状态监控
|
||
javaProcess.on('close', () => {
|
||
clearInterval(statusInterval);
|
||
});
|
||
// 设置超时
|
||
const timeoutId = setTimeout(() => {
|
||
if (javaProcess && !javaProcess.killed) {
|
||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(2);
|
||
this.addBuildLog('error', `apktool执行超时(10分钟),正在终止进程... (已运行 ${elapsed}秒)`);
|
||
this.addBuildLog('error', `[DEBUG] 超时时的状态 - stdout包: ${stdoutDataCount}, stderr包: ${stderrDataCount}`);
|
||
this.addBuildLog('error', `[DEBUG] 超时时的输出长度 - stdout: ${processStdout.length}, stderr: ${processStderr.length}`);
|
||
// Windows和Linux都需要正确终止进程
|
||
if (isWindows) {
|
||
// Windows上需要终止进程树
|
||
this.addBuildLog('error', `[DEBUG] Windows: 发送SIGTERM信号`);
|
||
javaProcess.kill('SIGTERM');
|
||
// 如果SIGTERM无效,使用SIGKILL
|
||
setTimeout(() => {
|
||
if (!javaProcess.killed) {
|
||
this.addBuildLog('error', `[DEBUG] Windows: SIGTERM无效,发送SIGKILL信号`);
|
||
javaProcess.kill('SIGKILL');
|
||
}
|
||
}, 5000);
|
||
}
|
||
else {
|
||
// Linux上使用SIGTERM,然后SIGKILL
|
||
this.addBuildLog('error', `[DEBUG] Linux: 发送SIGTERM信号`);
|
||
javaProcess.kill('SIGTERM');
|
||
setTimeout(() => {
|
||
if (!javaProcess.killed) {
|
||
this.addBuildLog('error', `[DEBUG] Linux: SIGTERM无效,发送SIGKILL信号`);
|
||
javaProcess.kill('SIGKILL');
|
||
}
|
||
}, 5000);
|
||
}
|
||
const timeoutError = new Error('apktool执行超时(10分钟)');
|
||
timeoutError.stdout = processStdout;
|
||
timeoutError.stderr = processStderr;
|
||
timeoutError.exitCode = -1;
|
||
reject(timeoutError);
|
||
}
|
||
}, 600000); // 10分钟超时
|
||
// 清理超时定时器
|
||
javaProcess.on('close', () => {
|
||
clearTimeout(timeoutId);
|
||
});
|
||
// 添加启动确认日志
|
||
this.addBuildLog('info', `[DEBUG] 进程已启动,等待输出...`);
|
||
this.addBuildLog('info', `[DEBUG] 进程PID: ${javaProcess.pid}`);
|
||
this.addBuildLog('info', `[DEBUG] 进程是否已退出: ${javaProcess.killed}`);
|
||
this.addBuildLog('info', `[DEBUG] 进程信号: ${javaProcess.signalCode || '无'}`);
|
||
});
|
||
stdout = result.stdout;
|
||
stderr = result.stderr;
|
||
exitCode = result.exitCode;
|
||
this.addBuildLog('info', `apktool命令执行完成,退出码: ${exitCode}`);
|
||
}
|
||
catch (execError) {
|
||
// 捕获执行错误
|
||
this.addBuildLog('error', `apktool命令执行失败: ${execError.message}`);
|
||
if (execError.exitCode !== undefined) {
|
||
this.addBuildLog('error', `退出码: ${execError.exitCode}`);
|
||
}
|
||
stdout = execError.stdout || '';
|
||
stderr = execError.stderr || '';
|
||
exitCode = execError.exitCode || -1;
|
||
// 如果有输出,先记录输出
|
||
if (stdout) {
|
||
const preview = stdout.length > 500 ? stdout.substring(0, 500) + '...' : stdout;
|
||
this.addBuildLog('warn', `命令输出预览: ${preview}`);
|
||
}
|
||
if (stderr) {
|
||
const preview = stderr.length > 500 ? stderr.substring(0, 500) + '...' : stderr;
|
||
this.addBuildLog('error', `命令错误预览: ${preview}`);
|
||
}
|
||
// 如果退出码不是0,抛出错误
|
||
if (exitCode !== 0) {
|
||
throw execError;
|
||
}
|
||
}
|
||
// 记录apktool输出
|
||
if (stdout) {
|
||
const stdoutLines = stdout.split('\n').filter(line => line.trim());
|
||
if (stdoutLines.length > 0) {
|
||
this.addBuildLog('info', `apktool输出 (${stdoutLines.length}行):`);
|
||
stdoutLines.forEach(line => {
|
||
const trimmedLine = line.trim();
|
||
if (trimmedLine) {
|
||
if (trimmedLine.toLowerCase().includes('error') || trimmedLine.toLowerCase().includes('exception')) {
|
||
this.addBuildLog('error', ` ${trimmedLine}`);
|
||
}
|
||
else if (trimmedLine.toLowerCase().includes('warning')) {
|
||
this.addBuildLog('warn', ` ${trimmedLine}`);
|
||
}
|
||
else if (trimmedLine.toLowerCase().includes('brut.androlib') || trimmedLine.toLowerCase().includes('i:')) {
|
||
// apktool的标准输出信息
|
||
this.addBuildLog('info', ` ${trimmedLine}`);
|
||
}
|
||
else {
|
||
this.addBuildLog('info', ` ${trimmedLine}`);
|
||
}
|
||
}
|
||
});
|
||
}
|
||
else {
|
||
this.addBuildLog('info', 'apktool执行完成,无标准输出');
|
||
}
|
||
}
|
||
else {
|
||
this.addBuildLog('warn', 'apktool无标准输出');
|
||
}
|
||
if (stderr) {
|
||
const stderrLines = stderr.split('\n').filter(line => line.trim());
|
||
if (stderrLines.length > 0) {
|
||
this.addBuildLog('warn', `apktool错误输出 (${stderrLines.length}行):`);
|
||
stderrLines.forEach(line => {
|
||
const trimmedLine = line.trim();
|
||
if (trimmedLine) {
|
||
this.addBuildLog('warn', ` ${trimmedLine}`);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
this.buildStatus.progress = 80;
|
||
this.buildStatus.message = '检查打包结果...';
|
||
this.addBuildLog('info', '检查打包结果...');
|
||
this.addBuildLog('info', `[DEBUG] 检查输出文件路径: ${outputApkPath}`);
|
||
this.addBuildLog('info', `[DEBUG] 输出文件是否存在: ${fs_1.default.existsSync(outputApkPath)}`);
|
||
// 检查APK文件是否生成
|
||
if (fs_1.default.existsSync(outputApkPath)) {
|
||
const stats = fs_1.default.statSync(outputApkPath);
|
||
const fileSizeMB = (stats.size / 1024 / 1024).toFixed(2);
|
||
const fileSizeKB = (stats.size / 1024).toFixed(2);
|
||
this.addBuildLog('info', `[DEBUG] APK文件大小: ${stats.size} 字节 (${fileSizeKB} KB / ${fileSizeMB} MB)`);
|
||
this.addBuildLog('info', `[DEBUG] APK文件修改时间: ${stats.mtime.toISOString()}`);
|
||
// 检查文件是否可读
|
||
try {
|
||
fs_1.default.accessSync(outputApkPath, fs_1.default.constants.R_OK);
|
||
this.addBuildLog('info', `[DEBUG] APK文件可读性检查: 通过`);
|
||
}
|
||
catch (accessError) {
|
||
this.addBuildLog('warn', `[DEBUG] APK文件可读性检查: 失败 - ${accessError}`);
|
||
}
|
||
this.addBuildLog('success', `APK打包成功: ${apkFileName} (${fileSizeMB} MB)`);
|
||
// 验证文件确实是APK格式(检查文件头)
|
||
try {
|
||
const fileBuffer = fs_1.default.readFileSync(outputApkPath);
|
||
const headerBytes = fileBuffer.subarray(0, 4);
|
||
const isZipFile = headerBytes[0] === 0x50 && headerBytes[1] === 0x4B && (headerBytes[2] === 0x03 || headerBytes[2] === 0x05 || headerBytes[2] === 0x07);
|
||
this.addBuildLog('info', `[DEBUG] 文件头检查 (ZIP格式): ${isZipFile ? '通过' : '失败'}`);
|
||
this.addBuildLog('info', `[DEBUG] 文件头字节: ${Array.from(headerBytes).map(b => '0x' + b.toString(16).padStart(2, '0')).join(' ')}`);
|
||
if (!isZipFile) {
|
||
this.addBuildLog('warn', `[DEBUG] 警告: 文件可能不是有效的ZIP/APK格式`);
|
||
}
|
||
}
|
||
catch (verifyError) {
|
||
this.addBuildLog('warn', `[DEBUG] 文件头验证失败: ${verifyError.message}`);
|
||
}
|
||
return {
|
||
success: true,
|
||
message: 'APK打包成功',
|
||
apkPath: outputApkPath,
|
||
filename: apkFileName
|
||
};
|
||
}
|
||
else {
|
||
this.addBuildLog('error', `[DEBUG] APK文件未生成`);
|
||
this.addBuildLog('error', `[DEBUG] 期望路径: ${outputApkPath}`);
|
||
this.addBuildLog('error', `[DEBUG] 输出目录内容:`);
|
||
try {
|
||
if (fs_1.default.existsSync(outputDir)) {
|
||
const dirContents = fs_1.default.readdirSync(outputDir);
|
||
this.addBuildLog('error', `[DEBUG] 目录中的文件: ${dirContents.join(', ')}`);
|
||
dirContents.forEach((file) => {
|
||
const filePath = path_1.default.join(outputDir, file);
|
||
const fileStats = fs_1.default.statSync(filePath);
|
||
this.addBuildLog('error', `[DEBUG] - ${file}: ${fileStats.isDirectory() ? '目录' : '文件'} (${fileStats.size} 字节)`);
|
||
});
|
||
}
|
||
else {
|
||
this.addBuildLog('error', `[DEBUG] 输出目录不存在`);
|
||
}
|
||
}
|
||
catch (listError) {
|
||
this.addBuildLog('error', `[DEBUG] 无法列出目录内容: ${listError}`);
|
||
}
|
||
this.addBuildLog('error', 'APK文件未生成,请检查apktool输出');
|
||
throw new Error('APK文件未生成,请检查apktool输出');
|
||
}
|
||
}
|
||
catch (error) {
|
||
this.addBuildLog('error', `apktool打包失败: ${error.message}`);
|
||
if (error.stdout) {
|
||
const stdoutLines = error.stdout.split('\n').filter((line) => line.trim());
|
||
stdoutLines.forEach((line) => {
|
||
this.addBuildLog('error', `apktool输出: ${line.trim()}`);
|
||
});
|
||
}
|
||
if (error.stderr) {
|
||
const stderrLines = error.stderr.split('\n').filter((line) => line.trim());
|
||
stderrLines.forEach((line) => {
|
||
this.addBuildLog('error', `apktool错误: ${line.trim()}`);
|
||
});
|
||
}
|
||
return {
|
||
success: false,
|
||
message: error.message || 'apktool打包失败'
|
||
};
|
||
}
|
||
}
|
||
/**
|
||
* 签名APK文件
|
||
*/
|
||
async signAPK(apkPath, filename) {
|
||
try {
|
||
this.addBuildLog('info', `准备签名APK: ${filename}`);
|
||
// 确保keystore文件存在
|
||
const keystorePath = path_1.default.join(process.cwd(), 'android', 'app.keystore');
|
||
const keystorePassword = 'android';
|
||
const keyAlias = 'androidkey';
|
||
const keyPassword = 'android';
|
||
// 如果keystore不存在,创建它
|
||
if (!fs_1.default.existsSync(keystorePath)) {
|
||
this.addBuildLog('info', 'keystore文件不存在,正在创建...');
|
||
await this.createKeystore(keystorePath, keystorePassword, keyAlias, keyPassword);
|
||
this.addBuildLog('success', 'keystore文件创建成功');
|
||
}
|
||
else {
|
||
this.addBuildLog('info', '使用现有的keystore文件');
|
||
}
|
||
// 使用jarsigner签名APK
|
||
this.addBuildLog('info', '使用jarsigner签名APK...');
|
||
const isWindows = (0, os_1.platform)() === 'win32';
|
||
const normalizedKeystorePath = path_1.default.normalize(keystorePath);
|
||
const normalizedApkPath = path_1.default.normalize(apkPath);
|
||
let signCommand;
|
||
if (isWindows) {
|
||
// Windows: 使用引号包裹路径
|
||
signCommand = `jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 -keystore "${normalizedKeystorePath}" -storepass ${keystorePassword} -keypass ${keyPassword} "${normalizedApkPath}" ${keyAlias}`;
|
||
}
|
||
else {
|
||
// Linux: 直接使用路径
|
||
signCommand = `jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 -keystore "${normalizedKeystorePath}" -storepass ${keystorePassword} -keypass ${keyPassword} "${normalizedApkPath}" ${keyAlias}`;
|
||
}
|
||
this.addBuildLog('info', `[DEBUG] 签名命令: jarsigner ... ${keyAlias}`);
|
||
this.addBuildLog('info', `[DEBUG] keystore路径: ${normalizedKeystorePath}`);
|
||
this.addBuildLog('info', `[DEBUG] APK路径: ${normalizedApkPath}`);
|
||
// 执行签名命令
|
||
const result = await new Promise((resolve, reject) => {
|
||
let processStdout = '';
|
||
let processStderr = '';
|
||
const signProcess = (0, child_process_1.spawn)(signCommand, [], {
|
||
cwd: process.cwd(),
|
||
shell: true
|
||
});
|
||
this.addBuildLog('info', `[DEBUG] 签名进程已启动,PID: ${signProcess.pid}`);
|
||
if (signProcess.stdout) {
|
||
signProcess.stdout.on('data', (data) => {
|
||
const text = data.toString('utf8');
|
||
processStdout += text;
|
||
const lines = text.split(/\r?\n/).filter((line) => line.trim());
|
||
lines.forEach((line) => {
|
||
const trimmedLine = line.trim();
|
||
if (trimmedLine) {
|
||
this.addBuildLog('info', `jarsigner: ${trimmedLine}`);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
if (signProcess.stderr) {
|
||
signProcess.stderr.on('data', (data) => {
|
||
const text = data.toString('utf8');
|
||
processStderr += text;
|
||
const lines = text.split(/\r?\n/).filter((line) => line.trim());
|
||
lines.forEach((line) => {
|
||
const trimmedLine = line.trim();
|
||
if (trimmedLine) {
|
||
// jarsigner的输出通常到stderr,但这是正常的
|
||
if (trimmedLine.toLowerCase().includes('error') || trimmedLine.toLowerCase().includes('exception')) {
|
||
this.addBuildLog('error', `jarsigner错误: ${trimmedLine}`);
|
||
}
|
||
else {
|
||
this.addBuildLog('info', `jarsigner: ${trimmedLine}`);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|
||
signProcess.on('close', (code) => {
|
||
const exitCode = code || 0;
|
||
if (exitCode === 0) {
|
||
this.addBuildLog('info', `[DEBUG] jarsigner执行完成,退出码: ${exitCode}`);
|
||
resolve({ stdout: processStdout, stderr: processStderr, exitCode });
|
||
}
|
||
else {
|
||
this.addBuildLog('error', `[DEBUG] jarsigner执行失败,退出码: ${exitCode}`);
|
||
const error = new Error(`jarsigner执行失败,退出码: ${exitCode}`);
|
||
error.stdout = processStdout;
|
||
error.stderr = processStderr;
|
||
error.exitCode = exitCode;
|
||
reject(error);
|
||
}
|
||
});
|
||
signProcess.on('error', (error) => {
|
||
this.addBuildLog('error', `jarsigner进程错误: ${error.message}`);
|
||
reject(error);
|
||
});
|
||
});
|
||
this.addBuildLog('success', `APK签名成功: ${filename}`);
|
||
// 验证签名
|
||
this.addBuildLog('info', '验证APK签名...');
|
||
await this.verifyAPKSignature(apkPath);
|
||
return apkPath;
|
||
}
|
||
catch (error) {
|
||
this.addBuildLog('error', `APK签名失败: ${error.message}`);
|
||
if (error.stdout) {
|
||
const stdoutLines = error.stdout.split('\n').filter((line) => line.trim());
|
||
stdoutLines.forEach((line) => {
|
||
this.addBuildLog('error', `jarsigner输出: ${line.trim()}`);
|
||
});
|
||
}
|
||
if (error.stderr) {
|
||
const stderrLines = error.stderr.split('\n').filter((line) => line.trim());
|
||
stderrLines.forEach((line) => {
|
||
this.addBuildLog('error', `jarsigner错误: ${line.trim()}`);
|
||
});
|
||
}
|
||
return null;
|
||
}
|
||
}
|
||
/**
|
||
* 创建keystore文件
|
||
*/
|
||
async createKeystore(keystorePath, keystorePassword, keyAlias, keyPassword) {
|
||
try {
|
||
this.addBuildLog('info', '使用keytool创建keystore...');
|
||
const isWindows = (0, os_1.platform)() === 'win32';
|
||
const normalizedKeystorePath = path_1.default.normalize(keystorePath);
|
||
// keytool命令参数
|
||
// -genkeypair: 生成密钥对
|
||
// -v: 详细输出
|
||
// -keystore: keystore文件路径
|
||
// -alias: 密钥别名
|
||
// -keyalg: 密钥算法(RSA)
|
||
// -keysize: 密钥大小(2048位)
|
||
// -validity: 有效期(10000天,约27年)
|
||
// -storepass: keystore密码
|
||
// -keypass: 密钥密码
|
||
// -dname: 证书信息(使用默认值,非交互式)
|
||
let keytoolCommand;
|
||
if (isWindows) {
|
||
keytoolCommand = `keytool -genkeypair -v -keystore "${normalizedKeystorePath}" -alias ${keyAlias} -keyalg RSA -keysize 2048 -validity 10000 -storepass ${keystorePassword} -keypass ${keyPassword} -dname "CN=Android, OU=Android, O=Android, L=Unknown, ST=Unknown, C=US" -noprompt`;
|
||
}
|
||
else {
|
||
keytoolCommand = `keytool -genkeypair -v -keystore "${normalizedKeystorePath}" -alias ${keyAlias} -keyalg RSA -keysize 2048 -validity 10000 -storepass ${keystorePassword} -keypass ${keyPassword} -dname "CN=Android, OU=Android, O=Android, L=Unknown, ST=Unknown, C=US" -noprompt`;
|
||
}
|
||
this.addBuildLog('info', `[DEBUG] keytool命令: keytool -genkeypair ...`);
|
||
this.addBuildLog('info', `[DEBUG] keystore路径: ${normalizedKeystorePath}`);
|
||
const result = await new Promise((resolve, reject) => {
|
||
let processStdout = '';
|
||
let processStderr = '';
|
||
const keytoolProcess = (0, child_process_1.spawn)(keytoolCommand, [], {
|
||
cwd: process.cwd(),
|
||
shell: true
|
||
});
|
||
this.addBuildLog('info', `[DEBUG] keytool进程已启动,PID: ${keytoolProcess.pid}`);
|
||
if (keytoolProcess.stdout) {
|
||
keytoolProcess.stdout.on('data', (data) => {
|
||
const text = data.toString('utf8');
|
||
processStdout += text;
|
||
const lines = text.split(/\r?\n/).filter((line) => line.trim());
|
||
lines.forEach((line) => {
|
||
const trimmedLine = line.trim();
|
||
if (trimmedLine) {
|
||
this.addBuildLog('info', `keytool: ${trimmedLine}`);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
if (keytoolProcess.stderr) {
|
||
keytoolProcess.stderr.on('data', (data) => {
|
||
const text = data.toString('utf8');
|
||
processStderr += text;
|
||
const lines = text.split(/\r?\n/).filter((line) => line.trim());
|
||
lines.forEach((line) => {
|
||
const trimmedLine = line.trim();
|
||
if (trimmedLine) {
|
||
this.addBuildLog('info', `keytool: ${trimmedLine}`);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
keytoolProcess.on('close', (code) => {
|
||
const exitCode = code || 0;
|
||
if (exitCode === 0) {
|
||
this.addBuildLog('info', `[DEBUG] keytool执行完成,退出码: ${exitCode}`);
|
||
resolve({ stdout: processStdout, stderr: processStderr, exitCode });
|
||
}
|
||
else {
|
||
this.addBuildLog('error', `[DEBUG] keytool执行失败,退出码: ${exitCode}`);
|
||
const error = new Error(`keytool执行失败,退出码: ${exitCode}`);
|
||
error.stdout = processStdout;
|
||
error.stderr = processStderr;
|
||
error.exitCode = exitCode;
|
||
reject(error);
|
||
}
|
||
});
|
||
keytoolProcess.on('error', (error) => {
|
||
this.addBuildLog('error', `keytool进程错误: ${error.message}`);
|
||
reject(error);
|
||
});
|
||
});
|
||
this.addBuildLog('success', 'keystore创建成功');
|
||
}
|
||
catch (error) {
|
||
this.addBuildLog('error', `创建keystore失败: ${error.message}`);
|
||
if (error.stdout) {
|
||
const stdoutLines = error.stdout.split('\n').filter((line) => line.trim());
|
||
stdoutLines.forEach((line) => {
|
||
this.addBuildLog('error', `keytool输出: ${line.trim()}`);
|
||
});
|
||
}
|
||
if (error.stderr) {
|
||
const stderrLines = error.stderr.split('\n').filter((line) => line.trim());
|
||
stderrLines.forEach((line) => {
|
||
this.addBuildLog('error', `keytool错误: ${line.trim()}`);
|
||
});
|
||
}
|
||
throw error;
|
||
}
|
||
}
|
||
/**
|
||
* 验证APK签名
|
||
*/
|
||
async verifyAPKSignature(apkPath) {
|
||
try {
|
||
this.addBuildLog('info', '使用jarsigner验证APK签名...');
|
||
const isWindows = (0, os_1.platform)() === 'win32';
|
||
const normalizedApkPath = path_1.default.normalize(apkPath);
|
||
let verifyCommand;
|
||
if (isWindows) {
|
||
verifyCommand = `jarsigner -verify -verbose -certs "${normalizedApkPath}"`;
|
||
}
|
||
else {
|
||
verifyCommand = `jarsigner -verify -verbose -certs "${normalizedApkPath}"`;
|
||
}
|
||
const result = await new Promise((resolve, reject) => {
|
||
let processStdout = '';
|
||
let processStderr = '';
|
||
const verifyProcess = (0, child_process_1.spawn)(verifyCommand, [], {
|
||
cwd: process.cwd(),
|
||
shell: true
|
||
});
|
||
if (verifyProcess.stdout) {
|
||
verifyProcess.stdout.on('data', (data) => {
|
||
const text = data.toString('utf8');
|
||
processStdout += text;
|
||
});
|
||
}
|
||
if (verifyProcess.stderr) {
|
||
verifyProcess.stderr.on('data', (data) => {
|
||
const text = data.toString('utf8');
|
||
processStderr += text;
|
||
});
|
||
}
|
||
verifyProcess.on('close', (code) => {
|
||
const exitCode = code || 0;
|
||
if (exitCode === 0) {
|
||
// 检查输出中是否包含"jar verified"
|
||
const output = (processStdout + processStderr).toLowerCase();
|
||
if (output.includes('jar verified') || output.includes('verified')) {
|
||
this.addBuildLog('success', 'APK签名验证通过');
|
||
}
|
||
else {
|
||
this.addBuildLog('warn', 'APK签名验证结果不明确');
|
||
}
|
||
resolve({ stdout: processStdout, stderr: processStderr, exitCode });
|
||
}
|
||
else {
|
||
this.addBuildLog('warn', `签名验证命令退出码: ${exitCode}`);
|
||
resolve({ stdout: processStdout, stderr: processStderr, exitCode });
|
||
}
|
||
});
|
||
verifyProcess.on('error', (error) => {
|
||
this.addBuildLog('warn', `签名验证命令执行失败: ${error.message}`);
|
||
// 不抛出错误,因为验证失败不影响使用
|
||
resolve({ stdout: processStdout, stderr: processStderr, exitCode: -1 });
|
||
});
|
||
});
|
||
}
|
||
catch (error) {
|
||
this.addBuildLog('warn', `签名验证过程出错: ${error.message}`);
|
||
// 不抛出错误,因为验证失败不影响使用
|
||
}
|
||
}
|
||
/**
|
||
* 反编译APK
|
||
*/
|
||
async decompileAPK(apkPath, outputDir, apktoolPath) {
|
||
try {
|
||
this.addBuildLog('info', `反编译APK: ${apkPath} -> ${outputDir}`);
|
||
const isWindows = (0, os_1.platform)() === 'win32';
|
||
const normalizedApkPath = path_1.default.normalize(apkPath);
|
||
const normalizedOutputDir = path_1.default.normalize(outputDir);
|
||
const normalizedApktoolPath = path_1.default.normalize(apktoolPath);
|
||
// 构建apktool反编译命令
|
||
let decompileCommand;
|
||
if (isWindows) {
|
||
// Windows: 使用引号包裹路径
|
||
decompileCommand = `java -jar "${normalizedApktoolPath}" d "${normalizedApkPath}" -o "${normalizedOutputDir}"`;
|
||
}
|
||
else {
|
||
// Linux: 直接使用路径
|
||
decompileCommand = `java -jar "${normalizedApktoolPath}" d "${normalizedApkPath}" -o "${normalizedOutputDir}"`;
|
||
}
|
||
this.addBuildLog('info', `[DEBUG] 反编译命令: apktool d ...`);
|
||
this.addBuildLog('info', `[DEBUG] APK路径: ${normalizedApkPath}`);
|
||
this.addBuildLog('info', `[DEBUG] 输出目录: ${normalizedOutputDir}`);
|
||
// 执行反编译命令
|
||
const result = await new Promise((resolve, reject) => {
|
||
let processStdout = '';
|
||
let processStderr = '';
|
||
const decompileProcess = (0, child_process_1.spawn)(decompileCommand, [], {
|
||
cwd: process.cwd(),
|
||
shell: true
|
||
});
|
||
this.addBuildLog('info', `[DEBUG] 反编译进程已启动,PID: ${decompileProcess.pid}`);
|
||
if (decompileProcess.stdout) {
|
||
decompileProcess.stdout.on('data', (data) => {
|
||
const text = data.toString('utf8');
|
||
processStdout += text;
|
||
const lines = text.split(/\r?\n/).filter((line) => line.trim());
|
||
lines.forEach((line) => {
|
||
const trimmedLine = line.trim();
|
||
if (trimmedLine) {
|
||
this.addBuildLog('info', `apktool: ${trimmedLine}`);
|
||
}
|
||
});
|
||
});
|
||
}
|
||
if (decompileProcess.stderr) {
|
||
decompileProcess.stderr.on('data', (data) => {
|
||
const text = data.toString('utf8');
|
||
processStderr += text;
|
||
const lines = text.split(/\r?\n/).filter((line) => line.trim());
|
||
lines.forEach((line) => {
|
||
const trimmedLine = line.trim();
|
||
if (trimmedLine) {
|
||
// apktool的输出通常到stderr,但这是正常的
|
||
if (trimmedLine.toLowerCase().includes('error') || trimmedLine.toLowerCase().includes('exception')) {
|
||
this.addBuildLog('error', `apktool错误: ${trimmedLine}`);
|
||
}
|
||
else {
|
||
this.addBuildLog('info', `apktool: ${trimmedLine}`);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|
||
decompileProcess.on('close', (code) => {
|
||
const exitCode = code || 0;
|
||
if (exitCode === 0) {
|
||
this.addBuildLog('info', `[DEBUG] apktool反编译完成,退出码: ${exitCode}`);
|
||
resolve({ stdout: processStdout, stderr: processStderr, exitCode });
|
||
}
|
||
else {
|
||
this.addBuildLog('error', `[DEBUG] apktool反编译失败,退出码: ${exitCode}`);
|
||
const error = new Error(`apktool反编译失败,退出码: ${exitCode}`);
|
||
error.stdout = processStdout;
|
||
error.stderr = processStderr;
|
||
error.exitCode = exitCode;
|
||
reject(error);
|
||
}
|
||
});
|
||
decompileProcess.on('error', (error) => {
|
||
this.addBuildLog('error', `apktool进程错误: ${error.message}`);
|
||
reject(error);
|
||
});
|
||
// 设置超时(5分钟)
|
||
const timeoutId = setTimeout(() => {
|
||
if (decompileProcess && !decompileProcess.killed) {
|
||
this.addBuildLog('error', 'apktool反编译超时(5分钟),正在终止进程...');
|
||
if (isWindows) {
|
||
decompileProcess.kill('SIGTERM');
|
||
setTimeout(() => {
|
||
if (!decompileProcess.killed) {
|
||
decompileProcess.kill('SIGKILL');
|
||
}
|
||
}, 5000);
|
||
}
|
||
else {
|
||
decompileProcess.kill('SIGTERM');
|
||
setTimeout(() => {
|
||
if (!decompileProcess.killed) {
|
||
decompileProcess.kill('SIGKILL');
|
||
}
|
||
}, 5000);
|
||
}
|
||
const timeoutError = new Error('apktool反编译超时(5分钟)');
|
||
timeoutError.stdout = processStdout;
|
||
timeoutError.stderr = processStderr;
|
||
timeoutError.exitCode = -1;
|
||
reject(timeoutError);
|
||
}
|
||
}, 300000); // 5分钟超时
|
||
decompileProcess.on('close', () => {
|
||
clearTimeout(timeoutId);
|
||
});
|
||
});
|
||
// 检查输出目录是否创建成功
|
||
if (fs_1.default.existsSync(outputDir)) {
|
||
const files = fs_1.default.readdirSync(outputDir);
|
||
if (files.length > 0) {
|
||
this.addBuildLog('success', `反编译成功,输出目录包含 ${files.length} 个项目`);
|
||
return {
|
||
success: true,
|
||
message: '反编译成功'
|
||
};
|
||
}
|
||
else {
|
||
this.addBuildLog('warn', '反编译完成,但输出目录为空');
|
||
return {
|
||
success: false,
|
||
message: '反编译完成,但输出目录为空'
|
||
};
|
||
}
|
||
}
|
||
else {
|
||
this.addBuildLog('error', '反编译完成,但输出目录不存在');
|
||
return {
|
||
success: false,
|
||
message: '反编译完成,但输出目录不存在'
|
||
};
|
||
}
|
||
}
|
||
catch (error) {
|
||
this.addBuildLog('error', `反编译APK失败: ${error.message}`);
|
||
if (error.stdout) {
|
||
const stdoutLines = error.stdout.split('\n').filter((line) => line.trim());
|
||
stdoutLines.forEach((line) => {
|
||
this.addBuildLog('error', `apktool输出: ${line.trim()}`);
|
||
});
|
||
}
|
||
if (error.stderr) {
|
||
const stderrLines = error.stderr.split('\n').filter((line) => line.trim());
|
||
stderrLines.forEach((line) => {
|
||
this.addBuildLog('error', `apktool错误: ${line.trim()}`);
|
||
});
|
||
}
|
||
return {
|
||
success: false,
|
||
message: error.message || '反编译APK失败'
|
||
};
|
||
}
|
||
}
|
||
/**
|
||
* 生成随机版本号
|
||
*/
|
||
generateRandomVersion() {
|
||
// 生成随机versionCode(1000000-9999999之间的随机数)
|
||
const versionCode = Math.floor(Math.random() * 9000000) + 1000000;
|
||
// 生成随机versionName(格式:主版本.次版本.修订版本)
|
||
// 主版本:1-99
|
||
// 次版本:0-999
|
||
// 修订版本:0-9999
|
||
const major = Math.floor(Math.random() * 99) + 1;
|
||
const minor = Math.floor(Math.random() * 1000);
|
||
const patch = Math.floor(Math.random() * 10000);
|
||
const versionName = `${major}.${minor}.${patch}`;
|
||
return {
|
||
versionCode,
|
||
versionName
|
||
};
|
||
}
|
||
/**
|
||
* 修改APK版本号
|
||
*/
|
||
async changeVersion(sourceApkPath, versionCode, versionName) {
|
||
try {
|
||
this.addBuildLog('info', `开始修改版本号: versionCode=${versionCode}, versionName=${versionName}`);
|
||
// 1. 修改apktool.yml中的版本信息
|
||
const apktoolYmlPath = path_1.default.join(sourceApkPath, 'apktool.yml');
|
||
if (fs_1.default.existsSync(apktoolYmlPath)) {
|
||
let ymlContent = fs_1.default.readFileSync(apktoolYmlPath, 'utf8');
|
||
// 替换versionCode
|
||
ymlContent = ymlContent.replace(/versionCode:\s*\d+/g, `versionCode: ${versionCode}`);
|
||
// 替换versionName
|
||
ymlContent = ymlContent.replace(/versionName:\s*[\d.]+/g, `versionName: ${versionName}`);
|
||
fs_1.default.writeFileSync(apktoolYmlPath, ymlContent, 'utf8');
|
||
this.addBuildLog('info', 'apktool.yml中的版本号已更新');
|
||
}
|
||
// 2. 修改AndroidManifest.xml中的版本信息(如果存在)
|
||
const manifestPath = path_1.default.join(sourceApkPath, 'AndroidManifest.xml');
|
||
if (fs_1.default.existsSync(manifestPath)) {
|
||
let manifestContent = fs_1.default.readFileSync(manifestPath, 'utf8');
|
||
let modified = false;
|
||
// 替换android:versionCode(如果存在)
|
||
if (manifestContent.includes('android:versionCode')) {
|
||
manifestContent = manifestContent.replace(/android:versionCode=["']\d+["']/g, `android:versionCode="${versionCode}"`);
|
||
modified = true;
|
||
}
|
||
// 替换android:versionName(如果存在)
|
||
if (manifestContent.includes('android:versionName')) {
|
||
manifestContent = manifestContent.replace(/android:versionName=["'][^"']+["']/g, `android:versionName="${versionName}"`);
|
||
modified = true;
|
||
}
|
||
// 替换platformBuildVersionCode(如果存在)
|
||
if (manifestContent.includes('platformBuildVersionCode')) {
|
||
manifestContent = manifestContent.replace(/platformBuildVersionCode=["']\d+["']/g, `platformBuildVersionCode="${versionCode}"`);
|
||
modified = true;
|
||
}
|
||
// 替换platformBuildVersionName(如果存在)
|
||
if (manifestContent.includes('platformBuildVersionName')) {
|
||
manifestContent = manifestContent.replace(/platformBuildVersionName=["'][^"']+["']/g, `platformBuildVersionName="${versionName}"`);
|
||
modified = true;
|
||
}
|
||
if (modified) {
|
||
fs_1.default.writeFileSync(manifestPath, manifestContent, 'utf8');
|
||
this.addBuildLog('info', 'AndroidManifest.xml中的版本号已更新');
|
||
}
|
||
}
|
||
this.addBuildLog('success', '版本号修改完成');
|
||
}
|
||
catch (error) {
|
||
this.addBuildLog('error', `修改版本号失败: ${error.message}`);
|
||
throw error;
|
||
}
|
||
}
|
||
/**
|
||
* 生成随机包名
|
||
*/
|
||
generateRandomPackageName() {
|
||
// 生成类似 com.abc123def456 的随机包名
|
||
const randomString = () => {
|
||
const chars = 'abcdefghijklmnopqrstuvwxyz';
|
||
const nums = '0123456789';
|
||
let result = '';
|
||
// 3-5个小写字母
|
||
for (let i = 0; i < 3 + Math.floor(Math.random() * 3); i++) {
|
||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||
}
|
||
// 3-6个数字
|
||
for (let i = 0; i < 3 + Math.floor(Math.random() * 4); i++) {
|
||
result += nums.charAt(Math.floor(Math.random() * nums.length));
|
||
}
|
||
// 3-5个小写字母
|
||
for (let i = 0; i < 3 + Math.floor(Math.random() * 3); i++) {
|
||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||
}
|
||
return result;
|
||
};
|
||
// 生成两级包名:com.xxxxx
|
||
const firstLevel = ['com', 'net', 'org', 'io'][Math.floor(Math.random() * 4)];
|
||
const secondLevel = randomString();
|
||
return `${firstLevel}.${secondLevel}`;
|
||
}
|
||
/**
|
||
* 修改APK包名
|
||
*/
|
||
async changePackageName(sourceApkPath, oldPackageName, newPackageName) {
|
||
try {
|
||
this.addBuildLog('info', `开始修改包名: ${oldPackageName} -> ${newPackageName}`);
|
||
// 1. 修改AndroidManifest.xml
|
||
const manifestPath = path_1.default.join(sourceApkPath, 'AndroidManifest.xml');
|
||
if (fs_1.default.existsSync(manifestPath)) {
|
||
let manifestContent = fs_1.default.readFileSync(manifestPath, 'utf8');
|
||
// 替换package属性
|
||
manifestContent = manifestContent.replace(new RegExp(`package=["']${oldPackageName.replace(/\./g, '\\.')}["']`, 'g'), `package="${newPackageName}"`);
|
||
// 替换所有包名引用(在android:name等属性中)
|
||
const oldPackageRegex = new RegExp(oldPackageName.replace(/\./g, '\\.'), 'g');
|
||
manifestContent = manifestContent.replace(oldPackageRegex, newPackageName);
|
||
fs_1.default.writeFileSync(manifestPath, manifestContent, 'utf8');
|
||
this.addBuildLog('info', 'AndroidManifest.xml已更新');
|
||
}
|
||
// 2. 先更新所有smali文件中的包名引用(必须在重命名目录之前)
|
||
// 这是关键步骤:先更新文件内容,再重命名目录,避免引用不匹配
|
||
this.addBuildLog('info', '开始更新所有smali文件中的包名引用(关键步骤:先更新文件内容)...');
|
||
await this.updateAllSmaliFiles(sourceApkPath, oldPackageName, newPackageName);
|
||
this.addBuildLog('success', '所有smali文件中的包名引用已更新');
|
||
// 3. 重命名smali目录结构(使用复制+删除方式,避免Windows权限问题)
|
||
this.addBuildLog('info', '开始重命名smali目录结构...');
|
||
await this.renameAllSmaliDirectories(sourceApkPath, oldPackageName, newPackageName);
|
||
// 4. 更新apktool.yml文件(如果存在)
|
||
const apktoolYmlPath = path_1.default.join(sourceApkPath, 'apktool.yml');
|
||
if (fs_1.default.existsSync(apktoolYmlPath)) {
|
||
try {
|
||
let ymlContent = fs_1.default.readFileSync(apktoolYmlPath, 'utf8');
|
||
const oldPackageRegex = new RegExp(oldPackageName.replace(/\./g, '\\.'), 'g');
|
||
if (ymlContent.includes(oldPackageName)) {
|
||
ymlContent = ymlContent.replace(oldPackageRegex, newPackageName);
|
||
fs_1.default.writeFileSync(apktoolYmlPath, ymlContent, 'utf8');
|
||
this.addBuildLog('info', 'apktool.yml已更新');
|
||
}
|
||
}
|
||
catch (error) {
|
||
this.addBuildLog('warn', `更新apktool.yml失败: ${error.message}`);
|
||
}
|
||
}
|
||
// 5. 替换其他可能包含包名的文件
|
||
// 检查res目录下的XML文件
|
||
const resDir = path_1.default.join(sourceApkPath, 'res');
|
||
if (fs_1.default.existsSync(resDir)) {
|
||
this.addBuildLog('info', '检查res目录中的包名引用...');
|
||
await this.replacePackageNameInDirectory(resDir, oldPackageName, newPackageName, ['.xml']);
|
||
}
|
||
this.addBuildLog('success', '包名修改完成');
|
||
}
|
||
catch (error) {
|
||
this.addBuildLog('error', `修改包名失败: ${error.message}`);
|
||
throw error;
|
||
}
|
||
}
|
||
/**
|
||
* 复制目录(递归)
|
||
*/
|
||
async copyDirectory(src, dest) {
|
||
// 确保目标目录存在
|
||
if (!fs_1.default.existsSync(dest)) {
|
||
fs_1.default.mkdirSync(dest, { recursive: true });
|
||
}
|
||
const entries = fs_1.default.readdirSync(src, { withFileTypes: true });
|
||
for (const entry of entries) {
|
||
const srcPath = path_1.default.join(src, entry.name);
|
||
const destPath = path_1.default.join(dest, entry.name);
|
||
if (entry.isDirectory()) {
|
||
await this.copyDirectory(srcPath, destPath);
|
||
}
|
||
else {
|
||
fs_1.default.copyFileSync(srcPath, destPath);
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* 删除目录(带重试机制,跨平台兼容)
|
||
*/
|
||
async deleteDirectoryWithRetry(dirPath, maxRetries = 3) {
|
||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||
try {
|
||
if (fs_1.default.existsSync(dirPath)) {
|
||
// 先尝试删除文件,再删除目录
|
||
const entries = fs_1.default.readdirSync(dirPath, { withFileTypes: true });
|
||
for (const entry of entries) {
|
||
const entryPath = path_1.default.join(dirPath, entry.name);
|
||
if (entry.isDirectory()) {
|
||
await this.deleteDirectoryWithRetry(entryPath, maxRetries);
|
||
}
|
||
else {
|
||
// 尝试删除文件,如果失败则等待后重试
|
||
let fileDeleted = false;
|
||
for (let fileAttempt = 1; fileAttempt <= maxRetries; fileAttempt++) {
|
||
try {
|
||
fs_1.default.unlinkSync(entryPath);
|
||
fileDeleted = true;
|
||
break;
|
||
}
|
||
catch (error) {
|
||
if (fileAttempt < maxRetries) {
|
||
this.addBuildLog('warn', `删除文件失败,等待后重试 (${fileAttempt}/${maxRetries}): ${entryPath}`);
|
||
await new Promise(resolve => setTimeout(resolve, 500 * fileAttempt));
|
||
}
|
||
else {
|
||
throw error;
|
||
}
|
||
}
|
||
}
|
||
if (!fileDeleted) {
|
||
throw new Error(`无法删除文件: ${entryPath}`);
|
||
}
|
||
}
|
||
}
|
||
// 删除空目录(跨平台兼容)
|
||
try {
|
||
fs_1.default.rmdirSync(dirPath);
|
||
}
|
||
catch (rmdirError) {
|
||
// 如果rmdirSync失败,尝试使用rmSync(Node.js 14.14.0+)
|
||
if (typeof fs_1.default.rmSync === 'function') {
|
||
try {
|
||
fs_1.default.rmSync(dirPath, { recursive: true, force: true });
|
||
}
|
||
catch (rmError) {
|
||
throw rmdirError; // 如果都失败,抛出原始错误
|
||
}
|
||
}
|
||
else {
|
||
throw rmdirError;
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
}
|
||
catch (error) {
|
||
if (attempt < maxRetries) {
|
||
this.addBuildLog('warn', `删除目录失败,等待后重试 (${attempt}/${maxRetries}): ${dirPath}`);
|
||
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
|
||
}
|
||
else {
|
||
this.addBuildLog('error', `删除目录失败,已重试${maxRetries}次: ${dirPath}`);
|
||
// 不抛出错误,继续执行(可能被其他进程占用)
|
||
this.addBuildLog('warn', '目录可能被其他进程占用,将在后续清理中处理');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* 清理空的目录
|
||
*/
|
||
cleanupEmptyDirectories(baseDir, packageParts) {
|
||
let currentDir = baseDir;
|
||
for (let i = packageParts.length - 1; i >= 0; i--) {
|
||
currentDir = path_1.default.join(currentDir, packageParts[i]);
|
||
if (fs_1.default.existsSync(currentDir)) {
|
||
try {
|
||
const files = fs_1.default.readdirSync(currentDir);
|
||
if (files.length === 0) {
|
||
fs_1.default.rmdirSync(currentDir);
|
||
this.addBuildLog('info', `已删除空目录: ${currentDir}`);
|
||
}
|
||
else {
|
||
break;
|
||
}
|
||
}
|
||
catch {
|
||
// 忽略错误
|
||
}
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* 更新所有smali文件中的包名引用(包括smali和smali_classes*目录)
|
||
*/
|
||
async updateAllSmaliFiles(sourceApkPath, oldPackageName, newPackageName) {
|
||
const oldPackageSmali = oldPackageName.replace(/\./g, '/');
|
||
const newPackageSmali = newPackageName.replace(/\./g, '/');
|
||
// 处理主smali目录
|
||
const smaliDir = path_1.default.join(sourceApkPath, 'smali');
|
||
if (fs_1.default.existsSync(smaliDir)) {
|
||
this.addBuildLog('info', '更新smali目录中的文件...');
|
||
await this.replacePackageNameInSmaliFiles(smaliDir, oldPackageName, newPackageName, oldPackageSmali, newPackageSmali);
|
||
}
|
||
// 处理smali_classes2, smali_classes3等目录(如果有)
|
||
for (let i = 2; i <= 10; i++) {
|
||
const smaliClassDir = path_1.default.join(sourceApkPath, `smali_classes${i}`);
|
||
if (fs_1.default.existsSync(smaliClassDir)) {
|
||
this.addBuildLog('info', `更新smali_classes${i}目录中的文件...`);
|
||
await this.replacePackageNameInSmaliFiles(smaliClassDir, oldPackageName, newPackageName, oldPackageSmali, newPackageSmali);
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* 重命名所有smali目录结构(包括smali和smali_classes*目录)
|
||
*/
|
||
async renameAllSmaliDirectories(sourceApkPath, oldPackageName, newPackageName) {
|
||
const oldPackagePath = oldPackageName.split('.');
|
||
const newPackagePath = newPackageName.split('.');
|
||
// 处理主smali目录
|
||
const smaliDir = path_1.default.join(sourceApkPath, 'smali');
|
||
if (fs_1.default.existsSync(smaliDir)) {
|
||
await this.renameSmaliDirectory(smaliDir, oldPackagePath, newPackagePath);
|
||
}
|
||
// 处理smali_classes2, smali_classes3等目录
|
||
for (let i = 2; i <= 10; i++) {
|
||
const smaliClassDir = path_1.default.join(sourceApkPath, `smali_classes${i}`);
|
||
if (fs_1.default.existsSync(smaliClassDir)) {
|
||
await this.renameSmaliDirectory(smaliClassDir, oldPackagePath, newPackagePath);
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* 重命名单个smali目录
|
||
*/
|
||
async renameSmaliDirectory(smaliDir, oldPackagePath, newPackagePath) {
|
||
const oldSmaliPath = path_1.default.join(smaliDir, ...oldPackagePath);
|
||
const newSmaliPath = path_1.default.join(smaliDir, ...newPackagePath);
|
||
if (fs_1.default.existsSync(oldSmaliPath)) {
|
||
// 确保新目录的父目录存在
|
||
const newSmaliParent = path_1.default.dirname(newSmaliPath);
|
||
if (!fs_1.default.existsSync(newSmaliParent)) {
|
||
fs_1.default.mkdirSync(newSmaliParent, { recursive: true });
|
||
}
|
||
// 如果新目录已存在,先删除(跨平台兼容)
|
||
if (fs_1.default.existsSync(newSmaliPath)) {
|
||
this.addBuildLog('info', '删除已存在的新目录...');
|
||
await this.deleteDirectoryWithRetry(newSmaliPath, 1);
|
||
}
|
||
// 使用复制+删除方式,避免Windows权限问题(跨平台兼容)
|
||
// 使用path.sep显示路径,但smali路径始终使用/(Android标准)
|
||
const displayOldPath = oldPackagePath.join('/');
|
||
const displayNewPath = newPackagePath.join('/');
|
||
this.addBuildLog('info', `复制目录: ${displayOldPath} -> ${displayNewPath}`);
|
||
await this.copyDirectory(oldSmaliPath, newSmaliPath);
|
||
// 删除旧目录(使用重试机制)
|
||
this.addBuildLog('info', '删除旧目录...');
|
||
await this.deleteDirectoryWithRetry(oldSmaliPath, 3);
|
||
this.addBuildLog('success', `smali目录已重命名: ${oldPackagePath.join('.')} -> ${newPackagePath.join('.')}`);
|
||
// 清理空的旧目录
|
||
this.cleanupEmptyDirectories(smaliDir, oldPackagePath);
|
||
}
|
||
else {
|
||
this.addBuildLog('warn', `旧目录不存在: ${oldSmaliPath}`);
|
||
}
|
||
}
|
||
/**
|
||
* 递归替换smali文件中的包名
|
||
*/
|
||
async replacePackageNameInSmaliFiles(dir, oldPackageName, newPackageName, oldPackageSmali, newPackageSmali) {
|
||
// 如果没有提供smali格式的包名,自动生成
|
||
if (!oldPackageSmali) {
|
||
oldPackageSmali = oldPackageName.replace(/\./g, '/');
|
||
}
|
||
if (!newPackageSmali) {
|
||
newPackageSmali = newPackageName.replace(/\./g, '/');
|
||
}
|
||
const files = fs_1.default.readdirSync(dir);
|
||
for (const file of files) {
|
||
const filePath = path_1.default.join(dir, file);
|
||
const stat = fs_1.default.statSync(filePath);
|
||
if (stat.isDirectory()) {
|
||
await this.replacePackageNameInSmaliFiles(filePath, oldPackageName, newPackageName);
|
||
}
|
||
else if (file.endsWith('.smali')) {
|
||
try {
|
||
let content = fs_1.default.readFileSync(filePath, 'utf8');
|
||
// 替换包名引用(Lcom/hikoncont/... -> L新包名/...)
|
||
const oldPackagePath = oldPackageName.replace(/\./g, '/');
|
||
const newPackagePath = newPackageName.replace(/\./g, '/');
|
||
// 1. 替换类定义中的包名(.class public Lcom/hikoncont/...)
|
||
content = content.replace(new RegExp(`\\.class[^\\n]*L${oldPackagePath.replace(/\//g, '\\/')}/`, 'g'), (match) => match.replace(`L${oldPackagePath}/`, `L${newPackagePath}/`));
|
||
// 2. 替换类路径引用(Lcom/hikoncont/... -> L新包名/...)
|
||
// 使用单词边界确保不会误替换
|
||
content = content.replace(new RegExp(`L${oldPackagePath.replace(/\//g, '\\/')}/`, 'g'), `L${newPackagePath}/`);
|
||
// 3. 替换完整类名引用(com.hikoncont.ClassName -> 新包名.ClassName)
|
||
content = content.replace(new RegExp(oldPackageName.replace(/\./g, '\\.'), 'g'), newPackageName);
|
||
// 4. 替换字符串中的包名引用("com.hikoncont" -> "新包名")
|
||
content = content.replace(new RegExp(`"${oldPackageName.replace(/\./g, '\\.')}"`, 'g'), `"${newPackageName}"`);
|
||
// 5. 替换字符串中的包名引用('com.hikoncont' -> '新包名')
|
||
content = content.replace(new RegExp(`'${oldPackageName.replace(/\./g, '\\.')}'`, 'g'), `'${newPackageName}'`);
|
||
fs_1.default.writeFileSync(filePath, content, 'utf8');
|
||
}
|
||
catch (error) {
|
||
this.addBuildLog('warn', `替换文件失败 ${filePath}: ${error.message}`);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* 在目录中递归替换包名(用于XML等文件)
|
||
*/
|
||
async replacePackageNameInDirectory(dir, oldPackageName, newPackageName, extensions) {
|
||
if (!fs_1.default.existsSync(dir)) {
|
||
return;
|
||
}
|
||
const files = fs_1.default.readdirSync(dir);
|
||
for (const file of files) {
|
||
const filePath = path_1.default.join(dir, file);
|
||
const stat = fs_1.default.statSync(filePath);
|
||
if (stat.isDirectory()) {
|
||
await this.replacePackageNameInDirectory(filePath, oldPackageName, newPackageName, extensions);
|
||
}
|
||
else {
|
||
const ext = path_1.default.extname(file);
|
||
if (extensions.includes(ext)) {
|
||
try {
|
||
let content = fs_1.default.readFileSync(filePath, 'utf8');
|
||
const oldPackageRegex = new RegExp(oldPackageName.replace(/\./g, '\\.'), 'g');
|
||
if (content.includes(oldPackageName)) {
|
||
content = content.replace(oldPackageRegex, newPackageName);
|
||
fs_1.default.writeFileSync(filePath, content, 'utf8');
|
||
}
|
||
}
|
||
catch (error) {
|
||
// 忽略错误
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
/**
|
||
* 检查构建环境(用于apktool打包)
|
||
*/
|
||
async checkBuildEnvironment() {
|
||
const result = {
|
||
hasJava: false,
|
||
javaVersion: undefined,
|
||
errors: []
|
||
};
|
||
try {
|
||
// 检查Java(必需)
|
||
try {
|
||
const { stdout } = await execAsync('java -version', { timeout: 10000 });
|
||
result.hasJava = true;
|
||
result.javaVersion = stdout.split('\n')[0];
|
||
}
|
||
catch {
|
||
result.errors.push('Java未安装或未在PATH中');
|
||
}
|
||
// 检查apktool
|
||
const apktoolPath = path_1.default.join(process.cwd(), 'android/apktool.jar');
|
||
if (!fs_1.default.existsSync(apktoolPath)) {
|
||
result.errors.push('apktool不存在: android/apktool.jar');
|
||
}
|
||
// 检查source.apk文件
|
||
const sourceApkFile = path_1.default.join(process.cwd(), 'android/source.apk');
|
||
if (!fs_1.default.existsSync(sourceApkFile)) {
|
||
result.errors.push('source.apk文件不存在: android/source.apk');
|
||
}
|
||
}
|
||
catch (error) {
|
||
this.logger.error('检查构建环境失败:', error);
|
||
result.errors.push(error.message);
|
||
}
|
||
return result;
|
||
}
|
||
/**
|
||
* 销毁服务
|
||
*/
|
||
destroy() {
|
||
this.cloudflareService.destroy();
|
||
}
|
||
}
|
||
exports.default = APKBuildService;
|
||
//# sourceMappingURL=APKBuildService.js.map
|