Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/build-tools/src/builders/ios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ async function buildAsync(ctx: BuildContext<Ios.Job>): Promise<void> {
}

await ctx.runBuildPhase(BuildPhase.RUN_FASTLANE, async () => {
const scheme = resolveScheme(ctx);
const scheme = await resolveScheme(ctx);
const entitlements = await readEntitlementsAsync(ctx, { scheme, buildConfiguration });
await runFastlaneGym(ctx, {
credentials,
Expand Down
40 changes: 40 additions & 0 deletions packages/build-tools/src/ios/__tests__/schemeCollision.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { vol } from 'memfs';

import { assertNoPodSchemeNameCollision } from '../schemeCollision';

describe(assertNoPodSchemeNameCollision, () => {
const projectRoot = '/app';

it('does not throw when there is no pod scheme collision', () => {
vol.fromJSON(
{
'ios/testapp.xcodeproj/xcshareddata/xcschemes/FruitVision.xcscheme': 'fakecontents',
},
projectRoot
);

expect(() =>
assertNoPodSchemeNameCollision({
projectDir: projectRoot,
buildScheme: 'FruitVision',
})
).not.toThrow();
});

it('throws when pod scheme name collides with app scheme', () => {
vol.fromJSON(
{
'ios/testapp.xcodeproj/xcshareddata/xcschemes/FruitVision.xcscheme': 'fakecontents',
'ios/Pods/Pods.xcodeproj/xcshareddata/xcschemes/FruitVision.xcscheme': 'fakecontents',
},
projectRoot
);

expect(() =>
assertNoPodSchemeNameCollision({
projectDir: projectRoot,
buildScheme: 'FruitVision',
})
).toThrow(/scheme name collision/);
});
});
41 changes: 37 additions & 4 deletions packages/build-tools/src/ios/resolve.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,53 @@
import { IOSConfig } from '@expo/config-plugins';
import { Ios } from '@expo/eas-build-job';
import { asyncResult } from '@expo/results';
import assert from 'assert';

import { assertNoPodSchemeNameCollision } from './schemeCollision';
import { BuildContext } from '../context';

export function resolveScheme(ctx: BuildContext<Ios.Job>): string {
export async function resolveScheme(ctx: BuildContext<Ios.Job>): Promise<string> {
const projectDir = ctx.getReactNativeProjectDirectory();
if (ctx.job.scheme) {
await warnIfPodSchemeNameCollisionAsync({
ctx,
projectDir,
buildScheme: ctx.job.scheme,
});
return ctx.job.scheme;
}
const schemes = IOSConfig.BuildScheme.getSchemesFromXcodeproj(
ctx.getReactNativeProjectDirectory()
);
const schemes = IOSConfig.BuildScheme.getSchemesFromXcodeproj(projectDir);
assert(schemes.length === 1, 'Ejected project should have exactly one scheme');
await warnIfPodSchemeNameCollisionAsync({
ctx,
projectDir,
buildScheme: schemes[0],
});
return schemes[0];
}

async function warnIfPodSchemeNameCollisionAsync({
ctx,
projectDir,
buildScheme,
}: {
ctx: BuildContext<Ios.Job>;
projectDir: string;
buildScheme: string;
}): Promise<void> {
const collisionCheckResult = await asyncResult(
(async () => assertNoPodSchemeNameCollision({ projectDir, buildScheme }))()
);
if (!collisionCheckResult.ok) {
ctx.logger.warn(
{ err: collisionCheckResult.reason },
`Detected an iOS scheme name collision for "${buildScheme}". ` +
'Xcode may select a Pods scheme instead of the app scheme. Continuing with the selected scheme.'
);
ctx.markBuildPhaseHasWarnings();
}
}

export function resolveArtifactPath(ctx: BuildContext<Ios.Job>): string {
if (ctx.job.applicationArchivePath) {
return ctx.job.applicationArchivePath;
Expand Down
32 changes: 32 additions & 0 deletions packages/build-tools/src/ios/schemeCollision.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { UserFacingError } from '@expo/eas-build-job/dist/errors';
import fs from 'fs-extra';
import path from 'path';

export function assertNoPodSchemeNameCollision({
projectDir,
buildScheme,
}: {
projectDir: string;
buildScheme: string;
}): void {
const podSchemePath = path.join(
projectDir,
'ios',
'Pods',
'Pods.xcodeproj',
'xcshareddata',
'xcschemes',
`${buildScheme}.xcscheme`
);
if (fs.existsSync(podSchemePath)) {
throw new UserFacingError(
'SCHEME_NAME_COLLISION',
`Detected an iOS scheme name collision for "${buildScheme}".\n` +
`A CocoaPods shared scheme with the same name exists at: ${podSchemePath}\n\n` +
'This is unsafe because Xcode may resolve the Pods scheme instead of your application scheme.\n\n' +
'To fix this:\n' +
'- If you use CNG (managed workflow): create a non-conflicting iOS build scheme via a config plugin or prebuild script, then set "build.ios.scheme" in eas.json to that scheme.\n' +
'- If you do not use CNG (native ios/ directory): rename the app scheme in Xcode so it does not match the Pod scheme name, then set "build.ios.scheme" in eas.json to that scheme.'
);
}
}
2 changes: 1 addition & 1 deletion packages/build-tools/src/ios/tvos.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { BuildContext } from '../context';
* @returns true if this is an Apple TV configuration, false otherwise
*/
export async function isTVOS(ctx: BuildContext<Ios.Job>): Promise<boolean> {
const scheme = resolveScheme(ctx);
const scheme = await resolveScheme(ctx);

const project = IOSConfig.XcodeUtils.getPbxproj(ctx.getReactNativeProjectDirectory());

Expand Down
33 changes: 32 additions & 1 deletion packages/eas-cli/src/project/ios/__tests__/scheme-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { vol } from 'memfs';
import os from 'os';

import { promptAsync } from '../../../prompts';
import { selectSchemeAsync } from '../scheme';
import { assertNoPodSchemeNameCollisionAsync, selectSchemeAsync } from '../scheme';

jest.mock('fs');
jest.mock('../../../prompts');
Expand Down Expand Up @@ -67,3 +67,34 @@ describe(selectSchemeAsync, () => {
});
});
});

describe(assertNoPodSchemeNameCollisionAsync, () => {
const projectDir = '/app';

it('does not throw when there is no pod scheme collision', async () => {
vol.fromJSON(
{
'ios/multitarget.xcodeproj/xcshareddata/xcschemes/FruitVision.xcscheme': 'fakecontents',
},
projectDir
);

await expect(
assertNoPodSchemeNameCollisionAsync({ projectDir, buildScheme: 'FruitVision' })
).resolves.toBe(undefined);
});

it('throws when pod scheme name collides with app scheme', async () => {
vol.fromJSON(
{
'ios/multitarget.xcodeproj/xcshareddata/xcschemes/FruitVision.xcscheme': 'fakecontents',
'ios/Pods/Pods.xcodeproj/xcshareddata/xcschemes/FruitVision.xcscheme': 'fakecontents',
},
projectDir
);

await expect(
assertNoPodSchemeNameCollisionAsync({ projectDir, buildScheme: 'FruitVision' })
).rejects.toThrow(/scheme name collision/);
});
});
53 changes: 53 additions & 0 deletions packages/eas-cli/src/project/ios/scheme.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { ExpoConfig } from '@expo/config';
import { IOSConfig } from '@expo/config-plugins';
import { Platform, Workflow } from '@expo/eas-build-job';
import { BuildProfile } from '@expo/eas-json';
import { asyncResult } from '@expo/results';
import chalk from 'chalk';
import fs from 'fs-extra';
import path from 'path';

import Log from '../../log';
import { promptAsync } from '../../prompts';
Expand Down Expand Up @@ -32,6 +35,7 @@ export async function resolveXcodeBuildContextAsync(
projectDir,
nonInteractive,
}));
await warnIfPodSchemeNameCollisionAsync({ projectDir, buildScheme });
return {
buildScheme,
buildConfiguration:
Expand All @@ -56,6 +60,25 @@ export async function resolveXcodeBuildContextAsync(
}
}

async function warnIfPodSchemeNameCollisionAsync({
projectDir,
buildScheme,
}: {
projectDir: string;
buildScheme: string;
}): Promise<void> {
const collisionCheckResult = await asyncResult(
assertNoPodSchemeNameCollisionAsync({ projectDir, buildScheme })
);
if (!collisionCheckResult.ok) {
Log.warn(
collisionCheckResult.reason instanceof Error
? collisionCheckResult.reason.message
: String(collisionCheckResult.reason)
);
}
}

export async function selectSchemeAsync({
projectDir,
nonInteractive = false,
Expand Down Expand Up @@ -97,3 +120,33 @@ export async function selectSchemeAsync({
return selectedScheme as string;
}
}

export async function assertNoPodSchemeNameCollisionAsync(
{
projectDir,
buildScheme,
}: {
projectDir: string;
buildScheme: string;
}
): Promise<void> {
const podSchemePath = path.join(
projectDir,
'ios',
'Pods',
'Pods.xcodeproj',
'xcshareddata',
'xcschemes',
`${buildScheme}.xcscheme`
);
if (await fs.pathExists(podSchemePath)) {
throw new Error(
`Detected an iOS scheme name collision for "${buildScheme}".\n` +
`A CocoaPods shared scheme with the same name exists at: ${podSchemePath}\n\n` +
'This is unsafe because Xcode may resolve the Pods scheme instead of your application scheme.\n\n' +
'To fix this:\n' +
'- If you use CNG (managed workflow): create a non-conflicting iOS build scheme via a config plugin or prebuild script, then set "build.ios.scheme" in eas.json to that scheme.\n' +
'- If you do not use CNG (native ios/ directory): rename the app scheme in Xcode so it does not match the Pod scheme name, then set "build.ios.scheme" in eas.json to that scheme.'
);
}
}
Loading