Files
server/dist/services/APKBuildService.js
wdvipa 450367dea2 111
2026-02-09 16:34:01 +08:00

2024 lines
103 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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