From 36dde71d1e8ef6d0e97695e55592ae21de36b23e Mon Sep 17 00:00:00 2001 From: Wojciech Kozyra Date: Tue, 18 Aug 2020 15:21:00 +0200 Subject: [PATCH] [expo-cli] command for syncing credentials.json and credentials on www (#2460) * [expo-cli] command for syncing credentials.json and credentials on www * review feedback * update CHANGELOG.md --- CHANGELOG.md | 2 + .../builders/__tests__/AndroidBuilder-test.ts | 10 ++ .../builders/__tests__/iOSBuilder-test.ts | 10 ++ .../eas-build/build/builders/iOSBuilder.ts | 71 +-------- .../src/commands/eas-build/build/utils/ios.ts | 70 +++++++++ .../eas-build/credentialsSync/action.ts | 98 ++++++++++++ .../expo-cli/src/commands/eas-build/index.ts | 6 + .../__tests__/read-test.ts} | 24 +-- .../read.ts} | 10 +- .../src/credentials/credentialsJson/update.ts | 140 ++++++++++++++++++ .../expo-cli/src/credentials/local/index.ts | 1 - .../provider/AndroidCredentialsProvider.ts | 10 +- .../provider/iOSCredentialsProvider.ts | 8 +- .../credentials/views/SetupAndroidKeystore.ts | 19 +++ .../views/SetupIosBuildCredentials.ts | 65 +++++++- 15 files changed, 446 insertions(+), 98 deletions(-) create mode 100644 packages/expo-cli/src/commands/eas-build/build/utils/ios.ts create mode 100644 packages/expo-cli/src/commands/eas-build/credentialsSync/action.ts rename packages/expo-cli/src/credentials/{local/__tests__/credentials-json-test.ts => credentialsJson/__tests__/read-test.ts} (85%) rename packages/expo-cli/src/credentials/{local/credentialsJson.ts => credentialsJson/read.ts} (89%) create mode 100644 packages/expo-cli/src/credentials/credentialsJson/update.ts delete mode 100644 packages/expo-cli/src/credentials/local/index.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 709db1fa40..7735048ef7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ This is the log of notable changes to Expo CLI and related packages. ### 🎉 New features +- [expo-cli]: EAS Build: add command `eas:credentials:sync` ([#2460](https://github.com/expo/expo-cli/pull/2460)) by [@wkozyra95](https://github.com/wkozyra95) + ### 🐛 Bug fixes ### 📦 Packages updated diff --git a/packages/expo-cli/src/commands/eas-build/build/builders/__tests__/AndroidBuilder-test.ts b/packages/expo-cli/src/commands/eas-build/build/builders/__tests__/AndroidBuilder-test.ts index 0e0175a066..7591d768dd 100644 --- a/packages/expo-cli/src/commands/eas-build/build/builders/__tests__/AndroidBuilder-test.ts +++ b/packages/expo-cli/src/commands/eas-build/build/builders/__tests__/AndroidBuilder-test.ts @@ -35,6 +35,16 @@ function setupCredentialsConfig() { }); } +const originalWarn = console.warn; +const originalLog = console.log; +beforeAll(() => { + console.warn = jest.fn(); + console.log = jest.fn(); +}); +afterAll(() => { + console.warn = originalWarn; + console.log = originalLog; +}); beforeEach(() => { vol.reset(); }); diff --git a/packages/expo-cli/src/commands/eas-build/build/builders/__tests__/iOSBuilder-test.ts b/packages/expo-cli/src/commands/eas-build/build/builders/__tests__/iOSBuilder-test.ts index faa20fc403..f012f028ec 100644 --- a/packages/expo-cli/src/commands/eas-build/build/builders/__tests__/iOSBuilder-test.ts +++ b/packages/expo-cli/src/commands/eas-build/build/builders/__tests__/iOSBuilder-test.ts @@ -40,6 +40,16 @@ function setupCredentialsConfig() { }); } +const originalWarn = console.warn; +const originalLog = console.log; +beforeAll(() => { + console.warn = jest.fn(); + console.log = jest.fn(); +}); +afterAll(() => { + console.warn = originalWarn; + console.log = originalLog; +}); beforeEach(() => { vol.reset(); }); 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 357f0cd06f..07ff0aa272 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 @@ -2,7 +2,6 @@ import { BuildType, Job, Platform, iOS, sanitizeJob } from '@expo/build-tools'; import { IOSConfig } from '@expo/config'; import chalk from 'chalk'; import figures from 'figures'; -import once from 'lodash/once'; import ora from 'ora'; import iOSCredentialsProvider, { @@ -16,10 +15,10 @@ import { iOSManagedBuildProfile, } from '../../../../easJson'; import log from '../../../../log'; -import prompts from '../../../../prompts'; import { ensureCredentialsAsync } from '../credentials'; import { Builder, BuilderContext } from '../types'; import * as gitUtils from '../utils/git'; +import { getBundleIdentifier } from '../utils/ios'; interface CommonJobProperties { platform: Platform.iOS; @@ -58,7 +57,7 @@ class iOSBuilder implements Builder { if (!this.shouldLoadCredentials()) { return; } - const bundleIdentifier = await getBundleIdentifier(this.ctx); + const bundleIdentifier = await getBundleIdentifier(this.ctx.projectDir, this.ctx.exp); const provider = new iOSCredentialsProvider(this.ctx.projectDir, { projectName: this.ctx.projectName, accountName: this.ctx.accountName, @@ -80,7 +79,7 @@ class iOSBuilder implements Builder { if (!this.credentials) { throw new Error('Call ensureCredentialsAsync first!'); } - const bundleIdentifier = await getBundleIdentifier(this.ctx); + const bundleIdentifier = await getBundleIdentifier(this.ctx.projectDir, this.ctx.exp); const spinner = ora('Making sure your iOS project is set up properly'); @@ -173,69 +172,5 @@ class iOSBuilder implements Builder { ); } } -const getBundleIdentifier = once(_getBundleIdentifier); - -async function _getBundleIdentifier(ctx: BuilderContext): Promise { - const bundleIdentifierFromPbxproj = IOSConfig.BundleIdenitifer.getBundleIdentifierFromPbxproj( - ctx.projectDir - ); - const bundleIdentifierFromConfig = IOSConfig.BundleIdenitifer.getBundleIdentifier(ctx.exp); - if (bundleIdentifierFromPbxproj !== null && bundleIdentifierFromConfig !== null) { - if (bundleIdentifierFromPbxproj === bundleIdentifierFromConfig) { - return bundleIdentifierFromPbxproj; - } else { - log.newLine(); - log( - log.chalk.yellow( - `We detected that your Xcode project is configured with a different bundle identifier than the one defined in app.json/app.config.js. -If you choose the one defined in app.json/app.config.js we'll automatically configure your Xcode project with it. -However, if you choose the one defined in the Xcode project you'll have to update app.json/app.config.js on your own. -Otherwise, you'll see this prompt again in the future.` - ) - ); - log.newLine(); - const { bundleIdentifier } = await prompts({ - type: 'select', - name: 'bundleIdentifier', - message: 'Which bundle identifier should we use?', - choices: [ - { - title: `Defined in the Xcode project: ${log.chalk.bold(bundleIdentifierFromPbxproj)}`, - value: bundleIdentifierFromPbxproj, - }, - { - title: `Defined in app.json/app.config.js: ${log.chalk.bold( - bundleIdentifierFromConfig - )}`, - value: bundleIdentifierFromConfig, - }, - ], - }); - return bundleIdentifier; - } - } else if (bundleIdentifierFromPbxproj === null && bundleIdentifierFromConfig === null) { - throw new Error('Please define "expo.ios.bundleIdentifier" in app.json/app.config.js'); - } else { - if (bundleIdentifierFromPbxproj !== null) { - log( - `Using ${log.chalk.bold( - bundleIdentifierFromPbxproj - )} as the bundle identifier (read from the Xcode project).` - ); - return bundleIdentifierFromPbxproj; - } else { - // bundleIdentifierFromConfig is never null in this case - // the following line is to satisfy TS - const bundleIdentifier = bundleIdentifierFromConfig ?? ''; - log( - `Using ${log.chalk.bold( - bundleIdentifier - )} as the bundle identifier (read from app.json/app.config.js). -We'll automatically configure your Xcode project using this value.` - ); - return bundleIdentifier; - } - } -} export default iOSBuilder; diff --git a/packages/expo-cli/src/commands/eas-build/build/utils/ios.ts b/packages/expo-cli/src/commands/eas-build/build/utils/ios.ts new file mode 100644 index 0000000000..dfa29dd279 --- /dev/null +++ b/packages/expo-cli/src/commands/eas-build/build/utils/ios.ts @@ -0,0 +1,70 @@ +import { ExpoConfig, IOSConfig } from '@expo/config'; +import once from 'lodash/once'; + +import log from '../../../../log'; +import prompts from '../../../../prompts'; + +export const getBundleIdentifier = once(_getBundleIdentifier); + +async function _getBundleIdentifier(projectDir: string, manifest: ExpoConfig): Promise { + const bundleIdentifierFromPbxproj = IOSConfig.BundleIdenitifer.getBundleIdentifierFromPbxproj( + projectDir + ); + const bundleIdentifierFromConfig = IOSConfig.BundleIdenitifer.getBundleIdentifier(manifest); + if (bundleIdentifierFromPbxproj !== null && bundleIdentifierFromConfig !== null) { + if (bundleIdentifierFromPbxproj === bundleIdentifierFromConfig) { + return bundleIdentifierFromPbxproj; + } else { + log.newLine(); + log( + log.chalk.yellow( + `We detected that your Xcode project is configured with a different bundle identifier than the one defined in app.json/app.config.js. +If you choose the one defined in app.json/app.config.js we'll automatically configure your Xcode project with it. +However, if you choose the one defined in the Xcode project you'll have to update app.json/app.config.js on your own. +Otherwise, you'll see this prompt again in the future.` + ) + ); + log.newLine(); + const { bundleIdentifier } = await prompts({ + type: 'select', + name: 'bundleIdentifier', + message: 'Which bundle identifier should we use?', + choices: [ + { + title: `Defined in the Xcode project: ${log.chalk.bold(bundleIdentifierFromPbxproj)}`, + value: bundleIdentifierFromPbxproj, + }, + { + title: `Defined in app.json/app.config.js: ${log.chalk.bold( + bundleIdentifierFromConfig + )}`, + value: bundleIdentifierFromConfig, + }, + ], + }); + return bundleIdentifier; + } + } else if (bundleIdentifierFromPbxproj === null && bundleIdentifierFromConfig === null) { + throw new Error('Please define "expo.ios.bundleIdentifier" in app.json/app.config.js'); + } else { + if (bundleIdentifierFromPbxproj !== null) { + log( + `Using ${log.chalk.bold( + bundleIdentifierFromPbxproj + )} as the bundle identifier (read from the Xcode project).` + ); + return bundleIdentifierFromPbxproj; + } else { + // bundleIdentifierFromConfig is never null in this case + // the following line is to satisfy TS + const bundleIdentifier = bundleIdentifierFromConfig ?? ''; + log( + `Using ${log.chalk.bold( + bundleIdentifier + )} as the bundle identifier (read from app.json/app.config.js). +We'll automatically configure your Xcode project using this value.` + ); + return bundleIdentifier; + } + } +} diff --git a/packages/expo-cli/src/commands/eas-build/credentialsSync/action.ts b/packages/expo-cli/src/commands/eas-build/credentialsSync/action.ts new file mode 100644 index 0000000000..9c2f37b2c3 --- /dev/null +++ b/packages/expo-cli/src/commands/eas-build/credentialsSync/action.ts @@ -0,0 +1,98 @@ +import CommandError from '../../../CommandError'; +import { Context } from '../../../credentials/context'; +import * as credentialsJsonUpdateUtils from '../../../credentials/credentialsJson/update'; +import { runCredentialsManager } from '../../../credentials/route'; +import { SetupAndroidBuildCredentialsFromLocal } from '../../../credentials/views/SetupAndroidKeystore'; +import { SetupIosBuildCredentialsFromLocal } from '../../../credentials/views/SetupIosBuildCredentials'; +import log from '../../../log'; +import prompts from '../../../prompts'; +import { getBundleIdentifier } from '../build/utils/ios'; +import { BuildCommandPlatform } from '../types'; + +interface Options { + parent: { + nonInteractive?: boolean; + }; +} + +export default async function credentialsSyncAction(projectDir: string, options: Options) { + if (options.parent.nonInteractive) { + throw new CommandError('This command is not supported in --non-interactive mode'); + } + const { update, platform } = await prompts([ + { + type: 'select', + name: 'update', + message: 'What do you want to do?', + choices: [ + { + title: 'Update credentials on the Expo servers with the local credentials.json contents', + value: 'remote', + }, + { + title: 'Update or create local credentials.json with credentials from the Expo servers', + value: 'local', + }, + ], + }, + { + type: 'select', + name: 'platform', + message: 'Which platform would you like to update?', + choices: [ + { title: 'Android', value: BuildCommandPlatform.ANDROID }, + { title: 'iOS', value: BuildCommandPlatform.IOS }, + { title: 'both', value: BuildCommandPlatform.ALL }, + ], + }, + ]); + if (update === 'local') { + await updateLocalCredentialsAsync(projectDir, platform); + } else { + await updateRemoteCredentialsAsync(projectDir, platform); + } +} + +async function updateRemoteCredentialsAsync( + projectDir: string, + platform: BuildCommandPlatform +): Promise { + const ctx = new Context(); + await ctx.init(projectDir); + if (!ctx.hasProjectContext) { + throw new Error('project context is required'); // should be checked earlier + } + if ([BuildCommandPlatform.ALL, BuildCommandPlatform.ANDROID].includes(platform)) { + const experienceName = `@${ctx.manifest.owner || ctx.user.username}/${ctx.manifest.slug}`; + await runCredentialsManager(ctx, new SetupAndroidBuildCredentialsFromLocal(experienceName)); + } + if ([BuildCommandPlatform.ALL, BuildCommandPlatform.IOS].includes(platform)) { + const bundleIdentifier = await getBundleIdentifier(projectDir, ctx.manifest); + const appLookupParams = { + accountName: ctx.manifest.owner ?? ctx.user.username, + projectName: ctx.manifest.slug, + bundleIdentifier, + }; + await runCredentialsManager(ctx, new SetupIosBuildCredentialsFromLocal(appLookupParams)); + } +} + +export async function updateLocalCredentialsAsync( + projectDir: string, + platform: BuildCommandPlatform +): Promise { + const ctx = new Context(); + await ctx.init(projectDir); + if (!ctx.hasProjectContext) { + throw new Error('project context is required'); // should be checked earlier + } + if ([BuildCommandPlatform.ALL, BuildCommandPlatform.ANDROID].includes(platform)) { + log('Updating Android credentials in credentials.json'); + await credentialsJsonUpdateUtils.updateAndroidCredentialsAsync(ctx); + } + if ([BuildCommandPlatform.ALL, BuildCommandPlatform.IOS].includes(platform)) { + const bundleIdentifier = await getBundleIdentifier(projectDir, ctx.manifest); + log('Updating iOS credentials in credentials.json'); + await credentialsJsonUpdateUtils.updateIosCredentialsAsync(ctx, bundleIdentifier); + } +} diff --git a/packages/expo-cli/src/commands/eas-build/index.ts b/packages/expo-cli/src/commands/eas-build/index.ts index 7832bf430c..db8da792bf 100644 --- a/packages/expo-cli/src/commands/eas-build/index.ts +++ b/packages/expo-cli/src/commands/eas-build/index.ts @@ -3,6 +3,7 @@ import fs from 'fs-extra'; import path from 'path'; import buildAction from './build/action'; +import credentialsSyncAction from './credentialsSync/action'; import statusAction from './status/action'; export default function (program: Command) { @@ -12,6 +13,11 @@ export default function (program: Command) { return; } + program + .command('eas:credentials:sync [project-dir]') + .description('Update credentials.json with credentials stored on Expo servers') + .asyncActionProjectDir(credentialsSyncAction, { checkConfig: true }); + program .command('eas:build [project-dir]') .description('Build an app binary for your project.') diff --git a/packages/expo-cli/src/credentials/local/__tests__/credentials-json-test.ts b/packages/expo-cli/src/credentials/credentialsJson/__tests__/read-test.ts similarity index 85% rename from packages/expo-cli/src/credentials/local/__tests__/credentials-json-test.ts rename to packages/expo-cli/src/credentials/credentialsJson/__tests__/read-test.ts index 734d34694c..3b76f7bccb 100644 --- a/packages/expo-cli/src/credentials/local/__tests__/credentials-json-test.ts +++ b/packages/expo-cli/src/credentials/credentialsJson/__tests__/read-test.ts @@ -1,6 +1,6 @@ import { vol } from 'memfs'; -import credentialsJson from '../credentialsJson'; +import * as credentialsJsonReader from '../read'; jest.mock('fs'); @@ -24,7 +24,7 @@ describe('credentialsJson', () => { }), 'keystore.jks': 'somebinarydata', }); - const result = await credentialsJson.readAndroidAsync('.'); + const result = await credentialsJsonReader.readAndroidCredentialsAsync('.'); expect(result).toEqual({ keystore: { keystore: 'c29tZWJpbmFyeWRhdGE=', @@ -35,7 +35,7 @@ describe('credentialsJson', () => { }); }); it('should throw error when credentials.json is missing', async () => { - const promise = credentialsJson.readAndroidAsync('.'); + const promise = credentialsJsonReader.readAndroidCredentialsAsync('.'); await expect(promise).rejects.toThrow( 'credentials.json must exist in the project root directory and contain a valid JSON' ); @@ -46,7 +46,7 @@ describe('credentialsJson', () => { './credentials.json': JSON.stringify({}), 'keystore.jks': 'somebinarydata', }); - const promise = credentialsJson.readAndroidAsync('.'); + const promise = credentialsJsonReader.readAndroidCredentialsAsync('.'); await expect(promise).rejects.toThrow( 'Android credentials are missing from credentials.json' ); @@ -64,7 +64,7 @@ describe('credentialsJson', () => { }), 'keystore.jks': 'somebinarydata', }); - const promise = credentialsJson.readAndroidAsync('.'); + const promise = credentialsJsonReader.readAndroidCredentialsAsync('.'); await expect(promise).rejects.toThrow( 'credentials.json is not valid [ValidationError: "android.keystore.keystorePath" is required]' ); @@ -82,7 +82,7 @@ describe('credentialsJson', () => { }, }), }); - const promise = credentialsJson.readAndroidAsync('.'); + const promise = credentialsJsonReader.readAndroidCredentialsAsync('.'); await expect(promise).rejects.toThrow( "ENOENT: no such file or directory, open 'keystore.jks'" ); @@ -104,7 +104,7 @@ describe('credentialsJson', () => { './pprofile': 'somebinarycontent', './cert.p12': 'somebinarycontent2', }); - const result = await credentialsJson.readIosAsync('.'); + const result = await credentialsJsonReader.readIosCredentialsAsync('.'); expect(result).toEqual({ provisioningProfile: 'c29tZWJpbmFyeWNvbnRlbnQ=', distributionCertificate: { @@ -114,7 +114,7 @@ describe('credentialsJson', () => { }); }); it('should throw error when credentials.json is missing', async () => { - const promise = credentialsJson.readIosAsync('.'); + const promise = credentialsJsonReader.readIosCredentialsAsync('.'); await expect(promise).rejects.toThrow( 'credentials.json must exist in the project root directory and contain a valid JSON' ); @@ -125,7 +125,7 @@ describe('credentialsJson', () => { './pprofile': 'somebinarycontent', './cert.p12': 'somebinarycontent2', }); - const promise = credentialsJson.readIosAsync('.'); + const promise = credentialsJsonReader.readIosCredentialsAsync('.'); await expect(promise).rejects.toThrow('iOS credentials are missing from credentials.json'); }); it('should throw error if some field is missing', async () => { @@ -141,7 +141,7 @@ describe('credentialsJson', () => { './pprofile': 'somebinarycontent', './cert.p12': 'somebinarycontent2', }); - const promise = credentialsJson.readIosAsync('.'); + const promise = credentialsJsonReader.readIosCredentialsAsync('.'); await expect(promise).rejects.toThrow( 'credentials.json is not valid [ValidationError: "ios.provisioningProfilePath" is required]' ); @@ -159,7 +159,7 @@ describe('credentialsJson', () => { }), './pprofile': 'somebinarycontent', }); - const promise = credentialsJson.readIosAsync('.'); + const promise = credentialsJsonReader.readIosCredentialsAsync('.'); await expect(promise).rejects.toThrow("ENOENT: no such file or directory, open 'cert.p12'"); }); it('should throw error if provisioningProfile file is missing', async () => { @@ -175,7 +175,7 @@ describe('credentialsJson', () => { }), './cert.p12': 'somebinarycontent2', }); - const promise = credentialsJson.readIosAsync('.'); + const promise = credentialsJsonReader.readIosCredentialsAsync('.'); await expect(promise).rejects.toThrow("ENOENT: no such file or directory, open 'pprofile'"); }); }); diff --git a/packages/expo-cli/src/credentials/local/credentialsJson.ts b/packages/expo-cli/src/credentials/credentialsJson/read.ts similarity index 89% rename from packages/expo-cli/src/credentials/local/credentialsJson.ts rename to packages/expo-cli/src/credentials/credentialsJson/read.ts index db7f8bd668..0f8b155903 100644 --- a/packages/expo-cli/src/credentials/local/credentialsJson.ts +++ b/packages/expo-cli/src/credentials/credentialsJson/read.ts @@ -52,11 +52,11 @@ interface iOSCredentials { }; } -async function fileExistsAsync(projectDir: string): Promise { +export async function fileExistsAsync(projectDir: string): Promise { return await fs.pathExists(path.join(projectDir, 'credentials.json')); } -async function readAndroidAsync(projectDir: string): Promise { +export async function readAndroidCredentialsAsync(projectDir: string): Promise { const credentialsJson = await readAsync(projectDir); if (!credentialsJson.android) { throw new Error('Android credentials are missing from credentials.json'); // TODO: add fyi @@ -72,7 +72,7 @@ async function readAndroidAsync(projectDir: string): Promise }; } -async function readIosAsync(projectDir: string): Promise { +export async function readIosCredentialsAsync(projectDir: string): Promise { const credentialsJson = await readAsync(projectDir); if (!credentialsJson.ios) { throw new Error('iOS credentials are missing from credentials.json'); // TODO: add fyi @@ -107,7 +107,7 @@ async function readAsync(projectDir: string): Promise { return credentialsJson; } -async function readRawAsync(projectDir: string): Promise { +export async function readRawAsync(projectDir: string): Promise { const credentialsJsonFilePath = path.join(projectDir, 'credentials.json'); try { const credentialsJSONContents = await fs.readFile(credentialsJsonFilePath, 'utf8'); @@ -121,5 +121,3 @@ async function readRawAsync(projectDir: string): Promise { const getAbsolutePath = (projectDir: string, filePath: string): string => path.isAbsolute(filePath) ? filePath : path.join(projectDir, filePath); - -export default { readAndroidAsync, readIosAsync, readRawAsync, fileExistsAsync }; diff --git a/packages/expo-cli/src/credentials/credentialsJson/update.ts b/packages/expo-cli/src/credentials/credentialsJson/update.ts new file mode 100644 index 0000000000..59fb683118 --- /dev/null +++ b/packages/expo-cli/src/credentials/credentialsJson/update.ts @@ -0,0 +1,140 @@ +import fs from 'fs-extra'; +import path from 'path'; +import prompts from 'prompts'; + +import log from '../../log'; +import { Context } from '../context'; + +export async function updateAndroidCredentialsAsync(ctx: Context) { + const credentialsJsonFilePath = path.join(ctx.projectDir, 'credentials.json'); + let rawCredentialsJsonObject: any = {}; + if (await fs.pathExists(credentialsJsonFilePath)) { + try { + const rawFile = await fs.readFile(credentialsJsonFilePath, 'utf-8'); + rawCredentialsJsonObject = JSON.parse(rawFile); + } catch (error) { + log.error(`There was an error while reading credentials.json [${error}]`); + log.error('Make sure that file is correct (or remove it) and rerun this command.'); + throw error; + } + } + const experienceName = `@${ctx.manifest.owner || ctx.user.username}/${ctx.manifest.slug}`; + const keystore = await ctx.android.fetchKeystore(experienceName); + if (!keystore) { + log.error('There are no credentials configured for this project on Expo servers'); + return; + } + + const isKeystoreComplete = + keystore.keystore && keystore.keystorePassword && keystore.keyPassword && keystore.keyAlias; + + if (!isKeystoreComplete) { + const { confirm } = await prompts({ + type: 'confirm', + name: 'confirm', + message: + 'Credentials on Expo servers might be invalid or incomplete. Are you sure you want to continue?', + }); + if (!confirm) { + log.warn('Aborting...'); + return; + } + } + + const keystorePath = + rawCredentialsJsonObject?.android?.keystorePath ?? './android/keystores/keystore.jks'; + log(`Writing Keystore to ${keystorePath}`); + await updateFileAsync(ctx.projectDir, keystorePath, keystore.keystore); + + rawCredentialsJsonObject.android = { + keystore: { + keystorePath, + keystorePassword: keystore.keystorePassword, + keyAlias: keystore.keyAlias, + keyPassword: keystore.keyPassword, + }, + }; + await fs.writeJson(credentialsJsonFilePath, rawCredentialsJsonObject, { + spaces: 2, + }); +} + +export async function updateIosCredentialsAsync(ctx: Context, bundleIdentifier: string) { + const credentialsJsonFilePath = path.join(ctx.projectDir, 'credentials.json'); + let rawCredentialsJsonObject: any = {}; + if (await fs.pathExists(credentialsJsonFilePath)) { + try { + const rawFile = await fs.readFile(credentialsJsonFilePath, 'utf-8'); + rawCredentialsJsonObject = JSON.parse(rawFile); + } catch (error) { + log.error(`There was an error while reading credentials.json [${error}]`); + log.error('Make sure that file is correct (or remove it) and rerun this command.'); + throw error; + } + } + + const appLookupParams = { + accountName: ctx.manifest.owner ?? ctx.user.username, + projectName: ctx.manifest.slug, + bundleIdentifier, + }; + const pprofilePath = + rawCredentialsJsonObject?.ios?.provisioningProfilePath ?? './ios/certs/profile.mobileprovision'; + const distCertPath = + rawCredentialsJsonObject?.ios?.distributionCertificate.path ?? './ios/certs/dist-cert.p12'; + const appCredentials = await ctx.ios.getAppCredentials(appLookupParams); + const distCredentials = await ctx.ios.getDistCert(appLookupParams); + if (!appCredentials && !distCredentials) { + log.error('There are no credentials configured for this project on Expo servers'); + return; + } + + const areCredentialsComplete = + appCredentials?.credentials?.provisioningProfile && + distCredentials?.certP12 && + distCredentials?.certPassword; + + if (!areCredentialsComplete) { + const { confirm } = await prompts({ + type: 'confirm', + name: 'confirm', + message: + 'Credentials on Expo servers might be invalid or incomplete. Are you sure you want to continue?', + }); + if (!confirm) { + log.warn('Aborting...'); + return; + } + } + + log(`Writing Provisioning Profile to ${pprofilePath}`); + await updateFileAsync( + ctx.projectDir, + pprofilePath, + appCredentials?.credentials?.provisioningProfile + ); + log(`Writing Distribution Certificate to ${distCertPath}`); + await updateFileAsync(ctx.projectDir, distCertPath, distCredentials?.certP12); + + rawCredentialsJsonObject.ios = { + provisioningProfilePath: pprofilePath, + distributionCertificate: { + path: distCertPath, + password: distCredentials?.certPassword, + }, + }; + await fs.writeJson(credentialsJsonFilePath, rawCredentialsJsonObject, { + spaces: 2, + }); +} + +async function updateFileAsync(projectDir: string, filePath: string, base64Data?: string) { + const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(projectDir, filePath); + if (await fs.pathExists(absolutePath)) { + await fs.remove(absolutePath); + } + if (base64Data) { + await fs.mkdirp(path.dirname(filePath)); + await fs.writeFile(filePath, Buffer.from(base64Data, 'base64')); + } +} diff --git a/packages/expo-cli/src/credentials/local/index.ts b/packages/expo-cli/src/credentials/local/index.ts deleted file mode 100644 index 5724560094..0000000000 --- a/packages/expo-cli/src/credentials/local/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { default as credentialsJson } from './credentialsJson'; diff --git a/packages/expo-cli/src/credentials/provider/AndroidCredentialsProvider.ts b/packages/expo-cli/src/credentials/provider/AndroidCredentialsProvider.ts index 41c2189fb9..af6f8b773f 100644 --- a/packages/expo-cli/src/credentials/provider/AndroidCredentialsProvider.ts +++ b/packages/expo-cli/src/credentials/provider/AndroidCredentialsProvider.ts @@ -2,7 +2,7 @@ import { CredentialsSource } from '../../easJson'; import log from '../../log'; import { Context } from '../context'; import { Keystore } from '../credentials'; -import { credentialsJson } from '../local'; +import * as credentialsJsonReader from '../credentialsJson/read'; import { runCredentialsManager } from '../route'; import { SetupAndroidKeystore } from '../views/SetupAndroidKeystore'; import { CredentialsProvider } from './provider'; @@ -39,11 +39,11 @@ export default class AndroidCredentialsProvider implements CredentialsProvider { } public async hasLocalAsync(): Promise { - if (!(await credentialsJson.fileExistsAsync(this.projectDir))) { + if (!(await credentialsJsonReader.fileExistsAsync(this.projectDir))) { return false; } try { - const rawCredentialsJson = await credentialsJson.readRawAsync(this.projectDir); + const rawCredentialsJson = await credentialsJsonReader.readRawAsync(this.projectDir); return !!rawCredentialsJson?.android; } catch (err) { log.error(err); // malformed json @@ -55,7 +55,7 @@ export default class AndroidCredentialsProvider implements CredentialsProvider { try { const [remote, local] = await Promise.all([ this.ctx.android.fetchKeystore(this.projectFullName), - await credentialsJson.readAndroidAsync(this.projectDir), + await credentialsJsonReader.readAndroidCredentialsAsync(this.projectDir), ]); const r = remote!; const l = local?.keystore!; @@ -97,7 +97,7 @@ export default class AndroidCredentialsProvider implements CredentialsProvider { } private async getLocalAsync(): Promise { - const credentials = await credentialsJson.readAndroidAsync(this.projectDir); + const credentials = await credentialsJsonReader.readAndroidCredentialsAsync(this.projectDir); if (!this.isValidKeystore(credentials.keystore)) { throw new Error('Invalid keystore in credentials.json'); } diff --git a/packages/expo-cli/src/credentials/provider/iOSCredentialsProvider.ts b/packages/expo-cli/src/credentials/provider/iOSCredentialsProvider.ts index dd4423d284..6cfb48e204 100644 --- a/packages/expo-cli/src/credentials/provider/iOSCredentialsProvider.ts +++ b/packages/expo-cli/src/credentials/provider/iOSCredentialsProvider.ts @@ -2,7 +2,7 @@ import { CredentialsSource } from '../../easJson'; import log from '../../log'; import { AppLookupParams } from '../api/IosApi'; import { Context } from '../context'; -import { credentialsJson } from '../local'; +import * as credentialsJsonReader from '../credentialsJson/read'; import { runCredentialsManager } from '../route'; import { SetupIosBuildCredentials } from '../views/SetupIosBuildCredentials'; import { CredentialsProvider } from './provider'; @@ -35,11 +35,11 @@ export default class iOSCredentialsProvider implements CredentialsProvider { } public async hasLocalAsync(): Promise { - if (!(await credentialsJson.fileExistsAsync(this.projectDir))) { + if (!(await credentialsJsonReader.fileExistsAsync(this.projectDir))) { return false; } try { - const rawCredentialsJson = await credentialsJson.readRawAsync(this.projectDir); + const rawCredentialsJson = await credentialsJsonReader.readRawAsync(this.projectDir); return !!rawCredentialsJson?.ios; } catch (err) { log.error(err); // malformed json @@ -74,7 +74,7 @@ export default class iOSCredentialsProvider implements CredentialsProvider { } private async getLocalAsync(): Promise { - return await credentialsJson.readIosAsync(this.projectDir); + return await credentialsJsonReader.readIosCredentialsAsync(this.projectDir); } private async getRemoteAsync(): Promise { await runCredentialsManager(this.ctx, new SetupIosBuildCredentials(this.app)); diff --git a/packages/expo-cli/src/credentials/views/SetupAndroidKeystore.ts b/packages/expo-cli/src/credentials/views/SetupAndroidKeystore.ts index 4ad7c6c694..cff7e8d782 100644 --- a/packages/expo-cli/src/credentials/views/SetupAndroidKeystore.ts +++ b/packages/expo-cli/src/credentials/views/SetupAndroidKeystore.ts @@ -2,6 +2,7 @@ import commandExists from 'command-exists'; import log from '../../log'; import { Context, IView } from '../context'; +import * as credentialsJsonReader from '../credentialsJson/read'; import { UpdateKeystore } from './AndroidKeystore'; interface Options { @@ -40,6 +41,24 @@ export class SetupAndroidKeystore implements IView { } } +export class SetupAndroidBuildCredentialsFromLocal implements IView { + constructor(private experienceName: string) {} + + async open(ctx: Context): Promise { + let localCredentials; + try { + localCredentials = await credentialsJsonReader.readAndroidCredentialsAsync(ctx.projectDir); + } catch (error) { + log.error( + 'Reading credentials from credentials.json failed. Make sure this file is correct and all credentials are present there.' + ); + throw error; + } + await ctx.android.updateKeystore(this.experienceName, localCredentials.keystore); + return null; + } +} + async function keytoolCommandExists(): Promise { try { await commandExists('keytool'); diff --git a/packages/expo-cli/src/credentials/views/SetupIosBuildCredentials.ts b/packages/expo-cli/src/credentials/views/SetupIosBuildCredentials.ts index 7b303536a5..664028ee54 100644 --- a/packages/expo-cli/src/credentials/views/SetupIosBuildCredentials.ts +++ b/packages/expo-cli/src/credentials/views/SetupIosBuildCredentials.ts @@ -3,10 +3,12 @@ import chalk from 'chalk'; import CommandError from '../../CommandError'; import * as appleApi from '../../appleApi'; import log from '../../log'; -import prompt from '../../prompts'; +import prompts from '../../prompts'; import { AppLookupParams } from '../api/IosApi'; import { Context, IView } from '../context'; +import * as credentialsJsonReader from '../credentialsJson/read'; import { runCredentialsManager } from '../route'; +import { readAppleTeam } from '../utils/provisioningProfile'; import { SetupIosDist } from './SetupIosDist'; import { SetupIosProvisioningProfile } from './SetupIosProvisioningProfile'; @@ -56,7 +58,7 @@ export class SetupIosBuildCredentials implements IView { return; } - const { confirm } = await prompt([ + const { confirm } = await prompts([ { type: 'confirm', name: 'confirm', @@ -74,3 +76,62 @@ export class SetupIosBuildCredentials implements IView { } } } + +export class SetupIosBuildCredentialsFromLocal implements IView { + constructor(private app: AppLookupParams) {} + + async open(ctx: Context): Promise { + let localCredentials; + try { + localCredentials = await credentialsJsonReader.readIosCredentialsAsync(ctx.projectDir); + } catch (error) { + log.error( + 'Reading credentials from credentials.json failed. Make sure this file is correct and all credentials are present there.' + ); + throw error; + } + + const team = await readAppleTeam(localCredentials.provisioningProfile); + await ctx.ios.updateProvisioningProfile(this.app, { + ...team, + provisioningProfile: localCredentials.provisioningProfile, + }); + const credentials = await ctx.ios.getAllCredentials(this.app.accountName); + const distCert = await ctx.ios.getDistCert(this.app); + const appsUsingCert = distCert?.id + ? (credentials.appCredentials || []).filter(cred => cred.distCredentialsId === distCert.id) + : []; + + const appInfo = `@${this.app.accountName}/${this.app.projectName} (${this.app.bundleIdentifier})`; + const newDistCert = { + ...team, + certP12: localCredentials.distributionCertificate.certP12, + certPassword: localCredentials.distributionCertificate.certPassword, + }; + + if (appsUsingCert.length > 1 && distCert?.id) { + const { update } = await prompts({ + type: 'select', + name: 'update', + message: + 'Current distribution certificate is used by multiple apps. Do you want to update all of them?', + choices: [ + { title: 'Update all apps', value: 'all' }, + { title: `Update only ${appInfo}`, value: 'app' }, + ], + }); + if (update === 'all') { + await ctx.ios.updateDistCert(distCert.id, this.app.accountName, newDistCert); + } else { + const createdDistCert = await ctx.ios.createDistCert(this.app.accountName, newDistCert); + await ctx.ios.useDistCert(this.app, createdDistCert.id); + } + } else if (distCert?.id) { + await ctx.ios.updateDistCert(distCert.id, this.app.accountName, newDistCert); + } else { + const createdDistCert = await ctx.ios.createDistCert(this.app.accountName, newDistCert); + await ctx.ios.useDistCert(this.app, createdDistCert.id); + } + return null; + } +}