Compare commits

...
Author SHA1 Message Date
Christian Falch 08155e12cf codereview: removed unused import 2025-05-27 11:48:21 +02:00
Christian Falch 03e91819d6 codereview: fixed bug in c++ detection code 2025-05-27 11:47:34 +02:00
Christian Falch d4e57d5243 [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
2025-05-26 10:01:11 +02:00
Christian Falch 3a59941a73 [ios][prebuild] build swift package from prebuild script
This commit adds building the swift package from the prebuild script:

- Added swiftpackage.js for building the swift package
- Added calling building from the main script
- Added configurable build type from the main script.
- Removed params in jsdoc from the link method
2025-05-26 09:58:08 +02:00
3 changed files with 471 additions and 10 deletions
+70 -10
View File
@@ -12,11 +12,13 @@ const {prepareHermesArtifactsAsync} = require('./ios-prebuild/hermes');
const {
prepareReactNativeDependenciesArtifactsAsync,
} = require('./ios-prebuild/reactNativeDependencies');
const {buildSwiftPackage} = require('./ios-prebuild/swiftpackage');
const {
createFolderIfNotExists,
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');
@@ -53,14 +55,10 @@ async function main() {
* Creates a hard link from one path to another. For each subfolder
* in the source path, it creates a link in the target path with an
* underscore prefix.
* @param {string} fromPath - The path to the source file or directory
* @param {string} includePath - Path in the headers folder to create the link
* @throws {Error} If the source path does not exist or if the link creation fails
* @returns {void}
*/
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);
@@ -99,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
@@ -113,10 +115,21 @@ async function main() {
});
};
// HERMES ARTIFACTS
await prepareHermesArtifactsAsync(currentVersion, 'debug');
// BUILD TYPE
const buildType = process.env.BUILD_TYPE ?? 'debug';
if (buildType !== 'debug' && buildType !== 'release') {
throw new Error(
`Invalid build type: ${buildType}. Must be either "debug" or "release".`,
);
}
await prepareReactNativeDependenciesArtifactsAsync(currentVersion, 'debug');
// HERMES ARTIFACTS
await prepareHermesArtifactsAsync(currentVersion, buildType);
await prepareReactNativeDependenciesArtifactsAsync(
currentVersion,
buildType,
);
// CODEGEN
const codegenPath = path.join(root, '.build/codegen');
@@ -146,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',
@@ -160,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');
@@ -167,6 +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,77 @@
/**
* 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 {createLogger} = require('./utils');
const {execSync} = require('child_process');
const fs = require('fs');
const glob = require('glob');
const path = require('path');
const buildLog = createLogger('SPM');
function buildSwiftPackage(
rootFolder /*: string */,
buildFolder /*: string */,
buildType /*: 'debug' | 'release' */,
) /*: Array<string> */ {
const outputFolder = path.join(buildFolder, 'output', 'spm', buildType);
const buildPlatform = (platform /*:string*/) => {
const buildCommand =
`xcodebuild -scheme React -destination "generic/platform=${platform}" -derivedDataPath "${outputFolder}" ` +
`-configuration "${buildType}" SKIP_INSTALL=NO BUILD_LIBRARY_FOR_DISTRIBUTION=YES OTHER_SWIFT_FLAGS="-no-verify-emitted-module-interface"`;
buildLog('Building Swift package for ' + 'debug');
buildLog(buildCommand);
execSync(buildCommand, {
cwd: rootFolder,
stdio: 'inherit',
});
};
// TODO: Extend with platforms/targets
buildPlatform('iOS simulator');
buildPlatform('iOS');
// Use glob to find all the frameworks in the output folder
const productsFolder = path.join(outputFolder, 'Build/Products');
if (!fs.existsSync(productsFolder)) {
throw new Error(
`Output folder does not exist: ${productsFolder}. Did the build fail?`,
);
}
// The frameworks are in the products folder under a platform/buildType folder and are directories ending with .framework
const frameworks = glob.sync('**/*.framework', {
cwd: productsFolder,
absolute: true,
});
if (frameworks.length === 0) {
throw new Error(
`No frameworks found in the output folder: ${productsFolder}`,
);
}
const frameworkPaths = frameworks.filter(p => p.endsWith('React.framework'));
if (frameworkPaths.length === 0) {
throw new Error(
`No React.framework found in the output folder: ${productsFolder}`,
);
}
buildLog('React.frameworks:');
frameworkPaths.forEach(p => {
buildLog(' ' + p);
});
return frameworkPaths;
}
module.exports = {
buildSwiftPackage,
};
@@ -0,0 +1,324 @@
/**
* 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(headerFilePath /*: string */) /*: boolean */ {
// Check if there is a cpp or mm file with the same name
const fileName = path.basename(headerFilePath, path.extname(headerFilePath));
const dirName = path.dirname(headerFilePath);
const checkFileExists = (extension /*: string */) /*: boolean */ => {
const cppFilePath = path.join(dirName, fileName + extension);
if (fs.existsSync(cppFilePath)) {
const fileStat = fs.statSync(cppFilePath);
return fileStat.isFile();
}
return false;
};
if (checkFileExists('.cpp') || checkFileExists('.mm')) {
// If there is a cpp or mm file with the same name, we assume it is a C++ header file
return true;
}
// Check if the file contains c++ code
const fileContent = fs.readFileSync(headerFilePath, '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,
};