From 22a7cf49738cc040b29446fd38ee3ad4a6bc2b00 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Fri, 15 Aug 2025 18:20:31 +0100 Subject: [PATCH] feat: implement modification of the Xcodeproject --- .../scripts/swiftpm/prepare-app.js | 57 +++++- .../scripts/swiftpm/update-xcodeproject.js | 162 ++++++++++++++++++ 2 files changed, 214 insertions(+), 5 deletions(-) create mode 100644 packages/react-native/scripts/swiftpm/update-xcodeproject.js diff --git a/packages/react-native/scripts/swiftpm/prepare-app.js b/packages/react-native/scripts/swiftpm/prepare-app.js index 72bccbcb261..5ec33bc4db1 100644 --- a/packages/react-native/scripts/swiftpm/prepare-app.js +++ b/packages/react-native/scripts/swiftpm/prepare-app.js @@ -22,6 +22,7 @@ const {execSync} = require('child_process'); // Import functions from other scripts const {prepareAppDependenciesHeaders} = require('./prepare-app-dependencies-headers'); const {createSymlinks: createSymlinksFunction} = require('./create-symlinks'); +const {integrateSwiftPackagesInXcode} = require('./update-xcodeproject'); const codegenExecutor = require('../codegen/generate-artifacts-executor'); /** @@ -29,8 +30,9 @@ const codegenExecutor = require('../codegen/generate-artifacts-executor'); * @param {string} appPath - Path to the app (e.g., '../../private/helloworld') * @param {string} reactNativePath - Path to the React Native package (e.g., '.') * @param {string} appXcodeProject - Name of the Xcode project (e.g., 'HelloWorld.xcodeproj') + * @param {string} targetName - Name of the app target (e.g., 'HelloWorld') */ -async function prepareApp(appPath = '../../private/helloworld', reactNativePath = '.', appXcodeProject = 'HelloWorld.xcodeproj') { +async function prepareApp(appPath = '../../private/helloworld', reactNativePath = '.', appXcodeProject = 'HelloWorld.xcodeproj', targetName = 'HelloWorld') { console.log('šŸš€ Starting app preparation for SwiftPM build from source...'); // Resolve absolute paths @@ -80,8 +82,12 @@ async function prepareApp(appPath = '../../private/helloworld', reactNativePath console.log('\nšŸ”§ Step 7: Fixing REACT_NATIVE_PATH in Xcode project...'); await fixReactNativePath(appIosPath, absoluteReactNativePath, appXcodeProject); - // Step 8: Open Xcode project - console.log('\nšŸ“± Step 8: Opening Xcode project...'); + // Step 8: Integrate SwiftPM packages in Xcode + console.log('\nšŸ“¦ Step 8: Integrating SwiftPM packages in Xcode...'); + await integrateSwiftPMPackages(appIosPath, absoluteReactNativePath, absoluteAppPath, appXcodeProject, targetName); + + // Step 9: Open Xcode project + console.log('\nšŸ“± Step 9: Opening Xcode project...'); await openXcodeProject(appIosPath, appXcodeProject); console.log('\nāœ… App preparation completed successfully!'); @@ -268,6 +274,40 @@ async function fixReactNativePath(appIosPath, reactNativePath, appXcodeProject) } } +/** + * Integrate SwiftPM packages into Xcode project + */ +async function integrateSwiftPMPackages(appIosPath, reactNativePath, appPath, appXcodeProject, targetName) { + try { + console.log('Preparing SwiftPM package integrations...'); + + // Calculate relative paths from appIosPath + const relativeReactNativePath = path.relative(appIosPath, reactNativePath); + const relativeGeneratedPath = path.join('build', 'generated', 'ios'); + + // Create PackageSwift objects + const packageSwiftObjects = [ + { + "relativePath": relativeReactNativePath, + "targets": ["React"] + }, + { + "relativePath": relativeGeneratedPath, + "targets": ["ReactCodegen", "ReactAppDependencyProvider"] + } + ]; + + const xcodeProjectPath = path.join(appIosPath, appXcodeProject); + + // Call integrateSwiftPackagesInXcode function + integrateSwiftPackagesInXcode(xcodeProjectPath, packageSwiftObjects, targetName); + + console.log('āœ“ SwiftPM packages integrated into Xcode project'); + } catch (error) { + throw new Error(`SwiftPM integration failed: ${error.message}`); + } +} + /** * Open Xcode project */ @@ -296,6 +336,7 @@ if (require.main === module) { let appPath = '../../private/helloworld'; let reactNativePath = '.'; let appXcodeProject = 'HelloWorld.xcodeproj'; + let targetName = 'HelloWorld'; if (args.length >= 1) { appPath = args[0]; @@ -309,12 +350,17 @@ if (require.main === module) { appXcodeProject = args[2]; } - console.log('Usage: node prepare-app.js [appPath] [reactNativePath] [appXcodeProject]'); + if (args.length >= 4) { + targetName = args[3]; + } + + console.log('Usage: node prepare-app.js [appPath] [reactNativePath] [appXcodeProject] [targetName]'); console.log(`Using App path: ${appPath}`); console.log(`Using React Native path: ${reactNativePath}`); console.log(`Using App Xcode project: ${appXcodeProject}`); + console.log(`Using Target name: ${targetName}`); - prepareApp(appPath, reactNativePath, appXcodeProject) + prepareApp(appPath, reactNativePath, appXcodeProject, targetName) .then(() => { console.log('\nšŸŽ‰ All done! Your app is ready for SwiftPM build from source.'); process.exit(0); @@ -334,5 +380,6 @@ module.exports = { generateCodegenArtifacts, prepareHeaders, fixReactNativePath, + integrateSwiftPMPackages, openXcodeProject }; diff --git a/packages/react-native/scripts/swiftpm/update-xcodeproject.js b/packages/react-native/scripts/swiftpm/update-xcodeproject.js new file mode 100644 index 00000000000..e5e11ebe846 --- /dev/null +++ b/packages/react-native/scripts/swiftpm/update-xcodeproject.js @@ -0,0 +1,162 @@ +/** + * 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 crypto = require('crypto'); +const {execSync} = require('child_process'); + +/** + * Generate a random string of 24 HEX characters (capital letters) for Xcode object IDs + * @returns {string} A 24-character hexadecimal string in uppercase + */ +function generateXcodeObjectId() { + return crypto.randomBytes(12).toString('hex').toUpperCase(); +} + +/** + * Convert Xcode project.pbxproj file to JSON format + * @param {string} projectPath - Path to the project.pbxproj file + * @returns {Object} Parsed JSON object of the Xcode project + */ +function convertXcodeProjectToJSON(projectPath) { + const command = `plutil -convert json -o - "${projectPath}"`; + const jsonOutput = execSync(command, { encoding: 'utf8' }); + return JSON.parse(jsonOutput); +} + +/** + * Add local SwiftPM package references and product dependencies to Xcode project + * @param {string} relativePath - The relative path of where the Package.swift is located + * @param {Array} productNames - List of product names exposed by the Package.swift files + * @param {Object} xcodeProject - The xcode project converted in JSON format + * @param {string} targetName - The name of the target to add dependencies to + */ +function addLocalSwiftPM(relativePath, productNames, xcodeProject, targetName) { + // For the relative path: create XCLocalSwiftPackageReference + const packageReferenceId = generateXcodeObjectId(); + xcodeProject.objects[packageReferenceId] = { + "isa": "XCLocalSwiftPackageReference", + "relativePath": relativePath + }; + + // Find PBXProject object and update packageReferences + const objects = xcodeProject.objects; + for (const objectId in objects) { + const object = objects[objectId]; + if (object.isa === "PBXProject") { + if (!object.packageReferences) { + object.packageReferences = []; + } + object.packageReferences.push(packageReferenceId); + break; + } + } + + // For each product: create XCSwiftPackageProductDependency and PBXBuildFile + for (const productName of productNames) { + // Generate XcodeID for XCSwiftPackageProductDependency + const productDependencyId = generateXcodeObjectId(); + xcodeProject.objects[productDependencyId] = { + "isa": "XCSwiftPackageProductDependency", + "productName": productName + }; + + // Generate second XcodeID for PBXBuildFile + const buildFileId = generateXcodeObjectId(); + xcodeProject.objects[buildFileId] = { + "isa": "PBXBuildFile", + "productRef": productDependencyId + }; + + // Find PBXNativeTarget with matching name + for (const objectId in objects) { + const object = objects[objectId]; + if (object.isa === "PBXNativeTarget" && object.name === targetName) { + // Iterate over buildPhases to find PBXFrameworksBuildPhase + for (const buildPhaseId of object.buildPhases) { + const buildPhaseObject = objects[buildPhaseId]; + if (buildPhaseObject && buildPhaseObject.isa === "PBXFrameworksBuildPhase") { + // Add buildFileId to the files array + if (!buildPhaseObject.files) { + buildPhaseObject.files = []; + } + buildPhaseObject.files.push(buildFileId); + break; + } + } + break; + } + } + } +} + +/** + * Integrate Swift packages into Xcode project + * @param {string} xcodeProjectPath - Path to the app.xcodeproj file + * @param {Array} packageSwiftObjects - List of PackageSwift objects with relativePath and targets + * @param {string} appTargetName - Name of the app target + */ +function integrateSwiftPackagesInXcode(xcodeProjectPath, packageSwiftObjects, appTargetName) { + const fs = require('fs'); + const path = require('path'); + + // Construct path to project.pbxproj + const projectPbxprojPath = path.join(xcodeProjectPath, 'project.pbxproj'); + + if (!fs.existsSync(projectPbxprojPath)) { + throw new Error(`Project file not found: ${projectPbxprojPath}`); + } + + // Convert to JSON + const xcodeProject = convertXcodeProjectToJSON(projectPbxprojPath); + + // Iterate over PackageSwift objects and execute addLocalSwiftPM + for (const packageSwift of packageSwiftObjects) { + addLocalSwiftPM( + packageSwift.relativePath, + packageSwift.targets, + xcodeProject, + appTargetName + ); + } + + // Write JSON directly to the project.pbxproj file + fs.writeFileSync(projectPbxprojPath, JSON.stringify(xcodeProject)); +} + +// CLI usage +if (require.main === module) { + const args = process.argv.slice(2); + + if (args.length < 3) { + console.log('Usage: node update-xcodeproject.js '); + console.log('Example: node update-xcodeproject.js ./MyApp.xcodeproj MyApp \'[{"relativePath":"../react-native","targets":["ReactCommon","React-Core"]}]\''); + process.exit(1); + } + + const xcodeProjectPath = args[0]; + const appTargetName = args[1]; + const packageSwiftObjectsJSON = args[2]; + + try { + const packageSwiftObjects = JSON.parse(packageSwiftObjectsJSON); + integrateSwiftPackagesInXcode(xcodeProjectPath, packageSwiftObjects, appTargetName); + console.log('āœ… Successfully integrated Swift packages into Xcode project'); + } catch (error) { + console.error('āŒ Error:', error.message); + process.exit(1); + } +} + +module.exports = { + generateXcodeObjectId, + convertXcodeProjectToJSON, + addLocalSwiftPM, + integrateSwiftPackagesInXcode +};