setup-android-sdk.js 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. // setup-android-sdk.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. // 配置
  9. const SDK_URL = 'https://dl.google.com/android/repository/commandlinetools-win-11076708_latest.zip';
  10. const SDK_ROOT = 'C:\\android-sdk';
  11. const CMDLINE_TOOLS_DIR = path.join(SDK_ROOT, 'cmdline-tools', 'latest');
  12. const TEMP_ZIP = path.join(process.env.TEMP || '/tmp', 'cmdtools.zip');
  13. // 从环境变量获取代理设置
  14. const PROXY = process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
  15. console.log('🚀 开始安装 Android SDK...');
  16. console.log(`📦 SDK Root: ${SDK_ROOT}`);
  17. if (PROXY) {
  18. console.log(`🌐 使用代理: ${PROXY}`);
  19. }
  20. // 下载文件(支持代理)
  21. function downloadFile(url, dest) {
  22. return new Promise((resolve, reject) => {
  23. console.log(`📥 下载: ${url}`);
  24. const file = fs.createWriteStream(dest);
  25. const urlObj = new URL(url);
  26. let requestLib = https;
  27. let options = {
  28. hostname: urlObj.hostname,
  29. path: urlObj.pathname + urlObj.search,
  30. method: 'GET',
  31. headers: {
  32. 'User-Agent': 'Node.js'
  33. }
  34. };
  35. // 如果有代理,使用代理设置
  36. if (PROXY) {
  37. const proxyUrl = new URL(PROXY);
  38. requestLib = proxyUrl.protocol === 'https:' ? https : http;
  39. options = {
  40. hostname: proxyUrl.hostname,
  41. port: proxyUrl.port,
  42. path: url, // 使用完整 URL 作为路径
  43. method: 'GET',
  44. headers: {
  45. 'Host': urlObj.hostname,
  46. 'User-Agent': 'Node.js'
  47. }
  48. };
  49. }
  50. const request = requestLib.get(options, (response) => {
  51. // 处理重定向
  52. if (response.statusCode === 301 || response.statusCode === 302) {
  53. const redirectUrl = response.headers.location;
  54. console.log(`🔀 重定向到: ${redirectUrl}`);
  55. file.close();
  56. fs.unlinkSync(dest);
  57. return downloadFile(redirectUrl, dest).then(resolve).catch(reject);
  58. }
  59. if (response.statusCode !== 200) {
  60. reject(new Error(`下载失败,状态码: ${response.statusCode}`));
  61. return;
  62. }
  63. const totalSize = parseInt(response.headers['content-length'], 10);
  64. let downloadedSize = 0;
  65. response.on('data', (chunk) => {
  66. downloadedSize += chunk.length;
  67. const percent = ((downloadedSize / totalSize) * 100).toFixed(2);
  68. process.stdout.write(`\r下载进度: ${percent}% (${downloadedSize}/${totalSize} bytes)`);
  69. });
  70. response.pipe(file);
  71. file.on('finish', () => {
  72. file.close();
  73. console.log('\n✅ 下载完成');
  74. resolve();
  75. });
  76. });
  77. request.on('error', (err) => {
  78. fs.unlinkSync(dest);
  79. reject(err);
  80. });
  81. file.on('error', (err) => {
  82. fs.unlinkSync(dest);
  83. reject(err);
  84. });
  85. });
  86. }
  87. // 解压 ZIP 文件
  88. function extractZip(zipPath, destDir) {
  89. console.log(`📂 解压到: ${destDir}`);
  90. // 确保目标目录存在
  91. fs.mkdirSync(destDir, { recursive: true });
  92. // 在 Windows 上使用 PowerShell 的 Expand-Archive
  93. if (process.platform === 'win32') {
  94. try {
  95. const cmd = `powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"`;
  96. execSync(cmd, { stdio: 'inherit' });
  97. console.log('✅ 解压完成');
  98. } catch (error) {
  99. throw new Error(`解压失败: ${error.message}`);
  100. }
  101. } else {
  102. // Linux 使用 unzip
  103. try {
  104. execSync(`unzip -q "${zipPath}" -d "${destDir}"`, { stdio: 'inherit' });
  105. console.log('✅ 解压完成');
  106. } catch (error) {
  107. throw new Error(`解压失败: ${error.message}`);
  108. }
  109. }
  110. }
  111. // 设置环境变量(写入 GITHUB_ENV)
  112. function setGitHubEnv(name, value) {
  113. const envFile = process.env.GITHUB_ENV;
  114. if (envFile) {
  115. fs.appendFileSync(envFile, `${name}=${value}\n`);
  116. console.log(`✅ 设置环境变量: ${name}=${value}`);
  117. } else {
  118. console.log(`⚠️ GITHUB_ENV 未定义,跳过设置: ${name}=${value}`);
  119. }
  120. }
  121. // 运行 sdkmanager
  122. function runSdkManager(args) {
  123. const sdkManagerPath = path.join(CMDLINE_TOOLS_DIR, 'bin', 'sdkmanager.bat');
  124. if (!fs.existsSync(sdkManagerPath)) {
  125. throw new Error(`sdkmanager 不存在: ${sdkManagerPath}`);
  126. }
  127. console.log(`🔧 运行: sdkmanager ${args.join(' ')}`);
  128. try {
  129. // 分离包名和参数
  130. const packages = args.filter(arg => !arg.startsWith('--'));
  131. const flags = args.filter(arg => arg.startsWith('--'));
  132. // 只对包名加引号,参数不加引号
  133. const quotedPackages = packages.map(pkg => `"${pkg}"`).join(' ');
  134. const cmd = `"${sdkManagerPath}" ${quotedPackages} ${flags.join(' ')}`;
  135. console.log(`执行命令: ${cmd}`);
  136. execSync(cmd, {
  137. stdio: 'inherit',
  138. shell: 'cmd.exe'
  139. });
  140. console.log('✅ sdkmanager 执行成功');
  141. } catch (error) {
  142. throw new Error(`sdkmanager 执行失败: ${error.message}`);
  143. }
  144. }
  145. // 主函数
  146. async function main() {
  147. try {
  148. // 1. 下载 SDK
  149. await downloadFile(SDK_URL, TEMP_ZIP);
  150. // 2. 解压到临时目录
  151. const tempExtractDir = path.join(SDK_ROOT, 'cmdline-tools', 'temp');
  152. extractZip(TEMP_ZIP, tempExtractDir);
  153. // 3. 移动内部的 cmdline-tools 到 latest(ZIP 内部包含 cmdline-tools 目录)
  154. const extractedCmdlineTools = path.join(tempExtractDir, 'cmdline-tools');
  155. if (fs.existsSync(extractedCmdlineTools)) {
  156. console.log('📦 重新组织目录结构...');
  157. // 如果 latest 目录已存在,先删除
  158. if (fs.existsSync(CMDLINE_TOOLS_DIR)) {
  159. fs.rmSync(CMDLINE_TOOLS_DIR, { recursive: true, force: true });
  160. }
  161. fs.renameSync(extractedCmdlineTools, CMDLINE_TOOLS_DIR);
  162. // 删除临时目录
  163. fs.rmSync(tempExtractDir, { recursive: true, force: true });
  164. console.log('✅ 目录结构已调整');
  165. } else {
  166. throw new Error(`解压后未找到 cmdline-tools 目录: ${extractedCmdlineTools}`);
  167. }
  168. // 4. 设置环境变量
  169. setGitHubEnv('ANDROID_HOME', SDK_ROOT);
  170. setGitHubEnv('ANDROID_SDK_ROOT', SDK_ROOT);
  171. // 5. 先接受 licenses(使用 --yes 参数)
  172. console.log('📝 接受 SDK licenses...');
  173. runSdkManager([
  174. '--licenses',
  175. `--sdk_root=${SDK_ROOT}`,
  176. '--yes'
  177. ]);
  178. // 6. 然后安装 platforms 和 build-tools
  179. console.log('📦 安装 Android SDK 组件...');
  180. runSdkManager([
  181. 'platforms;android-35',
  182. 'build-tools;35.0.0',
  183. `--sdk_root=${SDK_ROOT}`
  184. ]);
  185. console.log('🎉 Android SDK 安装完成!');
  186. // 清理临时文件
  187. if (fs.existsSync(TEMP_ZIP)) {
  188. fs.unlinkSync(TEMP_ZIP);
  189. console.log('🧹 已清理临时文件');
  190. }
  191. } catch (error) {
  192. console.error('❌ 错误:', error.message);
  193. process.exit(1);
  194. }
  195. }
  196. // 运行
  197. main();