From 7aa5df32a4be708697271b0974b252c97146703e Mon Sep 17 00:00:00 2001 From: Christian Falch Date: Wed, 26 Feb 2025 08:19:39 -0800 Subject: [PATCH] Refactor `setupDependencies` script (#49670) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/49670 This change refactors the script to prebuild ios dependencies by factoring out the code that setups native dependencies. bypass-github-export-checks ## Changelog: [INTERNAL] - Factor out code to setup native dependencies Test Plan: Tested in this `AppDelegate.mm`: Imports: ```obj-c #import "AppDelegate.h" #import #import #include #include #include #include #import ``` Code: ```obj-c - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { std::string input = "3.1416 xyz "; double_conversion::DoubleToStringConverter::EcmaScriptConverter(); LOG(INFO) << "Hello from GLOG"; fmt::print("Hello, world from FMT!\n"); BOOST_ASSERT(100 == 100); double result; fast_float::from_chars(input.data(), input.data() + input.size(), result); LOG(INFO) << "Answer :" << result; NSArray *frameworks = [NSBundle allFrameworks]; for (NSBundle *framework in frameworks) { NSString *frameworkName = framework.bundleURL.lastPathComponent; if ([frameworkName isEqualToString: @"ReactNativeDependencies.framework"]) { NSBundle *bundle = [NSBundle bundleWithURL:[framework bundleURL]]; NSURL *bundleURL = [bundle URLForResource:@"ReactNativeDependencies_glog" withExtension:@"bundle"]; NSBundle *resourceBundle = [NSBundle bundleWithURL:bundleURL]; NSURL* url = [resourceBundle URLForResource:@"PrivacyInfo" withExtension:@"xcprivacy"]; if (url == nil) { throw [NSException exceptionWithName:@"ResourceNotFoundException" reason:@"Could not find PrivacyInfo.xcprivacy in ReactNativeDependencies_glog bundle" userInfo:nil]; } break; } } return YES; } ``` Reviewed By: cortinico Differential Revision: D70175729 Pulled By: cipolleschi fbshipit-source-id: 4e1c19b5482b4335ab66c2b605abcb9db47091d3 --- .../releases/prebuild/setupDependencies.js | 250 ++++++++++++++++++ 1 file changed, 250 insertions(+) create mode 100644 scripts/releases/prebuild/setupDependencies.js diff --git a/scripts/releases/prebuild/setupDependencies.js b/scripts/releases/prebuild/setupDependencies.js new file mode 100644 index 00000000000..b3eee2fd379 --- /dev/null +++ b/scripts/releases/prebuild/setupDependencies.js @@ -0,0 +1,250 @@ +/** + * 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 + * @oncall react_native + */ + +const { + HEADERS_FOLDER, + RESOURCES_FOLDER, + SCRIPTS_FOLDER, + SOURCE_FOLDER, + TARGET_FOLDER, +} = require('./constants'); +const {execSync} = require('child_process'); +const fs = require('fs'); +const glob = require('glob'); +const path = require('path'); +const util = require('util'); + +/*:: +import type {Dependency} from './types'; +*/ + +const exec = util.promisify(require('child_process').exec); + +/** + * This function downloads, prepares and creates the structure for the dependencies + * according to the configuration provided. + */ +async function setupDependencies( + dependencies /*: $ReadOnlyArray */, + rootFolder /*: string */, +) { + console.log('✅ Setting up dependencies...'); + + // Setup dependencies + await Promise.all(dependencies.map(dep => setupDepenency(dep, rootFolder))); +} + +/** + * Sets up a single dependency. This function will download the dependency, run the + * prepare script (if any), create the build structure and copy the headers and any resources + */ +async function setupDepenency( + dependency /*: Dependency */, + rootFolder /*: string */, +) { + await downloadDependency(dependency, rootFolder); + await runPrepareDependencyScript(dependency, rootFolder); + await createBuildStructure(dependency, rootFolder); + await createHeaderStructure(dependency, rootFolder); + await copyResources(dependency, rootFolder); +} + +/** + * Downloads a dependency from a URL and extracts it to a destination folder. + */ +async function downloadDependency( + dependency /*: Dependency */, + rootFolder /*: string */, +) { + const filename = `${dependency.name}.tgz`; + const archiveDestination = `/tmp/${filename}`; + const command = `curl -L ${dependency.url.toString()} --output ${archiveDestination}`; + console.log(`Downloading ${filename}...`); + await exec(command); + + const targetFolder = path.join(rootFolder, dependency.name, SOURCE_FOLDER); + console.log(`Building target folder ${targetFolder}...`); + fs.mkdirSync(targetFolder, {recursive: true}); + console.log(`Extracting ${filename} to ${targetFolder}...`); + await exec( + `tar -xzf ${archiveDestination} -C ${targetFolder} --strip-components 1`, + ); + console.log(`Cleaning up ${filename}...`); + await exec(`rm ${archiveDestination}`); +} + +/** + * Runs a prepare script for a dependency if it exists + */ +async function runPrepareDependencyScript( + dependency /*: Dependency */, + rootFolder /*: string */, +) { + if (dependency.prepareScript == null) { + return; + } + + // Folder for the dependency source + const sourceFolder = path.join(rootFolder, dependency.name, SOURCE_FOLDER); + + // Check if the prepare script is a file or a command + if (dependency.prepareScript && !fs.existsSync(dependency.prepareScript)) { + console.log(`Running prepare script for ${dependency.name}...`); + dependency.prepareScript && + execSync(dependency.prepareScript, {cwd: sourceFolder, stdio: 'inherit'}); + return; + } + + // Target folder for the prepare script + const targetFolder = path.join(rootFolder, dependency.name, SCRIPTS_FOLDER); + fs.mkdirSync(targetFolder, {recursive: true}); + + const finalPath = path.join(targetFolder, 'prepare.sh'); + console.log( + `Running prepare script for ${dependency.name} in ${finalPath}...`, + ); + + if (dependency.prepareScript) { + fs.copyFileSync(dependency.prepareScript, finalPath); + execSync(`${targetFolder}/prepare.sh`, { + cwd: sourceFolder, + stdio: 'inherit', + }); + } +} + +/** + * Creates the structure for the build + */ +async function createBuildStructure( + dependency /*: Dependency */, + rootFolder /*: string */, +) { + const targetFolder = path.join(rootFolder, dependency.name, TARGET_FOLDER); + const sourceFolder = path.join(rootFolder, dependency.name, SOURCE_FOLDER); + console.log( + 'Creating build structure for dependency', + dependency.name, + 'in', + targetFolder, + 'from', + sourceFolder, + ); + fs.mkdirSync(targetFolder, {recursive: true}); + + // Now let's use glob to get the final list of files + const sources = dependency.files.sources; + sources.forEach(source => { + const sourceFiles = glob.sync(source, {cwd: sourceFolder}); + sourceFiles.forEach(sourceFile => { + const sourcePath = path.join(sourceFolder, sourceFile); + const targetPath = path.join(targetFolder, sourceFile); + // Create the folder structure + const targetDir = path.dirname(targetPath); + fs.mkdirSync(targetDir, {recursive: true}); + // Copy the file + fs.copyFileSync(sourcePath, targetPath); + }); + }); +} + +/** + * Creates the correct header structure for the dependency. This structure will be copied + * into the final frameworks under the Headers folder. + */ +async function createHeaderStructure( + dependency /* :Dependency */, + rootFolder /*: string */, +) { + // Get the target for header files if necessary + const headerTarget = dependency.files.headerTargetFolder ?? ''; + const targetFolder = path.join( + rootFolder, + dependency.name, + TARGET_FOLDER, + HEADERS_FOLDER, + headerTarget, + ); + const sourceFolder = path.join(rootFolder, dependency.name, SOURCE_FOLDER); + console.log('Copying header files for dependency', dependency.name); + + fs.mkdirSync(targetFolder, {recursive: true}); + + // Now let's use glob to get the final list of files + const headers = dependency.files.headers; + headers.forEach(source => { + const sourceFiles = glob.sync(source, {cwd: sourceFolder}); + sourceFiles.forEach(sourceFile => { + // get source path + const sourcePath = path.join(sourceFolder, sourceFile); + // get source path relative to the source folder and remove + // the headerSkipFolderNames if it exists in the dependency + const resolvedSourcePath = path + .relative(sourceFolder, sourcePath) + .replace(dependency.files.headerSkipFolderNames ?? '', ''); + + const targetPath = path.join(targetFolder, resolvedSourcePath); + + // Create the folder structure if necessary - each sourceFile is + // expanded using glob, so we have no guarantee that the target + // folder exists after the first sourceFile has been processed. + const targetDir = path.dirname(targetPath); + if (!fs.existsSync(targetDir)) { + fs.mkdirSync(targetDir, {recursive: true}); + } + // Copy the file + fs.copyFileSync(sourcePath, targetPath); + }); + }); +} + +/** + * Copies the resources for the dependency into the final framework. A resource is a file + * that should be bundled with the final framewwork. + */ +async function copyResources( + dependency /* :Dependency */, + rootFolder /*: string */, +) { + const resources = dependency.files.resources ?? []; + if (resources.length === 0) { + return; + } + + console.log( + `Copying ${resources.length} resources for dependency`, + dependency.name, + ); + const targetFolder = path.join( + rootFolder, + dependency.name, + TARGET_FOLDER, + RESOURCES_FOLDER, + ); + if (!fs.existsSync(targetFolder)) { + fs.mkdirSync(targetFolder, {recursive: true}); + } + + // Copy all resources + resources.forEach(source => { + const sourceFiles = glob.sync(source, {cwd: rootFolder}); + sourceFiles.forEach(sourceFile => { + const sourcePath = path.resolve(rootFolder, sourceFile); + const targetPath = path.join(targetFolder, path.basename(sourceFile)); + // Copy the file + fs.copyFileSync(sourcePath, targetPath); + }); + }); +} + +module.exports = { + setupDependencies, +};