1import { Command } from '@expo/commander'; 2import fs from 'fs-extra'; 3import got from 'got'; 4import os from 'os'; 5import path from 'path'; 6import semver from 'semver'; 7import stream from 'stream'; 8import { promisify } from 'util'; 9 10import { ANDROID_DIR } from '../Constants'; 11import { getSDKVersionsAsync } from '../ProjectVersions'; 12import { runWithSpinner } from '../Utils'; 13 14const DOWNLOAD_BASE = 'https://github.com/expo/react-native/releases/download/'; 15 16/** 17 * Gets file mtime 18 */ 19async function getFileModifedAsync(filePath: string): Promise<string | null> { 20 let result: string | null = null; 21 try { 22 const stat = await fs.stat(filePath); 23 result = stat.mtime.toUTCString(); 24 } catch { 25 result = null; 26 } 27 return result; 28} 29 30/** 31 * Downloads data and save to file. 32 * 33 * - Supports streaming 34 * - If original file is existed, trying to send `If-Modified-Since` header 35 */ 36async function downloadFileAsync(url: string, outputPath: string) { 37 const headers = {}; 38 39 const outputModified = await getFileModifedAsync(outputPath); 40 if (outputModified) { 41 headers['If-Modified-Since'] = outputModified; 42 } 43 44 const pipeline = promisify(stream.pipeline); 45 const fileName = path.basename(url); 46 const tmpFilePath = path.join(os.tmpdir(), fileName); 47 await pipeline(got.stream(url, { headers }), fs.createWriteStream(tmpFilePath)); 48 const fileSize = (await fs.stat(tmpFilePath)).size; 49 50 if (fileSize === 0) { 51 // If-Modified-Since cache hit 52 await fs.remove(tmpFilePath); 53 } else { 54 await fs.move(tmpFilePath, outputPath, { overwrite: true }); 55 } 56} 57 58/** 59 * Variant of `downloadFileAsync` without throwing exceptions even downloading failed. 60 * We need this when adding versioned code before uploading aar to GitHub. 61 */ 62async function downloadFileNonThrowAsync(url: string, outputPath: string) { 63 try { 64 await downloadFileAsync(url, outputPath); 65 } catch {} 66} 67 68/** 69 * Download a versioned aar 70 */ 71async function downloadVersionedAarAsync(sdkVersion: string) { 72 const abiVersion = `abi${sdkVersion.replace(/\./g, '_')}`; 73 const url = `${DOWNLOAD_BASE}sdk-${sdkVersion}/reactandroid-${abiVersion}-1.0.0.aar`; 74 const outputFile = path.join( 75 ANDROID_DIR, 76 `versioned-abis/expoview-${abiVersion}`, 77 `maven/host/exp/reactandroid-${abiVersion}/1.0.0/reactandroid-${abiVersion}-1.0.0.aar` 78 ); 79 await runWithSpinner( 80 `Downloading versioned AAR for SDK ${sdkVersion}: ${url}`, 81 () => downloadFileNonThrowAsync(url, outputFile), 82 `Downloaded versioned AAR for SDK ${sdkVersion}`, 83 { 84 // Gradle does not handle the interactive spinner well. Let's use in plain text mode. 85 isEnabled: false, 86 } 87 ); 88} 89 90async function action(): Promise<void> { 91 const sdkVersions = await getSDKVersionsAsync('android'); 92 for (const sdkVersion of sdkVersions) { 93 if (semver.gte(sdkVersion, '48.0.0')) { 94 await downloadVersionedAarAsync(sdkVersion); 95 } 96 } 97} 98 99export default (program: Command) => { 100 program 101 .command('android-download-versioned-aars') 102 .description('Download versioned AAR files for Expo Go.') 103 .asyncAction(action); 104}; 105