From 69e028da45471dfd1f65bc630a71ec4ece9e57fb Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Mon, 23 Jun 2025 08:13:56 -0700 Subject: [PATCH] Update the xcframework.js script to support swift (#52135) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/52135 Update the xcframework.js script to support Swift ## Context This PR introduces the first working version of building React Native apps on iOS using prebuilt RNCore and cocoapods. - Added React-Core-prebuilt.podspec for installing/consuming XCFrameworks - Added logic in react_native_pods.rb for switching between build from source and using prebuilts - Added rncore.rb - utilities for the ReactCore prebuilts - Updated rndependencies with some extra error handling modelled after rncode.rb - Added support for hard linking headers and modules in each inner framework in the XCFramework in xcframework.js ## Swift: To enable support for the objective-c types from swift, the swift compiler uses a module map to gather exports from the framework (module.modulemap). This file basically points to an umbrella header file that exports the valid objective-c types (non c++) to Swift. In addition these files are read from the DerivedData and not the project source - so it is a bit hard to control everyting. I was initially not able to use cocoapods own module definitions (module_name, module_file props) to use a custom module map. I finally found that these files are expected in the deriveddata (build folder) where only the active inner framework is copied - so then I had to hard link both module map and header files for each arch. ## Changelog: [INTERNAL] - Update the xcframework.js script to support Swift Pull Request resolved: https://github.com/facebook/react-native/pull/52109 Test Plan: Run with RN Tester. We need to remove all extra pods from RNTester pod file since none of them are yet compatible with prebuilt (they reference non-prebuilt pods) Rollback Plan: Reviewed By: cortinico Differential Revision: D76980285 Pulled By: cipolleschi fbshipit-source-id: 4e5486b79c406ba4b375e2ada24cbe5450e2346f --- .../scripts/ios-prebuild/xcframework.js | 120 +++++++++++++++++- 1 file changed, 115 insertions(+), 5 deletions(-) diff --git a/packages/react-native/scripts/ios-prebuild/xcframework.js b/packages/react-native/scripts/ios-prebuild/xcframework.js index ffecf7a74e2..af341175546 100644 --- a/packages/react-native/scripts/ios-prebuild/xcframework.js +++ b/packages/react-native/scripts/ios-prebuild/xcframework.js @@ -69,6 +69,8 @@ const HEADERFILE_IGNORE_LIST = [ 'JsArgumentHelpers-inl.h', 'RCTJscInstance.h', 'RCTJSThreadManager.h', + 'YGEnums.h', + 'YGNode.h', ]; function buildXCFrameworks( @@ -106,8 +108,9 @@ function buildXCFrameworks( } catch (error) { frameworkLog( `Error building XCFramework: ${error.message}. Check if the build was successful.`, - 'warning', + 'error', ); + return; } // Copy header files from the headers folder that we used to build the swift package @@ -124,7 +127,22 @@ function buildXCFrameworks( ); // Create the module map file - createModuleMapFile(outputPath, umbrellaHeaderFile); + const moduleMapFile = createModuleMapFile(outputPath, umbrellaHeaderFile); + if (!moduleMapFile) { + frameworkLog( + 'Failed to create module map file. The XCFramework may not work correctly. Stopping.', + 'error', + ); + return; + } + + // Get the platforms in the framework folder and copy modulemaps and headers into each platform folder + linkArchFolders( + outputPath, + moduleMapFile, + umbrellaHeaderFile, + outputHeaderFiles, + ); // Copy Symbols to symbols folder const symbolPaths = frameworkFolders.map(framework => @@ -144,6 +162,91 @@ function buildXCFrameworks( } } +function linkArchFolders( + outputPath /*:string*/, + moduleMapFile /*:string*/, + umbrellaHeaderFile /*:string*/, + outputHeaderFiles /*: Array */, +) { + frameworkLog('Linking modules and headers to platform folders...'); + const headerRootFolder = path.dirname(umbrellaHeaderFile); + + fs.readdirSync(outputPath) + .filter(folder => { + const folderPath = path.join(outputPath, folder); + return ( + fs.statSync(folderPath).isDirectory() && + folder !== 'Headers' && + folder !== 'Modules' + ); + }) + .forEach(folder => { + // Get full platform folder path + const platformFolder = path.join(outputPath, folder); + // Link the Modules folder into the platform folder + const targetModulesFolder = path.join( + platformFolder, + 'React.Framework', + 'Modules', + ); + createFolderIfNotExists(targetModulesFolder); + try { + fs.linkSync( + moduleMapFile, + path.join(targetModulesFolder, path.basename(moduleMapFile)), + ); + } catch (error) { + frameworkLog( + `Error copying module map file: ${error.message}. Check if the file exists at ${moduleMapFile}.`, + 'error', + ); + } + // Copy headers folder into the platform folder + const targetHeadersFolder = path.join( + platformFolder, + 'React.Framework', + 'Headers', + ); + // Link header files into the platform folder + outputHeaderFiles.forEach(headerFile => { + // Get the relative path of the header file based on the root folder + const relativePath = path.relative(headerRootFolder, headerFile); + // Create the target folder for the header file + const targetFolder = path.join( + targetHeadersFolder, + path.dirname(relativePath), + ); + // Create the target folder if it doesn't exist + createFolderIfNotExists(targetFolder); + // Link the header file to the target folder + try { + fs.linkSync( + headerFile, + path.join(targetFolder, path.basename(headerFile)), + ); + } catch (error) { + frameworkLog( + `Error linking header file: ${error.message}. Check if the file exists.`, + 'error', + ); + } + }); + // Link the umbrella header file to the target headers folder + try { + const targetUmbrellaPath = path.join( + targetHeadersFolder, + 'React-umbrella.h', + ); + fs.linkSync(umbrellaHeaderFile, targetUmbrellaPath); + } catch (error) { + frameworkLog( + `Error linking umbrella header file: ${error.message}. Check if the file exists.`, + 'error', + ); + } + }); +} + function copyHeaderFiles( headersSourceFolder /*: string */, outputPath /*: string */, @@ -249,7 +352,11 @@ function createUmbrellaHeaderFile( return umbrellaHeaderFile; } -const cppHeaderRegex = /(#include|#import)\s*<[^.>]+>|\bnamespace\s+[\w:]+::/; +// This regex matches some C++ construct that might be present in a header file. +// To uniquely identify them. We need to exclude headers with C++ constructs from the module map +// otherwise Swift won't be able to import the React.xcframework +const cppHeaderRegex = + /(#include|#import)\s*<[^.>]+>|\bnamespace\s+[\w:]+::|NS_ENUM\s*\([^)]*\)|NS_OPTIONS\s*\([^)]*\)|typedef\s+enum|static\s+const|@interface|static\s+inline/; function isCppHeaderFile(headerFilePath /*: string */) /*: boolean */ { // Check if there is a cpp or mm file with the same name @@ -299,21 +406,24 @@ function createModuleMapFile( // Create the module map file const moduleMapFile = path.join(moduleMapFolder, 'module.modulemap'); frameworkLog('Creating module map file: ' + moduleMapFile); - const moduleMapContent = `module React { - umbrella header "../Headers/${path.basename(umbrellaPath)}" + const moduleMapContent = `framework module React { + umbrella header "${path.basename(umbrellaPath)}" export * module * { export * } }`; try { fs.writeFileSync(moduleMapFile, moduleMapContent); + return moduleMapFile; } catch (error) { frameworkLog( `Error creating module map file: ${error.message}. Check if the file exists.`, 'warning', ); + return null; } } + function cleanPlatformFolders(outputPath /*:string*/) { if (!fs.existsSync(outputPath)) { return;