build-android.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590
  1. // build-android.js - 统一Android SDK安装和构建脚本
  2. const fs = require('fs');
  3. const path = require('path');
  4. const https = require('https');
  5. const http = require('http');
  6. const { execSync } = require('child_process');
  7. const { URL } = require('url');
  8. const os = require('os');
  9. const { rmSync } = require('fs');
  10. // 配置 - 跨平台设置
  11. const isWindows = process.platform === 'win32';
  12. const isMac = process.platform === 'darwin';
  13. // 根据平台选择相应的 ZIP URL
  14. const platformZipMap = {
  15. win32: 'commandlinetools-win-11076708_latest.zip',
  16. linux: 'commandlinetools-linux-11076708_latest.zip',
  17. darwin: 'commandlinetools-mac-11076708_latest.zip'
  18. };
  19. // 按优先级确定 SDK 安装路径:环境变量 > 平台默认路径
  20. const defaultSdkRoot = isWindows
  21. ? path.join(process.env.APPDATA, 'Android', 'Sdk')
  22. : path.join(os.homedir(), 'android-sdk');
  23. const SDK_ROOT = process.env.ANDROID_SDK_ROOT || process.env.ANDROID_HOME || defaultSdkRoot;
  24. const SDK_URL = `https://dl.google.com/android/repository/${platformZipMap[process.platform] || platformZipMap.linux}`;
  25. const CMDLINE_TOOLS_DIR = path.join(SDK_ROOT, 'cmdline-tools', 'latest');
  26. const TEMP_ZIP = path.join(process.env.TEMP || (isWindows ? process.env.TMP : '/tmp'), 'cmdtools.zip');
  27. const VERSION_FILE = path.join(SDK_ROOT, '.sdk-version');
  28. const EXPECTED_VERSION = 'platforms-35-buildtools-35.0.0';
  29. // 从环境变量获取代理设置
  30. const PROXY = process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
  31. // 读取 package.json 获取版本号
  32. const packageJson = require(path.join(__dirname, '..', 'package.json'));
  33. // 计算各路径
  34. const rootDir = path.join(__dirname, '..'); // 项目根目录
  35. const cordovaPrjDir = path.join(__dirname, 'dros'); // .build/dros
  36. const srcDir = path.join(rootDir, 'dist', 'h5'); // ../dist/h5
  37. const dstDir = path.join(cordovaPrjDir, 'www');
  38. console.log('🚀 Android SDK 安装和构建统一脚本启动...');
  39. console.log(`📦 SDK Root: ${SDK_ROOT}`);
  40. if (PROXY) {
  41. console.log(`🌐 使用代理: ${PROXY}`);
  42. }
  43. // 检查 SDK 是否已安装且版本正确
  44. function isSdkInstalled() {
  45. console.log('🔍 检测 Android SDK 安装状态...');
  46. const sdkManagerExe = isWindows ? 'sdkmanager.bat' : 'sdkmanager';
  47. const sdkManagerPath = path.join(CMDLINE_TOOLS_DIR, 'bin', sdkManagerExe);
  48. const platformPath = path.join(SDK_ROOT, 'platforms', 'android-35');
  49. const buildToolsPath = path.join(SDK_ROOT, 'build-tools', '35.0.0');
  50. // 检查关键文件是否存在
  51. console.log(' 📂 检查 sdkmanager...');
  52. if (!fs.existsSync(sdkManagerPath)) {
  53. console.log(' ❌ sdkmanager 不存在');
  54. return false;
  55. }
  56. console.log(' ✅ sdkmanager 存在');
  57. console.log(' 📂 检查 platforms;android-35...');
  58. if (!fs.existsSync(platformPath)) {
  59. console.log(' ❌ platforms;android-35 不存在');
  60. return false;
  61. }
  62. console.log(' ✅ platforms;android-35 存在');
  63. console.log(' 📂 检查 build-tools;35.0.0...');
  64. if (!fs.existsSync(buildToolsPath)) {
  65. console.log(' ❌ build-tools;35.0.0 不存在');
  66. return false;
  67. }
  68. console.log(' ✅ build-tools;35.0.0 存在');
  69. // 验证版本标记文件
  70. console.log(' 📝 检查版本标记...');
  71. if (fs.existsSync(VERSION_FILE)) {
  72. const installedVersion = fs.readFileSync(VERSION_FILE, 'utf8').trim();
  73. console.log(` 已安装版本: ${installedVersion}`);
  74. console.log(` 期望版本: ${EXPECTED_VERSION}`);
  75. if (installedVersion === EXPECTED_VERSION) {
  76. console.log(' ✅ 版本匹配');
  77. return true;
  78. } else {
  79. console.log(' ⚠️ 版本不匹配,需要重新安装');
  80. return false;
  81. }
  82. } else {
  83. console.log(' ⚠️ 版本标记文件不存在');
  84. return false;
  85. }
  86. }
  87. // 下载文件(支持代理)
  88. function downloadFile(url, dest) {
  89. return new Promise((resolve, reject) => {
  90. console.log(`📥 下载: ${url}`);
  91. const file = fs.createWriteStream(dest);
  92. const urlObj = new URL(url);
  93. let requestLib = https;
  94. let options = {
  95. hostname: urlObj.hostname,
  96. path: urlObj.pathname + urlObj.search,
  97. method: 'GET',
  98. headers: {
  99. 'User-Agent': 'Node.js'
  100. }
  101. };
  102. // 如果有代理,使用代理设置
  103. if (PROXY) {
  104. const proxyUrl = new URL(PROXY);
  105. requestLib = proxyUrl.protocol === 'https:' ? https : http;
  106. options = {
  107. hostname: proxyUrl.hostname,
  108. port: proxyUrl.port,
  109. path: url, // 使用完整 URL 作为路径
  110. method: 'GET',
  111. headers: {
  112. 'Host': urlObj.hostname,
  113. 'User-Agent': 'Node.js'
  114. }
  115. };
  116. }
  117. const request = requestLib.get(options, (response) => {
  118. // 处理重定向
  119. if (response.statusCode === 301 || response.statusCode === 302) {
  120. const redirectUrl = response.headers.location;
  121. console.log(`🔀 重定向到: ${redirectUrl}`);
  122. file.close();
  123. fs.unlinkSync(dest);
  124. return downloadFile(redirectUrl, dest).then(resolve).catch(reject);
  125. }
  126. if (response.statusCode !== 200) {
  127. reject(new Error(`下载失败,状态码: ${response.statusCode}`));
  128. return;
  129. }
  130. const totalSize = parseInt(response.headers['content-length'], 10);
  131. let downloadedSize = 0;
  132. response.on('data', (chunk) => {
  133. downloadedSize += chunk.length;
  134. const percent = ((downloadedSize / totalSize) * 100).toFixed(2);
  135. process.stdout.write(`\r下载进度: ${percent}% (${downloadedSize}/${totalSize} bytes)`);
  136. });
  137. response.pipe(file);
  138. file.on('finish', () => {
  139. file.close();
  140. console.log('\n✅ 下载完成');
  141. resolve();
  142. });
  143. });
  144. request.on('error', (err) => {
  145. fs.unlinkSync(dest);
  146. reject(err);
  147. });
  148. file.on('error', (err) => {
  149. fs.unlinkSync(dest);
  150. reject(err);
  151. });
  152. });
  153. }
  154. // 解压 ZIP 文件
  155. function extractZip(zipPath, destDir) {
  156. console.log(`📂 解压到: ${destDir}`);
  157. // 确保目标目录存在
  158. fs.mkdirSync(destDir, { recursive: true });
  159. // 在 Windows 上使用 PowerShell 的 Expand-Archive
  160. if (process.platform === 'win32') {
  161. try {
  162. const cmd = `powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"`;
  163. execSync(cmd, { stdio: 'inherit' });
  164. console.log('✅ 解压完成');
  165. } catch (error) {
  166. throw new Error(`解压失败: ${error.message}`);
  167. }
  168. } else {
  169. // Linux 使用 unzip
  170. try {
  171. execSync(`unzip -q "${zipPath}" -d "${destDir}"`, { stdio: 'inherit' });
  172. console.log('✅ 解压完成');
  173. } catch (error) {
  174. throw new Error(`解压失败: ${error.message}`);
  175. }
  176. }
  177. }
  178. // 设置环境变量(写入 GITHUB_ENV)
  179. function setGitHubEnv(name, value) {
  180. const envFile = process.env.GITHUB_ENV;
  181. if (envFile) {
  182. fs.appendFileSync(envFile, `${name}=${value}\n`);
  183. console.log(`✅ 设置环境变量: ${name}=${value}`);
  184. } else {
  185. console.log(`⚠️ GITHUB_ENV 未定义,跳过设置: ${name}=${value}`);
  186. }
  187. }
  188. // 安装 Gradle 到 PATH
  189. async function installGradleToPath() {
  190. const gradleVersion = '9.0.0'; // 使用稳定版本,可根据需要更新
  191. const gradleUrl = `https://mirrors.cloud.tencent.com/gradle/gradle-${gradleVersion}-bin.zip`;
  192. const gradleInstallDir = isWindows
  193. ? path.join(process.env.USERPROFILE, 'gradle')
  194. : path.join(os.homedir(), 'gradle');
  195. const gradleBin = path.join(gradleInstallDir, `gradle-${gradleVersion}`, 'bin');
  196. // 检查 Gradle 是否已存在
  197. const gradleExe = isWindows ? 'gradle.bat' : 'gradle';
  198. const fullGradleExe = path.join(gradleBin, gradleExe);
  199. if (fs.existsSync(fullGradleExe)) {
  200. console.log('✅ Gradle 已安装');
  201. // 添加到 PATH(即使已安装也要确保在PATH中)
  202. const pathEnvFile = process.env.GITHUB_PATH;
  203. if (pathEnvFile) {
  204. fs.appendFileSync(pathEnvFile, `${gradleBin}\n`);
  205. console.log(`✅ Gradle 已添加到 PATH: ${gradleBin}`);
  206. }
  207. return;
  208. }
  209. console.log(`📦 下载 Gradle ${gradleVersion}...`);
  210. await downloadFile(gradleUrl, TEMP_ZIP);
  211. console.log(`📂 解压 Gradle 到: ${gradleInstallDir}`);
  212. extractZip(TEMP_ZIP, gradleInstallDir);
  213. // 添加到 PATH
  214. const pathEnvFile = process.env.GITHUB_PATH;
  215. if (pathEnvFile) {
  216. fs.appendFileSync(pathEnvFile, `${gradleBin}\n`);
  217. console.log(`✅ Gradle 已添加到 PATH: ${gradleBin}`);
  218. } else {
  219. console.log(`⚠️ 请手动将 ${gradleBin} 添加到 PATH`);
  220. }
  221. // 测试 Gradle
  222. try {
  223. execSync(`"${fullGradleExe}" --version`, { stdio: 'pipe' });
  224. console.log('✅ Gradle 安装成功');
  225. } catch (error) {
  226. console.warn('⚠️ Gradle 测试失败:', error.message);
  227. }
  228. }
  229. // 运行 sdkmanager
  230. function runSdkManager(args) {
  231. const sdkManagerExe = isWindows ? 'sdkmanager.bat' : 'sdkmanager';
  232. const sdkManagerPath = path.join(CMDLINE_TOOLS_DIR, 'bin', sdkManagerExe);
  233. if (!fs.existsSync(sdkManagerPath)) {
  234. throw new Error(`sdkmanager 不存在: ${sdkManagerPath}`);
  235. }
  236. console.log(`🔧 运行: sdkmanager ${args.join(' ')}`);
  237. try {
  238. // 分离包名和参数
  239. const packages = args.filter(arg => !arg.startsWith('--'));
  240. const flags = args.filter(arg => arg.startsWith('--'));
  241. // 只对包名加引号,参数不加引号
  242. const quotedPackages = packages.map(pkg => `"${pkg}"`).join(' ');
  243. const cmd = `"${sdkManagerPath}" ${quotedPackages} ${flags.join(' ')}`;
  244. console.log(`执行命令: ${cmd}`);
  245. // 根据平台设置不同的 shell 选项
  246. const execOptions = isWindows
  247. ? { stdio: 'inherit', shell: 'cmd.exe' }
  248. : { stdio: 'inherit' };
  249. execSync(cmd, execOptions);
  250. console.log('✅ sdkmanager 执行成功');
  251. } catch (error) {
  252. throw new Error(`sdkmanager 执行失败: ${error.message}`);
  253. }
  254. }
  255. // 安装和配置 Android SDK
  256. async function setupAndroidSdk() {
  257. console.log('🔧 检查 Gradle 安装...');
  258. await installGradleToPath();
  259. // 0. 检查 SDK 是否已安装
  260. if (isSdkInstalled()) {
  261. console.log('\n✅ Android SDK 已安装且版本正确,跳过安装流程');
  262. console.log(`📦 SDK Root: ${SDK_ROOT}`);
  263. // 仍然需要设置环境变量(GitHub Actions 每次运行都需要)
  264. console.log('\n⚙️ 设置环境变量...');
  265. setGitHubEnv('ANDROID_HOME', SDK_ROOT);
  266. setGitHubEnv('ANDROID_SDK_ROOT', SDK_ROOT);
  267. console.log('\n🎉 Android SDK 环境变量设置完成!');
  268. console.log('⏱️ 耗时: ~1-2秒(本地缓存加速)');
  269. return;
  270. }
  271. console.log('\n📦 未检测到有效的 Android SDK,开始完整安装流程...');
  272. // 1. 下载 SDK
  273. await downloadFile(SDK_URL, TEMP_ZIP);
  274. // 2. 解压到临时目录
  275. const tempExtractDir = path.join(SDK_ROOT, 'cmdline-tools', 'temp');
  276. extractZip(TEMP_ZIP, tempExtractDir);
  277. // 3. 移动内部的 cmdline-tools 到 latest(ZIP 内部包含 cmdline-tools 目录)
  278. const extractedCmdlineTools = path.join(tempExtractDir, 'cmdline-tools');
  279. if (fs.existsSync(extractedCmdlineTools)) {
  280. console.log('📦 重新组织目录结构...');
  281. // 如果 latest 目录已存在,先删除
  282. if (fs.existsSync(CMDLINE_TOOLS_DIR)) {
  283. fs.rmSync(CMDLINE_TOOLS_DIR, { recursive: true, force: true });
  284. }
  285. fs.renameSync(extractedCmdlineTools, CMDLINE_TOOLS_DIR);
  286. // 删除临时目录
  287. fs.rmSync(tempExtractDir, { recursive: true, force: true });
  288. console.log('✅ 目录结构已调整');
  289. } else {
  290. throw new Error(`解压后未找到 cmdline-tools 目录: ${extractedCmdlineTools}`);
  291. }
  292. // 4. 设置环境变量
  293. setGitHubEnv('ANDROID_HOME', SDK_ROOT);
  294. setGitHubEnv('ANDROID_SDK_ROOT', SDK_ROOT);
  295. // 5. 先接受 licenses(使用输入重定向文件)
  296. console.log('📝 接受 SDK licenses...');
  297. const sdkManagerExe = isWindows ? 'sdkmanager.bat' : 'sdkmanager';
  298. const sdkManagerPath = path.join(CMDLINE_TOOLS_DIR, 'bin', sdkManagerExe);
  299. // 创建包含多个 y 的临时文件(每个 y 一行)
  300. const tempInputFileDir = isWindows ? process.env.TEMP || process.env.TMP : '/tmp';
  301. const tempInputFile = path.join(tempInputFileDir, 'sdk-licenses-input.txt');
  302. // 根据日志显示有 7 个许可证,我们用 20 个 y 确保足够
  303. fs.writeFileSync(tempInputFile, 'y\n'.repeat(20));
  304. // 使用输入重定向 < 而不是管道 |
  305. const licenseCmd = `"${sdkManagerPath}" --licenses --sdk_root=${SDK_ROOT} < "${tempInputFile}"`;
  306. // 根据平台设置不同的 shell 选项
  307. const licenseExecOptions = isWindows
  308. ? { stdio: 'inherit', shell: 'cmd.exe' }
  309. : { stdio: 'inherit' };
  310. execSync(licenseCmd, licenseExecOptions);
  311. console.log('✅ licenses 接受完成');
  312. // 清理临时文件
  313. if (fs.existsSync(tempInputFile)) {
  314. fs.unlinkSync(tempInputFile);
  315. }
  316. // 6. 然后安装 platforms 和 build-tools
  317. console.log('📦 安装 Android SDK 组件...');
  318. runSdkManager([
  319. 'platforms;android-35',
  320. 'build-tools;35.0.0',
  321. `--sdk_root=${SDK_ROOT}`
  322. ]);
  323. console.log('🎉 Android SDK 安装完成!');
  324. // 7. 创建版本标记文件
  325. console.log('📝 创建版本标记文件...');
  326. fs.writeFileSync(VERSION_FILE, EXPECTED_VERSION, 'utf8');
  327. console.log(`✅ 版本标记已创建: ${EXPECTED_VERSION}`);
  328. // 清理临时文件
  329. if (fs.existsSync(TEMP_ZIP)) {
  330. fs.unlinkSync(TEMP_ZIP);
  331. console.log('🧹 已清理临时文件');
  332. }
  333. }
  334. // 配置 Gradle Wrapper 使用我们的 Gradle 版本
  335. function configureGradleWrapper() {
  336. const gradleWrapperPropsPath = path.join(cordovaPrjDir, 'platforms', 'android', 'gradle', 'wrapper', 'gradle-wrapper.properties');
  337. if (!fs.existsSync(gradleWrapperPropsPath)) {
  338. console.log('⚠️ Gradle wrapper 配置文件不存在,跳过配置');
  339. return;
  340. }
  341. console.log('🔧 配置 Gradle wrapper 使用 Gradle 9.0.0...');
  342. try {
  343. // 读取现有的配置文件
  344. let content = fs.readFileSync(gradleWrapperPropsPath, 'utf8');
  345. // 替换 distributionUrl 为我们的 Gradle 版本
  346. const oldUrlPattern = /distributionUrl=.*/;
  347. const newUrl = 'distributionUrl=https\\://services.gradle.org/distributions/gradle-9.0.0-bin.zip';
  348. if (oldUrlPattern.test(content)) {
  349. content = content.replace(oldUrlPattern, newUrl);
  350. fs.writeFileSync(gradleWrapperPropsPath, content, 'utf8');
  351. console.log('✅ Gradle wrapper 已配置为使用 Gradle 9.0.0');
  352. } else {
  353. console.log('⚠️ 未找到 distributionUrl 配置,添加新配置');
  354. content += `\n${newUrl}\n`;
  355. fs.writeFileSync(gradleWrapperPropsPath, content, 'utf8');
  356. console.log('✅ 已添加 Gradle 9.0.0 配置');
  357. }
  358. } catch (error) {
  359. console.warn('⚠️ 配置 Gradle wrapper 失败:', error.message);
  360. }
  361. }
  362. // 配置 Android 构建环境变量
  363. function getAndroidEnv() {
  364. // 计算 Gradle bin 路径
  365. const gradleVersion = '9.0.0';
  366. const gradleBin = isWindows
  367. ? path.join(process.env.USERPROFILE || '', 'gradle', `gradle-${gradleVersion}`, 'bin')
  368. : path.join(process.env.HOME || '/root', 'gradle', `gradle-${gradleVersion}`, 'bin');
  369. return {
  370. ...process.env,
  371. // 使用配置的SDK路径
  372. ANDROID_HOME: SDK_ROOT,
  373. ANDROID_SDK_ROOT: SDK_ROOT,
  374. // 确保 Java 路径正确
  375. JAVA_HOME: process.env.JAVA_HOME,
  376. // 将 Gradle 添加到 PATH 前缀,确保 Cordova 子进程能找到它
  377. PATH: `${gradleBin}${path.delimiter}${process.env.PATH}`
  378. };
  379. }
  380. // 构建 Android APK
  381. function buildAndroidApk() {
  382. const androidEnv = getAndroidEnv();
  383. rmSync('.build/dros', { recursive: true, force: true });
  384. // 做一些环境检查
  385. execSync('npx cordova --version', { stdio: 'inherit', env: androidEnv });
  386. // 1. 在项目根目录执行 cordova create
  387. execSync('npx cordova create .build/dros zskk.dros dros', { cwd: rootDir, stdio: 'inherit', env: androidEnv });
  388. // 2. 在 .build/dros 中执行 cordova platform add android
  389. execSync('npx cordova platform add android', { cwd: cordovaPrjDir, stdio: 'inherit', env: androidEnv });
  390. // 2.5. 配置 Gradle wrapper 使用我们的 Gradle 版本
  391. configureGradleWrapper();
  392. // 3. 复制 dist/h5 → .build/dros/www
  393. fs.rmSync(dstDir, { recursive: true, force: true });
  394. fs.mkdirSync(dstDir, { recursive: true });
  395. function copy(src, dst) {
  396. const stat = fs.statSync(src);
  397. if (stat.isDirectory()) {
  398. fs.mkdirSync(dst, { recursive: true });
  399. for (const entry of fs.readdirSync(src)) {
  400. copy(path.join(src, entry), path.join(dst, entry));
  401. }
  402. } else {
  403. fs.copyFileSync(src, dst);
  404. }
  405. }
  406. copy(srcDir, dstDir);
  407. // 3.1 需要修改 index.html,加入 Cordova 脚本引用
  408. const indexPath = path.join(dstDir, 'index.html');
  409. let indexContent = fs.readFileSync(indexPath, 'utf8');
  410. if (!indexContent.includes('cordova.js')) {
  411. indexContent = indexContent.replace(
  412. '<head>',
  413. '<head> \n \
  414. <!--在业务代码执行之前行加载cordova--> \n \
  415. <script src="cordova.js"></script>\n'
  416. );
  417. fs.writeFileSync(indexPath, indexContent, 'utf8');
  418. console.log('✅ 已在 index.html 中添加 cordova.js 引用');
  419. } else {
  420. console.log('✅ index.html 已包含 cordova.js 引用,跳过添加');
  421. }
  422. // 3.2 把.build\dros\platforms\android\platform_www\cordova.js复制到.build\dros\www目录下
  423. const cordovaJsSrc = path.join(cordovaPrjDir, 'platforms', 'android', 'platform_www', 'cordova.js');
  424. const cordovaJsDst = path.join(dstDir, 'cordova.js');
  425. fs.copyFileSync(cordovaJsSrc, cordovaJsDst);
  426. console.log('✅ 已复制 cordova.js 到 www 目录');
  427. execSync('npx cordova plugin add cordova-plugin-file', { cwd: cordovaPrjDir, stdio: 'inherit' });
  428. // 4. 安装插件 : 为了本地保存网络配置等数据
  429. execSync('npx cordova plugin add cordova-plugin-nativestorage', { cwd: cordovaPrjDir, stdio: 'inherit' });
  430. execSync('npx cordova plugin add cordova-plugin-camera', { cwd: cordovaPrjDir, stdio: 'inherit' });
  431. // 5. 复制预配置好的config.xml
  432. fs.copyFileSync(path.join(__dirname, 'config.xml'), path.join(cordovaPrjDir, 'config.xml'));
  433. // 6. 在 .build/dros 中执行 cordova build android
  434. execSync('npx cordova build android --verbose', { cwd: cordovaPrjDir, stdio: 'inherit', env: androidEnv });
  435. // 7. 重命名apk文件
  436. const version = packageJson.version;
  437. const pkg = 'dros';
  438. const src = path.join(__dirname, 'dros/platforms/android/app/build/outputs/apk/debug/app-debug.apk');
  439. const dst = path.join(__dirname, `dros/platforms/android/app/build/outputs/apk/debug/${pkg}-v${version}.apk`);
  440. fs.copyFileSync(src, dst);
  441. console.log(`✅ 已生成 ${dst}`);
  442. // 安装到连接的安卓设备(CI 环境跳过)
  443. if (process.env.CI !== 'true') {
  444. try {
  445. execSync(`adb install "${dst}"`, { stdio: 'inherit', env: androidEnv });
  446. console.log(`✅ 已安装到安卓设备 ${dst}`);
  447. } catch (error) {
  448. console.warn('⚠️ ADB 安装失败,可能没有连接设备:', error.message);
  449. }
  450. } else {
  451. console.log('ℹ️ CI 环境,跳过 ADB 安装步骤');
  452. }
  453. // 8. 部署到服务器(可选)
  454. (async () => {
  455. if (process.env.DEPLOY_ANDROID === 'true') {
  456. console.log('\n📤 开始部署到服务器...');
  457. console.log('='.repeat(50));
  458. try {
  459. const { deployAndroidToServer } = require('./deploy-android-to-server');
  460. await deployAndroidToServer({
  461. privateKey: process.env.DEPLOY_KEY,
  462. host: process.env.DEPLOY_HOST,
  463. username: process.env.DEPLOY_USER,
  464. remotePath: process.env.DEPLOY_PATH,
  465. apkPath: dst,
  466. appVersion: version
  467. });
  468. console.log('='.repeat(50));
  469. console.log('✅ 部署完成!');
  470. } catch (error) {
  471. console.error('='.repeat(50));
  472. console.error('❌ 部署失败:', error.message);
  473. process.exit(1);
  474. }
  475. } else {
  476. console.log('\n💡 提示: 设置 DEPLOY_ANDROID=true 可自动部署到服务器');
  477. }
  478. })();
  479. }
  480. // 主函数
  481. async function main() {
  482. try {
  483. // 1. 安装和配置 Android SDK
  484. await setupAndroidSdk();
  485. // 2. 构建 Android APK
  486. console.log('\n🏗️ 开始构建 Android APK...');
  487. buildAndroidApk();
  488. } catch (error) {
  489. console.error('❌ 错误:', error.message);
  490. process.exit(1);
  491. }
  492. }
  493. // 运行
  494. main();