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
3 changes: 2 additions & 1 deletion __e2e__/__snapshots__/config.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ exports[`shows up current config without unnecessary output 1`] = `
"project": {
"ios": {
"sourceDir": "<<REPLACED_ROOT>>/TestProject/ios",
"assets": []
"assets": [],
"buildSystem": "cocoapods"
},
"android": {
"sourceDir": "<<REPLACED_ROOT>>/TestProject/android",
Expand Down
2 changes: 2 additions & 0 deletions docs/projects.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ If set to `true`, you can skip running `pod install` manually whenever it's need

> Note: Starting from React Native 0.73, CLI's `init` command scaffolds the project with `react-native.config.js` file with this value set to `true` by default. Older projects can opt-in after migrating to 0.73. Please note that if your setup does not follow the standard React Native template, e.g. you are not using Gems to install CocoaPods, this might not work properly for you. Starting from React Native 0.79, users shouldn't install CocoaPods manually, but can still opt-out of automatic installation by setting this value to `false`.

> Note: Projects migrated to Swift Package Manager with `react-native spm` (React Native 0.87+) are detected automatically and CocoaPods is skipped for them, even when a leftover `Podfile` is still there. Pass `--force-pods` or `--only-pods` to run CocoaPods anyway, which is how pods unrelated to React Native can be kept next to Swift Package Manager.

### project.ios.assets

Array of folder paths that will be passed to the `npx react-native link-assets` command to specify the assets to be linked to iOS project.
Expand Down
47 changes: 47 additions & 0 deletions packages/cli-config-apple/src/__tests__/pods.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,4 +147,51 @@ describe('resolvePods', () => {

expect(installPods).toHaveBeenCalled();
});

it('should skip CocoaPods when there is no Podfile (SwiftPM project)', async () => {
writeFiles(DIR, {'package.json': JSON.stringify(packageJson)});

await expect(
resolvePods(DIR, path.join(DIR, 'ios'), {}, 'ios', ''),
).resolves.toBe(false);

expect(installPods).not.toHaveBeenCalled();
});

it('should skip CocoaPods for a Swift Package Manager project keeping a Podfile', async () => {
createTempFiles();

await expect(
resolvePods(DIR, path.join(DIR, 'ios'), {}, 'ios', '', {
buildSystem: 'spm',
}),
).resolves.toBe(false);

expect(installPods).not.toHaveBeenCalled();
});

it('should install CocoaPods for a Swift Package Manager project when explicitly asked', async () => {
createTempFiles();

await expect(
resolvePods(DIR, path.join(DIR, 'ios'), {}, 'ios', '', {
buildSystem: 'spm',
forceInstall: true,
}),
).resolves.toBe(true);

expect(installPods).toHaveBeenCalled();
});

it('should fail when pods were explicitly requested but there is no Podfile', async () => {
writeFiles(DIR, {'package.json': JSON.stringify(packageJson)});

await expect(
resolvePods(DIR, path.join(DIR, 'ios'), {}, 'ios', '', {
forceInstall: true,
}),
).rejects.toThrow(/No Podfile found/);

expect(installPods).not.toHaveBeenCalled();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@ jest.mock('fs');

const fs = require('fs');

const xcodeProject = (extra: Record<string, string> = {}) => ({
'project.pbxproj': '',
...extra,
});

// Written inside the `.xcodeproj` bundle by `react-native spm`.
const spmMarker = {'.spm-injected.json': '{}'};
describe('ios::getProjectConfig', () => {
beforeAll(() => {
fs.__setMockFilesystem({
Expand All @@ -34,6 +41,45 @@ describe('ios::getProjectConfig', () => {
Podfile: '',
},
},
// Migrated with `react-native spm`, Podfile gone.
spm: {
ios: {
'TestApp.xcodeproj': xcodeProject(spmMarker),
'.xcode.env': '',
},
},
// `react-native spm` strips React Native from the Podfile but leaves it
// on disk, so the marker has precedence.
spmWithLeftoverPodfile: {
ios: {
Podfile: '',
'TestApp.xcodeproj': xcodeProject(spmMarker),
},
},
// Migrated project and leftover Podfile live in different folders.
spmAndStalePodfile: {
'TestApp.xcodeproj': xcodeProject(spmMarker),
ios: {
Podfile: '',
},
},
noMarker: {
ios: {
'TestApp.xcodeproj': xcodeProject(),
'.xcode.env': '',
},
},
spmOutsidePlatformDir: {
'TestApp.xcodeproj': xcodeProject(spmMarker),
},
customSpmLocation: {
'ios-dev': {
'TestApp.xcodeproj': xcodeProject(spmMarker),
},
ios: {
'TestApp.xcodeproj': xcodeProject(),
},
},
});
});

Expand All @@ -45,6 +91,7 @@ describe('ios::getProjectConfig', () => {
Object {
"assets": Array [],
"automaticPodsInstallation": undefined,
"buildSystem": "cocoapods",
"sourceDir": "/flat/ios",
"watchModeCommandParams": undefined,
"xcodeProject": null,
Expand All @@ -56,10 +103,64 @@ describe('ios::getProjectConfig', () => {
Object {
"assets": Array [],
"automaticPodsInstallation": undefined,
"buildSystem": "cocoapods",
"sourceDir": "/multiple/ios",
"watchModeCommandParams": undefined,
"xcodeProject": null,
}
`);
});
it('returns project configuration for Swift Package Manager projects without a Podfile', () => {
expect(projectConfig('/spm', {})).toMatchInlineSnapshot(`
Object {
"assets": Array [],
"automaticPodsInstallation": undefined,
"buildSystem": "spm",
"sourceDir": "/spm/ios",
"watchModeCommandParams": undefined,
"xcodeProject": Object {
"isWorkspace": false,
"name": "TestApp.xcodeproj",
"path": ".",
},
}
`);
});
it('uses the Swift Package Manager project when a Podfile is left behind', () => {
expect(projectConfig('/spmWithLeftoverPodfile', {})).toMatchObject({
buildSystem: 'spm',
sourceDir: '/spmWithLeftoverPodfile/ios',
});
});
it('prefers the migrated project over a Podfile in another folder', () => {
expect(projectConfig('/spmAndStalePodfile', {})).toMatchObject({
buildSystem: 'spm',
sourceDir: '/spmAndStalePodfile',
});
});
it('returns `null` when there is no Podfile and no migrated Xcode project', () => {
expect(projectConfig('/noMarker', {})).toBe(null);
});
it('finds a Swift Package Manager project outside of the platform folder', () => {
expect(projectConfig('/spmOutsidePlatformDir', {}).sourceDir).toBe(
'/spmOutsidePlatformDir',
);
});
it('uses project.ios.sourceDir as a search location for Swift Package Manager projects', () => {
expect(projectConfig('/customSpmLocation', {sourceDir: 'ios-dev'}))
.toMatchInlineSnapshot(`
Object {
"assets": Array [],
"automaticPodsInstallation": undefined,
"buildSystem": "spm",
"sourceDir": "/customSpmLocation/ios-dev",
"watchModeCommandParams": undefined,
"xcodeProject": Object {
"isWorkspace": false,
"name": "TestApp.xcodeproj",
"path": ".",
},
}
`);
});
});
49 changes: 49 additions & 0 deletions packages/cli-config-apple/src/config/findSpmProjectDir.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import fs from 'fs';
import path from 'path';
import {ApplePlatform} from '../types';

/**
* Written by `react-native spm` (React Native 0.87+) inside the `.xcodeproj`
* bundle it migrated to Swift Package Manager, and removed by
* `react-native spm deinit`.
*/
const SPM_INJECTED_MARKER = '.spm-injected.json';

/**
* Returns the folder holding an Xcode project migrated to Swift Package
* Manager, or `null` when there is none.
*/
export default function findSpmProjectDir(
cwd: string,
platformName: ApplePlatform,
): string | null {
const searchDirs =
path.basename(cwd) === platformName
? [cwd]
: [path.join(cwd, platformName), cwd];

return searchDirs.find(isSpmProjectDir) ?? null;
}

function isSpmProjectDir(dir: string): boolean {
let entries: string[];
try {
entries = fs.readdirSync(dir);
} catch {
return false;
}

return entries.some(
(entry) =>
entry.endsWith('.xcodeproj') &&
fs.existsSync(path.join(dir, entry, SPM_INJECTED_MARKER)),
);
}
16 changes: 10 additions & 6 deletions packages/cli-config-apple/src/config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import path from 'path';
import pico from 'picocolors';
import fs from 'fs';
import findPodfilePath from './findPodfilePath';
import findSpmProjectDir from './findSpmProjectDir';
import findXcodeProject from './findXcodeProject';
import findPodspec from './findPodspec';
import findAllPodfilePaths from './findAllPodfilePaths';
Expand All @@ -33,18 +34,20 @@ export const getProjectConfig =
}

const src = path.join(folder, userConfig.sourceDir ?? '');
const podfile = findPodfilePath(src, platformName);

/**
* In certain repos, the Xcode project can be generated by a tool.
* The only file that we can assume to exist on disk is `Podfile`.
* `react-native spm` leaves the `Podfile` on disk, so the marker it injects
* into the migrated Xcode project takes precedence: React Native comes from
* either Swift Package Manager or CocoaPods, never both.
*/
if (!podfile) {
const spmSourceDir = findSpmProjectDir(src, platformName);
const podfile = spmSourceDir ? null : findPodfilePath(src, platformName);
const sourceDir = spmSourceDir ?? (podfile && path.dirname(podfile));

if (!sourceDir) {
return null;
}

const sourceDir = path.dirname(podfile);

const xcodeProject = findXcodeProject(fs.readdirSync(sourceDir));

return {
Expand All @@ -53,6 +56,7 @@ export const getProjectConfig =
xcodeProject,
automaticPodsInstallation: userConfig.automaticPodsInstallation,
assets: userConfig.assets ?? [],
buildSystem: spmSourceDir ? 'spm' : 'cocoapods',
};
};

Expand Down
38 changes: 36 additions & 2 deletions packages/cli-config-apple/src/tools/pods.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@ import {
CLIError,
cacheManager,
getLoader,
logger,
} from '@react-native-community/cli-tools';
import installPods from './installPods';
import {
AppleBuildSystem,
DependencyConfig,
IOSDependencyConfig,
} from '@react-native-community/cli-types';
Expand All @@ -19,6 +21,7 @@ import execa from 'execa';
interface ResolvePodsOptions {
forceInstall?: boolean;
newArchEnabled?: boolean;
buildSystem?: AppleBuildSystem;
}

interface NativeDependencies {
Expand Down Expand Up @@ -127,9 +130,38 @@ export default async function resolvePods(
platformName: ApplePlatform,
reactNativePath: string,
options?: ResolvePodsOptions,
) {
): Promise<boolean> {
const podfilePath = path.join(sourceDir, 'Podfile');

/**
* A migrated project keeps its `Podfile` on disk, with the React Native
* directives stripped from it. `--force-pods` and `--only-pods` still run
* CocoaPods, for pods that live next to Swift Package Manager.
*/
if (options?.buildSystem === 'spm' && !options.forceInstall) {
logger.debug(
'Skipping CocoaPods installation: this project uses Swift Package Manager.',
);
return false;
}

if (!fs.existsSync(podfilePath)) {
if (options?.forceInstall) {
throw new CLIError(
`No Podfile found in ${pico.bold(
sourceDir,
)}, so CocoaPods cannot be installed. If this project uses Swift Package Manager (React Native 0.87+), run ${pico.bold(
'react-native spm',
)} and drop the CocoaPods flags.`,
);
}
logger.debug(
`Skipping CocoaPods installation: no Podfile in ${sourceDir}.`,
);
return false;
}

const packageJson = getPackageJson(root);
const podfilePath = path.join(sourceDir, 'Podfile'); // sourceDir is calculated based on Podfile location, see getProjectConfig()

const podfileLockPath = path.join(sourceDir, 'Podfile.lock');
const platformFolderPath = podfilePath
Expand Down Expand Up @@ -215,6 +247,8 @@ export default async function resolvePods(
);
}
}

return true;
}

export async function execaPod(args: string[], options?: execa.Options) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const createBuild =
? await getArchitecture(platformConfig.sourceDir)
: undefined;

await resolvePods(
installedPods = await resolvePods(
ctx.root,
platformConfig.sourceDir,
ctx.dependencies,
Expand All @@ -42,10 +42,9 @@ const createBuild =
{
forceInstall: args.forcePods || args.onlyPods,
newArchEnabled: isAppRunningNewArchitecture,
buildSystem: platformConfig.buildSystem,
},
);

installedPods = true;
}

if (args.onlyPods) {
Expand Down
Loading
Loading