setup-android-sdk.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  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. const VERSION_FILE = path.join(SDK_ROOT, '.sdk-version');
  14. const EXPECTED_VERSION = 'platforms-35-buildtools-35.0.0';
  15. // 从环境变量获取代理设置
  16. const PROXY = process.env.HTTPS_PROXY || process.env.HTTP_PROXY;
  17. console.log('🚀 Android SDK 安装检查...');
  18. console.log(`📦 SDK Root: ${SDK_ROOT}`);
  19. if (PROXY) {
  20. console.log(`🌐 使用代理: ${PROXY}`);
  21. }
  22. // 检查 SDK 是否已安装且版本正确
  23. function isSdkInstalled() {
  24. console.log('🔍 检测 Android SDK 安装状态...');
  25. const sdkManagerPath = path.join(CMDLINE_TOOLS_DIR, 'bin', 'sdkmanager.bat');
  26. const platformPath = path.join(SDK_ROOT, 'platforms', 'android-35');
  27. const buildToolsPath = path.join(SDK_ROOT, 'build-tools', '35.0.0');
  28. // 检查关键文件是否存在
  29. console.log(' 📂 检查 sdkmanager...');
  30. if (!fs.existsSync(sdkManagerPath)) {
  31. console.log(' ❌ sdkmanager 不存在');
  32. return false;
  33. }
  34. console.log(' ✅ sdkmanager 存在');
  35. console.log(' 📂 检查 platforms;android-35...');
  36. if (!fs.existsSync(platformPath)) {
  37. console.log(' ❌ platforms;android-35 不存在');
  38. return false;
  39. }
  40. console.log(' ✅ platforms;android-35 存在');
  41. console.log(' 📂 检查 build-tools;35.0.0...');
  42. if (!fs.existsSync(buildToolsPath)) {
  43. console.log(' ❌ build-tools;35.0.0 不存在');
  44. return false;
  45. }
  46. console.log(' ✅ build-tools;35.0.0 存在');
  47. // 验证版本标记文件
  48. console.log(' 📝 检查版本标记...');
  49. if (fs.existsSync(VERSION_FILE)) {
  50. const installedVersion = fs.readFileSync(VERSION_FILE, 'utf8').trim();
  51. console.log(` 已安装版本: ${installedVersion}`);
  52. console.log(` 期望版本: ${EXPECTED_VERSION}`);
  53. if (installedVersion === EXPECTED_VERSION) {
  54. console.log(' ✅ 版本匹配');
  55. return true;
  56. } else {
  57. console.log(' ⚠️ 版本不匹配,需要重新安装');
  58. return false;
  59. }
  60. } else {
  61. console.log(' ⚠️ 版本标记文件不存在');
  62. return false;
  63. }
  64. }
  65. // 下载文件(支持代理)
  66. function downloadFile(url, dest) {
  67. return new Promise((resolve, reject) => {
  68. console.log(`📥 下载: ${url}`);
  69. const file = fs.createWriteStream(dest);
  70. const urlObj = new URL(url);
  71. let requestLib = https;
  72. let options = {
  73. hostname: urlObj.hostname,
  74. path: urlObj.pathname + urlObj.search,
  75. method: 'GET',
  76. headers: {
  77. 'User-Agent': 'Node.js'
  78. }
  79. };
  80. // 如果有代理,使用代理设置
  81. if (PROXY) {
  82. const proxyUrl = new URL(PROXY);
  83. requestLib = proxyUrl.protocol === 'https:' ? https : http;
  84. options = {
  85. hostname: proxyUrl.hostname,
  86. port: proxyUrl.port,
  87. path: url, // 使用完整 URL 作为路径
  88. method: 'GET',
  89. headers: {
  90. 'Host': urlObj.hostname,
  91. 'User-Agent': 'Node.js'
  92. }
  93. };
  94. }
  95. const request = requestLib.get(options, (response) => {
  96. // 处理重定向
  97. if (response.statusCode === 301 || response.statusCode === 302) {
  98. const redirectUrl = response.headers.location;
  99. console.log(`🔀 重定向到: ${redirectUrl}`);
  100. file.close();
  101. fs.unlinkSync(dest);
  102. return downloadFile(redirectUrl, dest).then(resolve).catch(reject);
  103. }
  104. if (response.statusCode !== 200) {
  105. reject(new Error(`下载失败,状态码: ${response.statusCode}`));
  106. return;
  107. }
  108. const totalSize = parseInt(response.headers['content-length'], 10);
  109. let downloadedSize = 0;
  110. response.on('data', (chunk) => {
  111. downloadedSize += chunk.length;
  112. const percent = ((downloadedSize / totalSize) * 100).toFixed(2);
  113. process.stdout.write(`\r下载进度: ${percent}% (${downloadedSize}/${totalSize} bytes)`);
  114. });
  115. response.pipe(file);
  116. file.on('finish', () => {
  117. file.close();
  118. console.log('\n✅ 下载完成');
  119. resolve();
  120. });
  121. });
  122. request.on('error', (err) => {
  123. fs.unlinkSync(dest);
  124. reject(err);
  125. });
  126. file.on('error', (err) => {
  127. fs.unlinkSync(dest);
  128. reject(err);
  129. });
  130. });
  131. }
  132. // 解压 ZIP 文件
  133. function extractZip(zipPath, destDir) {
  134. console.log(`📂 解压到: ${destDir}`);
  135. // 确保目标目录存在
  136. fs.mkdirSync(destDir, { recursive: true });
  137. // 在 Windows 上使用 PowerShell 的 Expand-Archive
  138. if (process.platform === 'win32') {
  139. try {
  140. const cmd = `powershell -Command "Expand-Archive -Path '${zipPath}' -DestinationPath '${destDir}' -Force"`;
  141. execSync(cmd, { stdio: 'inherit' });
  142. console.log('✅ 解压完成');
  143. } catch (error) {
  144. throw new Error(`解压失败: ${error.message}`);
  145. }
  146. } else {
  147. // Linux 使用 unzip
  148. try {
  149. execSync(`unzip -q "${zipPath}" -d "${destDir}"`, { stdio: 'inherit' });
  150. console.log('✅ 解压完成');
  151. } catch (error) {
  152. throw new Error(`解压失败: ${error.message}`);
  153. }
  154. }
  155. }
  156. // 设置环境变量(写入 GITHUB_ENV)
  157. function setGitHubEnv(name, value) {
  158. const envFile = process.env.GITHUB_ENV;
  159. if (envFile) {
  160. fs.appendFileSync(envFile, `${name}=${value}\n`);
  161. console.log(`✅ 设置环境变量: ${name}=${value}`);
  162. } else {
  163. console.log(`⚠️ GITHUB_ENV 未定义,跳过设置: ${name}=${value}`);
  164. }
  165. }
  166. // 运行 sdkmanager
  167. function runSdkManager(args) {
  168. const sdkManagerPath = path.join(CMDLINE_TOOLS_DIR, 'bin', 'sdkmanager.bat');
  169. if (!fs.existsSync(sdkManagerPath)) {
  170. throw new Error(`sdkmanager 不存在: ${sdkManagerPath}`);
  171. }
  172. console.log(`🔧 运行: sdkmanager ${args.join(' ')}`);
  173. try {
  174. // 分离包名和参数
  175. const packages = args.filter(arg => !arg.startsWith('--'));
  176. const flags = args.filter(arg => arg.startsWith('--'));
  177. // 只对包名加引号,参数不加引号
  178. const quotedPackages = packages.map(pkg => `"${pkg}"`).join(' ');
  179. const cmd = `"${sdkManagerPath}" ${quotedPackages} ${flags.join(' ')}`;
  180. console.log(`执行命令: ${cmd}`);
  181. execSync(cmd, {
  182. stdio: 'inherit',
  183. shell: 'cmd.exe'
  184. });
  185. console.log('✅ sdkmanager 执行成功');
  186. } catch (error) {
  187. throw new Error(`sdkmanager 执行失败: ${error.message}`);
  188. }
  189. }
  190. // 主函数
  191. async function main() {
  192. try {
  193. // 0. 检查 SDK 是否已安装
  194. if (isSdkInstalled()) {
  195. console.log('\n✅ Android SDK 已安装且版本正确,跳过安装流程');
  196. console.log(`📦 SDK Root: ${SDK_ROOT}`);
  197. // 仍然需要设置环境变量(GitHub Actions 每次运行都需要)
  198. console.log('\n⚙️ 设置环境变量...');
  199. setGitHubEnv('ANDROID_HOME', SDK_ROOT);
  200. setGitHubEnv('ANDROID_SDK_ROOT', SDK_ROOT);
  201. console.log('\n🎉 Android SDK 环境变量设置完成!');
  202. console.log('⏱️ 耗时: ~1-2秒(本地缓存加速)');
  203. return;
  204. }
  205. console.log('\n📦 未检测到有效的 Android SDK,开始完整安装流程...');
  206. // 1. 下载 SDK
  207. await downloadFile(SDK_URL, TEMP_ZIP);
  208. // 2. 解压到临时目录
  209. const tempExtractDir = path.join(SDK_ROOT, 'cmdline-tools', 'temp');
  210. extractZip(TEMP_ZIP, tempExtractDir);
  211. // 3. 移动内部的 cmdline-tools 到 latest(ZIP 内部包含 cmdline-tools 目录)
  212. const extractedCmdlineTools = path.join(tempExtractDir, 'cmdline-tools');
  213. if (fs.existsSync(extractedCmdlineTools)) {
  214. console.log('📦 重新组织目录结构...');
  215. // 如果 latest 目录已存在,先删除
  216. if (fs.existsSync(CMDLINE_TOOLS_DIR)) {
  217. fs.rmSync(CMDLINE_TOOLS_DIR, { recursive: true, force: true });
  218. }
  219. fs.renameSync(extractedCmdlineTools, CMDLINE_TOOLS_DIR);
  220. // 删除临时目录
  221. fs.rmSync(tempExtractDir, { recursive: true, force: true });
  222. console.log('✅ 目录结构已调整');
  223. } else {
  224. throw new Error(`解压后未找到 cmdline-tools 目录: ${extractedCmdlineTools}`);
  225. }
  226. // 4. 设置环境变量
  227. setGitHubEnv('ANDROID_HOME', SDK_ROOT);
  228. setGitHubEnv('ANDROID_SDK_ROOT', SDK_ROOT);
  229. // 5. 先接受 licenses(使用输入重定向文件)
  230. console.log('📝 接受 SDK licenses...');
  231. const sdkManagerPath = path.join(CMDLINE_TOOLS_DIR, 'bin', 'sdkmanager.bat');
  232. // 创建包含多个 y 的临时文件(每个 y 一行)
  233. const tempInputFile = path.join(process.env.TEMP, 'sdk-licenses-input.txt');
  234. // 根据日志显示有 7 个许可证,我们用 20 个 y 确保足够
  235. fs.writeFileSync(tempInputFile, 'y\n'.repeat(20));
  236. // 使用输入重定向 < 而不是管道 |
  237. const licenseCmd = `"${sdkManagerPath}" --licenses --sdk_root=${SDK_ROOT} < "${tempInputFile}"`;
  238. execSync(licenseCmd, {
  239. stdio: 'inherit',
  240. shell: 'cmd.exe'
  241. });
  242. console.log('✅ licenses 接受完成');
  243. // 清理临时文件
  244. if (fs.existsSync(tempInputFile)) {
  245. fs.unlinkSync(tempInputFile);
  246. }
  247. // 6. 然后安装 platforms 和 build-tools
  248. console.log('📦 安装 Android SDK 组件...');
  249. runSdkManager([
  250. 'platforms;android-35',
  251. 'build-tools;35.0.0',
  252. `--sdk_root=${SDK_ROOT}`
  253. ]);
  254. console.log('🎉 Android SDK 安装完成!');
  255. // 7. 创建版本标记文件
  256. console.log('📝 创建版本标记文件...');
  257. fs.writeFileSync(VERSION_FILE, EXPECTED_VERSION, 'utf8');
  258. console.log(`✅ 版本标记已创建: ${EXPECTED_VERSION}`);
  259. // 清理临时文件
  260. if (fs.existsSync(TEMP_ZIP)) {
  261. fs.unlinkSync(TEMP_ZIP);
  262. console.log('🧹 已清理临时文件');
  263. }
  264. } catch (error) {
  265. console.error('❌ 错误:', error.message);
  266. process.exit(1);
  267. }
  268. }
  269. // 运行
  270. main();