xref: /expo/tools/src/commands/ClientBuild.ts (revision a272999e)
1eeffdb10STomasz Sapetaimport { Command } from '@expo/commander';
2eeffdb10STomasz Sapetaimport aws from 'aws-sdk';
3eeffdb10STomasz Sapetaimport chalk from 'chalk';
4eeffdb10STomasz Sapetaimport fs from 'fs-extra';
5eeffdb10STomasz Sapetaimport inquirer from 'inquirer';
6eeffdb10STomasz Sapetaimport path from 'path';
7eeffdb10STomasz Sapeta
8eeffdb10STomasz Sapetaimport { EXPO_DIR } from '../Constants';
9eeffdb10STomasz Sapetaimport { link } from '../Formatter';
10eeffdb10STomasz Sapetaimport Git from '../Git';
11eeffdb10STomasz Sapetaimport logger from '../Logger';
12eeffdb10STomasz Sapetaimport { getNewestSDKVersionAsync } from '../ProjectVersions';
13*a272999eSBartosz Kaszubowskiimport { modifySdkVersionsAsync, getSdkVersionsAsync } from '../Versions';
14*a272999eSBartosz Kaszubowskiimport AndroidClientBuilder from '../client-build/AndroidClientBuilder';
15*a272999eSBartosz Kaszubowskiimport IosClientBuilder from '../client-build/IosClientBuilder';
16*a272999eSBartosz Kaszubowskiimport { ClientBuilder, ClientBuildFlavor, Platform } from '../client-build/types';
17*a272999eSBartosz Kaszubowskiimport askForPlatformAsync from '../utils/askForPlatformAsync';
18*a272999eSBartosz Kaszubowskiimport askForSdkVersionAsync from '../utils/askForSDKVersionAsync';
19eeffdb10STomasz Sapeta
20eeffdb10STomasz Sapetaconst s3Client = new aws.S3({ region: 'us-east-1' });
21eeffdb10STomasz Sapetaconst { yellow, blue, magenta } = chalk;
22eeffdb10STomasz Sapeta
23eeffdb10STomasz Sapetatype ActionOptions = {
24eeffdb10STomasz Sapeta  platform?: Platform;
25eeffdb10STomasz Sapeta  release: boolean;
26cf276e6eSTomasz Sapeta  flavor: ClientBuildFlavor;
27eeffdb10STomasz Sapeta};
28eeffdb10STomasz Sapeta
29cf276e6eSTomasz Sapetaconst flavors = ['versioned', 'unversioned'];
30cf276e6eSTomasz Sapeta
31eeffdb10STomasz Sapetaexport default (program: Command) => {
32eeffdb10STomasz Sapeta  program
33eeffdb10STomasz Sapeta    .command('client-build')
34eeffdb10STomasz Sapeta    .alias('cb')
35eeffdb10STomasz Sapeta    .description(
36eeffdb10STomasz Sapeta      'Builds Expo client for iOS simulator or APK for Android, uploads the archive to S3 and saves its url to versions endpoint.'
37eeffdb10STomasz Sapeta    )
38eeffdb10STomasz Sapeta    .option('-p, --platform [string]', 'Platform for which the client will be built.')
39eeffdb10STomasz Sapeta    .option(
40eeffdb10STomasz Sapeta      '-r, --release',
41eeffdb10STomasz Sapeta      'Whether to upload and release the client build to staging versions endpoint.',
42eeffdb10STomasz Sapeta      false
43eeffdb10STomasz Sapeta    )
44cf276e6eSTomasz Sapeta    .option(
45cf276e6eSTomasz Sapeta      '-f, --flavor [string]',
46cf276e6eSTomasz Sapeta      `Which build flavor to use. Possible values: ${flavors}`,
47cf276e6eSTomasz Sapeta      flavors[0]
48cf276e6eSTomasz Sapeta    )
49eeffdb10STomasz Sapeta    .asyncAction(main);
50eeffdb10STomasz Sapeta};
51eeffdb10STomasz Sapeta
52eeffdb10STomasz Sapetaasync function main(options: ActionOptions) {
53eeffdb10STomasz Sapeta  const platform = options.platform || (await askForPlatformAsync());
54eeffdb10STomasz Sapeta  const sdkBranchVersion = await Git.getSDKVersionFromBranchNameAsync();
55eeffdb10STomasz Sapeta
56eeffdb10STomasz Sapeta  if (options.release && !sdkBranchVersion) {
57eeffdb10STomasz Sapeta    throw new Error(`Client builds can be released only from the release branch!`);
58eeffdb10STomasz Sapeta  }
59cf276e6eSTomasz Sapeta  if (!Object.values(ClientBuildFlavor).includes(options.flavor)) {
60cf276e6eSTomasz Sapeta    throw new Error(`Flavor "${options.flavor}" is not valid, use one of: ${flavors}`);
61cf276e6eSTomasz Sapeta  }
62eeffdb10STomasz Sapeta
63eeffdb10STomasz Sapeta  const builder = getBuilderForPlatform(platform);
64eeffdb10STomasz Sapeta  const sdkVersion =
65eeffdb10STomasz Sapeta    sdkBranchVersion ||
66eeffdb10STomasz Sapeta    (await askForSdkVersionAsync(platform, await getNewestSDKVersionAsync(platform)));
67eeffdb10STomasz Sapeta  const appVersion = await builder.getAppVersionAsync();
68eeffdb10STomasz Sapeta
69cf276e6eSTomasz Sapeta  await buildOrUseCacheAsync(builder, options.flavor);
70eeffdb10STomasz Sapeta
71eeffdb10STomasz Sapeta  if (sdkVersion && options.release) {
72eeffdb10STomasz Sapeta    await uploadAsync(builder, sdkVersion, appVersion);
73eeffdb10STomasz Sapeta    await releaseAsync(builder, sdkVersion, appVersion);
74eeffdb10STomasz Sapeta  }
75eeffdb10STomasz Sapeta}
76eeffdb10STomasz Sapeta
77eeffdb10STomasz Sapetafunction getBuilderForPlatform(platform: Platform): ClientBuilder {
78eeffdb10STomasz Sapeta  switch (platform) {
79eeffdb10STomasz Sapeta    case 'ios':
80eeffdb10STomasz Sapeta      return new IosClientBuilder();
81eeffdb10STomasz Sapeta    case 'android':
82eeffdb10STomasz Sapeta      return new AndroidClientBuilder();
83eeffdb10STomasz Sapeta    default: {
84eeffdb10STomasz Sapeta      throw new Error(`Platform "${platform}" is not supported yet!`);
85eeffdb10STomasz Sapeta    }
86eeffdb10STomasz Sapeta  }
87eeffdb10STomasz Sapeta}
88eeffdb10STomasz Sapeta
89eeffdb10STomasz Sapetaasync function askToRecreateSimulatorBuildAsync(): Promise<boolean> {
90eeffdb10STomasz Sapeta  if (process.env.CI) {
91eeffdb10STomasz Sapeta    return false;
92eeffdb10STomasz Sapeta  }
93eeffdb10STomasz Sapeta  const { createNew } = await inquirer.prompt<{ createNew: boolean }>([
94eeffdb10STomasz Sapeta    {
95eeffdb10STomasz Sapeta      type: 'confirm',
96eeffdb10STomasz Sapeta      name: 'createNew',
97eeffdb10STomasz Sapeta      message: 'Do you want to create a fresh one?',
98eeffdb10STomasz Sapeta      default: true,
99eeffdb10STomasz Sapeta    },
100eeffdb10STomasz Sapeta  ]);
101eeffdb10STomasz Sapeta  return createNew;
102eeffdb10STomasz Sapeta}
103eeffdb10STomasz Sapeta
104eeffdb10STomasz Sapetaasync function askToOverrideBuildAsync(): Promise<boolean> {
105eeffdb10STomasz Sapeta  if (process.env.CI) {
106eeffdb10STomasz Sapeta    // we should never override anything in CI, too easy to accidentally mess something up in prod
107eeffdb10STomasz Sapeta    return false;
108eeffdb10STomasz Sapeta  }
109eeffdb10STomasz Sapeta  const { override } = await inquirer.prompt<{ override: boolean }>([
110eeffdb10STomasz Sapeta    {
111eeffdb10STomasz Sapeta      type: 'confirm',
112eeffdb10STomasz Sapeta      name: 'override',
113eeffdb10STomasz Sapeta      message: 'Do you want to override it?',
114eeffdb10STomasz Sapeta      default: true,
115eeffdb10STomasz Sapeta    },
116eeffdb10STomasz Sapeta  ]);
117eeffdb10STomasz Sapeta  return override;
118eeffdb10STomasz Sapeta}
119eeffdb10STomasz Sapeta
120cf276e6eSTomasz Sapetaasync function buildOrUseCacheAsync(
121cf276e6eSTomasz Sapeta  builder: ClientBuilder,
122cf276e6eSTomasz Sapeta  flavor: ClientBuildFlavor
123cf276e6eSTomasz Sapeta): Promise<void> {
124eeffdb10STomasz Sapeta  const appPath = builder.getAppPath();
125eeffdb10STomasz Sapeta
126eeffdb10STomasz Sapeta  // Build directory already exists, we could reuse that one — especially useful on the CI.
127eeffdb10STomasz Sapeta  if (await fs.pathExists(appPath)) {
128eeffdb10STomasz Sapeta    const relativeAppPath = path.relative(EXPO_DIR, appPath);
129eeffdb10STomasz Sapeta    logger.info(`Client build already exists at ${magenta.bold(relativeAppPath)}`);
130eeffdb10STomasz Sapeta
131eeffdb10STomasz Sapeta    if (!(await askToRecreateSimulatorBuildAsync())) {
132eeffdb10STomasz Sapeta      logger.info('Skipped building the app, using cached build instead...');
133eeffdb10STomasz Sapeta      return;
134eeffdb10STomasz Sapeta    }
135eeffdb10STomasz Sapeta  }
136cf276e6eSTomasz Sapeta  await builder.buildAsync(flavor);
137eeffdb10STomasz Sapeta}
138eeffdb10STomasz Sapeta
139eeffdb10STomasz Sapetaasync function uploadAsync(
140eeffdb10STomasz Sapeta  builder: ClientBuilder,
141eeffdb10STomasz Sapeta  sdkVersion: string,
142eeffdb10STomasz Sapeta  appVersion: string
143eeffdb10STomasz Sapeta): Promise<void> {
144eeffdb10STomasz Sapeta  const sdkVersions = await getSdkVersionsAsync(sdkVersion);
145eeffdb10STomasz Sapeta
146eeffdb10STomasz Sapeta  // Target app url already defined in versions endpoint.
147eeffdb10STomasz Sapeta  // We make this check to reduce the risk of unintentional overrides.
148eeffdb10STomasz Sapeta  if (sdkVersions?.[`${builder.platform}ClientUrl`] === builder.getClientUrl(appVersion)) {
149eeffdb10STomasz Sapeta    logger.info(`Build ${yellow.bold(appVersion)} is already defined in versions endpoint.`);
150eeffdb10STomasz Sapeta    logger.info('The new build would be uploaded onto the same URL.');
151eeffdb10STomasz Sapeta
152eeffdb10STomasz Sapeta    if (!(await askToOverrideBuildAsync())) {
153eeffdb10STomasz Sapeta      logger.warn('Refused overriding the build, exiting the proces...');
154eeffdb10STomasz Sapeta      process.exit(0);
155eeffdb10STomasz Sapeta      return;
156eeffdb10STomasz Sapeta    }
157eeffdb10STomasz Sapeta  }
158eeffdb10STomasz Sapeta  logger.info(`Uploading ${yellow.bold(appVersion)} build`);
159eeffdb10STomasz Sapeta
160eeffdb10STomasz Sapeta  await builder.uploadBuildAsync(s3Client, appVersion);
161eeffdb10STomasz Sapeta}
162eeffdb10STomasz Sapeta
163eeffdb10STomasz Sapetaasync function releaseAsync(
164eeffdb10STomasz Sapeta  builder: ClientBuilder,
165eeffdb10STomasz Sapeta  sdkVersion: string,
166eeffdb10STomasz Sapeta  appVersion: string
167eeffdb10STomasz Sapeta): Promise<void> {
168eeffdb10STomasz Sapeta  const clientUrl = builder.getClientUrl(appVersion);
169eeffdb10STomasz Sapeta
170eeffdb10STomasz Sapeta  logger.info(
171eeffdb10STomasz Sapeta    `Updating versions endpoint with client url ${blue.bold(link(clientUrl, clientUrl))}`
172eeffdb10STomasz Sapeta  );
173eeffdb10STomasz Sapeta
174eeffdb10STomasz Sapeta  await updateClientUrlAndVersionAsync(builder, sdkVersion, appVersion);
175eeffdb10STomasz Sapeta}
176eeffdb10STomasz Sapeta
177eeffdb10STomasz Sapetaasync function updateClientUrlAndVersionAsync(
178eeffdb10STomasz Sapeta  builder: ClientBuilder,
179eeffdb10STomasz Sapeta  sdkVersion: string,
180eeffdb10STomasz Sapeta  appVersion: string
181eeffdb10STomasz Sapeta) {
182eeffdb10STomasz Sapeta  await modifySdkVersionsAsync(sdkVersion, (sdkVersions) => {
183eeffdb10STomasz Sapeta    sdkVersions[`${builder.platform}ClientUrl`] = builder.getClientUrl(appVersion);
184eeffdb10STomasz Sapeta    sdkVersions[`${builder.platform}ClientVersion`] = appVersion;
185eeffdb10STomasz Sapeta    return sdkVersions;
186eeffdb10STomasz Sapeta  });
187eeffdb10STomasz Sapeta}
188