import { Command } from '@expo/commander'; import aws from 'aws-sdk'; import chalk from 'chalk'; import fs from 'fs-extra'; import inquirer from 'inquirer'; import path from 'path'; import { EXPO_DIR } from '../Constants'; import { link } from '../Formatter'; import Git from '../Git'; import logger from '../Logger'; import { getNewestSDKVersionAsync } from '../ProjectVersions'; import { modifySdkVersionsAsync, getSdkVersionsAsync } from '../Versions'; import AndroidClientBuilder from '../client-build/AndroidClientBuilder'; import IosClientBuilder from '../client-build/IosClientBuilder'; import { ClientBuilder, ClientBuildFlavor, Platform } from '../client-build/types'; import askForPlatformAsync from '../utils/askForPlatformAsync'; import askForSdkVersionAsync from '../utils/askForSDKVersionAsync'; const s3Client = new aws.S3({ region: 'us-east-1' }); const { yellow, blue, magenta } = chalk; type ActionOptions = { platform?: Platform; release: boolean; flavor: ClientBuildFlavor; }; const flavors = ['versioned', 'unversioned']; export default (program: Command) => { program .command('client-build') .alias('cb') .description( 'Builds Expo client for iOS simulator or APK for Android, uploads the archive to S3 and saves its url to versions endpoint.' ) .option('-p, --platform [string]', 'Platform for which the client will be built.') .option( '-r, --release', 'Whether to upload and release the client build to staging versions endpoint.', false ) .option( '-f, --flavor [string]', `Which build flavor to use. Possible values: ${flavors}`, flavors[0] ) .asyncAction(main); }; async function main(options: ActionOptions) { const platform = options.platform || (await askForPlatformAsync()); const sdkBranchVersion = await Git.getSDKVersionFromBranchNameAsync(); if (options.release && !sdkBranchVersion) { throw new Error(`Client builds can be released only from the release branch!`); } if (!Object.values(ClientBuildFlavor).includes(options.flavor)) { throw new Error(`Flavor "${options.flavor}" is not valid, use one of: ${flavors}`); } const builder = getBuilderForPlatform(platform); const sdkVersion = sdkBranchVersion || (await askForSdkVersionAsync(platform, await getNewestSDKVersionAsync(platform))); const appVersion = await builder.getAppVersionAsync(); await buildOrUseCacheAsync(builder, options.flavor); if (sdkVersion && options.release) { await uploadAsync(builder, sdkVersion, appVersion); await releaseAsync(builder, sdkVersion, appVersion); } } function getBuilderForPlatform(platform: Platform): ClientBuilder { switch (platform) { case 'ios': return new IosClientBuilder(); case 'android': return new AndroidClientBuilder(); default: { throw new Error(`Platform "${platform}" is not supported yet!`); } } } async function askToRecreateSimulatorBuildAsync(): Promise { if (process.env.CI) { return false; } const { createNew } = await inquirer.prompt<{ createNew: boolean }>([ { type: 'confirm', name: 'createNew', message: 'Do you want to create a fresh one?', default: true, }, ]); return createNew; } async function askToOverrideBuildAsync(): Promise { if (process.env.CI) { // we should never override anything in CI, too easy to accidentally mess something up in prod return false; } const { override } = await inquirer.prompt<{ override: boolean }>([ { type: 'confirm', name: 'override', message: 'Do you want to override it?', default: true, }, ]); return override; } async function buildOrUseCacheAsync( builder: ClientBuilder, flavor: ClientBuildFlavor ): Promise { const appPath = builder.getAppPath(); // Build directory already exists, we could reuse that one — especially useful on the CI. if (await fs.pathExists(appPath)) { const relativeAppPath = path.relative(EXPO_DIR, appPath); logger.info(`Client build already exists at ${magenta.bold(relativeAppPath)}`); if (!(await askToRecreateSimulatorBuildAsync())) { logger.info('Skipped building the app, using cached build instead...'); return; } } await builder.buildAsync(flavor); } async function uploadAsync( builder: ClientBuilder, sdkVersion: string, appVersion: string ): Promise { const sdkVersions = await getSdkVersionsAsync(sdkVersion); // Target app url already defined in versions endpoint. // We make this check to reduce the risk of unintentional overrides. if (sdkVersions?.[`${builder.platform}ClientUrl`] === builder.getClientUrl(appVersion)) { logger.info(`Build ${yellow.bold(appVersion)} is already defined in versions endpoint.`); logger.info('The new build would be uploaded onto the same URL.'); if (!(await askToOverrideBuildAsync())) { logger.warn('Refused overriding the build, exiting the proces...'); process.exit(0); return; } } logger.info(`Uploading ${yellow.bold(appVersion)} build`); await builder.uploadBuildAsync(s3Client, appVersion); } async function releaseAsync( builder: ClientBuilder, sdkVersion: string, appVersion: string ): Promise { const clientUrl = builder.getClientUrl(appVersion); logger.info( `Updating versions endpoint with client url ${blue.bold(link(clientUrl, clientUrl))}` ); await updateClientUrlAndVersionAsync(builder, sdkVersion, appVersion); } async function updateClientUrlAndVersionAsync( builder: ClientBuilder, sdkVersion: string, appVersion: string ) { await modifySdkVersionsAsync(sdkVersion, (sdkVersions) => { sdkVersions[`${builder.platform}ClientUrl`] = builder.getClientUrl(appVersion); sdkVersions[`${builder.platform}ClientVersion`] = appVersion; return sdkVersions; }); }