[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
This commit is contained in:
Christian Falch
2025-05-26 10:01:11 +02:00
parent 3a59941a73
commit d4e57d5243
2 changed files with 372 additions and 3 deletions
+51 -3
View File
@@ -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!');
@@ -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<string> */,
buildType /*: 'debug' | 'release' */,
) /*: Array<string> */ {
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<string> */ = [];
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> */,
) /*: 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 <UIKit/UIKit.h>
#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,
};