From 0e920fdd34700cc705be7e61a0eb62bfd574a20c Mon Sep 17 00:00:00 2001 From: Satyajit Sahoo Date: Sun, 9 Aug 2020 18:37:14 +0200 Subject: [PATCH] [expo-cli] Add a eas:build:init command We want to add a separate command to configure the project for eas builds without having to actually start a build. --- .../src/commands/eas-build/build/action.ts | 54 +++------- .../build/builders/AndroidBuilder.ts | 35 ++++-- .../eas-build/build/builders/iOSBuilder.ts | 14 ++- .../src/commands/eas-build/build/types.ts | 26 ----- .../expo-cli/src/commands/eas-build/index.ts | 16 ++- .../src/commands/eas-build/init/action.ts | 102 ++++++++++++++++++ .../expo-cli/src/commands/eas-build/types.ts | 27 ++++- .../utils/createBuilderContextAsync.ts | 39 +++++++ .../eas-build/{build => }/utils/git.ts | 8 +- .../eas-build/{build => }/utils/misc.ts | 8 +- 10 files changed, 236 insertions(+), 93 deletions(-) delete mode 100644 packages/expo-cli/src/commands/eas-build/build/types.ts create mode 100644 packages/expo-cli/src/commands/eas-build/init/action.ts create mode 100644 packages/expo-cli/src/commands/eas-build/utils/createBuilderContextAsync.ts rename packages/expo-cli/src/commands/eas-build/{build => }/utils/git.ts (90%) rename packages/expo-cli/src/commands/eas-build/{build => }/utils/misc.ts (87%) diff --git a/packages/expo-cli/src/commands/eas-build/build/action.ts b/packages/expo-cli/src/commands/eas-build/build/action.ts index 03f426fbfa..35c5931cd6 100644 --- a/packages/expo-cli/src/commands/eas-build/build/action.ts +++ b/packages/expo-cli/src/commands/eas-build/build/action.ts @@ -1,5 +1,4 @@ -import { getConfig } from '@expo/config'; -import { ApiV2, User, UserManager } from '@expo/xdl'; +import { ApiV2 } from '@expo/xdl'; import chalk from 'chalk'; import delayAsync from 'delay-async'; import fs from 'fs-extra'; @@ -8,18 +7,19 @@ import os from 'os'; import path from 'path'; import { v4 as uuidv4 } from 'uuid'; +import CommandError from '../../../CommandError'; import { EasConfig, EasJsonReader } from '../../../easJson'; import log from '../../../log'; import { ensureProjectExistsAsync } from '../../../projects'; import { UploadType, uploadAsync } from '../../../uploads'; import { createProgressTracker } from '../../utils/progress'; import { platformDisplayNames } from '../constants'; -import { Build, BuildCommandPlatform, BuildStatus } from '../types'; +import { Build, BuildCommandPlatform, BuildStatus, Builder, BuilderContext } from '../types'; +import createBuilderContextAsync from '../utils/createBuilderContextAsync'; +import { ensureGitStatusIsCleanAsync, makeProjectTarballAsync } from '../utils/git'; +import { printBuildResults, printLogsUrls } from '../utils/misc'; import AndroidBuilder from './builders/AndroidBuilder'; import iOSBuilder from './builders/iOSBuilder'; -import { Builder, BuilderContext } from './types'; -import { ensureGitStatusIsCleanAsync, makeProjectTarballAsync } from './utils/git'; -import { printBuildResults, printLogsUrls } from './utils/misc'; interface BuildOptions { platform: BuildCommandPlatform; @@ -72,40 +72,6 @@ async function buildAction(projectDir: string, options: BuildOptions): Promise { - const user: User = await UserManager.ensureLoggedInAsync(); - const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true }); - const accountName = exp.owner || user.username; - const projectName = exp.slug; - - return { - eas, - projectDir, - user, - accountName, - projectName, - exp, - platform, - nonInteractive, - skipCredentialsCheck, - skipProjectConfiguration, - }; -} - async function startBuildsAsync( ctx: BuilderContext, projectId: string @@ -140,7 +106,13 @@ async function startBuildAsync( await builder.ensureCredentialsAsync(); if (!builder.ctx.skipProjectConfiguration) { - await builder.configureProjectAsync(); + if (builder.ctx.platform === BuildCommandPlatform.IOS) { + await builder.configureProjectAsync(); + } else if (!(await builder.isProjectConfiguredAsync())) { + throw new CommandError( + 'Project is not configured. Please run "expo eas:build:init" first to configure the project' + ); + } } const fileSize = await makeProjectTarballAsync(tarPath); diff --git a/packages/expo-cli/src/commands/eas-build/build/builders/AndroidBuilder.ts b/packages/expo-cli/src/commands/eas-build/build/builders/AndroidBuilder.ts index 428938804a..e41d3f3ab4 100644 --- a/packages/expo-cli/src/commands/eas-build/build/builders/AndroidBuilder.ts +++ b/packages/expo-cli/src/commands/eas-build/build/builders/AndroidBuilder.ts @@ -16,10 +16,10 @@ import { } from '../../../../easJson'; import { gitAddAsync } from '../../../../git'; import log from '../../../../log'; +import { Builder, BuilderContext } from '../../types'; +import * as gitUtils from '../../utils/git'; import { ensureCredentialsAsync } from '../credentials'; import gradleContent from '../templates/gradleContent'; -import { Builder, BuilderContext } from '../types'; -import * as gitUtils from '../utils/git'; interface CommonJobProperties { platform: Platform.Android; @@ -60,9 +60,29 @@ class AndroidBuilder implements Builder { this.credentials = await provider.getCredentialsAsync(credentialsSource); } + public async isProjectConfiguredAsync(): Promise { + const androidAppDir = path.join(this.ctx.projectDir, 'android', 'app'); + const buildGradlePath = path.join(androidAppDir, 'build.gradle'); + + const buildGradleContent = await fs.readFile(path.join(buildGradlePath), 'utf-8'); + const applyEasGradle = 'apply from: "./eas-build.gradle"'; + + const isAlreadyConfigured = buildGradleContent + .split('\n') + // Check for both single and double quotes + .some(line => line === applyEasGradle || line === applyEasGradle.replace(/"/g, "'")); + + return isAlreadyConfigured; + } + public async configureProjectAsync(): Promise { const spinner = ora('Making sure your Android project is set up properly'); + if (await this.isProjectConfiguredAsync()) { + spinner.succeed('Android project is already configured'); + return; + } + const { projectDir } = this.ctx; const androidAppDir = path.join(projectDir, 'android', 'app'); @@ -75,14 +95,7 @@ class AndroidBuilder implements Builder { const buildGradleContent = await fs.readFile(path.join(buildGradlePath), 'utf-8'); const applyEasGradle = 'apply from: "./eas-build.gradle"'; - const isAlreadyConfigured = buildGradleContent - .split('\n') - // Check for both single and double quotes - .some(line => line === applyEasGradle || line === applyEasGradle.replace(/"/g, "'")); - - if (!isAlreadyConfigured) { - await fs.writeFile(buildGradlePath, `${buildGradleContent.trim()}\n${applyEasGradle}\n`); - } + await fs.writeFile(buildGradlePath, `${buildGradleContent.trim()}\n${applyEasGradle}\n`); try { await gitUtils.ensureGitStatusIsCleanAsync(); @@ -100,7 +113,7 @@ class AndroidBuilder implements Builder { log(`${chalk.green(figures.tick)} Successfully committed the configuration changes.`); } catch (e) { throw new Error( - "Aborting, run the build command once you're ready. Make sure to commit any changes you've made." + "Aborting, run the command again once you're ready. Make sure to commit any changes you've made." ); } } else { diff --git a/packages/expo-cli/src/commands/eas-build/build/builders/iOSBuilder.ts b/packages/expo-cli/src/commands/eas-build/build/builders/iOSBuilder.ts index 07ff0aa272..c7c7bffbfe 100644 --- a/packages/expo-cli/src/commands/eas-build/build/builders/iOSBuilder.ts +++ b/packages/expo-cli/src/commands/eas-build/build/builders/iOSBuilder.ts @@ -15,9 +15,9 @@ import { iOSManagedBuildProfile, } from '../../../../easJson'; import log from '../../../../log'; +import { Builder, BuilderContext } from '../../types'; +import * as gitUtils from '../../utils/git'; import { ensureCredentialsAsync } from '../credentials'; -import { Builder, BuilderContext } from '../types'; -import * as gitUtils from '../utils/git'; import { getBundleIdentifier } from '../utils/ios'; interface CommonJobProperties { @@ -73,16 +73,22 @@ class iOSBuilder implements Builder { this.credentials = await provider.getCredentialsAsync(credentialsSource); } + public async isProjectConfiguredAsync(): Promise { + // TODO: implement this later + throw new Error('Not implemented.'); + } + public async configureProjectAsync(): Promise { // TODO: add simulator flow // assuming we're building for app store if (!this.credentials) { throw new Error('Call ensureCredentialsAsync first!'); } - const bundleIdentifier = await getBundleIdentifier(this.ctx.projectDir, this.ctx.exp); const spinner = ora('Making sure your iOS project is set up properly'); + const bundleIdentifier = await getBundleIdentifier(this.ctx.projectDir, this.ctx.exp); + const profileName = ProvisioningProfileUtils.readProfileName( this.credentials.provisioningProfile ); @@ -111,7 +117,7 @@ class iOSBuilder implements Builder { log(`${chalk.green(figures.tick)} Successfully committed the configuration changes.`); } catch (e) { throw new Error( - "Aborting, run the build command once you're ready. Make sure to commit any changes you've made." + "Aborting, run the command again once you're ready. Make sure to commit any changes you've made." ); } } else { diff --git a/packages/expo-cli/src/commands/eas-build/build/types.ts b/packages/expo-cli/src/commands/eas-build/build/types.ts deleted file mode 100644 index e9b1948132..0000000000 --- a/packages/expo-cli/src/commands/eas-build/build/types.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Job } from '@expo/build-tools'; -import { ExpoConfig } from '@expo/config'; -import { User } from '@expo/xdl'; - -import { EasConfig } from '../../../easJson'; -import { BuildCommandPlatform } from '../types'; - -export interface BuilderContext { - projectDir: string; - eas: EasConfig; - user: User; - accountName: string; - projectName: string; - exp: ExpoConfig; - platform: BuildCommandPlatform; - nonInteractive: boolean; - skipCredentialsCheck: boolean; - skipProjectConfiguration: boolean; -} - -export interface Builder { - ctx: BuilderContext; - ensureCredentialsAsync(): Promise; - configureProjectAsync(): Promise; - prepareJobAsync(archiveUrl: string): Promise; -} diff --git a/packages/expo-cli/src/commands/eas-build/index.ts b/packages/expo-cli/src/commands/eas-build/index.ts index fff58b80d0..0443bddc20 100644 --- a/packages/expo-cli/src/commands/eas-build/index.ts +++ b/packages/expo-cli/src/commands/eas-build/index.ts @@ -4,12 +4,24 @@ import path from 'path'; import buildAction from './build/action'; import credentialsSyncAction from './credentialsSync/action'; +import initAction from './init/action'; import statusAction from './status/action'; export default function (program: Command) { // don't register `expo eas:build:*` commands if eas.json doesn't exist const easJsonPath = path.join(process.cwd(), 'eas.json'); - if (!fs.pathExistsSync(easJsonPath)) { + const hasEasJson = fs.pathExistsSync(easJsonPath); + + if (hasEasJson || process.argv[2] !== '--help') { + // We don't want to show this in the help output for now + program + .command('eas:build:init [project-dir]') + .description('Initialize build configuration for your project.') + .option('--skip-credentials-check', 'Skip checking credentials', false) + .asyncActionProjectDir(initAction, { checkConfig: true }); + } + + if (!hasEasJson) { return; } @@ -26,7 +38,7 @@ export default function (program: Command) { .description('Build an app binary for your project.') .option( '-p --platform ', - 'Build for specified platform: ios, android, all', + 'Build for the specified platform: ios, android, all', /^(all|android|ios)$/i ) .option('--skip-credentials-check', 'Skip checking credentials', false) diff --git a/packages/expo-cli/src/commands/eas-build/init/action.ts b/packages/expo-cli/src/commands/eas-build/init/action.ts new file mode 100644 index 0000000000..be7d1470b3 --- /dev/null +++ b/packages/expo-cli/src/commands/eas-build/init/action.ts @@ -0,0 +1,102 @@ +import chalk from 'chalk'; +import figures from 'figures'; +import fs from 'fs-extra'; +import ora from 'ora'; +import path from 'path'; + +import { EasConfig, EasJsonReader } from '../../../easJson'; +import { gitAddAsync } from '../../../git'; +import log from '../../../log'; +import AndroidBuilder from '../build/builders/AndroidBuilder'; +import iOSBuilder from '../build/builders/iOSBuilder'; +import { BuildCommandPlatform } from '../types'; +import createBuilderContextAsync from '../utils/createBuilderContextAsync'; +import { + DirtyGitTreeError, + ensureGitStatusIsCleanAsync, + reviewAndCommitChangesAsync, +} from '../utils/git'; + +interface BuildOptions { + platform: BuildCommandPlatform; + skipCredentialsCheck?: boolean; // TODO: noop for now + profile: string; + parent?: { + nonInteractive: boolean; + }; +} + +async function initAction(projectDir: string, options: BuildOptions): Promise { + const nonInteractive = options.parent?.nonInteractive === true; + + const spinner = ora('Checking for eas.json file'); + + await ensureGitStatusIsCleanAsync(); + + const easJsonPath = path.join(projectDir, 'eas.json'); + const easJson = { + builds: { + android: { + release: { + workflow: 'generic', + }, + }, + ios: { + release: { + workflow: 'generic', + }, + }, + }, + }; + + if (!(await fs.pathExists(easJsonPath))) { + await fs.writeFile(easJsonPath, `${JSON.stringify(easJson, null, 2)}\n`); + await gitAddAsync(easJsonPath, { intentToAdd: true }); + } + + try { + await ensureGitStatusIsCleanAsync(); + spinner.succeed('Found existing eas.json file'); + } catch (err) { + if (err instanceof DirtyGitTreeError) { + spinner.succeed('We created a minimal eas.json file'); + log.newLine(); + + try { + await reviewAndCommitChangesAsync('Create minimal eas.json', { nonInteractive }); + + log(`${chalk.green(figures.tick)} Successfully committed eas.json.`); + } catch (e) { + throw new Error( + "Aborting, run the command again once you're ready. Make sure to commit any changes you've made." + ); + } + } else { + spinner.fail(); + throw err; + } + } + + const easConfig: EasConfig = await new EasJsonReader( + projectDir, + BuildCommandPlatform.ALL + ).readAsync('release'); + + const ctx = await createBuilderContextAsync(projectDir, easConfig, { + platform: BuildCommandPlatform.ALL, + nonInteractive, + skipCredentialsCheck: options?.skipCredentialsCheck, + skipProjectConfiguration: false, + }); + + const androidBuilder = new AndroidBuilder(ctx); + + await androidBuilder.configureProjectAsync(); + + const iosBuilder = new iOSBuilder(ctx); + + await iosBuilder.ensureCredentialsAsync(); + await iosBuilder.configureProjectAsync(); +} + +export default initAction; diff --git a/packages/expo-cli/src/commands/eas-build/types.ts b/packages/expo-cli/src/commands/eas-build/types.ts index af23977b53..0fb808ebab 100644 --- a/packages/expo-cli/src/commands/eas-build/types.ts +++ b/packages/expo-cli/src/commands/eas-build/types.ts @@ -1,4 +1,8 @@ -import { Platform } from '@expo/build-tools'; +import { Job, Platform } from '@expo/build-tools'; +import { ExpoConfig } from '@expo/config'; +import { User } from '@expo/xdl'; + +import { EasConfig } from '../../easJson'; export enum BuildCommandPlatform { ANDROID = 'android', @@ -25,3 +29,24 @@ interface BuildArtifacts { buildUrl?: string; logsUrl: string; } + +export interface Builder { + ctx: BuilderContext; + ensureCredentialsAsync(): Promise; + isProjectConfiguredAsync(): Promise; + configureProjectAsync(): Promise; + prepareJobAsync(archiveUrl: string): Promise; +} + +export interface BuilderContext { + projectDir: string; + eas: EasConfig; + user: User; + accountName: string; + projectName: string; + exp: ExpoConfig; + platform: BuildCommandPlatform; + nonInteractive: boolean; + skipCredentialsCheck: boolean; + skipProjectConfiguration: boolean; +} diff --git a/packages/expo-cli/src/commands/eas-build/utils/createBuilderContextAsync.ts b/packages/expo-cli/src/commands/eas-build/utils/createBuilderContextAsync.ts new file mode 100644 index 0000000000..17e1fa71ee --- /dev/null +++ b/packages/expo-cli/src/commands/eas-build/utils/createBuilderContextAsync.ts @@ -0,0 +1,39 @@ +import { getConfig } from '@expo/config'; +import { User, UserManager } from '@expo/xdl'; + +import { EasConfig } from '../../../easJson'; +import { BuildCommandPlatform, BuilderContext } from '../types'; + +export default async function createBuilderContextAsync( + projectDir: string, + eas: EasConfig, + { + platform = BuildCommandPlatform.ALL, + nonInteractive = false, + skipCredentialsCheck = false, + skipProjectConfiguration = false, + }: { + platform?: BuildCommandPlatform; + nonInteractive?: boolean; + skipCredentialsCheck?: boolean; + skipProjectConfiguration?: boolean; + } +): Promise { + const user: User = await UserManager.ensureLoggedInAsync(); + const { exp } = getConfig(projectDir, { skipSDKVersionRequirement: true }); + const accountName = exp.owner || user.username; + const projectName = exp.slug; + + return { + eas, + projectDir, + user, + accountName, + projectName, + exp, + platform, + nonInteractive, + skipCredentialsCheck, + skipProjectConfiguration, + }; +} diff --git a/packages/expo-cli/src/commands/eas-build/build/utils/git.ts b/packages/expo-cli/src/commands/eas-build/utils/git.ts similarity index 90% rename from packages/expo-cli/src/commands/eas-build/build/utils/git.ts rename to packages/expo-cli/src/commands/eas-build/utils/git.ts index ee2e0d8463..3c1c5169a9 100644 --- a/packages/expo-cli/src/commands/eas-build/build/utils/git.ts +++ b/packages/expo-cli/src/commands/eas-build/utils/git.ts @@ -2,10 +2,10 @@ import spawnAsync from '@expo/spawn-async'; import fs from 'fs-extra'; import ora from 'ora'; -import CommandError from '../../../../CommandError'; -import { gitDiffAsync, gitStatusAsync } from '../../../../git'; -import log from '../../../../log'; -import prompts from '../../../../prompts'; +import CommandError from '../../../CommandError'; +import { gitDiffAsync, gitStatusAsync } from '../../../git'; +import log from '../../../log'; +import prompts from '../../../prompts'; async function ensureGitStatusIsCleanAsync(): Promise { const changes = await gitStatusAsync(); diff --git a/packages/expo-cli/src/commands/eas-build/build/utils/misc.ts b/packages/expo-cli/src/commands/eas-build/utils/misc.ts similarity index 87% rename from packages/expo-cli/src/commands/eas-build/build/utils/misc.ts rename to packages/expo-cli/src/commands/eas-build/utils/misc.ts index f423a4d1a6..a49b2e601c 100644 --- a/packages/expo-cli/src/commands/eas-build/build/utils/misc.ts +++ b/packages/expo-cli/src/commands/eas-build/utils/misc.ts @@ -1,9 +1,9 @@ import { UserManager } from '@expo/xdl'; -import log from '../../../../log'; -import * as UrlUtils from '../../../utils/url'; -import { platformDisplayNames } from '../../constants'; -import { Build } from '../../types'; +import log from '../../../log'; +import * as UrlUtils from '../../utils/url'; +import { platformDisplayNames } from '../constants'; +import { Build } from '../types'; async function printLogsUrls( accountName: string,