Skip to content

Commit f0725f7

Browse files
cipolleschimeta-codesync[bot]
authored andcommitted
Add script to find the directory that contains the Xcodeproj (#53669)
Summary: Pull Request resolved: #53669 ## Context When configuring an app to build with SwiftPM from source, there is a sequence of operations we need to run in order to prepare the project correctly. ## Changed Add a function that given the root of the app and the name of the xcodeproject file, can return the path to the Xcode project file ## Changelog: [Internal] - Reviewed By: cortinico Differential Revision: D81778456 fbshipit-source-id: f7050bcb049d75a5b1cabf340a5b98f4736e60b3
1 parent 53464e8 commit f0725f7

2 files changed

Lines changed: 203 additions & 0 deletions

File tree

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @format
8+
* @noflow
9+
*/
10+
11+
'use strict';
12+
13+
const {findXcodeProjectDirectory} = require('../prepare-app-utils');
14+
15+
// Mock child_process module
16+
jest.mock('child_process');
17+
18+
describe('findXcodeProjectDirectory', () => {
19+
let mockExecSync;
20+
21+
beforeEach(() => {
22+
// Setup mock
23+
const childProcess = require('child_process');
24+
mockExecSync = childProcess.execSync;
25+
26+
// Reset all mocks
27+
jest.clearAllMocks();
28+
});
29+
30+
it('should find Xcode project directory successfully', () => {
31+
// Setup
32+
const appPath = '/path/to/app';
33+
const xcodeProjectName = 'MyApp.xcodeproj';
34+
const mockResult = '/path/to/app/ios/MyApp.xcodeproj';
35+
36+
mockExecSync.mockReturnValue(mockResult + '\n');
37+
38+
// Execute
39+
const result = findXcodeProjectDirectory(appPath, xcodeProjectName);
40+
41+
// Assert
42+
expect(result).toBe('/path/to/app/ios');
43+
expect(mockExecSync).toHaveBeenCalledWith(
44+
`find "${appPath}" -name "${xcodeProjectName}" -type d -print`,
45+
{encoding: 'utf8'},
46+
);
47+
expect(mockExecSync).toHaveBeenCalledTimes(1);
48+
});
49+
50+
it('should find Xcode project in nested subdirectory', () => {
51+
// Setup
52+
const appPath = '/Users/developer/ReactNativeApp';
53+
const xcodeProjectName = 'ReactNativeApp.xcodeproj';
54+
const mockResult =
55+
'/Users/developer/ReactNativeApp/ios/sub/ReactNativeApp.xcodeproj';
56+
57+
mockExecSync.mockReturnValue(mockResult + '\n');
58+
59+
// Execute
60+
const result = findXcodeProjectDirectory(appPath, xcodeProjectName);
61+
62+
// Assert
63+
expect(result).toBe('/Users/developer/ReactNativeApp/ios/sub');
64+
expect(mockExecSync).toHaveBeenCalledWith(
65+
`find "${appPath}" -name "${xcodeProjectName}" -type d -print`,
66+
{encoding: 'utf8'},
67+
);
68+
});
69+
70+
it('should handle project found at root level', () => {
71+
// Setup
72+
const appPath = '/path/to/project';
73+
const xcodeProjectName = 'RootProject.xcodeproj';
74+
const mockResult = '/path/to/project/RootProject.xcodeproj';
75+
76+
mockExecSync.mockReturnValue(mockResult + '\n');
77+
78+
// Execute
79+
const result = findXcodeProjectDirectory(appPath, xcodeProjectName);
80+
81+
// Assert
82+
expect(result).toBe('/path/to/project');
83+
expect(mockExecSync).toHaveBeenCalledWith(
84+
`find "${appPath}" -name "${xcodeProjectName}" -type d -print`,
85+
{encoding: 'utf8'},
86+
);
87+
});
88+
89+
it('should handle paths with spaces in directory names', () => {
90+
// Setup
91+
const appPath = '/path/to/my app';
92+
const xcodeProjectName = 'My App.xcodeproj';
93+
const mockResult = '/path/to/my app/ios folder/My App.xcodeproj';
94+
95+
mockExecSync.mockReturnValue(mockResult + '\n');
96+
97+
// Execute
98+
const result = findXcodeProjectDirectory(appPath, xcodeProjectName);
99+
100+
// Assert
101+
expect(result).toBe('/path/to/my app/ios folder');
102+
expect(mockExecSync).toHaveBeenCalledWith(
103+
`find "${appPath}" -name "${xcodeProjectName}" -type d -print`,
104+
{encoding: 'utf8'},
105+
);
106+
});
107+
108+
it('should throw error when Xcode project is not found', () => {
109+
// Setup
110+
const appPath = '/path/to/app';
111+
const xcodeProjectName = 'NonExistent.xcodeproj';
112+
113+
mockExecSync.mockReturnValue('');
114+
115+
// Execute & Assert
116+
expect(() => findXcodeProjectDirectory(appPath, xcodeProjectName)).toThrow(
117+
`Xcode project 'NonExistent.xcodeproj' not found in '/path/to/app' or its subdirectories`,
118+
);
119+
120+
expect(mockExecSync).toHaveBeenCalledWith(
121+
`find "${appPath}" -name "${xcodeProjectName}" -type d -print`,
122+
{encoding: 'utf8'},
123+
);
124+
});
125+
126+
it('should throw error when find command returns only whitespace', () => {
127+
// Setup
128+
const appPath = '/path/to/app';
129+
const xcodeProjectName = 'Missing.xcodeproj';
130+
131+
mockExecSync.mockReturnValue(' \n \t ');
132+
133+
// Execute & Assert
134+
expect(() => findXcodeProjectDirectory(appPath, xcodeProjectName)).toThrow(
135+
`Xcode project 'Missing.xcodeproj' not found in '/path/to/app' or its subdirectories`,
136+
);
137+
});
138+
139+
it('should properly escape quotes in app path', () => {
140+
// Setup
141+
const appPath = '/path/to/app with "quotes"';
142+
const xcodeProjectName = 'MyApp.xcodeproj';
143+
const mockResult = '/path/to/app with "quotes"/ios/MyApp.xcodeproj';
144+
145+
mockExecSync.mockReturnValue(mockResult + '\n');
146+
147+
// Execute
148+
const result = findXcodeProjectDirectory(appPath, xcodeProjectName);
149+
150+
// Assert
151+
expect(result).toBe('/path/to/app with "quotes"/ios');
152+
expect(mockExecSync).toHaveBeenCalledWith(
153+
`find "${appPath}" -name "${xcodeProjectName}" -type d -print`,
154+
{encoding: 'utf8'},
155+
);
156+
});
157+
});
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict-local
8+
* @format
9+
*/
10+
11+
const {execSync} = require('child_process');
12+
const path = require('path');
13+
14+
/**
15+
* Find the directory containing the Xcode project within the app path
16+
* @param {string} appPath - The root app path to search in
17+
* @param {string} xcodeProjectName - The name of the Xcode project file (e.g., 'HelloWorld.xcodeproj')
18+
* @returns {string} - The path to the directory containing the Xcode project
19+
*/
20+
function findXcodeProjectDirectory(
21+
appPath /*: string */,
22+
xcodeProjectName /*: string */,
23+
) /*: string */ {
24+
try {
25+
// Use find command to search for the Xcode project
26+
const findCommand = `find "${appPath}" -name "${xcodeProjectName}" -type d -print`;
27+
const result = execSync(findCommand, {encoding: 'utf8'}).trim();
28+
29+
if (!result) {
30+
throw new Error(
31+
`Xcode project '${xcodeProjectName}' not found in '${appPath}' or its subdirectories`,
32+
);
33+
}
34+
35+
// Return the directory containing the Xcode project (parent of the .xcodeproj file)
36+
return path.dirname(result);
37+
} catch (error) {
38+
throw new Error(
39+
`Failed to find Xcode project '${xcodeProjectName}': ${error.message}`,
40+
);
41+
}
42+
}
43+
44+
module.exports = {
45+
findXcodeProjectDirectory,
46+
};

0 commit comments

Comments
 (0)