1import spawnAsync from '@expo/spawn-async'; 2import assert from 'assert'; 3import chalk from 'chalk'; 4import { PromisyClass, TaskQueue } from 'cwait'; 5import fs from 'fs-extra'; 6import glob from 'glob-promise'; 7import inquirer from 'inquirer'; 8import path from 'path'; 9import semver from 'semver'; 10 11import { runReactNativeCodegenAsync } from '../../Codegen'; 12import { 13 EXPO_DIR, 14 IOS_DIR, 15 REACT_NATIVE_SUBMODULE_DIR, 16 REACT_NATIVE_SUBMODULE_MONOREPO_ROOT, 17 VERSIONED_RN_IOS_DIR, 18} from '../../Constants'; 19import logger from '../../Logger'; 20import { getListOfPackagesAsync, Package } from '../../Packages'; 21import { copyFileWithTransformsAsync } from '../../Transforms'; 22import type { FileTransforms, StringTransform } from '../../Transforms.types'; 23import { renderExpoKitPodspecAsync } from '../../dynamic-macros/IosMacrosGenerator'; 24import { runTransformPipelineAsync } from './transforms'; 25import { injectMacros } from './transforms/injectMacros'; 26import { kernelFilesTransforms } from './transforms/kernelFilesTransforms'; 27import { podspecTransforms } from './transforms/podspecTransforms'; 28import { postTransforms } from './transforms/postTransforms'; 29import { getVersionedDirectory, getVersionedExpoKitPath } from './utils'; 30import { versionExpoModulesAsync } from './versionExpoModules'; 31import { 32 MODULES_PROVIDER_POD_NAME, 33 versionExpoModulesProviderAsync, 34} from './versionExpoModulesProvider'; 35import { createVersionedHermesTarball } from './versionHermes'; 36import { 37 versionVendoredModulesAsync, 38 removeVersionedVendoredModulesAsync, 39} from './versionVendoredModules'; 40 41export { versionVendoredModulesAsync, versionExpoModulesAsync }; 42 43const UNVERSIONED_PLACEHOLDER = '__UNVERSIONED__'; 44const RELATIVE_RN_PATH = path.relative(EXPO_DIR, REACT_NATIVE_SUBMODULE_DIR); 45 46const EXTERNAL_REACT_ABI_DEPENDENCIES = [ 47 'Analytics', 48 'AppAuth', 49 'FBAudienceNetwork', 50 'FBSDKCoreKit', 51 'GoogleSignIn', 52 'GoogleMaps', 53 'Google-Maps-iOS-Utils', 54 'lottie-ios', 55 'JKBigInteger', 56 'Branch', 57 'Google-Mobile-Ads-SDK', 58 'RCT-Folly', 59]; 60 61const EXCLUDED_POD_DEPENDENCIES = ['ExpoModulesTestCore']; 62 63/** 64 * Transform and rename the given react native source code files. 65 * @param filenames list of files to transform 66 * @param versionPrefix A version-specific prefix to apply to all symbols in the code, e.g. 67 * RCTSomeClass becomes {versionPrefix}RCTSomeClass 68 * @param versionedPodNames mapping from unversioned cocoapods names to versioned cocoapods names, 69 * e.g. React -> ReactABI99_0_0 70 */ 71async function namespaceReactNativeFilesAsync(filenames, versionPrefix, versionedPodNames) { 72 const reactPodName = versionedPodNames.React; 73 const transformRules = _getReactNativeTransformRules(versionPrefix, reactPodName); 74 const taskQueue = new TaskQueue(Promise as PromisyClass, 4); // Transform up to 4 files simultaneously. 75 const transformRulesCache = {}; 76 77 const transformSingleFile = taskQueue.wrap(async (filename) => { 78 if (_isDirectory(filename)) { 79 return; 80 } 81 // protect contents of EX_UNVERSIONED macro 82 const unversionedCaptures: string[] = []; 83 await _transformFileContentsAsync(filename, (fileString) => { 84 const pattern = /EX_UNVERSIONED\((.*?)\)/g; 85 let match = pattern.exec(fileString); 86 while (match != null) { 87 unversionedCaptures.push(match[1]); 88 match = pattern.exec(fileString); 89 } 90 if (unversionedCaptures.length) { 91 return fileString.replace(pattern, UNVERSIONED_PLACEHOLDER); 92 } 93 return null; 94 }); 95 96 // rename file 97 const dirname = path.dirname(filename); 98 const basename = path.basename(filename); 99 const versionedBasename = !basename.startsWith(versionPrefix) 100 ? `${versionPrefix}${basename}` 101 : basename; 102 const targetPath = path.join(dirname, versionedBasename); 103 104 // filter transformRules to patterns which apply to this dirname 105 const filteredTransformRules = 106 transformRulesCache[dirname] || _getTransformRulesForDirname(transformRules, dirname); 107 transformRulesCache[dirname] = filteredTransformRules; 108 109 // Perform sed find & replace. 110 for (const rule of filteredTransformRules) { 111 await spawnAsync('sed', [rule.flags || '-i', '--', rule.pattern, filename]); 112 } 113 114 // Rename file to be prefixed. 115 if (filename !== targetPath) { 116 await fs.move(filename, targetPath); 117 } 118 119 // perform transforms that sed can't express 120 await _transformFileContentsAsync(targetPath, async (fileString) => { 121 // rename misc imports, e.g. Layout.h 122 fileString = fileString.replace( 123 /#(include|import)\s+"((?:[^"\/]+\/)?)([^"]+\.h)"/g, 124 (match, p1, p2, p3) => { 125 return p3.startsWith(versionPrefix) ? match : `#${p1} "${p2}${versionPrefix}${p3}"`; 126 } 127 ); 128 129 // [hermes] the transform above will replace 130 // #include "hermes/inspector/detail/Thread.h" -> #include "hermes/ABIX_0_0inspector/detail/Thread.h" 131 // that is not correct. 132 // because hermes podspec doesn't use header_dir, we only use the header basename for versioning. 133 // this transform would replace 134 // #include "hermes/ABIX_0_0inspector/detail/Thread.h" -> #include "hermes/inspector/detail/ABIX_0_0Thread.h" 135 // note that the rule should be placed after the "rename misc imports" transform. 136 fileString = fileString.replace( 137 new RegExp(`^(#import|#include\\s+["<])(${versionPrefix}hermes\\/.+\\.h)([">])$`, 'gm'), 138 (match, prefix, header, suffix) => { 139 const headers = header.split('/').map((part) => part.replace(versionPrefix, '')); 140 assert(headers.length > 1); 141 const lastPart = headers[headers.length - 1]; 142 headers[headers.length - 1] = `${versionPrefix}${lastPart}`; 143 return `${prefix}${headers.join('/')}${suffix}`; 144 } 145 ); 146 147 // restore EX_UNVERSIONED contents 148 if (unversionedCaptures) { 149 let index = 0; 150 do { 151 fileString = fileString.replace(UNVERSIONED_PLACEHOLDER, unversionedCaptures[index]); 152 index++; 153 } while (fileString.indexOf(UNVERSIONED_PLACEHOLDER) !== -1); 154 } 155 156 const injectedMacrosOutput = await runTransformPipelineAsync({ 157 pipeline: injectMacros(versionPrefix), 158 input: fileString, 159 targetPath, 160 }); 161 162 return await runTransformPipelineAsync({ 163 pipeline: postTransforms(versionPrefix), 164 input: injectedMacrosOutput, 165 targetPath, 166 }); 167 }); 168 // process `filename` 169 }); 170 171 await Promise.all(filenames.map(transformSingleFile)); 172} 173 174/** 175 * Transform and rename all code files we care about under `rnPath` 176 */ 177async function transformReactNativeAsync(rnPath, versionName, versionedPodNames) { 178 const filenameQueries = [`${rnPath}/**/*.[hmSc]`, `${rnPath}/**/*.mm`, `${rnPath}/**/*.cpp`]; 179 let filenames: string[] = []; 180 await Promise.all( 181 filenameQueries.map(async (query) => { 182 const queryFilenames = (await glob(query)) as string[]; 183 if (queryFilenames) { 184 filenames = filenames.concat(queryFilenames); 185 } 186 }) 187 ); 188 189 return namespaceReactNativeFilesAsync(filenames, versionName, versionedPodNames); 190} 191 192/** 193 * For all files matching the given glob query, namespace and rename them 194 * with the given version number. This utility is mainly useful for backporting 195 * small changes into an existing SDK. To create a new SDK version, use `addVersionAsync` 196 * instead. 197 * @param globQuery a string to pass to glob which matches some file paths 198 * @param versionNumber Exponent SDK version, e.g. 42.0.0 199 */ 200export async function versionReactNativeIOSFilesAsync(globQuery, versionNumber) { 201 const filenames = await glob(globQuery); 202 if (!filenames || !filenames.length) { 203 throw new Error(`No files matched the given pattern: ${globQuery}`); 204 } 205 const { versionName, versionedPodNames } = await getConfigsFromArguments(versionNumber); 206 console.log(`Versioning ${filenames.length} files with SDK version ${versionNumber}...`); 207 return namespaceReactNativeFilesAsync(filenames, versionName, versionedPodNames); 208} 209 210async function generateVersionedReactNativeAsync(versionName: string): Promise<void> { 211 const versionedReactNativePath = getVersionedReactNativePath(versionName); 212 213 await fs.mkdirs(versionedReactNativePath); 214 215 // Clone react native latest version 216 console.log(`Copying files from ${chalk.magenta(RELATIVE_RN_PATH)} ...`); 217 218 const filesToCopy = [ 219 'React', 220 'Libraries', 221 'React.podspec', 222 'React-Core.podspec', 223 'ReactCommon/ReactCommon.podspec', 224 'ReactCommon/React-Fabric.podspec', 225 'ReactCommon/React-rncore.podspec', 226 'ReactCommon/hermes/React-hermes.podspec', 227 'sdks/hermes-engine/hermes-engine.podspec', 228 'package.json', 229 ]; 230 231 for (const fileToCopy of filesToCopy) { 232 await fs.copy( 233 path.join(EXPO_DIR, RELATIVE_RN_PATH, fileToCopy), 234 path.join(versionedReactNativePath, fileToCopy) 235 ); 236 } 237 238 console.log(`Removing unnecessary ${chalk.magenta('*.js')} files ...`); 239 240 const jsFiles = (await glob(path.join(versionedReactNativePath, '**', '*.js'))) as string[]; 241 242 for (const jsFile of jsFiles) { 243 await fs.remove(jsFile); 244 } 245 await Promise.all(jsFiles.map((jsFile) => fs.remove(jsFile))); 246 247 console.log('Running react-native-codegen'); 248 await runReactNativeCodegenAsync({ 249 reactNativeRoot: path.join(EXPO_DIR, RELATIVE_RN_PATH), 250 codegenPkgRoot: path.join( 251 REACT_NATIVE_SUBMODULE_MONOREPO_ROOT, 252 'packages', 253 'react-native-codegen' 254 ), 255 outputDir: path.join(versionedReactNativePath, 'codegen', 'ios'), 256 name: `${versionName}FBReactNativeSpec`, 257 type: 'modules', 258 platform: 'ios', 259 jsSrcsDir: path.join(EXPO_DIR, RELATIVE_RN_PATH, 'Libraries'), 260 keepIntermediateSchema: true, 261 }); 262 console.log(`Removing unused generated FBReactNativeSpecJSI files for 0.72`); 263 await Promise.all( 264 [ 265 `${versionName}FBReactNativeSpecJSI.h`, 266 `${versionName}FBReactNativeSpecJSI-generated.cpp`, 267 ].map((file) => { 268 const filePath = path.join(versionedReactNativePath, 'codegen', 'ios', file); 269 return fs.remove(filePath); 270 }) 271 ); 272 273 console.log( 274 `Copying cpp libraries from ${chalk.magenta(path.join(RELATIVE_RN_PATH, 'ReactCommon'))} ...` 275 ); 276 const cppLibraries = getCppLibrariesToVersion(); 277 278 await fs.mkdirs(path.join(versionedReactNativePath, 'ReactCommon')); 279 280 for (const library of cppLibraries) { 281 await fs.copy( 282 path.join(EXPO_DIR, RELATIVE_RN_PATH, 'ReactCommon', library.libName), 283 path.join(versionedReactNativePath, 'ReactCommon', library.libName) 284 ); 285 } 286 // remove hermes test files in ReactCommon/hermes copied above 287 const hermesTestFiles = await glob('**/{cli,tests,tools}', { 288 cwd: path.join(versionedReactNativePath, 'ReactCommon', 'hermes'), 289 absolute: true, 290 }); 291 await Promise.all(hermesTestFiles.map((file) => fs.remove(file))); 292 293 await generateReactNativePodScriptAsync(versionedReactNativePath, versionName); 294 await generateReactNativePodspecsAsync(versionedReactNativePath, versionName); 295} 296 297/** 298 * There are some kernel files that unfortunately have to call versioned code directly. 299 * This function applies the specified changes in the kernel codebase. 300 * The nature of kernel modifications is that they are temporary and at one point these have to be rollbacked. 301 * @param versionName SDK version, e.g. 21.0.0, 37.0.0, etc. 302 * @param rollback flag indicating whether to invoke rollbacking modification. 303 */ 304async function modifyKernelFilesAsync( 305 versionName: string, 306 rollback: boolean = false 307): Promise<void> { 308 const kernelFilesPath = path.join(IOS_DIR, 'Exponent/kernel'); 309 const filenameQueries = [`${kernelFilesPath}/**/EXAppViewController.m`]; 310 let filenames: string[] = []; 311 await Promise.all( 312 filenameQueries.map(async (query) => { 313 const queryFilenames = (await glob(query)) as string[]; 314 if (queryFilenames) { 315 filenames = filenames.concat(queryFilenames); 316 } 317 }) 318 ); 319 await Promise.all( 320 filenames.map(async (filename) => { 321 console.log(`Modifying ${chalk.magenta(path.relative(EXPO_DIR, filename))}:`); 322 await _transformFileContentsAsync(filename, (fileContents) => 323 runTransformPipelineAsync({ 324 pipeline: kernelFilesTransforms(versionName, rollback), 325 targetPath: filename, 326 input: fileContents, 327 }) 328 ); 329 }) 330 ); 331} 332/** 333 * - Copies `scripts/react_native_pods.rb` script into versioned ReactNative directory. 334 * - Removes pods installed from third-party-podspecs (we don't version them). 335 * - Versions `use_react_native` method and all pods it declares. 336 */ 337async function generateReactNativePodScriptAsync( 338 versionedReactNativePath: string, 339 versionName: string 340): Promise<void> { 341 const reactCodegenDependencies = [ 342 'FBReactNativeSpec', 343 'React-jsiexecutor', 344 'RCTRequired', 345 'RCTTypeSafety', 346 'React-Core', 347 'React-jsi', 348 'React-NativeModulesApple', 349 'ReactCommon/turbomodule/core', 350 'ReactCommon/turbomodule/bridging', 351 'React-graphics', 352 'React-rncore', 353 'hermes-engine', 354 'React-jsc', 355 ]; 356 357 const reactNativePodScriptTransforms: StringTransform[] = [ 358 { 359 find: /\b(def (use_react_native|use_react_native_codegen|setup_jsc))!/g, 360 replaceWith: `$1_${versionName}!`, 361 }, 362 { 363 find: /(\bpod\s+([^\n]+)\/third-party-podspecs\/([^\n]+))/g, 364 replaceWith: '# $1', 365 }, 366 { 367 find: /\bpod\s+'([^\']+)'/g, 368 replaceWith: `pod '${versionName}$1'`, 369 }, 370 { 371 find: /(:path => "[^"]+")/g, 372 replaceWith: `$1, :project_name => '${versionName}'`, 373 }, 374 375 // Removes duplicated constants 376 { 377 find: "DEFAULT_OTHER_CPLUSPLUSFLAGS = '$(inherited)'", 378 replaceWith: '', 379 }, 380 { 381 find: "NEW_ARCH_OTHER_CPLUSPLUSFLAGS = '$(inherited) -DRCT_NEW_ARCH_ENABLED=1 -DFOLLY_NO_CONFIG -DFOLLY_MOBILE=1 -DFOLLY_USE_LIBCPP=1'", 382 replaceWith: '', 383 }, 384 385 // Since `React-Codegen.podspec` is generated during `pod install`, versioning should be done in the pod script. 386 { 387 find: "$CODEGEN_OUTPUT_DIR = 'build/generated/ios'", 388 replaceWith: `$CODEGEN_OUTPUT_DIR = '${path.relative( 389 IOS_DIR, 390 versionedReactNativePath 391 )}/codegen/ios'`, 392 }, 393 { 394 find: /\$(CODEGEN_OUTPUT_DIR)\b/g, 395 replaceWith: `$${versionName}$1`, 396 }, 397 { find: /\b(React-Codegen)\b/g, replaceWith: `${versionName}$1` }, 398 { find: /(\$\(PODS_ROOT\)\/Headers\/Private\/)React-/g, replaceWith: `$1${versionName}React-` }, 399 { 400 find: /^\s+CodegenUtils\.clean_up_build_folder\(.+$/gm, 401 replaceWith: '', 402 }, 403 { 404 find: /^\s+build_codegen!\(.+$/gm, 405 replaceWith: '', 406 }, 407 ]; 408 409 const hermesVersion = await fs.readFile( 410 path.join(REACT_NATIVE_SUBMODULE_DIR, 'sdks', '.hermesversion'), 411 'utf8' 412 ); 413 const hermesTransforms: StringTransform[] = [ 414 { find: /^\s+prepare_hermes[.\s\S]*abort unless prep_status == 0\n$/gm, replaceWith: '' }, 415 { 416 find: new RegExp( 417 `^\\s*pod '${versionName}hermes-engine', :podspec => "#\\{react_native_path\\}\\/sdks\\/hermes-engine\\/hermes-engine.podspec", :tag => hermestag`, 418 'gm' 419 ), 420 replaceWith: ` 421 if File.exist?("#{react_native_path}/sdks/hermes-engine/destroot") 422 pod '${versionName}hermes-engine', :path => "#{react_native_path}/sdks/hermes-engine", :project_name => '${versionName}', :tag => '${hermesVersion}' 423 else 424 pod '${versionName}hermes-engine', :podspec => "#{react_native_path}/sdks/hermes-engine/${versionName}hermes-engine.podspec", :project_name => '${versionName}', :tag => '${hermesVersion}' 425 end`, 426 }, 427 { find: new RegExp(`\\b${versionName}(libevent)\\b`, 'g'), replaceWith: '$1' }, 428 ]; 429 430 const commonMethodTransforms = [ 431 'get_script_phases_with_codegen_discovery', 432 'get_script_phases_no_codegen_discovery', 433 'get_script_template', 434 'setup_jsc', 435 'setup_hermes', 436 'run_codegen', 437 ]; 438 439 const transforms: FileTransforms = { 440 content: [ 441 ...reactNativePodScriptTransforms.map((stringTransform) => ({ 442 path: 'react_native_pods.rb', 443 ...stringTransform, 444 })), 445 ...hermesTransforms.map((stringTransform) => ({ 446 paths: 'jsengine.rb', 447 ...stringTransform, 448 })), 449 { 450 paths: 'codegen_utils.rb', 451 find: new RegExp(`["'](${reactCodegenDependencies.join('|')})["']:(\\s*\\[\\],?)`, 'g'), 452 replaceWith: `"${versionName}$1":$2`, 453 }, 454 { 455 paths: [ 456 'react_native_pods.rb', 457 'script_phases.rb', 458 'jsengine.rb', 459 'codegen.rb', 460 'codegen_utils.rb', 461 ], 462 find: new RegExp(`\\b(${commonMethodTransforms.join('|')})\\b`, 'g'), 463 replaceWith: `$1_${versionName}`, 464 }, 465 ], 466 }; 467 468 const reactNativeScriptsDir = path.join(EXPO_DIR, RELATIVE_RN_PATH, 'scripts'); 469 const scriptFiles = await glob('**/*', { cwd: reactNativeScriptsDir, nodir: true, dot: true }); 470 await Promise.all( 471 scriptFiles.map(async (file) => { 472 await copyFileWithTransformsAsync({ 473 sourceFile: file, 474 sourceDirectory: reactNativeScriptsDir, 475 targetDirectory: path.join(versionedReactNativePath, 'scripts'), 476 transforms, 477 keepFileMode: true, 478 }); 479 }) 480 ); 481 482 await fs.copy( 483 path.join(EXPO_DIR, RELATIVE_RN_PATH, 'sdks', 'hermes-engine', 'hermes-utils.rb'), 484 path.join(versionedReactNativePath, 'sdks', 'hermes-engine', 'hermes-utils.rb') 485 ); 486} 487 488async function generateReactNativePodspecsAsync( 489 versionedReactNativePath: string, 490 versionName: string 491): Promise<void> { 492 const podspecFiles = await glob(path.join(versionedReactNativePath, '**', '*.podspec')); 493 494 for (const podspecFile of podspecFiles) { 495 const basename = path.basename(podspecFile, '.podspec'); 496 497 if (/^react$/i.test(basename)) { 498 continue; 499 } 500 501 console.log( 502 `Generating podspec for ${chalk.green(basename)} at ${chalk.magenta( 503 path.relative(versionedReactNativePath, podspecFile) 504 )} ...` 505 ); 506 507 const podspecSource = await fs.readFile(podspecFile, 'utf8'); 508 509 const podspecOutput = await runTransformPipelineAsync({ 510 pipeline: podspecTransforms(versionName), 511 input: podspecSource, 512 targetPath: podspecFile, 513 }); 514 515 // Write transformed podspec output to the prefixed file. 516 await fs.writeFile( 517 path.join(path.dirname(podspecFile), `${versionName}${basename}.podspec`), 518 podspecOutput 519 ); 520 521 // Remove original and unprefixed podspec. 522 await fs.remove(podspecFile); 523 } 524 525 await generateReactPodspecAsync(versionedReactNativePath, versionName); 526} 527 528/** 529 * @param versionName Version prefix (e.g. `ABI43_0_0`) 530 * @param sdkNumber Major version of the SDK 531 */ 532async function generateVersionedExpoAsync(versionName: string, sdkNumber: number): Promise<void> { 533 const versionedExpoKitPath = getVersionedExpoKitPath(versionName); 534 const versionedUnimodulePods = await getVersionedUnimodulePodsAsync(versionName); 535 536 await fs.mkdirs(versionedExpoKitPath); 537 538 // Copy versioned exponent modules into the clone 539 console.log(`Copying versioned native modules into the new Pod...`); 540 541 await fs.copy(path.join(IOS_DIR, 'Exponent', 'Versioned'), versionedExpoKitPath); 542 543 await fs.copy( 544 path.join(EXPO_DIR, 'ios', 'ExpoKit.podspec'), 545 path.join(versionedExpoKitPath, 'ExpoKit.podspec') 546 ); 547 548 console.log(`Generating podspec for ${chalk.green('ExpoKit')} ...`); 549 550 await generateExpoKitPodspecAsync( 551 versionedExpoKitPath, 552 versionedUnimodulePods, 553 versionName, 554 `${sdkNumber}.0.0` 555 ); 556 557 logger.info(' Generating Swift modules provider'); 558 559 await versionExpoModulesProviderAsync(sdkNumber); 560} 561 562/** 563 * Transforms ExpoKit.podspec, versioning Expo namespace, React pod name, replacing original ExpoKit podspecs 564 * with Expo and ExpoOptional. 565 * @param specfilePath location of ExpoKit.podspec to modify, e.g. /versioned-react-native/someversion/ 566 * @param versionedReactPodName name of the new pod (and podfile) 567 * @param universalModulesPodNames versioned names of universal modules 568 * @param versionNumber "XX.X.X" 569 */ 570async function generateExpoKitPodspecAsync( 571 specfilePath: string, 572 universalModulesPodNames: { [key: string]: string }, 573 versionName: string, 574 versionNumber: string 575): Promise<void> { 576 const versionedReactPodName = getVersionedReactPodName(versionName); 577 const versionedExpoKitPodName = getVersionedExpoKitPodName(versionName); 578 const specFilename = path.join(specfilePath, 'ExpoKit.podspec'); 579 580 // rename spec to newPodName 581 const sedPattern = `s/\\(s\\.name[[:space:]]*=[[:space:]]\\)"ExpoKit"/\\1"${versionedExpoKitPodName}"/g`; 582 583 await spawnAsync('sed', ['-i', '--', sedPattern, specFilename]); 584 585 // further processing that sed can't do very well 586 await _transformFileContentsAsync(specFilename, async (fileString) => { 587 // `universalModulesPodNames` contains only versioned unimodules, 588 // so we fall back to the original name if the module is not there 589 const universalModulesDependencies = (await getListOfPackagesAsync()) 590 .filter( 591 (pkg) => 592 pkg.isIncludedInExpoClientOnPlatform('ios') && 593 pkg.podspecName && 594 !EXCLUDED_POD_DEPENDENCIES.includes(pkg.podspecName) 595 ) 596 .map( 597 ({ podspecName }) => 598 `ss.dependency "${universalModulesPodNames[podspecName!] || podspecName}"` 599 ).join(` 600 `); 601 const externalDependencies = EXTERNAL_REACT_ABI_DEPENDENCIES.map( 602 (podName) => `ss.dependency "${podName}"` 603 ).join(` 604 `); 605 const subspec = `s.subspec "Expo" do |ss| 606 ss.source_files = "Core/**/*.{h,m,mm,cpp}" 607 608 ss.dependency "${versionedReactPodName}-Core" 609 ss.dependency "${versionedReactPodName}-Core/DevSupport" 610 ss.dependency "${versionedReactPodName}Common" 611 ss.dependency "${versionName}RCTRequired" 612 ss.dependency "${versionName}RCTTypeSafety" 613 ss.dependency "${versionName}React-hermes" 614 ${universalModulesDependencies} 615 ${externalDependencies} 616 ss.dependency "${versionName}${MODULES_PROVIDER_POD_NAME}" 617 end 618 619 s.subspec "ExpoOptional" do |ss| 620 ss.dependency "${versionedExpoKitPodName}/Expo" 621 ss.source_files = "Optional/**/*.{h,m,mm}" 622 end`; 623 fileString = fileString.replace( 624 /(s\.subspec ".+?"[\S\s]+?(?=end\b)end\b[\s]+)+/g, 625 `${subspec}\n` 626 ); 627 628 // correct version number 629 fileString = fileString.replace(/(?<=s.version = ").*?(?=")/g, versionNumber); 630 631 // add Reanimated V2 RCT-Folly dependency 632 fileString = fileString.replace( 633 /(?= s.subspec "Expo" do \|ss\|)/g, 634 ` 635 header_search_paths = [ 636 '"$(PODS_ROOT)/boost"', 637 '"$(PODS_ROOT)/glog"', 638 '"$(PODS_ROOT)/DoubleConversion"', 639 '"$(PODS_ROOT)/RCT-Folly"', 640 '"$(PODS_ROOT)/Headers/Private/${versionName}React-Core"', 641 '"$(PODS_CONFIGURATION_BUILD_DIR)/${versionName}ExpoModulesCore/Swift Compatibility Header"', 642 '"$(PODS_CONFIGURATION_BUILD_DIR)/${versionName}EXManifests/Swift Compatibility Header"', 643 '"$(PODS_CONFIGURATION_BUILD_DIR)/${versionName}EXUpdatesInterface/Swift Compatibility Header"', 644 '"$(PODS_CONFIGURATION_BUILD_DIR)/${versionName}EXUpdates/Swift Compatibility Header"', 645 ] 646 s.pod_target_xcconfig = { 647 "CLANG_CXX_LANGUAGE_STANDARD" => "c++17", 648 "USE_HEADERMAP" => "YES", 649 "DEFINES_MODULE" => "YES", 650 "HEADER_SEARCH_PATHS" => header_search_paths.join(' '), 651 } 652 \n\n` 653 ); 654 655 return fileString; 656 }); 657 658 // move podspec to ${versionedExpoKitPodName}.podspec 659 await fs.move(specFilename, path.join(specfilePath, `${versionedExpoKitPodName}.podspec`)); 660} 661 662/** 663 * @param specfilePath location of React.podspec to modify, e.g. /versioned-react-native/someversion/ 664 * @param versionedReactPodName name of the new pod (and podfile) 665 */ 666async function generateReactPodspecAsync(versionedReactNativePath, versionName) { 667 const versionedReactPodName = getVersionedReactPodName(versionName); 668 const versionedYogaPodName = getVersionedYogaPodName(versionName); 669 const versionedJSIPodName = getVersionedJSIPodName(versionName); 670 const specFilename = path.join(versionedReactNativePath, 'React.podspec'); 671 672 // rename spec to newPodName 673 const sedPattern = `s/\\(s\\.name[[:space:]]*=[[:space:]]\\)"React"/\\1"${versionedReactPodName}"/g`; 674 await spawnAsync('sed', ['-i', '--', sedPattern, specFilename]); 675 676 // rename header_dir 677 await spawnAsync('sed', [ 678 '-i', 679 '--', 680 `s/^\\(.*header_dir.*\\)React\\(.*\\)$/\\1${versionedReactPodName}\\2/`, 681 specFilename, 682 ]); 683 await spawnAsync('sed', [ 684 '-i', 685 '--', 686 `s/^\\(.*header_dir.*\\)jsireact\\(.*\\)$/\\1${versionedJSIPodName}\\2/`, 687 specFilename, 688 ]); 689 690 // point source at . 691 const newPodSource = `{ :path => "." }`; 692 await spawnAsync('sed', [ 693 '-i', 694 '--', 695 `s/\\(s\\.source[[:space:]]*=[[:space:]]\\).*/\\1${newPodSource}/g`, 696 specFilename, 697 ]); 698 699 // further processing that sed can't do very well 700 await _transformFileContentsAsync(specFilename, (fileString) => { 701 // replace React/* dependency with ${versionedReactPodName}/* 702 fileString = fileString.replace( 703 /(\.dependency\s+)"React([^"]+)"/g, 704 `$1"${versionedReactPodName}$2"` 705 ); 706 707 fileString = fileString.replace('/RCTTV', `/${versionName}RCTTV`); 708 709 // namespace cpp libraries 710 const cppLibraries = getCppLibrariesToVersion(); 711 cppLibraries.forEach(({ libName }) => { 712 fileString = fileString.replace( 713 new RegExp(`([^A-Za-z0-9_])${libName}([^A-Za-z0-9_])`, 'g'), 714 `$1${getVersionedLibraryName(libName, versionName)}$2` 715 ); 716 }); 717 718 // fix wrong Yoga pod name 719 fileString = fileString.replace( 720 /^(.*dependency.*["']).*yoga.*?(["'].*)$/m, 721 `$1${versionedYogaPodName}$2` 722 ); 723 724 return fileString; 725 }); 726 727 // move podspec to ${versionedReactPodName}.podspec 728 await fs.move( 729 specFilename, 730 path.join(versionedReactNativePath, `${versionedReactPodName}.podspec`) 731 ); 732} 733 734function getCFlagsToPrefixGlobals(prefix, globals) { 735 return globals.map((val) => `-D${val}=${prefix}${val}`); 736} 737 738/** 739 * Generates `dependencies.rb` and `postinstalls.rb` files for versioned code. 740 * @param versionNumber Semver-compliant version of the SDK/ABI 741 * @param versionName Version prefix used for versioned files, e.g. ABI99_0_0 742 * @param versionedPodNames mapping from pod names to versioned pod names, e.g. React -> ReactABI99_0_0 743 * @param versionedReactPodPath path of the new react pod 744 */ 745async function generatePodfileSubscriptsAsync( 746 versionNumber: string, 747 versionName: string, 748 versionedPodNames: Record<string, string>, 749 versionedReactPodPath: string 750) { 751 if (!versionedPodNames.React) { 752 throw new Error( 753 'Tried to add generate pod dependencies, but missing a name for the versioned library.' 754 ); 755 } 756 757 const relativeReactNativePath = path.relative(IOS_DIR, getVersionedReactNativePath(versionName)); 758 const relativeExpoKitPath = path.relative(IOS_DIR, getVersionedExpoKitPath(versionName)); 759 760 // Add a dependency on newPodName 761 const dependenciesContent = `# @generated by expotools 762 763require './${relativeReactNativePath}/scripts/react_native_pods.rb' 764 765use_react_native_${versionName}!( 766 :path => './${relativeReactNativePath}', 767 :hermes_enabled => true, 768 :fabric_enabled => false, 769) 770setup_jsc_${versionName}!( 771 :react_native_path => './${relativeReactNativePath}', 772 :fabric_enabled => false, 773) 774 775pod '${getVersionedExpoKitPodName(versionName)}', 776 :path => './${relativeExpoKitPath}', 777 :project_name => '${versionName}', 778 :subspecs => ['Expo', 'ExpoOptional'] 779 780use_pods! '{versioned,vendored}/sdk${semver.major( 781 versionNumber 782 )}/**/*.podspec.json', '${versionName}' 783`; 784 785 await fs.writeFile(path.join(versionedReactPodPath, 'dependencies.rb'), dependenciesContent); 786 787 // Add postinstall. 788 // In particular, resolve conflicting globals from React by redefining them. 789 const globals = { 790 React: [ 791 // RCTNavigator 792 'kNeverRequested', 793 'kNeverProgressed', 794 // react-native-maps 795 'kSMCalloutViewRepositionDelayForUIScrollView', 796 'regionAsJSON', 797 'unionRect', 798 // jschelpers 799 'JSNoBytecodeFileFormatVersion', 800 'JSSamplingProfilerEnabled', 801 // RCTInspectorPackagerConnection 802 'RECONNECT_DELAY_MS', 803 // RCTSpringAnimation 804 'MAX_DELTA_TIME', 805 ], 806 yoga: [ 807 'gCurrentGenerationCount', 808 'gPrintSkips', 809 'gPrintChanges', 810 'layoutNodeInternal', 811 'gDepth', 812 'gPrintTree', 813 'isUndefined', 814 'gNodeInstanceCount', 815 ], 816 }; 817 const configValues = getCFlagsToPrefixGlobals( 818 versionedPodNames.React, 819 globals.React.concat(globals.yoga) 820 ); 821 const indent = ' '.repeat(3); 822 const config = `# @generated by expotools 823 824if pod_name.start_with?('${versionedPodNames.React}') || pod_name == '${versionedPodNames.ExpoKit}' 825 target_installation_result.native_target.build_configurations.each do |config| 826 config.build_settings['OTHER_CFLAGS'] = %w[ 827 ${configValues.join(`\n${indent}`)} 828 -fmodule-map-file="\${PODS_ROOT}/Headers/Public/${versionName}React-Core/${versionName}React/${versionName}React-Core.modulemap" 829 -fmodule-map-file="\${PODS_ROOT}/Headers/Public/${versionName}ExpoModulesCore/${versionName}ExpoModulesCore.modulemap" 830 -fmodule-map-file="\${PODS_ROOT}/Headers/Public/${versionName}EXUpdates/${versionName}EXUpdates.modulemap" 831 -fmodule-map-file="\${PODS_ROOT}/Headers/Public/${versionName}EXUpdatesInterface/${versionName}EXUpdatesInterface.modulemap" 832 ] 833 config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)'] 834 config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}RCT_DEV=1' 835 config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}RCT_ENABLE_INSPECTOR=0' 836 config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}RCT_REMOTE_PROFILE=0' 837 config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}RCT_DEV_SETTINGS_ENABLE_PACKAGER_CONNECTION=0' 838 # Enable Google Maps support 839 config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}HAVE_GOOGLE_MAPS=1' 840 config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] << '${versionName}HAVE_GOOGLE_MAPS_UTILS=1' 841 end 842end 843`; 844 await fs.writeFile(path.join(versionedReactPodPath, 'postinstalls.rb'), config); 845} 846 847/** 848 * @param transformConfig function that takes a config dict and returns a new config dict. 849 */ 850async function modifyVersionConfigAsync(configPath, transformConfig) { 851 const jsConfigFilename = `${configPath}/sdkVersions.json`; 852 await _transformFileContentsAsync(jsConfigFilename, (jsConfigContents) => { 853 let jsConfig; 854 855 // read the existing json config and add the new version to the sdkVersions array 856 try { 857 jsConfig = JSON.parse(jsConfigContents); 858 } catch (e) { 859 console.log('Error parsing existing sdkVersions.json file, writing a new one...', e); 860 console.log('The erroneous file contents was:', jsConfigContents); 861 jsConfig = { 862 sdkVersions: [], 863 }; 864 } 865 // apply changes 866 jsConfig = transformConfig(jsConfig); 867 return JSON.stringify(jsConfig); 868 }); 869 870 // convert json config to plist for iOS 871 await spawnAsync('plutil', [ 872 '-convert', 873 'xml1', 874 jsConfigFilename, 875 '-o', 876 path.join(configPath, 'EXSDKVersions.plist'), 877 ]); 878} 879 880function validateAddVersionDirectories(rootPath, newVersionPath) { 881 // Make sure the paths we want to read are available 882 const relativePathsToCheck = [ 883 RELATIVE_RN_PATH, 884 'ios/versioned-react-native', 885 'ios/Exponent', 886 'ios/Exponent/Versioned', 887 ]; 888 let isValid = true; 889 relativePathsToCheck.forEach((path) => { 890 try { 891 fs.accessSync(`${rootPath}/${path}`, fs.constants.F_OK); 892 } catch { 893 console.log(`${rootPath}/${path} does not exist or is otherwise inaccessible`); 894 isValid = false; 895 } 896 }); 897 // Also, make sure the version we're about to write doesn't already exist 898 try { 899 // we want this to fail 900 fs.accessSync(newVersionPath, fs.constants.F_OK); 901 console.log(`${newVersionPath} already exists, will not overwrite`); 902 isValid = false; 903 } catch {} 904 905 return isValid; 906} 907 908function validateRemoveVersionDirectories(rootPath, newVersionPath) { 909 const pathsToCheck = [ 910 `${rootPath}/ios/versioned-react-native`, 911 `${rootPath}/ios/Exponent`, 912 newVersionPath, 913 ]; 914 let isValid = true; 915 pathsToCheck.forEach((path) => { 916 try { 917 fs.accessSync(path, fs.constants.F_OK); 918 } catch { 919 console.log(`${path} does not exist or is otherwise inaccessible`); 920 isValid = false; 921 } 922 }); 923 return isValid; 924} 925 926async function getConfigsFromArguments(versionNumber) { 927 let versionComponents = versionNumber.split('.'); 928 versionComponents = versionComponents.map((number) => parseInt(number, 10)); 929 const versionName = 'ABI' + versionNumber.replace(/\./g, '_'); 930 const rootPathComponents = EXPO_DIR.split('/'); 931 const versionPathComponents = path.join('ios', 'versioned-react-native', versionName).split('/'); 932 const newVersionPath = rootPathComponents.concat(versionPathComponents).join('/'); 933 934 const versionedPodNames = { 935 React: getVersionedReactPodName(versionName), 936 yoga: getVersionedYogaPodName(versionName), 937 ExpoKit: getVersionedExpoKitPodName(versionName), 938 jsireact: getVersionedJSIPodName(versionName), 939 }; 940 941 return { 942 sdkNumber: semver.major(versionNumber), 943 versionName, 944 newVersionPath, 945 versionedPodNames, 946 versionComponents, 947 }; 948} 949 950async function getVersionedUnimodulePodsAsync( 951 versionName: string 952): Promise<{ [key: string]: string }> { 953 const versionedUnimodulePods = {}; 954 const packages = await getListOfPackagesAsync(); 955 956 packages.forEach((pkg) => { 957 const podName = pkg.podspecName; 958 if (podName && pkg.isVersionableOnPlatform('ios')) { 959 versionedUnimodulePods[podName] = `${versionName}${podName}`; 960 } 961 }); 962 963 return versionedUnimodulePods; 964} 965 966function getVersionedReactPodName(versionName: string): string { 967 return getVersionedLibraryName('React', versionName); 968} 969 970function getVersionedYogaPodName(versionName: string): string { 971 return getVersionedLibraryName('Yoga', versionName); 972} 973 974function getVersionedJSIPodName(versionName: string): string { 975 return getVersionedLibraryName('jsiReact', versionName); 976} 977 978function getVersionedExpoKitPodName(versionName: string): string { 979 return getVersionedLibraryName('ExpoKit', versionName); 980} 981 982function getVersionedLibraryName(libraryName: string, versionName: string): string { 983 return `${versionName}${libraryName}`; 984} 985 986function getVersionedReactNativePath(versionName: string): string { 987 return path.join(VERSIONED_RN_IOS_DIR, versionName, 'ReactNative'); 988} 989 990function getVersionedExpoPath(versionName: string): string { 991 return path.join(VERSIONED_RN_IOS_DIR, versionName, 'Expo'); 992} 993 994function getCppLibrariesToVersion() { 995 return [ 996 { 997 libName: 'cxxreact', 998 }, 999 { 1000 libName: 'jsi', 1001 }, 1002 { 1003 libName: 'jsiexecutor', 1004 customHeaderDir: 'jsireact', 1005 }, 1006 { 1007 libName: 'jsinspector', 1008 }, 1009 { 1010 libName: 'yoga', 1011 }, 1012 { 1013 libName: 'react', 1014 }, 1015 { 1016 libName: 'callinvoker', 1017 customHeaderDir: 'ReactCommon', 1018 }, 1019 { 1020 libName: 'reactperflogger', 1021 }, 1022 { 1023 libName: 'runtimeexecutor', 1024 }, 1025 { 1026 libName: 'logger', 1027 }, 1028 { 1029 libName: 'hermes', 1030 }, 1031 { 1032 libName: 'jsc', 1033 }, 1034 { 1035 libName: 'butter', 1036 }, 1037 ]; 1038} 1039 1040export async function addVersionAsync(versionNumber: string, packages: Package[]) { 1041 const { sdkNumber, versionName, newVersionPath, versionedPodNames } = 1042 await getConfigsFromArguments(versionNumber); 1043 1044 // Validate the directories we need before doing anything 1045 console.log(`Validating root directory ${chalk.magenta(EXPO_DIR)} ...`); 1046 const isFilesystemReady = validateAddVersionDirectories(EXPO_DIR, newVersionPath); 1047 if (!isFilesystemReady) { 1048 throw new Error('Aborting: At least one directory we need is not available'); 1049 } 1050 1051 if (!versionedPodNames.React) { 1052 throw new Error('Missing name for versioned pod dependency.'); 1053 } 1054 1055 // Create ABIXX_0_0 directory. 1056 console.log( 1057 `Creating new ABI version ${chalk.cyan(versionNumber)} at ${chalk.magenta( 1058 path.relative(EXPO_DIR, newVersionPath) 1059 )}` 1060 ); 1061 await fs.mkdirs(newVersionPath); 1062 1063 // Generate new Podspec from the existing React.podspec 1064 console.log('Generating versioned ReactNative directory...'); 1065 await generateVersionedReactNativeAsync(versionName); 1066 1067 console.log( 1068 `Generating ${chalk.magenta( 1069 path.relative(EXPO_DIR, getVersionedExpoPath(versionName)) 1070 )} directory...` 1071 ); 1072 await generateVersionedExpoAsync(versionName, sdkNumber); 1073 1074 await versionExpoModulesAsync(sdkNumber, packages); 1075 1076 // Generate versioned Swift modules provider 1077 await versionExpoModulesProviderAsync(sdkNumber); 1078 1079 // Namespace the new React clone 1080 console.log('Namespacing/transforming files...'); 1081 await transformReactNativeAsync(newVersionPath, versionName, versionedPodNames); 1082 1083 // Generate Ruby scripts with versioned dependencies and postinstall actions that will be evaluated in the Expo client's Podfile. 1084 console.log('Adding dependency to root Podfile...'); 1085 await generatePodfileSubscriptsAsync( 1086 versionNumber, 1087 versionName, 1088 versionedPodNames, 1089 newVersionPath 1090 ); 1091 1092 // Add the new version to the iOS config list of available versions 1093 console.log('Registering new version under sdkVersions config...'); 1094 const addVersionToConfig = (config, versionNumber) => { 1095 config.sdkVersions.push(versionNumber); 1096 return config; 1097 }; 1098 await modifyVersionConfigAsync(path.join(IOS_DIR, 'Exponent', 'Supporting'), (config) => 1099 addVersionToConfig(config, versionNumber) 1100 ); 1101 await modifyVersionConfigAsync( 1102 path.join(EXPO_DIR, 'exponent-view-template', 'ios', 'exponent-view-template', 'Supporting'), 1103 (config) => addVersionToConfig(config, versionNumber) 1104 ); 1105 1106 // Modifying kernel files 1107 console.log(`Modifying ${chalk.bold('kernel files')} to incorporate new SDK version...`); 1108 await modifyKernelFilesAsync(versionName); 1109 1110 console.log('Removing any `filename--` files from the new pod ...'); 1111 1112 try { 1113 const minusMinusFiles = [ 1114 ...(await glob(path.join(newVersionPath, '**', '*--'))), 1115 ...(await glob(path.join(IOS_DIR, 'build', versionName, 'generated', 'ios', '**', '*--'))), 1116 ]; 1117 for (const minusMinusFile of minusMinusFiles) { 1118 await fs.remove(minusMinusFile); 1119 } 1120 } catch { 1121 console.warn( 1122 "The script wasn't able to remove any possible `filename--` files created by sed. Please ensure there are no such files manually." 1123 ); 1124 } 1125 1126 logger.info('\n Starting to build versioned Hermes tarball'); 1127 const versionedReactNativeRoot = getVersionedReactNativePath(versionName); 1128 const hermesTarball = await createVersionedHermesTarball(versionedReactNativeRoot, versionName, { 1129 verbose: true, 1130 }); 1131 await spawnAsync('tar', ['xfz', hermesTarball], { 1132 cwd: path.join(versionedReactNativeRoot, 'sdks', 'hermes-engine'), 1133 }); 1134 1135 console.log('Finished creating new version.'); 1136 1137 console.log( 1138 '\n' + 1139 chalk.yellow( 1140 '################################################################################################################' 1141 ) + 1142 `\nIf you want to commit the versioned code to git, please also upload the versioned Hermes tarball at ${chalk.cyan( 1143 hermesTarball 1144 )} to:\n` + 1145 chalk.cyan( 1146 `https://github.com/expo/react-native/releases/download/sdk-${sdkNumber}.0.0/${versionName}hermes.tar.gz` 1147 ) + 1148 '\n' + 1149 chalk.yellow( 1150 '################################################################################################################' 1151 ) + 1152 '\n' 1153 ); 1154} 1155 1156async function askToReinstallPodsAsync(): Promise<boolean> { 1157 if (process.env.CI) { 1158 // If we're on the CI, let's regenerate Pods by default. 1159 return true; 1160 } 1161 const { result } = await inquirer.prompt<{ result: boolean }>([ 1162 { 1163 type: 'confirm', 1164 name: 'result', 1165 message: 'Do you want to reinstall pods?', 1166 default: true, 1167 }, 1168 ]); 1169 return result; 1170} 1171 1172export async function reinstallPodsAsync(force?: boolean, preventReinstall?: boolean) { 1173 if ( 1174 preventReinstall !== true && 1175 (force || (force !== false && (await askToReinstallPodsAsync()))) 1176 ) { 1177 await spawnAsync('pod', ['install'], { stdio: 'inherit', cwd: IOS_DIR }); 1178 console.log( 1179 'Regenerated Podfile and installed new pods. You can now try to build the project in Xcode.' 1180 ); 1181 } else { 1182 console.log( 1183 'Skipped pods regeneration. You might want to run `et ios-generate-dynamic-macros`, then `pod install` in `ios` to configure Xcode project.' 1184 ); 1185 } 1186} 1187 1188export async function removeVersionAsync(versionNumber: string) { 1189 const { sdkNumber, newVersionPath, versionedPodNames, versionName } = 1190 await getConfigsFromArguments(versionNumber); 1191 1192 console.log( 1193 `Removing SDK version ${chalk.cyan(versionNumber)} from ${chalk.magenta( 1194 path.relative(EXPO_DIR, newVersionPath) 1195 )} with Pod name ${chalk.green(versionedPodNames.React)}` 1196 ); 1197 1198 // Validate the directories we need before doing anything 1199 console.log(`Validating root directory ${chalk.magenta(EXPO_DIR)} ...`); 1200 const isFilesystemReady = validateRemoveVersionDirectories(EXPO_DIR, newVersionPath); 1201 if (!isFilesystemReady) { 1202 console.log('Aborting: At least one directory we expect is not available'); 1203 return; 1204 } 1205 1206 // remove directory 1207 console.log( 1208 `Removing versioned files under ${chalk.magenta(path.relative(EXPO_DIR, newVersionPath))}...` 1209 ); 1210 await fs.remove(newVersionPath); 1211 await fs.remove(getVersionedDirectory(sdkNumber)); 1212 1213 console.log('Removing vendored libraries...'); 1214 await removeVersionedVendoredModulesAsync(semver.major(versionNumber)); 1215 1216 // remove dep from main podfile 1217 console.log(`Removing ${chalk.green(versionedPodNames.React)} dependency from root Podfile...`); 1218 1219 // remove from sdkVersions.json 1220 console.log('Unregistering version from sdkVersions config...'); 1221 const removeVersionFromConfig = (config, versionNumber) => { 1222 const index = config.sdkVersions.indexOf(versionNumber); 1223 if (index > -1) { 1224 // modify in place 1225 config.sdkVersions.splice(index, 1); 1226 } 1227 return config; 1228 }; 1229 await modifyVersionConfigAsync(path.join(IOS_DIR, 'Exponent', 'Supporting'), (config) => 1230 removeVersionFromConfig(config, versionNumber) 1231 ); 1232 await modifyVersionConfigAsync( 1233 path.join(EXPO_DIR, 'exponent-view-template', 'ios', 'exponent-view-template', 'Supporting'), 1234 (config) => removeVersionFromConfig(config, versionNumber) 1235 ); 1236 1237 // modify kernel files 1238 console.log('Rollbacking SDK modifications from kernel files...'); 1239 await modifyKernelFilesAsync(versionName, true); 1240 1241 // Update `ios/ExpoKit.podspec` with the newest SDK version 1242 logger.info(' Updating ExpoKit podspec'); 1243 await renderExpoKitPodspecAsync(EXPO_DIR, path.join(EXPO_DIR, 'template-files')); 1244 1245 await reinstallPodsAsync(); 1246} 1247 1248/** 1249 * @return an array of objects representing react native transform rules. 1250 * objects must contain 'pattern' and may optionally contain 'paths' to limit 1251 * the transform to certain file paths. 1252 * 1253 * the rules are applied in order! 1254 */ 1255function _getReactNativeTransformRules(versionPrefix, reactPodName) { 1256 const cppLibraries = getCppLibrariesToVersion().map((lib) => lib.customHeaderDir || lib.libName); 1257 const versionedLibs = [...cppLibraries, 'React', 'FBLazyVector', 'FBReactNativeSpec']; 1258 1259 return [ 1260 { 1261 // Change Obj-C symbols prefix 1262 pattern: `s/RCT/${versionPrefix}RCT/g`, 1263 }, 1264 { 1265 pattern: `s/^EX/${versionPrefix}EX/g`, 1266 // paths: 'EX', 1267 }, 1268 { 1269 pattern: `s/^UM/${versionPrefix}UM/g`, 1270 // paths: 'EX', 1271 }, 1272 { 1273 pattern: `s/\\([^\\<\\/"]\\)YG/\\1${versionPrefix}YG/g`, 1274 }, 1275 { 1276 pattern: `s/\\([\\<,]\\)YG/\\1${versionPrefix}YG/g`, 1277 }, 1278 { 1279 pattern: `s/^YG/${versionPrefix}YG/g`, 1280 }, 1281 { 1282 paths: 'Components', 1283 pattern: `s/\\([^+]\\)AIR/\\1${versionPrefix}AIR/g`, 1284 }, 1285 { 1286 flags: '-Ei', 1287 pattern: `s/(^|[^A-Za-z0-9_+])(RN|REA|EX|UM|ART|SM)/\\1${versionPrefix}\\2/g`, 1288 }, 1289 { 1290 paths: 'Core/Api', 1291 pattern: `s/^RN/${versionPrefix}RN/g`, 1292 }, 1293 { 1294 paths: 'Core/Api', 1295 pattern: `s/HAVE_GOOGLE_MAPS/${versionPrefix}HAVE_GOOGLE_MAPS/g`, 1296 }, 1297 { 1298 paths: 'Core/Api', 1299 pattern: `s/#import "Branch/#import "${versionPrefix}Branch/g`, 1300 }, 1301 { 1302 paths: 'Core/Api', 1303 pattern: `s/#import "NSObject+RNBranch/#import "${versionPrefix}NSObject+RNBranch/g`, 1304 }, 1305 { 1306 // React will be prefixed in a moment 1307 pattern: `s/#import <${versionPrefix}RCTAnimation/#import <React/g`, 1308 }, 1309 { 1310 pattern: `s/^REA/${versionPrefix}REA/g`, 1311 paths: 'Core/Api/Reanimated', 1312 }, 1313 { 1314 // Prefixes all direct references to objects under `reanimated` namespace. 1315 // It must be applied before versioning `namespace reanimated` so 1316 // `using namespace reanimated::` don't get versioned twice. 1317 pattern: `s/reanimated::/${versionPrefix}reanimated::/g`, 1318 }, 1319 { 1320 // Prefixes reanimated namespace. 1321 pattern: `s/namespace reanimated/namespace ${versionPrefix}reanimated/g`, 1322 }, 1323 { 1324 // Fix imports in C++ libs in ReactCommon. 1325 // Extended syntax (-E) is required to use (a|b). 1326 flags: '-Ei', 1327 pattern: `s/([<"])(${versionedLibs.join( 1328 '|' 1329 )})\\//\\1${versionPrefix}\\2\\/${versionPrefix}/g`, 1330 }, 1331 { 1332 // Change React -> new pod name 1333 // e.g. threads and queues namespaced to com.facebook.react, 1334 // file paths beginning with the lib name, 1335 // the cpp facebook::react namespace, 1336 // iOS categories ending in +React 1337 flags: '-Ei', 1338 pattern: `s/[Rr]eact/${reactPodName}/g`, 1339 }, 1340 { 1341 // Imports from cxxreact and jsireact got prefixed twice. 1342 flags: '-Ei', 1343 pattern: `s/([<"])(${versionPrefix})(cxx|jsi)${versionPrefix}React/\\1\\2\\3react/g`, 1344 }, 1345 { 1346 // Fix imports from files like `UIView+React.*`. 1347 flags: '-Ei', 1348 pattern: `s/\\+${versionPrefix}React/\\+React/g`, 1349 }, 1350 { 1351 // Prefixes all direct references to objects under `facebook` and `JS` namespaces. 1352 // It must be applied before versioning `namespace facebook` so 1353 // `using namespace facebook::` don't get versioned twice. 1354 flags: '-Ei', 1355 pattern: `s/(facebook|JS|hermes)::/${versionPrefix}\\1::/g`, 1356 }, 1357 { 1358 // Prefixes facebook namespace. 1359 flags: '-Ei', 1360 pattern: `s/namespace (facebook|JS|hermes)/namespace ${versionPrefix}\\1/g`, 1361 }, 1362 { 1363 // Prefixes for `namespace h = ::facebook::hermes;` 1364 flags: '-Ei', 1365 pattern: `s/namespace (.+::)(hermes)/namespace \\1${versionPrefix}\\2/g`, 1366 }, 1367 { 1368 // For UMReactNativeAdapter 1369 // Fix names with 'React' substring occurring twice - only first one should be prefixed 1370 flags: '-Ei', 1371 pattern: `s/${versionPrefix}UM([[:alpha:]]*)${reactPodName}/${versionPrefix}UM\\1React/g`, 1372 }, 1373 { 1374 // For EXReactNativeAdapter 1375 pattern: `s/${versionPrefix}EX${reactPodName}/${versionPrefix}EXReact/g`, 1376 }, 1377 { 1378 // For EXConstants and EXNotifications so that when their migrators 1379 // try to access legacy storage for UUID migration, they access the proper value. 1380 pattern: `s/${versionPrefix}EXDeviceInstallUUIDKey/EXDeviceInstallUUIDKey/g`, 1381 paths: 'Expo', 1382 }, 1383 { 1384 // For EXConstants and EXNotifications so that the installation ID 1385 // stays the same between different SDK versions. (https://github.com/expo/expo/issues/11008#issuecomment-726370187) 1386 pattern: `s/${versionPrefix}EXDeviceInstallationUUIDKey/EXDeviceInstallationUUIDKey/g`, 1387 paths: 'Expo', 1388 }, 1389 { 1390 // RCTPlatform exports version of React Native 1391 pattern: `s/${reactPodName}NativeVersion/reactNativeVersion/g`, 1392 }, 1393 { 1394 pattern: `s/@"${versionPrefix}RCT"/@"RCT"/g`, 1395 }, 1396 { 1397 // Unprefix everything that got prefixed twice or more times. 1398 flags: '-Ei', 1399 pattern: `s/(${versionPrefix}){2,}/\\1/g`, 1400 }, 1401 { 1402 flags: '-Ei', 1403 pattern: `s/(#import |__has_include\\()<(Expo|RNReanimated)/\\1<${versionPrefix}\\2/g`, 1404 }, 1405 { 1406 // Unprefix jsc dirname in JSCExecutorFactory.mm 1407 paths: 'CxxBridge', 1408 flags: '-Ei', 1409 pattern: `s/(#import )<${versionPrefix}jsc\\/${versionPrefix}JSCRuntime\.h>/\\1<jsc\\/${versionPrefix}JSCRuntime.h>/g`, 1410 }, 1411 ]; 1412} 1413 1414function _getTransformRulesForDirname(transformRules, dirname) { 1415 return transformRules.filter((rule) => { 1416 return ( 1417 // no paths specified, so apply rule to everything 1418 !rule.paths || 1419 // otherwise, limit this rule to paths specified 1420 dirname.indexOf(rule.paths) !== -1 1421 ); 1422 }); 1423} 1424 1425// TODO: use the one in XDL 1426function _isDirectory(dir) { 1427 try { 1428 if (fs.statSync(dir).isDirectory()) { 1429 return true; 1430 } 1431 1432 return false; 1433 } catch { 1434 return false; 1435 } 1436} 1437 1438// TODO: use the one in XDL 1439async function _transformFileContentsAsync( 1440 filename: string, 1441 transform: (fileString: string) => Promise<string> | string | null 1442) { 1443 const fileString = await fs.readFile(filename, 'utf8'); 1444 const newFileString = await transform(fileString); 1445 if (newFileString !== null) { 1446 await fs.writeFile(filename, newFileString); 1447 } 1448} 1449