From a01b8d7423866534619be6ccfa7afb03aa7f50c5 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Thu, 9 Oct 2025 16:48:36 -0700 Subject: [PATCH] Add script to find the directory that contains the Xcodeproj (#53669) Summary: ## 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 --- .../__tests__/prepare-app-utils-test.js | 157 ++++++++++++++++++ .../scripts/swiftpm/prepare-app-utils.js | 46 +++++ 2 files changed, 203 insertions(+) create mode 100644 packages/react-native/scripts/swiftpm/__tests__/prepare-app-utils-test.js create mode 100644 packages/react-native/scripts/swiftpm/prepare-app-utils.js diff --git a/packages/react-native/scripts/swiftpm/__tests__/prepare-app-utils-test.js b/packages/react-native/scripts/swiftpm/__tests__/prepare-app-utils-test.js new file mode 100644 index 000000000000..890c6ce9494b --- /dev/null +++ b/packages/react-native/scripts/swiftpm/__tests__/prepare-app-utils-test.js @@ -0,0 +1,157 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @noflow + */ + +'use strict'; + +const {findXcodeProjectDirectory} = require('../prepare-app-utils'); + +// Mock child_process module +jest.mock('child_process'); + +describe('findXcodeProjectDirectory', () => { + let mockExecSync; + + beforeEach(() => { + // Setup mock + const childProcess = require('child_process'); + mockExecSync = childProcess.execSync; + + // Reset all mocks + jest.clearAllMocks(); + }); + + it('should find Xcode project directory successfully', () => { + // Setup + const appPath = '/path/to/app'; + const xcodeProjectName = 'MyApp.xcodeproj'; + const mockResult = '/path/to/app/ios/MyApp.xcodeproj'; + + mockExecSync.mockReturnValue(mockResult + '\n'); + + // Execute + const result = findXcodeProjectDirectory(appPath, xcodeProjectName); + + // Assert + expect(result).toBe('/path/to/app/ios'); + expect(mockExecSync).toHaveBeenCalledWith( + `find "${appPath}" -name "${xcodeProjectName}" -type d -print`, + {encoding: 'utf8'}, + ); + expect(mockExecSync).toHaveBeenCalledTimes(1); + }); + + it('should find Xcode project in nested subdirectory', () => { + // Setup + const appPath = '/Users/developer/ReactNativeApp'; + const xcodeProjectName = 'ReactNativeApp.xcodeproj'; + const mockResult = + '/Users/developer/ReactNativeApp/ios/sub/ReactNativeApp.xcodeproj'; + + mockExecSync.mockReturnValue(mockResult + '\n'); + + // Execute + const result = findXcodeProjectDirectory(appPath, xcodeProjectName); + + // Assert + expect(result).toBe('/Users/developer/ReactNativeApp/ios/sub'); + expect(mockExecSync).toHaveBeenCalledWith( + `find "${appPath}" -name "${xcodeProjectName}" -type d -print`, + {encoding: 'utf8'}, + ); + }); + + it('should handle project found at root level', () => { + // Setup + const appPath = '/path/to/project'; + const xcodeProjectName = 'RootProject.xcodeproj'; + const mockResult = '/path/to/project/RootProject.xcodeproj'; + + mockExecSync.mockReturnValue(mockResult + '\n'); + + // Execute + const result = findXcodeProjectDirectory(appPath, xcodeProjectName); + + // Assert + expect(result).toBe('/path/to/project'); + expect(mockExecSync).toHaveBeenCalledWith( + `find "${appPath}" -name "${xcodeProjectName}" -type d -print`, + {encoding: 'utf8'}, + ); + }); + + it('should handle paths with spaces in directory names', () => { + // Setup + const appPath = '/path/to/my app'; + const xcodeProjectName = 'My App.xcodeproj'; + const mockResult = '/path/to/my app/ios folder/My App.xcodeproj'; + + mockExecSync.mockReturnValue(mockResult + '\n'); + + // Execute + const result = findXcodeProjectDirectory(appPath, xcodeProjectName); + + // Assert + expect(result).toBe('/path/to/my app/ios folder'); + expect(mockExecSync).toHaveBeenCalledWith( + `find "${appPath}" -name "${xcodeProjectName}" -type d -print`, + {encoding: 'utf8'}, + ); + }); + + it('should throw error when Xcode project is not found', () => { + // Setup + const appPath = '/path/to/app'; + const xcodeProjectName = 'NonExistent.xcodeproj'; + + mockExecSync.mockReturnValue(''); + + // Execute & Assert + expect(() => findXcodeProjectDirectory(appPath, xcodeProjectName)).toThrow( + `Xcode project 'NonExistent.xcodeproj' not found in '/path/to/app' or its subdirectories`, + ); + + expect(mockExecSync).toHaveBeenCalledWith( + `find "${appPath}" -name "${xcodeProjectName}" -type d -print`, + {encoding: 'utf8'}, + ); + }); + + it('should throw error when find command returns only whitespace', () => { + // Setup + const appPath = '/path/to/app'; + const xcodeProjectName = 'Missing.xcodeproj'; + + mockExecSync.mockReturnValue(' \n \t '); + + // Execute & Assert + expect(() => findXcodeProjectDirectory(appPath, xcodeProjectName)).toThrow( + `Xcode project 'Missing.xcodeproj' not found in '/path/to/app' or its subdirectories`, + ); + }); + + it('should properly escape quotes in app path', () => { + // Setup + const appPath = '/path/to/app with "quotes"'; + const xcodeProjectName = 'MyApp.xcodeproj'; + const mockResult = '/path/to/app with "quotes"/ios/MyApp.xcodeproj'; + + mockExecSync.mockReturnValue(mockResult + '\n'); + + // Execute + const result = findXcodeProjectDirectory(appPath, xcodeProjectName); + + // Assert + expect(result).toBe('/path/to/app with "quotes"/ios'); + expect(mockExecSync).toHaveBeenCalledWith( + `find "${appPath}" -name "${xcodeProjectName}" -type d -print`, + {encoding: 'utf8'}, + ); + }); +}); diff --git a/packages/react-native/scripts/swiftpm/prepare-app-utils.js b/packages/react-native/scripts/swiftpm/prepare-app-utils.js new file mode 100644 index 000000000000..f6ceb619dc7a --- /dev/null +++ b/packages/react-native/scripts/swiftpm/prepare-app-utils.js @@ -0,0 +1,46 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow strict-local + * @format + */ + +const {execSync} = require('child_process'); +const path = require('path'); + +/** + * Find the directory containing the Xcode project within the app path + * @param {string} appPath - The root app path to search in + * @param {string} xcodeProjectName - The name of the Xcode project file (e.g., 'HelloWorld.xcodeproj') + * @returns {string} - The path to the directory containing the Xcode project + */ +function findXcodeProjectDirectory( + appPath /*: string */, + xcodeProjectName /*: string */, +) /*: string */ { + try { + // Use find command to search for the Xcode project + const findCommand = `find "${appPath}" -name "${xcodeProjectName}" -type d -print`; + const result = execSync(findCommand, {encoding: 'utf8'}).trim(); + + if (!result) { + throw new Error( + `Xcode project '${xcodeProjectName}' not found in '${appPath}' or its subdirectories`, + ); + } + + // Return the directory containing the Xcode project (parent of the .xcodeproj file) + return path.dirname(result); + } catch (error) { + throw new Error( + `Failed to find Xcode project '${xcodeProjectName}': ${error.message}`, + ); + } +} + +module.exports = { + findXcodeProjectDirectory, +};