// build-android.js - 统一Android SDK安装和构建脚本 const fs = require('fs'); const path = require('path'); const https = require('https'); const http = require('http'); const { execSync } = require('child_process'); const { URL } = require('url'); const os = require('os'); const { rmSync } = require('fs'); // 配置 - 跨平台设置 const isWindows = process.platform === 'win32'; const isMac = process.platform === 'darwin'; // 根据平台选择相应的 ZIP URL const platformZipMap = { win32: 'commandlinetools-win-11076708_latest.zip', linux: 'commandlinetools-linux-11076708_latest.zip', darwin: 'commandlinetools-mac-11076708_latest.zip' }; // 按优先级确定 SDK 安装路径:环境变量 > 平台默认路径 const defaultSdkRoot = isWindows ? path.join(process.env.APPDATA, 'Android', 'Sdk') : path.join(os.homedir(), 'android-sdk'); const SDK_ROOT = process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME || defaultSdkRoot; const SDK_URL = `https://dl.google.com/android/repository/${platformZipMap[process.platform] || platformZipMap.linux}`; const CMDLINE_TOOLS_DIR = path.join(SDK_ROOT, 'cmdline-tools', 'latest'); const TEMP_ZIP = path.join(process.env.TEMP || (isWindows ? process.env.TMP : '/tmp'), 'cmdtools.zip'); const VERSION_FILE = path.join(SDK_ROOT, '.sdk-version'); const EXPECTED_VERSION = 'platforms-35-buildtools-35.0.0'; // 从环境变量获取代理设置 const PROXY = process.env.HTTPS_PROXY || process.env.HTTP_PROXY; // 读取 package.json 获取版本号 const packageJson = require(path.join(__dirname, '..', 'package.json')); // 计算各路径 const rootDir = path.join(__dirname, '..'); // 项目根目录 const cordovaPrjDir = path.join(__dirname, 'dros'); // .build/dros const srcDir = path.join(rootDir, 'dist', 'h5'); // ../dist/h5 const dstDir = path.join(cordovaPrjDir, 'www'); console.log('🚀 Android SDK 安装和构建统一脚本启动...'); console.log(`📦 SDK Root: ${SDK_ROOT}`); if (PROXY) { console.log(`🌐 使用代理: ${PROXY}`); } // 检查 SDK 是否已安装且版本正确 function isSdkInstalled() { console.log('🔍 检测 Android SDK 安装状态...'); const sdkManagerExe = isWindows ? 'sdkmanager.bat' : 'sdkmanager'; const sdkManagerPath = path.join(CMDLINE_TOOLS_DIR, 'bin', sdkManagerExe); const platformPath = path.join(SDK_ROOT, 'platforms', 'android-35'); const buildToolsPath = path.join(SDK_ROOT, 'build-tools', '35.0.0'); // 检查关键文件是否存在 console.log(' 📂 检查 sdkmanager...'); if (!fs.existsSync(sdkManagerPath)) { console.log(' ❌ sdkmanager 不存在'); return false; } console.log(' ✅ sdkmanager 存在'); console.log(' 📂 检查 platforms;android-35...'); if (!fs.existsSync(platformPath)) { console.log(' ❌ platforms;android-35 不存在'); return false; } console.log(' ✅ platforms;android-35 存在'); console.log(' 📂 检查 build-tools;35.0.0...'); if (!fs.existsSync(buildToolsPath)) { console.log(' ❌ build-tools;35.0.0 不存在'); return false; } console.log(' ✅ build-tools;35.0.0 存在'); // 验证版本标记文件 console.log(' 📝 检查版本标记...'); if (fs.existsSync(VERSION_FILE)) { const installedVersion = fs.readFileSync(VERSION_FILE, 'utf8').trim(); console.log(` 已安装版本: ${installedVersion}`); console.log(` 期望版本: ${EXPECTED_VERSION}`); if (installedVersion === EXPECTED_VERSION) { console.log(' ✅ 版本匹配'); return true; } else { console.log(' ⚠️ 版本不匹配,需要重新安装'); return false; } } else { console.log(' ⚠️ 版本标记文件不存在'); return false; } } // 下载文件(支持代理) function downloadFile(url, dest) { return new Promise((resolve, reject) => { console.log(`📥 下载: ${url}`); const file = fs.createWriteStream(dest); const urlObj = new URL(url); let requestLib = https; let options = { hostname: urlObj.hostname, path: urlObj.pathname + urlObj.search, method: 'GET', headers: { 'User-Agent': 'Node.js' } }; // 如果有代理,使用代理设置 if (PROXY) { const proxyUrl = new URL(PROXY); requestLib = proxyUrl.protocol === 'https:' ? https : http; options = { hostname: proxyUrl.hostname, port: proxyUrl.port, path: url, // 使用完整 URL 作为路径 method: 'GET', headers: { 'Host': urlObj.hostname, 'User-Agent': 'Node.js' } }; } const request = requestLib.get(options, (response) => { // 处理重定向 if (response.statusCode === 301 || response.statusCode === 302) { const redirectUrl = response.headers.location; console.log(`🔀 重定向到: ${redirectUrl}`); file.close(); fs.unlinkSync(dest); return downloadFile(redirectUrl, dest).then(resolve).catch(reject); } if (response.statusCode !== 200) { reject(new Error(`下载失败,状态码: ${response.statusCode}`)); return; } const totalSize = parseInt(response.headers['content-length'], 10); let downloadedSize = 0; response.on('data', (chunk) => { downloadedSize += chunk.length; const percent = ((downloadedSize / totalSize) * 100).toFixed(2); process.stdout.write(`\r下载进度: ${percent}% (${downloadedSize}/${totalSize} bytes)`); }); response.pipe(file); file.on('finish', () => { file.close(); console.log('\n✅ 下载完成'); resolve(); }); }); request.on('error', (err) => { fs.unlinkSync(dest); reject(err); }); file.on('error', (err) => { fs.unlinkSync(dest); reject(err); }); }); } // 解压 ZIP 文件 function extractZip(zipPath, destDir) { console.log(`📂 解压到: ${destDir}`); // 确保目标目录存在 fs.mkdirSync(destDir, { recursive: true }); // 在 Windows 上使用 PowerShell 的 Expand-Archive if (process.platform === 'win32') { try { const cmd = `powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"`; execSync(cmd, { stdio: 'inherit' }); console.log('✅ 解压完成'); } catch (error) { throw new Error(`解压失败: ${error.message}`); } } else { // Linux 使用 unzip try { execSync(`unzip -q "${zipPath}" -d "${destDir}"`, { stdio: 'inherit' }); console.log('✅ 解压完成'); } catch (error) { throw new Error(`解压失败: ${error.message}`); } } } // 设置环境变量(写入 GITHUB_ENV) function setGitHubEnv(name, value) { const envFile = process.env.GITHUB_ENV; if (envFile) { fs.appendFileSync(envFile, `${name}=${value}\n`); console.log(`✅ 设置环境变量: ${name}=${value}`); } else { console.log(`⚠️ GITHUB_ENV 未定义,跳过设置: ${name}=${value}`); } } // 安装 Gradle 到 PATH async function installGradleToPath() { const gradleVersion = '9.0.0'; // 使用稳定版本,可根据需要更新 const gradleUrl = `https://mirrors.cloud.tencent.com/gradle/gradle-${gradleVersion}-bin.zip`; const gradleInstallDir = isWindows ? path.join(process.env.USERPROFILE, 'gradle') : path.join(os.homedir(), 'gradle'); const gradleBin = path.join(gradleInstallDir, `gradle-${gradleVersion}`, 'bin'); // 检查 Gradle 是否已存在 const gradleExe = isWindows ? 'gradle.bat' : 'gradle'; const fullGradleExe = path.join(gradleBin, gradleExe); if (fs.existsSync(fullGradleExe)) { console.log('✅ Gradle 已安装'); // 添加到 PATH(即使已安装也要确保在PATH中) const pathEnvFile = process.env.GITHUB_PATH; if (pathEnvFile) { fs.appendFileSync(pathEnvFile, `${gradleBin}\n`); console.log(`✅ Gradle 已添加到 PATH: ${gradleBin}`); } return; } console.log(`📦 下载 Gradle ${gradleVersion}...`); await downloadFile(gradleUrl, TEMP_ZIP); console.log(`📂 解压 Gradle 到: ${gradleInstallDir}`); extractZip(TEMP_ZIP, gradleInstallDir); // 添加到 PATH const pathEnvFile = process.env.GITHUB_PATH; if (pathEnvFile) { fs.appendFileSync(pathEnvFile, `${gradleBin}\n`); console.log(`✅ Gradle 已添加到 PATH: ${gradleBin}`); } else { console.log(`⚠️ 请手动将 ${gradleBin} 添加到 PATH`); } // 测试 Gradle try { execSync(`"${fullGradleExe}" --version`, { stdio: 'pipe' }); console.log('✅ Gradle 安装成功'); } catch (error) { console.warn('⚠️ Gradle 测试失败:', error.message); } } // 运行 sdkmanager function runSdkManager(args) { const sdkManagerExe = isWindows ? 'sdkmanager.bat' : 'sdkmanager'; const sdkManagerPath = path.join(CMDLINE_TOOLS_DIR, 'bin', sdkManagerExe); if (!fs.existsSync(sdkManagerPath)) { throw new Error(`sdkmanager 不存在: ${sdkManagerPath}`); } console.log(`🔧 运行: sdkmanager ${args.join(' ')}`); try { // 分离包名和参数 const packages = args.filter(arg => !arg.startsWith('--')); const flags = args.filter(arg => arg.startsWith('--')); // 只对包名加引号,参数不加引号 const quotedPackages = packages.map(pkg => `"${pkg}"`).join(' '); const cmd = `"${sdkManagerPath}" ${quotedPackages} ${flags.join(' ')}`; console.log(`执行命令: ${cmd}`); // 根据平台设置不同的 shell 选项 const execOptions = isWindows ? { stdio: 'inherit', shell: 'cmd.exe' } : { stdio: 'inherit' }; execSync(cmd, execOptions); console.log('✅ sdkmanager 执行成功'); } catch (error) { throw new Error(`sdkmanager 执行失败: ${error.message}`); } } // 安装和配置 Android SDK async function setupAndroidSdk() { console.log('🔧 检查 Gradle 安装...'); await installGradleToPath(); // 0. 检查 SDK 是否已安装 if (isSdkInstalled()) { console.log('\n✅ Android SDK 已安装且版本正确,跳过安装流程'); console.log(`📦 SDK Root: ${SDK_ROOT}`); // 仍然需要设置环境变量(GitHub Actions 每次运行都需要) console.log('\n⚙️ 设置环境变量...'); setGitHubEnv('ANDROID_HOME', SDK_ROOT); setGitHubEnv('ANDROID_SDK_ROOT', SDK_ROOT); console.log('\n🎉 Android SDK 环境变量设置完成!'); console.log('⏱️ 耗时: ~1-2秒(本地缓存加速)'); return; } console.log('\n📦 未检测到有效的 Android SDK,开始完整安装流程...'); // 1. 下载 SDK await downloadFile(SDK_URL, TEMP_ZIP); // 2. 解压到临时目录 const tempExtractDir = path.join(SDK_ROOT, 'cmdline-tools', 'temp'); extractZip(TEMP_ZIP, tempExtractDir); // 3. 移动内部的 cmdline-tools 到 latest(ZIP 内部包含 cmdline-tools 目录) const extractedCmdlineTools = path.join(tempExtractDir, 'cmdline-tools'); if (fs.existsSync(extractedCmdlineTools)) { console.log('📦 重新组织目录结构...'); // 如果 latest 目录已存在,先删除 if (fs.existsSync(CMDLINE_TOOLS_DIR)) { fs.rmSync(CMDLINE_TOOLS_DIR, { recursive: true, force: true }); } fs.renameSync(extractedCmdlineTools, CMDLINE_TOOLS_DIR); // 删除临时目录 fs.rmSync(tempExtractDir, { recursive: true, force: true }); console.log('✅ 目录结构已调整'); } else { throw new Error(`解压后未找到 cmdline-tools 目录: ${extractedCmdlineTools}`); } // 4. 设置环境变量 setGitHubEnv('ANDROID_HOME', SDK_ROOT); setGitHubEnv('ANDROID_SDK_ROOT', SDK_ROOT); // 5. 先接受 licenses(使用输入重定向文件) console.log('📝 接受 SDK licenses...'); const sdkManagerExe = isWindows ? 'sdkmanager.bat' : 'sdkmanager'; const sdkManagerPath = path.join(CMDLINE_TOOLS_DIR, 'bin', sdkManagerExe); // 创建包含多个 y 的临时文件(每个 y 一行) const tempInputFileDir = isWindows ? process.env.TEMP || process.env.TMP : '/tmp'; const tempInputFile = path.join(tempInputFileDir, 'sdk-licenses-input.txt'); // 根据日志显示有 7 个许可证,我们用 20 个 y 确保足够 fs.writeFileSync(tempInputFile, 'y\n'.repeat(20)); // 使用输入重定向 < 而不是管道 | const licenseCmd = `"${sdkManagerPath}" --licenses --sdk_root=${SDK_ROOT} < "${tempInputFile}"`; // 根据平台设置不同的 shell 选项 const licenseExecOptions = isWindows ? { stdio: 'inherit', shell: 'cmd.exe' } : { stdio: 'inherit' }; execSync(licenseCmd, licenseExecOptions); console.log('✅ licenses 接受完成'); // 清理临时文件 if (fs.existsSync(tempInputFile)) { fs.unlinkSync(tempInputFile); } // 6. 然后安装 platforms 和 build-tools console.log('📦 安装 Android SDK 组件...'); runSdkManager([ 'platforms;android-35', 'build-tools;35.0.0', `--sdk_root=${SDK_ROOT}` ]); console.log('🎉 Android SDK 安装完成!'); // 7. 创建版本标记文件 console.log('📝 创建版本标记文件...'); fs.writeFileSync(VERSION_FILE, EXPECTED_VERSION, 'utf8'); console.log(`✅ 版本标记已创建: ${EXPECTED_VERSION}`); // 清理临时文件 if (fs.existsSync(TEMP_ZIP)) { fs.unlinkSync(TEMP_ZIP); console.log('🧹 已清理临时文件'); } } // 配置 Gradle Wrapper 使用我们的 Gradle 版本 function configureGradleWrapper() { const gradleWrapperPropsPath = path.join(cordovaPrjDir, 'platforms', 'android', 'gradle', 'wrapper', 'gradle-wrapper.properties'); if (!fs.existsSync(gradleWrapperPropsPath)) { console.log('⚠️ Gradle wrapper 配置文件不存在,跳过配置'); return; } console.log('🔧 配置 Gradle wrapper 使用 Gradle 8.13(腾讯云镜像)...'); try { // 读取现有的配置文件 let content = fs.readFileSync(gradleWrapperPropsPath, 'utf8'); // 替换 distributionUrl 为腾讯云镜像,并添加超时配置 const oldUrlPattern = /distributionUrl=.*/; const newUrl = 'distributionUrl=https\\://mirrors.cloud.tencent.com/gradle/gradle-8.13-bin.zip'; if (oldUrlPattern.test(content)) { content = content.replace(oldUrlPattern, newUrl); fs.writeFileSync(gradleWrapperPropsPath, content, 'utf8'); console.log('✅ Gradle wrapper 已配置为使用 Gradle 8.13(腾讯云镜像)'); } else { console.log('⚠️ 未找到 distributionUrl 配置,添加新配置'); content += `\n${newUrl}\n`; fs.writeFileSync(gradleWrapperPropsPath, content, 'utf8'); console.log('✅ 已添加 Gradle 8.13 配置'); } // 添加网络超时配置 if (!content.includes('networkTimeout')) { content += '\n# 网络超时设置(300秒 = 5分钟)\nnetworkTimeout=300000\n'; fs.writeFileSync(gradleWrapperPropsPath, content, 'utf8'); console.log('✅ 已添加网络超时配置'); } } catch (error) { console.warn('⚠️ 配置 Gradle wrapper 失败:', error.message); } } // 配置全局 Gradle 设置(超时时间、镜像等) function configureGradleGlobal() { const initGradlePath = path.join(cordovaPrjDir, 'init.gradle'); console.log('🔧 创建全局 Gradle 配置(init.gradle)...'); const initGradleContent = ` allprojects { repositories { maven { url 'https://maven.aliyun.com/repository/public' } maven { url 'https://maven.aliyun.com/repository/google' } maven { url 'https://maven.aliyun.com/repository/gradle-plugin' } mavenCentral() google() jcenter() } // 配置所有任务的网络超时 tasks.withType(JavaCompile) { options.fork = true options.forkOptions.memoryMaximumSize = '1g' } } task wrapper(type: Wrapper) { gradleVersion = '8.13' distributionUrl = 'https://mirrors.cloud.tencent.com/gradle/gradle-8.13-bin.zip' networkTimeout = 300000 // 5分钟超时 } // 全局超时设置 gradle.startParameter.taskExecutionTimeout = java.time.Duration.ofMinutes(10) `; fs.writeFileSync(initGradlePath, initGradleContent, 'utf8'); console.log('✅ 已创建 init.gradle 全局配置(5分钟超时 + 阿里云镜像)'); } // 配置 Android 构建环境变量 function getAndroidEnv() { // 计算 Gradle bin 路径 const gradleVersion = '9.0.0'; const gradleBin = isWindows ? path.join(process.env.USERPROFILE || '', 'gradle', `gradle-${gradleVersion}`, 'bin') : path.join(process.env.HOME || '/root', 'gradle', `gradle-${gradleVersion}`, 'bin'); return { ...process.env, // 使用配置的SDK路径 ANDROID_HOME: SDK_ROOT, ANDROID_SDK_ROOT: SDK_ROOT, // 确保 Java 路径正确 JAVA_HOME: process.env.JAVA_HOME, // 将 Gradle 添加到 PATH 前缀,确保 Cordova 子进程能找到它 PATH: `${gradleBin}${path.delimiter}${process.env.PATH}` }; } // 构建 Android APK function buildAndroidApk() { const androidEnv = getAndroidEnv(); rmSync('.build/dros', { recursive: true, force: true }); // 做一些环境检查 execSync('npx cordova --version', { stdio: 'inherit', env: androidEnv }); // 1. 在项目根目录执行 cordova create execSync('npx cordova create .build/dros zskk.dros dros', { cwd: rootDir, stdio: 'inherit', env: androidEnv }); // 2. 在 .build/dros 中执行 cordova platform add android execSync('npx cordova platform add android', { cwd: cordovaPrjDir, stdio: 'inherit', env: androidEnv }); // 2.5. 配置 Gradle wrapper 使用我们的 Gradle 版本 configureGradleWrapper(); // 2.6. 配置全局 Gradle 设置(超时时间、镜像等) configureGradleGlobal(); // 3. 复制 dist/h5 → .build/dros/www fs.rmSync(dstDir, { recursive: true, force: true }); fs.mkdirSync(dstDir, { recursive: true }); function copy(src, dst) { const stat = fs.statSync(src); if (stat.isDirectory()) { fs.mkdirSync(dst, { recursive: true }); for (const entry of fs.readdirSync(src)) { copy(path.join(src, entry), path.join(dst, entry)); } } else { fs.copyFileSync(src, dst); } } copy(srcDir, dstDir); // 3.1 需要修改 index.html,加入 Cordova 脚本引用 const indexPath = path.join(dstDir, 'index.html'); let indexContent = fs.readFileSync(indexPath, 'utf8'); if (!indexContent.includes('cordova.js')) { indexContent = indexContent.replace( '', ' \n \ \n \ \n' ); fs.writeFileSync(indexPath, indexContent, 'utf8'); console.log('✅ 已在 index.html 中添加 cordova.js 引用'); } else { console.log('✅ index.html 已包含 cordova.js 引用,跳过添加'); } // 3.2 把.build\dros\platforms\android\platform_www\cordova.js复制到.build\dros\www目录下 const cordovaJsSrc = path.join(cordovaPrjDir, 'platforms', 'android', 'platform_www', 'cordova.js'); const cordovaJsDst = path.join(dstDir, 'cordova.js'); fs.copyFileSync(cordovaJsSrc, cordovaJsDst); console.log('✅ 已复制 cordova.js 到 www 目录'); execSync('npx cordova plugin add cordova-plugin-file', { cwd: cordovaPrjDir, stdio: 'inherit' }); // 4. 安装插件 : 为了本地保存网络配置等数据 execSync('npx cordova plugin add cordova-plugin-nativestorage', { cwd: cordovaPrjDir, stdio: 'inherit' }); execSync('npx cordova plugin add cordova-plugin-camera', { cwd: cordovaPrjDir, stdio: 'inherit' }); // 5. 复制预配置好的config.xml fs.copyFileSync(path.join(__dirname, 'config.xml'), path.join(cordovaPrjDir, 'config.xml')); // 6. 在 .build/dros 中执行 cordova build android execSync('npx cordova build android --verbose', { cwd: cordovaPrjDir, stdio: 'inherit', env: androidEnv }); // 7. 重命名apk文件 const version = packageJson.version; const pkg = 'dros'; const src = path.join(__dirname, 'dros/platforms/android/app/build/outputs/apk/debug/app-debug.apk'); const dst = path.join(__dirname, `dros/platforms/android/app/build/outputs/apk/debug/${pkg}-v${version}.apk`); fs.copyFileSync(src, dst); console.log(`✅ 已生成 ${dst}`); // 安装到连接的安卓设备(CI 环境跳过) if (process.env.CI !== 'true') { try { execSync(`adb install "${dst}"`, { stdio: 'inherit', env: androidEnv }); console.log(`✅ 已安装到安卓设备 ${dst}`); } catch (error) { console.warn('⚠️ ADB 安装失败,可能没有连接设备:', error.message); } } else { console.log('ℹ️ CI 环境,跳过 ADB 安装步骤'); } // 8. 部署到服务器(可选) (async () => { if (process.env.DEPLOY_ANDROID === 'true') { console.log('\n📤 开始部署到服务器...'); console.log('='.repeat(50)); try { const { deployAndroidToServer } = require('./deploy-android-to-server'); await deployAndroidToServer({ privateKey: process.env.DEPLOY_KEY, host: process.env.DEPLOY_HOST, username: process.env.DEPLOY_USER, remotePath: process.env.DEPLOY_PATH, apkPath: dst, appVersion: version }); console.log('='.repeat(50)); console.log('✅ 部署完成!'); } catch (error) { console.error('='.repeat(50)); console.error('❌ 部署失败:', error.message); process.exit(1); } } else { console.log('\n💡 提示: 设置 DEPLOY_ANDROID=true 可自动部署到服务器'); } })(); } // 主函数 async function main() { try { // 1. 安装和配置 Android SDK await setupAndroidSdk(); // 2. 构建 Android APK console.log('\n🏗️ 开始构建 Android APK...'); buildAndroidApk(); } catch (error) { console.error('❌ 错误:', error.message); process.exit(1); } } // 运行 main();