Files
server/dist/services/APKBuildService.js

2024 lines
103 KiB
JavaScript
Raw Permalink Normal View History

2026-02-09 16:34:01 +08:00
"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() {
// 生成随机versionCode1000000-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失败尝试使用rmSyncNode.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