From d4e57d5243fd6928257a223fdab4c99cda637a95 Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Mon, 26 May 2025 10:01:11 +0200 Subject: [PATCH] [ios][prebuild] added building xcframework This commit adds support for building XCFrameworks from the prebuild-script - Added build script to help build xcframeworks - Added full header file structure - Added calling the build script from the main prebuild script TODO: We need to add resources / privacy bundles --- packages/react-native/scripts/ios-prebuild.js | 54 ++- .../scripts/ios-prebuild/xcframework.js | 321 ++++++++++++++++++ 2 files changed, 372 insertions(+), 3 deletions(-) create mode 100644 packages/react-native/scripts/ios-prebuild/xcframework.js diff --git a/packages/react-native/scripts/ios-prebuild.js b/packages/react-native/scripts/ios-prebuild.js index 01833a2bf3f..d9f67f4efd8 100644 --- a/packages/react-native/scripts/ios-prebuild.js +++ b/packages/react-native/scripts/ios-prebuild.js @@ -18,6 +18,7 @@ const { createLogger, throwIfOnEden, } = require('./ios-prebuild/utils'); +const {buildXCFrameworks} = require('./ios-prebuild/xcframework'); const {execSync} = require('child_process'); const fs = require('fs'); const path = require('path'); @@ -55,9 +56,9 @@ async function main() { * in the source path, it creates a link in the target path with an * underscore prefix. */ - const link = (fromPath /*:string*/, includePath /*:string*/) => { + const link = (fromPath /*:string*/, includePath /*:?string*/) => { const source = path.resolve(root, fromPath); - const target = path.resolve(linksFolder, includePath); + const target = path.resolve(linksFolder, includePath ?? fromPath); createFolderIfNotExists(target); @@ -96,12 +97,16 @@ async function main() { } if (linkedFiles > 0) { - prebuildLog(`Linking ${source} to ${target}...`); + prebuildLog( + `Linked ${path.relative(root, source)} → ${path.relative(root, target)}`, + ); } const subfolders = entries .filter(dirent => dirent.isDirectory()) .filter(dirent => dirent.name !== '__tests__') + .filter(dirent => dirent.name !== 'tests') + .filter(dirent => dirent.name !== 'platform') .map(dirent => dirent.name); // Create links for subfolders @@ -154,6 +159,8 @@ async function main() { link('React/Views/ScrollView', 'React'); link('React/Views/RefreshControl', 'React'); link('Libraries/Text', 'React'); + link('Libraries/AppDelegate'); + link('ReactApple/Libraries/RCTFoundation/RCTDeprecation/Exported', 'React'); link( 'ReactApple/Libraries/RCTFoundation/RCTDeprecation/Exported', 'RCTDeprecation', @@ -168,6 +175,10 @@ async function main() { link('Libraries/LinkingIOS', 'React'); link('Libraries/Settings', 'React'); + link('Libraries/PushNotificationIOS', 'React'); + link('Libraries/Settings', 'React'); + link('Libraries/Vibration', 'React'); + link('ReactCommon/hermes', 'reacthermes'); link('ReactCommon/hermes', 'jsireact'); @@ -175,10 +186,47 @@ async function main() { 'ReactCommon/react/renderer/imagemanager', 'react/renderer/imagemanager', ); + link('ReactCommon/yoga/Yoga', 'ReactCommon/yoga/Yoga'); + link('ReactCommon/callinvoker', 'ReactCommon'); + link('ReactCommon/react/renderer/componentregistry'); + link('ReactCommon/react/renderer/core'); + link('ReactCommon/react/bridging'); + link('ReactCommon/react/timing'); + link('ReactCommon/react/utils'); + link('ReactCommon/react/debug'); + link('ReactCommon/react/renderer/debug'); + link('ReactCommon/react/featureflags'); + link('ReactCommon/react/renderer/graphics'); + link( + 'ReactCommon/react/renderer/graphics/platform/ios', + 'ReactCommon/react/renderer/graphics', + ); + link('ReactCommon/react/nativemodule/core', 'ReactCommon'); + link('ReactCommon/react/nativemodule/core/platform/ios', 'ReactCommon'); + + link('ReactCommon/react/utils/platform/ios', 'ReactCommon/react/utils'); + link('ReactCommon/react/runtime'); + link('ReactCommon/react/runtime/platform/ios', 'ReactCommon/react/runtime'); + link('ReactCommon/jsitooling/react/runtime', 'ReactCommon/react/runtime'); + link('ReactCommon/react/renderer/components/legacyviewmanagerinterop'); + link('ReactCommon/react/renderer/components/view'); + link( + 'ReactCommon/react/renderer/components/view/platform/cxx', + 'ReactCommon/react/renderer/components/view', + ); + link('ReactCommon/react/renderer/mounting'); + link('ReactCommon/react/renderer/attributedstring'); + link('ReactCommon/runtimeexecutor/ReactCommon', 'ReactCommon'); + link('ReactCommon/jsinspector-modern'); + link('ReactCommon/cxxreact'); + + link('.build/codegen/build/generated/ios', 'ReactCodegen'); // BUILD SWIFT PACKAGE const frameworkPaths = buildSwiftPackage(root, buildFolder, buildType); + // GENERATE XCFrameworks + buildXCFrameworks(root, buildFolder, frameworkPaths, buildType); // Done! prebuildLog('🏁 Done!'); diff --git a/packages/react-native/scripts/ios-prebuild/xcframework.js b/packages/react-native/scripts/ios-prebuild/xcframework.js new file mode 100644 index 00000000000..90985d8a489 --- /dev/null +++ b/packages/react-native/scripts/ios-prebuild/xcframework.js @@ -0,0 +1,321 @@ +/** + * 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 + * @format + */ + +const {createFolderIfNotExists, createLogger} = require('./utils'); +const {execSync} = require('child_process'); +const fs = require('fs'); +const glob = require('glob'); +const path = require('path'); + +const frameworkLog = createLogger('XCFramework'); + +const HEADERFILE_IGNORE_LIST = [ + 'RCTCxxMethod.h', + 'RCTComponentViewClassDescriptor.h', + 'RCTActivityIndicatorViewManager.h', + 'RCTComponentViewDescriptor.h', + 'RCTComponentViewFactory.h', + 'RCTComponentViewProtocol.h', + 'RCTComponentViewRegistry.h', + 'RCTConstants.h', + 'RCTConversions.h', + 'RCTConvert+CoreLocation.h', + 'RCTConvert+Text.h', + 'RCTConvert+Transform.h', + 'RCTFabricComponentsPlugins.h', + 'RCTFabricModalHostViewController.h', + 'RCTFabricSurface.h', + 'RCTFollyConvert.h', + 'RCTLegacyViewManagerInteropCoordinatorAdapter.h', + 'RCTLinearGradient.h', + 'RCTRadialGradient.h', + 'RCTMountingManager.h', + 'RCTMountingTransactionObserving.h', + 'RCTParagraphComponentAccessibilityProvider.h', + 'RuntimeExecutor.h', + 'RCTSurfacePresenter.h', + 'RCTSurfacePresenterBridgeAdapter.h', + '*View.h', + '*ViewProtocol.h', + '*ComponentView.h', + 'RCTViewAccessibilityElement.h', + 'RCTImageManager*.h', + 'RCTImagePrimitivesConversions.h', + 'RCTSyncImageManager.h', + 'CallbackWrapper.h', + 'LongLivedObject.h', + 'Bridging.h', + 'RCTLegacyViewManagerInteropCoordinator.h', + 'conversions.h', + 'Float.h', + 'Geometry.h', + 'ObjCTimerRegistry.h', + 'RCTContextContainerHandling.h', + 'RCTHost*.h', + 'ReactCdp.h', + 'RCTInstance.h', + 'JsArgumentHelpers-inl.h', + 'RCTJscInstance.h', + 'RCTJSThreadManager.h', +]; + +function buildXCFrameworks( + rootFolder /*: string */, + buildFolder /*: string */, + frameworkFolders /*: Array */, + buildType /*: 'debug' | 'release' */, +) /*: Array */ { + const outputPath = path.join( + buildFolder, + 'output', + 'xcframeworks', + buildType, + 'React.xcframework', + ); + // Delete all target platform folders (everything but the Headers and Modules folders) + cleanPlatformFolders(outputPath); + + // Build the XCFrameworks by using each framework folder as input + const frameworks = frameworkFolders.map(frameworkFolder => { + const buildCommand = `xcodebuild -create-xcframework -framework "${frameworkFolder}" -output ${outputPath} -allow-internal-distribution`; + frameworkLog(`Building ${frameworkFolder}...`); + + frameworkLog(buildCommand); + try { + execSync(buildCommand, { + cwd: rootFolder, + stdio: 'inherit', + }); + } catch (error) { + frameworkLog( + `Error building XCFramework: ${error.message}. Check if the build was successful.`, + 'warning', + ); + } + + return outputPath; + }); + + // Copy header files from the headers folder that we used to build the swift package + const outputHeaderFiles = copyHeaderFiles( + path.join(buildFolder, 'Headers'), + outputPath, + true, + ); + + // Create the umbrella header file + const umbrellaHeaderFile = createUmbrellaHeaderFile( + outputPath, + outputHeaderFiles, + ); + + // Create the module map file + createModuleMapFile(outputPath, umbrellaHeaderFile); + + return frameworks; +} + +function copyHeaderFiles( + headersSourceFolder /*: string */, + outputPath /*: string */, + cleanOutputPath /*: boolean */ = false, +) { + // Re-create headers folder in the output XCFramework + const headersTargetFolder = path.join(outputPath, 'Headers'); + if (cleanOutputPath && fs.existsSync(headersTargetFolder)) { + // Delete the headers folder if it exists + try { + fs.rmSync(headersTargetFolder, {recursive: true, force: true}); + } catch (error) { + frameworkLog( + `Error deleting headers folder: ${error.message}. Check if the folder exists.`, + 'warning', + ); + } + } + createFolderIfNotExists(headersTargetFolder); + + // Now we can copy headers to the headers folder. We need to create the same folder structure as in the + // header files inside the Headers folder: + frameworkLog('Copying header files to: ' + headersTargetFolder); + const headerFiles = glob.sync('**/*.{h,hpp}', { + cwd: headersSourceFolder, + absolute: true, + }); + + const outputHeaderFiles /*: Array */ = []; + headerFiles.forEach(headerFile => { + // The headerFile is a full path to a header file. We need to get the relative path based on the + // rootpath parameter. + const relativePath = path.relative(headersSourceFolder, headerFile); + const targetFolder = path.join( + headersTargetFolder, + path.dirname(relativePath), + ); + const targetFile = path.join(headersTargetFolder, relativePath); + createFolderIfNotExists(targetFolder); + try { + // Check if the file contains c++ code + fs.copyFileSync(headerFile, targetFile); + outputHeaderFiles.push(targetFile); + } catch (error) { + frameworkLog( + `Error copying header file: ${error.message}. Check if the file exists.`, + 'warning', + ); + } + }); + + return outputHeaderFiles; +} + +function createUmbrellaHeaderFile( + outputPath /*: string */, + headerFiles /*: Array */, +) /*: string */ { + // Create the umbrella header file + const umbrellaHeaderPath = path.join(outputPath, 'Headers'); + const umbrellaHeaderFile = path.join(umbrellaHeaderPath, 'React-umbrella.h'); + + frameworkLog('Creating umbrella header file: ' + umbrellaHeaderFile); + + // Create the umbrella header file + let umbrellaHeader = ` +// Generated by React Native +// Do not edit this file directly. It is generated by the React Native build process. + +#ifdef __OBJC__ +#import +#else +#ifndef FOUNDATION_EXPORT +#if defined(__cplusplus) +#define FOUNDATION_EXPORT extern "C" +#else +#define FOUNDATION_EXPORT extern +#endif +#endif +#endif + +`; + headerFiles.forEach(headerFile => { + if (!isCppHeaderFile(headerFile) && !isHeaderFileIgnored(headerFile)) { + // The headerFile is a full path to a header file. We need to get the relative path based on the + // rootpath parameter. + const relativePath = path.relative(umbrellaHeaderPath, headerFile); + umbrellaHeader += `#import "${relativePath}"\n`; + } + }); + umbrellaHeader += '\n'; + + // Write out the umbrella header file + try { + fs.writeFileSync(umbrellaHeaderFile, umbrellaHeader); + } catch (error) { + frameworkLog( + `Error creating umbrella header file: ${error.message}. Check if the file exists.`, + 'warning', + ); + } + + return umbrellaHeaderFile; +} + +//const cppHeaderRegex = /(#include|#import)\s*<[^>]+>|namespace\s+[\w:]+::/; +const cppHeaderRegex = /(#include|#import)\s*<[^.>]+>|\bnamespace\s+[\w:]+::/; + +function isCppHeaderFile(filePath /*: string */) /*: boolean */ { + // Check if there is a cpp or mm file with the same name + const fileName = path.basename(filePath, path.extname(filePath)); + const cppFilePath = path.join( + path.dirname(filePath), + fileName + (fileName.endsWith('mm') ? '.cpp' : '.mm'), + ); + if (fs.existsSync(cppFilePath)) { + // Check if the file is a C++ file + const fileStat = fs.statSync(cppFilePath); + if (fileStat.isFile()) { + return true; + } + } + // Check if the file contains c++ code + const fileContent = fs.readFileSync(filePath, 'utf8'); + return cppHeaderRegex.test(fileContent); +} + +function isHeaderFileIgnored(headerFile /*: string */) /*: boolean */ { + // Check if the header file is in the ignore list + return HEADERFILE_IGNORE_LIST.some(pattern => + // Glob match the header file name to the ignore list + glob + .sync(pattern, { + cwd: path.dirname(headerFile), + absolute: true, + }) + .some(ignoredHeaderFile => { + return path.basename(headerFile) === path.basename(ignoredHeaderFile); + }), + ); +} + +function createModuleMapFile( + outputPath /*: string */, + umbrellaPath /*: string */, +) { + // Create/get the module map folder + const moduleMapFolder = path.join(outputPath, 'Modules'); + createFolderIfNotExists(moduleMapFolder); + + // 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)}" + export * + module * { export * } +}`; + + try { + fs.writeFileSync(moduleMapFile, moduleMapContent); + } catch (error) { + frameworkLog( + `Error creating module map file: ${error.message}. Check if the file exists.`, + 'warning', + ); + } +} +function cleanPlatformFolders(outputPath /*:string*/) { + if (!fs.existsSync(outputPath)) { + return; + } + const targetPlatformFolders = fs.readdirSync(outputPath).filter(folder => { + const folderPath = path.join(outputPath, folder); + return ( + fs.statSync(folderPath).isDirectory() && + folder !== 'Headers' && + folder !== 'Modules' + ); + }); + targetPlatformFolders.forEach(folder => { + const folderPath = path.join(outputPath, folder); + frameworkLog('Deleting folder: ' + folderPath); + try { + fs.rmSync(folderPath, {recursive: true, force: true}); + } catch (error) { + frameworkLog( + `Error deleting folder: ${folderPath}. Check if the folder exists.`, + 'warning', + ); + } + }); +} + +module.exports = { + buildXCFrameworks, +};