From 9565a6a9c4ce244cfbffc1dd1edeb0a9646783db Mon Sep 17 00:00:00 2001 From: Benoit Lemaire Date: Fri, 10 Apr 2020 14:41:31 -0700 Subject: [PATCH] Add react native 61+ support --- .../container/container-integration.md | 4 +- .../platform-parts/manifest/native-modules.md | 29 +- ern-api-impl-gen/src/ApiImpl.ts | 1 + .../src/AndroidGenerator.ts | 42 +- ern-container-gen-ios/package.json | 1 + ern-container-gen-ios/src/IosGenerator.ts | 129 +++++ .../Config/ElectrodeContainer-Debug.xcconfig | 11 +- .../ElectrodeContainer-QADeployment.xcconfig | 11 +- .../ElectrodeContainer-Release.xcconfig | 11 +- .../src/hull/Config/Project-Debug.xcconfig | 2 +- .../xcshareddata/xcschemes/ECDevice.xcscheme | 2 +- .../xcschemes/ElectrodeContainer.xcscheme | 2 +- ern-container-gen/src/generateContainer.ts | 5 +- ern-core/src/Manifest.ts | 71 ++- ern-core/src/android.ts | 41 +- .../injectReactNativeVersionKeysInObject.ts | 2 + ern-core/src/iosUtil.ts | 447 ++++++++++-------- ern-orchestrator/src/buildIosRunner.ts | 9 + ern-orchestrator/src/codepush.ts | 8 +- ern-runner-gen-ios/src/IosRunnerGenerator.ts | 18 +- .../contents.xcworkspacedata | 13 + yarn.lock | 5 + 22 files changed, 623 insertions(+), 241 deletions(-) create mode 100644 ern-runner-gen-ios/src/hull/ErnRunner.xcworkspace/contents.xcworkspacedata diff --git a/docs/platform-parts/container/container-integration.md b/docs/platform-parts/container/container-integration.md index dfa65df7d..52be05d30 100644 --- a/docs/platform-parts/container/container-integration.md +++ b/docs/platform-parts/container/container-integration.md @@ -172,14 +172,14 @@ It is possible to change these defaults, using the `androidConfig` object of `co "containerGenerator": { "androidConfig": { "jsEngine": "jsc", - "jscVersion": "245459", + "jscVersion": "^245459.0.0", "jscVariant": "android-jsc" } } } ``` -`jscVersion` is the version of the JavaScriptCore engine while `jscVariant` is the variant (`android-jsc` or `android-jsc-intl`). +`jscVersion` is the version (fixed or range) of the JavaScriptCore engine while `jscVariant` is the variant (`android-jsc` or `android-jsc-intl`). _Hermes_ diff --git a/docs/platform-parts/manifest/native-modules.md b/docs/platform-parts/manifest/native-modules.md index 68342c910..95c8206ac 100644 --- a/docs/platform-parts/manifest/native-modules.md +++ b/docs/platform-parts/manifest/native-modules.md @@ -118,7 +118,8 @@ This example shows how to replace the string `"RCTBridgeModule.h"` with `= 0.61.0** + +- `podFile` + +Path to a Podfile to use for the Container, relative to the directory containing the plugin config.json file.\ +Can only be set in 'react-native' plugin configuration. + + +- `podspec` + +Path to a podspec file to use for the plugin, relative to the directory containing the plugin config.json file.\ +Can be used in case a native module doesn't have yet an available podspec file or if the podspec file of the native module needs to be different than the one shipped with it. + +- `extraPods` + +Array of extra pod statements that will be injected in the Container Podfile. + +- `requiresManualLinking` + +Boolean flag that indicates whether this plugin requires manual linking.\ +If defined and set to `true`, all plugin directives will be processed. +If not defined (default) or set to false, only `podFile`, `podspec` and `extraPods` directives will be processed.\n +This should only be set to `true` in very rare cases, for plugins that do not support auto linking. diff --git a/ern-api-impl-gen/src/ApiImpl.ts b/ern-api-impl-gen/src/ApiImpl.ts index b5dfc3265..ca101f514 100644 --- a/ern-api-impl-gen/src/ApiImpl.ts +++ b/ern-api-impl-gen/src/ApiImpl.ts @@ -197,6 +197,7 @@ function ernifyPackageJson( }, ], }, + requiresManualLinking: true, }, } } diff --git a/ern-container-gen-android/src/AndroidGenerator.ts b/ern-container-gen-android/src/AndroidGenerator.ts index d9ddc73a8..0d0676f10 100644 --- a/ern-container-gen-android/src/AndroidGenerator.ts +++ b/ern-container-gen-android/src/AndroidGenerator.ts @@ -363,20 +363,24 @@ export default class AndroidGenerator implements ContainerGenerator { this.getJavaScriptEngine(config) === JavaScriptEngine.JSC ? await kax .task('Injecting JavaScript engine [JavaScriptCore]') - .run(this.injectJavaScriptCoreEngine(config)) + .run( + this.injectJavaScriptCoreEngine(config, reactNativePlugin.version) + ) : await kax .task('Injecting JavaScript engine [Hermes]') - .run(this.injectHermesEngine(config)) + .run(this.injectHermesEngine(config, reactNativePlugin.version)) } } public async postBundle( config: ContainerGeneratorConfig, - bundle: BundlingResult + bundle: BundlingResult, + reactNativeVersion: string ) { if (this.getJavaScriptEngine(config) === JavaScriptEngine.HERMES) { const hermesVersion = - config.androidConfig.hermesVersion || android.DEFAULT_HERMES_VERSION + config.androidConfig.hermesVersion || + android.getDefaultHermesVersion(reactNativeVersion) const hermesCli = await kax .task(`Installing hermes-engine@${hermesVersion}`) .run(HermesCli.fromVersion(hermesVersion)) @@ -421,10 +425,19 @@ export default class AndroidGenerator implements ContainerGenerator { * Container. This way, the JSC engine is shipped within the Container and * applications won't crash at runtime when trying to load this library. */ - public async injectJavaScriptCoreEngine(config: ContainerGeneratorConfig) { - const jscVersion = + public async injectJavaScriptCoreEngine( + config: ContainerGeneratorConfig, + reactNativeVersion: string + ) { + let jscVersion = (config.androidConfig && config.androidConfig.jscVersion) || - android.DEFAULT_JSC_VERSION + android.getDefaultJSCVersion(reactNativeVersion) + if (/^\d+$/.test(jscVersion)) { + // For backward compatibility, to avoid breaking clients + // that are already providing a version through config that + // only specifies major excluding minor/patch + jscVersion = `${jscVersion}.0.0` + } const jscVariant = (config.androidConfig && config.androidConfig.jscVariant) || android.DEFAULT_JSC_VARIANT @@ -432,13 +445,14 @@ export default class AndroidGenerator implements ContainerGenerator { try { shell.pushd(workingDir) await yarn.init() - await yarn.add(PackagePath.fromString(`jsc-android@${jscVersion}.0.0`)) + await yarn.add(PackagePath.fromString(`jsc-android@${jscVersion}`)) + const versionMajor = semver.major(semver.coerce(jscVersion)!.version) const jscVersionPath = path.resolve( - `./node_modules/jsc-android/dist/org/webkit/${jscVariant}/r${jscVersion}` + `./node_modules/jsc-android/dist/org/webkit/${jscVariant}/r${versionMajor}` ) const jscAARPath = path.join( jscVersionPath, - `${jscVariant}-r${jscVersion}.aar` + `${jscVariant}-r${versionMajor}.aar` ) return new Promise((resolve, reject) => { const unzipper = new DecompressZip(jscAARPath) @@ -464,10 +478,13 @@ export default class AndroidGenerator implements ContainerGenerator { * Inject hermes engine into the Container * Done in a similar way as injectJavaScriptCoreEngine method */ - public async injectHermesEngine(config: ContainerGeneratorConfig) { + public async injectHermesEngine( + config: ContainerGeneratorConfig, + reactNativeVersion: string + ) { const hermesVersion = (config.androidConfig && config.androidConfig.hermesVersion) || - android.DEFAULT_HERMES_VERSION + android.getDefaultHermesVersion(reactNativeVersion) const workingDir = createTmpDir() try { shell.pushd(workingDir) @@ -559,6 +576,7 @@ export default class AndroidGenerator implements ContainerGenerator { plugins: PackagePath[], outDir: string ): Promise { + const rnVersion = plugins.find(p => p.name === 'react-native')?.version! for (const plugin of plugins) { if (plugin.name === 'react-native') { continue diff --git a/ern-container-gen-ios/package.json b/ern-container-gen-ios/package.json index 2b7846598..d25a12901 100644 --- a/ern-container-gen-ios/package.json +++ b/ern-container-gen-ios/package.json @@ -41,6 +41,7 @@ "fs-extra": "^8.1.0", "fs-readdir-recursive": "^1.1.0", "lodash": "^4.17.14", + "semver": "^5.5.0", "xcode-ern": "^1.0.12" }, "devDependencies": { diff --git a/ern-container-gen-ios/src/IosGenerator.ts b/ern-container-gen-ios/src/IosGenerator.ts index 4ac87650e..8980e7104 100644 --- a/ern-container-gen-ios/src/IosGenerator.ts +++ b/ern-container-gen-ios/src/IosGenerator.ts @@ -1,4 +1,5 @@ import { + childProcess, manifest, iosUtil, injectReactNativeVersionKeysInObject, @@ -9,6 +10,9 @@ import { NativePlatform, kax, PluginConfig, + readPackageJson, + writePackageJson, + yarn, } from 'ern-core' import { ContainerGenerator, @@ -25,6 +29,7 @@ import xcode from 'xcode-ern' import _ from 'lodash' import readDir from 'fs-readdir-recursive' import { Composite } from 'ern-composite-gen' +import semver from 'semver' const ROOT_DIR = process.cwd() const PATH_TO_HULL_DIR = path.join(__dirname, 'hull') @@ -134,6 +139,130 @@ export default class IosGenerator implements ContainerGenerator { ) fs.writeFileSync(projectPath, iosProject.writeSync()) + + if (semver.gte(reactNativePlugin.version!, '0.61.0')) { + // + // Add all native dependencies to package.json dependencies so that + // !use_native_modules can detect them to add their pod to the Podfile + const dependencies = await config.composite.getNativeDependencies({}) + const resDependencies = [ + ...dependencies.thirdPartyInManifest, + ...dependencies.thirdPartyNotInManifest, + ] + const addDependencies: any = {} + resDependencies.forEach(p => { + addDependencies[p.name!] = p.version + }) + + // + // Create package.json in container directory root + // so that native modules pods can be resolved + // by use_native_modules! RN ruby script + const pjsonObj = { + dependencies: addDependencies, + name: 'container', + } + await writePackageJson(config.outDir, pjsonObj) + + // + // Copy all native dependencies from composite node_modules + // to container node_modules so that pods can be found local + // to the container directory + const containerNodeModulesPath = path.join(config.outDir, 'node_modules') + shell.mkdir('-p', containerNodeModulesPath) + resDependencies.forEach(p => { + shell.cp('-rf', p.basePath!, containerNodeModulesPath) + }) + // Add @react-native-community/cli-platform-ios because + // it contains the scripts needed for native modules pods linking + // look in composite to match proper version + const compositeNodeModulesPath = path.join( + config.composite.path, + 'node_modules' + ) + const cliPlatformIosPkg = '@react-native-community/cli-platform-ios' + const cliPlatformIosPkgVersion = ( + await readPackageJson( + path.join(compositeNodeModulesPath, cliPlatformIosPkg) + ) + ).version + shell.pushd(config.outDir) + try { + await yarn.add( + PackagePath.fromString( + `${cliPlatformIosPkg}@${cliPlatformIosPkgVersion}` + ) + ) + } finally { + shell.popd() + } + + // + // Run pod install + shell.pushd(config.outDir) + try { + await kax + .task('Running pod install') + .run(childProcess.spawnp('pod', ['install'])) + } finally { + shell.popd() + } + + // + // Clean node_modules by only keeping the directories that are + // needed for proper container build. + shell.pushd(config.outDir) + try { + // + // Look in the Pods pbxproj for any references to some files + // kepts in some node_module subdirectory (basically react-native + // as well as all native modules) + const f = fs.readFileSync('Pods/Pods.xcodeproj/project.pbxproj', { + encoding: 'utf8', + }) + + // + // Build an array of these directories + const re = RegExp('"../node_modules/([^"]+)"', 'g') + const matches = [] + let match = re.exec(f) + while (match !== null) { + matches.push(match[1]) + match = re.exec(f) + } + const res = matches + .map(r => r.split('/')) + .filter(x => x[0] !== 'react-native') + .map(x => x.join('/')) + .concat('react-native') + + // + // Copy all retained directories from 'node_modules' + // to a new directory 'node_modules_light' + const nodeModulesLightDir = 'node_modules_light' + const nodeModulesDir = 'node_modules' + shell.mkdir('-p', nodeModulesLightDir) + for (const b of res) { + shell.mkdir('-p', path.join(nodeModulesLightDir, b)) + shell.cp( + '-Rf', + path.join(nodeModulesDir, b, '{.*,*}'), + path.join(nodeModulesLightDir, b) + ) + } + // + // Replace the huge 'node_modules' directory with the skimmed one + shell.rm('-rf', nodeModulesDir) + shell.mv(nodeModulesLightDir, nodeModulesDir) + // + // Finally get rid of all android directories to further reduce + // overall 'node_modules' directory size, as they are not needed + // for iOS container builds. + shell.rm('-rf', path.join(nodeModulesDir, '**/android')) + } finally { + shell.popd() + } + } } // Code to keep backward compatibility diff --git a/ern-container-gen-ios/src/hull/Config/ElectrodeContainer-Debug.xcconfig b/ern-container-gen-ios/src/hull/Config/ElectrodeContainer-Debug.xcconfig index 6a743e5e0..f7fb947f9 100644 --- a/ern-container-gen-ios/src/hull/Config/ElectrodeContainer-Debug.xcconfig +++ b/ern-container-gen-ios/src/hull/Config/ElectrodeContainer-Debug.xcconfig @@ -5,6 +5,15 @@ // https://github.com/dempseyatgithub/BuildSettingExtractor // +{{#RN_VERSION_GTE_61}} +#include "Pods/Target Support Files/Pods-ElectrodeContainer/Pods-ElectrodeContainer.debug.xcconfig" +{{/RN_VERSION_GTE_61}} + +{{#RN_VERSION_LT_61}} +HEADER_SEARCH_PATHS = +OTHER_LDFLAGS = -ObjC -lc++ -lz +{{/RN_VERSION_LT_61}} + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES CLANG_ENABLE_MODULES = YES @@ -15,12 +24,10 @@ DYLIB_COMPATIBILITY_VERSION = 1 DYLIB_CURRENT_VERSION = 1 DYLIB_INSTALL_NAME_BASE = @rpath FRAMEWORK_SEARCH_PATHS = $(inherited) $(SRCROOT)/ElectrodeContainer/Frameworks -HEADER_SEARCH_PATHS = INFOPLIST_FILE = ElectrodeContainer/Info.plist INSTALL_PATH = $(LOCAL_LIBRARY_DIR)/Frameworks IPHONEOS_DEPLOYMENT_TARGET = 10.0 LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @loader_path/Frameworks -OTHER_LDFLAGS = -ObjC -lc++ -lz PRODUCT_BUNDLE_IDENTIFIER = com.walmartlabs.ern.ElectrodeContainer PRODUCT_NAME = $(TARGET_NAME) SKIP_INSTALL = YES diff --git a/ern-container-gen-ios/src/hull/Config/ElectrodeContainer-QADeployment.xcconfig b/ern-container-gen-ios/src/hull/Config/ElectrodeContainer-QADeployment.xcconfig index cbb0a36df..9fb9d6e21 100644 --- a/ern-container-gen-ios/src/hull/Config/ElectrodeContainer-QADeployment.xcconfig +++ b/ern-container-gen-ios/src/hull/Config/ElectrodeContainer-QADeployment.xcconfig @@ -5,6 +5,15 @@ // https://github.com/dempseyatgithub/BuildSettingExtractor // +{{#RN_VERSION_GTE_61}} +#include "Pods/Target Support Files/Pods-ElectrodeContainer/Pods-ElectrodeContainer.qadeployment.xcconfig" +{{/RN_VERSION_GTE_61}} + +{{#RN_VERSION_LT_61}} +HEADER_SEARCH_PATHS = +OTHER_LDFLAGS = -ObjC -lc++ -lz +{{/RN_VERSION_LT_61}} + ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES CLANG_ENABLE_MODULES = YES @@ -17,12 +26,10 @@ DYLIB_INSTALL_NAME_BASE = @rpath ENABLE_ON_DEMAND_RESOURCES = NO ENABLE_TESTABILITY = YES FRAMEWORK_SEARCH_PATHS = $(inherited) $(SRCROOT)/ElectrodeContainer/Frameworks -HEADER_SEARCH_PATHS = INFOPLIST_FILE = ElectrodeContainer/Info.plist INSTALL_PATH = $(LOCAL_LIBRARY_DIR)/Frameworks LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @loader_path/Frameworks ONLY_ACTIVE_ARCH = NO -OTHER_LDFLAGS = -ObjC -lc++ -lz PRODUCT_BUNDLE_IDENTIFIER = com.walmartlabs.ern.ElectrodeContainer PRODUCT_NAME = $(TARGET_NAME) SKIP_INSTALL = YES diff --git a/ern-container-gen-ios/src/hull/Config/ElectrodeContainer-Release.xcconfig b/ern-container-gen-ios/src/hull/Config/ElectrodeContainer-Release.xcconfig index dc3d5c4f5..353bbb168 100644 --- a/ern-container-gen-ios/src/hull/Config/ElectrodeContainer-Release.xcconfig +++ b/ern-container-gen-ios/src/hull/Config/ElectrodeContainer-Release.xcconfig @@ -5,6 +5,15 @@ // https://github.com/dempseyatgithub/BuildSettingExtractor // +{{#RN_VERSION_GTE_61}} +#include "Pods/Target Support Files/Pods-ElectrodeContainer/Pods-ElectrodeContainer.release.xcconfig" +{{/RN_VERSION_GTE_61}} + +{{#RN_VERSION_LT_61}} +HEADER_SEARCH_PATHS = +OTHER_LDFLAGS = -ObjC -lc++ -lz +{{/RN_VERSION_LT_61}} + CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES CODE_SIGN_IDENTITY = DEFINES_MODULE = YES @@ -12,12 +21,10 @@ DYLIB_COMPATIBILITY_VERSION = 1 DYLIB_CURRENT_VERSION = 1 DYLIB_INSTALL_NAME_BASE = @rpath FRAMEWORK_SEARCH_PATHS = $(inherited) $(SRCROOT)/ElectrodeContainer/Frameworks -HEADER_SEARCH_PATHS = INFOPLIST_FILE = ElectrodeContainer/Info.plist INSTALL_PATH = $(LOCAL_LIBRARY_DIR)/Frameworks IPHONEOS_DEPLOYMENT_TARGET = 10.0 LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks @loader_path/Frameworks -OTHER_LDFLAGS = -ObjC -lc++ -lz PRODUCT_BUNDLE_IDENTIFIER = com.walmartlabs.ern.ElectrodeContainer PRODUCT_NAME = $(TARGET_NAME) SKIP_INSTALL = YES diff --git a/ern-container-gen-ios/src/hull/Config/Project-Debug.xcconfig b/ern-container-gen-ios/src/hull/Config/Project-Debug.xcconfig index f7e555612..b1f2a2ab4 100644 --- a/ern-container-gen-ios/src/hull/Config/Project-Debug.xcconfig +++ b/ern-container-gen-ios/src/hull/Config/Project-Debug.xcconfig @@ -32,7 +32,7 @@ CLANG_WARN_UNREACHABLE_CODE = YES CODE_SIGN_IDENTITY[sdk=iphoneos*] = iPhone Developer COPY_PHASE_STRIP = NO CURRENT_PROJECT_VERSION = 1 -DEBUG_INFORMATION_FORMAT = dwarf +DEBUG_INFORMATION_FORMAT = dwarf-with-dsym ENABLE_STRICT_OBJC_MSGSEND = YES ENABLE_TESTABILITY = YES GCC_C_LANGUAGE_STANDARD = gnu99 diff --git a/ern-container-gen-ios/src/hull/ElectrodeContainer.xcodeproj/xcshareddata/xcschemes/ECDevice.xcscheme b/ern-container-gen-ios/src/hull/ElectrodeContainer.xcodeproj/xcshareddata/xcschemes/ECDevice.xcscheme index 84db79645..7a90adbdf 100644 --- a/ern-container-gen-ios/src/hull/ElectrodeContainer.xcodeproj/xcshareddata/xcschemes/ECDevice.xcscheme +++ b/ern-container-gen-ios/src/hull/ElectrodeContainer.xcodeproj/xcshareddata/xcschemes/ECDevice.xcscheme @@ -4,7 +4,7 @@ version = "1.3"> + buildImplicitDependencies = "YES"> + buildImplicitDependencies = "YES"> Promise export async function generateContainer( @@ -78,7 +79,7 @@ export async function generateContainer( ) if (postBundle) { - await postBundle(config, bundlingResult) + await postBundle(config, bundlingResult, reactNativePlugin?.version!) } const compositeMiniApps = await config.composite.getMiniApps() diff --git a/ern-core/src/Manifest.ts b/ern-core/src/Manifest.ts index 829a810cb..131629553 100644 --- a/ern-core/src/Manifest.ts +++ b/ern-core/src/Manifest.ts @@ -11,16 +11,21 @@ import { isDependencyApi, isDependencyApiImpl } from './utils' import config from './config' import log from './log' import { NativePlatform } from './NativePlatform' +import semver from 'semver' export type PluginConfig = T extends 'android' ? AndroidPluginConfig : IosPluginConfig +/** + * Represent the plugin structure as seen in the manifest + * i.e the config.json structure + */ export interface ManifestPluginConfig { - android?: AndroidPluginConfig - ios?: IosPluginConfig + android?: PluginConfig<'android'> + ios?: PluginConfig<'ios'> origin?: PluginOrigin - path: string + path?: string } /** @@ -176,6 +181,36 @@ export interface IosPluginConfig extends CommonPluginConfig { * Array of public headers to add to the container */ containerPublicHeader?: string[] + /** + * Path to a Podfile to use for the Container, relative to + * the directory containing the plugin config.json file + * Used for RN >= 0.61.0 only + * Can only be set for 'react-native' plugin configuration + */ + podfile?: string + /** + * Path to a podspec file to use for the plugin, relative to + * the directory containing the plugin config.json file + * Used for RN >= 0.61.0 only + * Can be used in case a native module doesn't have yet an + * available podspec file or if the podspec file of the native + * module needs to be different than the one shipped within + * the native module + */ + podspec?: string + /** + * Array of extra pod statements that will be injected in + * Container Podfile + */ + extraPods?: string[] + /** + * Indicates whether this plugin requires manual linking + * If that's the case, all plugin directives will be processed + * independently of the react native version used + * Otherwise, only `podfile`, `podspec` and `extraPods` directives + * will be processed + */ + requiresManualLinking?: boolean } /** @@ -311,8 +346,14 @@ export interface PluginApplyPatchDirective { patch: string /** * Relative path (from container out directory) from which to run git apply + * Mutually exclusive with inNodeModules */ root: string + /** + * If true, root will be set to root location of the plugin in node nodules + * Mutually exclusive with root + */ + inNodeModules?: boolean } /** @@ -653,7 +694,9 @@ export class Manifest { // Add default value (convention) for Android subsection for missing fields if (platform === 'android' && result.android) { - result.android.root = result.android.root ?? 'android' + const res = result.android + + res.root = res.root ?? 'android' const matchedFiles = shell .find(pluginConfigPath) @@ -662,7 +705,7 @@ export class Manifest { }) if (matchedFiles && matchedFiles.length === 1) { const pluginHookClass = path.basename(matchedFiles[0], '.java') - result.android.pluginHook = { + res.pluginHook = { configurable: false, name: pluginHookClass, } @@ -671,13 +714,15 @@ export class Manifest { .readFileSync(matchedFiles[0], 'utf-8') .includes('public static class Config') ) { - result.android.pluginHook.configurable = true + res.pluginHook.configurable = true } } - result.android.path = pluginConfigPath - return result.android + res.path = pluginConfigPath + return res } else if (platform === 'ios' && result.ios) { - result.ios.root = result.ios.root ?? 'ios' + const res = result.ios + + res.root = res.root ?? 'ios' const matchedHeaderFiles = shell .find(pluginConfigPath) @@ -697,13 +742,13 @@ export class Manifest { matchedSourceFiles.length === 1 ) { const pluginHookClass = path.basename(matchedHeaderFiles[0], '.h') - result.ios.pluginHook = { + res.pluginHook = { configurable: true, name: pluginHookClass, } } - result.ios.path = pluginConfigPath - return result.ios + res.path = pluginConfigPath + return res } } @@ -818,6 +863,7 @@ export class Manifest { }, ], }, + requiresManualLinking: true, root: 'ios', } } @@ -850,6 +896,7 @@ export class Manifest { }, ], }, + requiresManualLinking: true, root: 'ios', } } diff --git a/ern-core/src/android.ts b/ern-core/src/android.ts index ba08a8e62..b2047fa6c 100644 --- a/ern-core/src/android.ts +++ b/ern-core/src/android.ts @@ -7,6 +7,7 @@ import log from './log' import { execp, spawnp } from './childProcess' import os from 'os' import kax from './kax' +import semver from 'semver' // ============================================================================== // Default value for android build config @@ -18,9 +19,7 @@ export const DEFAULT_ANDROIDX_LIFECYCLE_EXTENSIONS_VERSION = '2.1.0' export const DEFAULT_BUILD_TOOLS_VERSION = '28.0.3' export const DEFAULT_COMPILE_SDK_VERSION = '28' export const DEFAULT_GRADLE_DISTRIBUTION_VERSION = '5.4.1' -export const DEFAULT_HERMES_VERSION = '0.2.1' export const DEFAULT_JSC_VARIANT = 'android-jsc' -export const DEFAULT_JSC_VERSION = '245459' export const DEFAULT_MIN_SDK_VERSION = '19' export const DEFAULT_SUPPORT_LIBRARY_VERSION = '28.0.0' export const DEFAULT_TARGET_SDK_VERSION = '28' @@ -406,3 +405,41 @@ export function androidEmulatorPath(): string { } return 'emulator' } + +/** + * Returns the default Hermes engine (hermes-engine) package version used by a given React Native version. + * Only works for versions of RN >= 0.60.0 as hermes-engine package was introduced in this version. + */ +export function getDefaultHermesVersion( + reactNativeVersion: string +): string | never { + if (semver.gte(reactNativeVersion, '0.62.0')) { + return '~0.4.0' + } else if (semver.gte(reactNativeVersion, '0.60.0')) { + return '^0.2.1' + } else { + throw new Error( + 'This function can only be called for versions of React Native >= 0.60.0' + ) + } +} + +/** + * Returns the default JavaScriptCore engine version used + * by a given React Native version. + * Only works for versions of RN >= 0.60.0 as dynamic jsc-android + * package was introduced in this version. + */ +export function getDefaultJSCVersion( + reactNativeVersion: string +): string | never { + if (semver.gte(reactNativeVersion, '0.61.0')) { + return '^245459.0.0' + } else if (semver.gte(reactNativeVersion, '0.60.0')) { + return '245459.0.0' + } else { + throw new Error( + 'This function can only be called for versions of React Native >= 0.60.0' + ) + } +} diff --git a/ern-core/src/injectReactNativeVersionKeysInObject.ts b/ern-core/src/injectReactNativeVersionKeysInObject.ts index 8d34d87da..dd85778a3 100644 --- a/ern-core/src/injectReactNativeVersionKeysInObject.ts +++ b/ern-core/src/injectReactNativeVersionKeysInObject.ts @@ -9,9 +9,11 @@ export function injectReactNativeVersionKeysInObject( RN_VERSION_GTE_54: semver.gte(reactNativeVersion, '0.54.0'), RN_VERSION_GTE_59: semver.gte(reactNativeVersion, '0.59.0'), RN_VERSION_GTE_60_1: semver.gte(reactNativeVersion, '0.60.1'), + RN_VERSION_GTE_61: semver.gte(reactNativeVersion, '0.61.0'), RN_VERSION_LT_54: semver.lt(reactNativeVersion, '0.54.0'), RN_VERSION_LT_58: semver.lt(reactNativeVersion, '0.58.0-rc.2'), RN_VERSION_LT_59: semver.lt(reactNativeVersion, '0.59.0'), + RN_VERSION_LT_61: semver.lt(reactNativeVersion, '0.61.0'), reactNativeVersion, }) } diff --git a/ern-core/src/iosUtil.ts b/ern-core/src/iosUtil.ts index dac76d0a5..f2bdf4ba5 100644 --- a/ern-core/src/iosUtil.ts +++ b/ern-core/src/iosUtil.ts @@ -5,7 +5,7 @@ import shell from './shell' import { manifest, PluginConfig } from './Manifest' import handleCopyDirective from './handleCopyDirective' import log from './log' -import { isDependencyJsApiImpl } from './utils' +import { isDependencyJsApiImpl, extractJsApiImplementations } from './utils' import path from 'path' import xcode from 'xcode-ern' import fs from 'fs-extra' @@ -40,14 +40,22 @@ export async function fillProjectHull( if (mustacheView) { log.debug(`iOS: reading template files to be rendered for plugins`) - const files = readDir(pathSpec.outputDir) + const a = path.join(pathSpec.outputDir, 'ElectrodeContainer') + const b = path.join(pathSpec.outputDir, 'config') + const files = [ + ...readDir(a).map(x => path.join(a, x)), + ...readDir(b).map(x => path.join(b, x)), + ] for (const file of files) { - if (file.endsWith('.h') || file.endsWith('.m')) { - const pathToOutputFile = path.join(pathSpec.outputDir, file) + if ( + file.endsWith('.h') || + file.endsWith('.m') || + file.endsWith('.xcconfig') + ) { await mustacheUtils.mustacheRenderToOutputFileUsingTemplateFile( - pathToOutputFile, + file, mustacheView, - pathToOutputFile + file ) } } @@ -67,6 +75,10 @@ export async function fillProjectHull( const injectPluginsTaskMsg = 'Injecting Native Dependencies' const injectPluginsKaxTask = kax.task(injectPluginsTaskMsg) + const rnVersion = plugins.find(p => p.name === 'react-native')?.version! + const additionalPods = [] + const destPodfilePath = path.join(pathSpec.outputDir, 'Podfile') + for (const plugin of plugins) { const pluginSourcePath = composite ? plugin.basePath @@ -109,6 +121,7 @@ export async function fillProjectHull( projectName: projectSpec.projectName, }) ) + pluginConfig!.requiresManualLinking = true } } @@ -117,251 +130,295 @@ export async function fillProjectHull( const { applyPatch, copy, + extraPods, + pbxproj, + podfile, + podspec, replaceInFile, + requiresManualLinking, setBuildSettings, - pbxproj, } = pluginConfig! - if (copy) { - for (const c of copy) { - if (switchToOldDirectoryStructure(pluginSourcePath, c.source)) { - log.debug( - `Handling copy directive: Falling back to old directory structure for API(Backward compatibility)` - ) - c.source = path.normalize('IOS/IOS/Classes/SwaggersAPIs') + if (requiresManualLinking) { + if (copy) { + for (const c of copy) { + if (switchToOldDirectoryStructure(pluginSourcePath, c.source)) { + log.debug( + `Handling copy directive: Falling back to old directory structure for API(Backward compatibility)` + ) + c.source = path.normalize('IOS/IOS/Classes/SwaggersAPIs') + } } + handleCopyDirective(pluginSourcePath, pathSpec.outputDir, copy) } - handleCopyDirective(pluginSourcePath, pathSpec.outputDir, copy) - } - if (replaceInFile) { - for (const r of replaceInFile) { - const pathToFile = path.join(pathSpec.outputDir, r.path) - const fileContent = await fs.readFile(pathToFile, 'utf8') - const patchedFileContent = fileContent.replace( - RegExp(r.string, 'g'), - r.replaceWith - ) - await fs.writeFile(pathToFile, patchedFileContent, { - encoding: 'utf8', - }) + if (replaceInFile) { + for (const r of replaceInFile) { + const pathToFile = path.join(pathSpec.outputDir, r.path) + const fileContent = await fs.readFile(pathToFile, 'utf8') + const patchedFileContent = fileContent.replace( + RegExp(r.string, 'g'), + r.replaceWith + ) + await fs.writeFile(pathToFile, patchedFileContent, { + encoding: 'utf8', + }) + } } - } - if (setBuildSettings) { - for (const s of setBuildSettings) { - const pathToPbxProj = path.join(pathSpec.outputDir, s.path) - // Add any missing section in the target pbxproj - // This is necessary for proper parsing and modification of - // the pbxproj with the xcode-ern library - xcode.pbxProjFileUtils().addMissingSectionsToPbxProj(pathToPbxProj) - const iosProj = await getIosProject(projectPath) - const buildSettings = - s.buildSettings instanceof Array - ? s.buildSettings - : [s.buildSettings] - for (const buildSettingsEntry of buildSettings) { - for (const buildType of buildSettingsEntry.configurations) { - for (const key of Object.keys(buildSettingsEntry.settings)) { - iosProj.updateBuildProperty( - key, - buildSettingsEntry.settings[key], - buildType - ) + if (setBuildSettings) { + for (const s of setBuildSettings) { + const pathToPbxProj = path.join(pathSpec.outputDir, s.path) + // Add any missing section in the target pbxproj + // This is necessary for proper parsing and modification of + // the pbxproj with the xcode-ern library + xcode.pbxProjFileUtils().addMissingSectionsToPbxProj(pathToPbxProj) + const iosProj = await getIosProject(projectPath) + const buildSettings = + s.buildSettings instanceof Array + ? s.buildSettings + : [s.buildSettings] + for (const buildSettingsEntry of buildSettings) { + for (const buildType of buildSettingsEntry.configurations) { + for (const key of Object.keys(buildSettingsEntry.settings)) { + iosProj.updateBuildProperty( + key, + buildSettingsEntry.settings[key], + buildType + ) + } } } + fs.writeFileSync(pathToPbxProj, iosProj.writeSync()) } - fs.writeFileSync(pathToPbxProj, iosProj.writeSync()) } - } - if (applyPatch) { - const { patch, root } = applyPatch - if (!patch) { - throw new Error('Missing "patch" property in "applyPatch" object') - } - if (!root) { - throw new Error('Missing "root" property in "applyPatch" object') + if (applyPatch) { + const { patch, root, inNodeModules } = applyPatch + if (!patch) { + throw new Error('Missing "patch" property in "applyPatch" object') + } + if (!root && !inNodeModules) { + throw new Error( + 'If "inNodeModules" is not set, "root" property must be set in "applyPatch" object' + ) + } + const [patchFile, rootDir] = [ + path.join(pluginConfig!.path!, patch), + inNodeModules + ? pluginSourcePath + : path.join(pathSpec.outputDir, root), + ] + await gitApply({ patchFile, rootDir }) } - const [patchFile, rootDir] = [ - path.join(pluginConfig!.path!, patch), - path.join(pathSpec.outputDir, root), - ] - await gitApply({ patchFile, rootDir }) - } - if (pbxproj) { - const { - addEmbeddedFramework, - addFile, - addFramework, - addFrameworkReference, - addFrameworkSearchPath, - addHeader, - addHeaderSearchPath, - addProject, - addSource, - addStaticLibrary, - } = pbxproj - - if (addSource) { - for (const source of addSource) { - // Multiple source files - if (source.from) { - if ( - switchToOldDirectoryStructure(pluginSourcePath, source.from) - ) { - log.debug( - `Source Copy: Falling back to old directory structure for API(Backward compatibility)` + if (pbxproj) { + const { + addEmbeddedFramework, + addFile, + addFramework, + addFrameworkReference, + addFrameworkSearchPath, + addHeader, + addHeaderSearchPath, + addProject, + addSource, + addStaticLibrary, + } = pbxproj + + if (addSource) { + for (const source of addSource) { + // Multiple source files + if (source.from) { + if ( + switchToOldDirectoryStructure(pluginSourcePath, source.from) + ) { + log.debug( + `Source Copy: Falling back to old directory structure for API(Backward compatibility)` + ) + source.from = path.normalize( + 'IOS/IOS/Classes/SwaggersAPIs/*.swift' + ) + } + const relativeSourcePath = path.dirname(source.from) + const pathToSourceFiles = path.join( + pluginSourcePath, + relativeSourcePath ) - source.from = path.normalize( - 'IOS/IOS/Classes/SwaggersAPIs/*.swift' + const fileNames = _.filter( + await fs.readdir(pathToSourceFiles), + f => f.endsWith(path.extname(source.from!)) ) - } - const relativeSourcePath = path.dirname(source.from) - const pathToSourceFiles = path.join( - pluginSourcePath, - relativeSourcePath - ) - const fileNames = _.filter( - await fs.readdir(pathToSourceFiles), - f => f.endsWith(path.extname(source.from!)) - ) - for (const fileName of fileNames) { - const fileNamePath = path.join(source.path, fileName) + for (const fileName of fileNames) { + const fileNamePath = path.join(source.path, fileName) + iosProject.addSourceFile( + fileNamePath, + null, + iosProject.findPBXGroupKey({ name: source.group }) + ) + } + } else { + // Single source file iosProject.addSourceFile( - fileNamePath, + source.path, null, iosProject.findPBXGroupKey({ name: source.group }) ) } - } else { - // Single source file - iosProject.addSourceFile( - source.path, - null, - iosProject.findPBXGroupKey({ name: source.group }) - ) } } - } - if (addHeader) { - for (const header of addHeader) { - // Multiple header files - if (header.from) { - if ( - switchToOldDirectoryStructure(pluginSourcePath, header.from) - ) { - log.debug( - `Header Copy: Falling back to old directory structure for API(Backward compatibility)` + if (addHeader) { + for (const header of addHeader) { + // Multiple header files + if (header.from) { + if ( + switchToOldDirectoryStructure(pluginSourcePath, header.from) + ) { + log.debug( + `Header Copy: Falling back to old directory structure for API(Backward compatibility)` + ) + header.from = path.normalize( + 'IOS/IOS/Classes/SwaggersAPIs/*.swift' + ) + } + const relativeHeaderPath = path.dirname(header.from) + const pathToHeaderFiles = path.join( + pluginSourcePath, + relativeHeaderPath ) - header.from = path.normalize( - 'IOS/IOS/Classes/SwaggersAPIs/*.swift' + const fileNames = _.filter( + await fs.readdir(pathToHeaderFiles), + f => f.endsWith(path.extname(header.from!)) ) - } - const relativeHeaderPath = path.dirname(header.from) - const pathToHeaderFiles = path.join( - pluginSourcePath, - relativeHeaderPath - ) - const fileNames = _.filter( - await fs.readdir(pathToHeaderFiles), - f => f.endsWith(path.extname(header.from!)) - ) - for (const fileName of fileNames) { - const fileNamePath = path.join(header.path, fileName) + for (const fileName of fileNames) { + const fileNamePath = path.join(header.path, fileName) + iosProject.addHeaderFile( + fileNamePath, + { public: header.public }, + iosProject.findPBXGroupKey({ name: header.group }) + ) + } + } else { + const headerPath = header.path iosProject.addHeaderFile( - fileNamePath, + headerPath, { public: header.public }, iosProject.findPBXGroupKey({ name: header.group }) ) } - } else { - const headerPath = header.path - iosProject.addHeaderFile( - headerPath, - { public: header.public }, - iosProject.findPBXGroupKey({ name: header.group }) + } + } + + if (addFile) { + for (const file of addFile) { + iosProject.addFile( + file.path, + iosProject.findPBXGroupKey({ name: file.group }) ) + // Add target dep in any case for now, will rework later + iosProject.addTargetDependency(target, [ + `"${path.basename(file.path)}"`, + ]) } } - } - if (addFile) { - for (const file of addFile) { - iosProject.addFile( - file.path, - iosProject.findPBXGroupKey({ name: file.group }) - ) - // Add target dep in any case for now, will rework later - iosProject.addTargetDependency(target, [ - `"${path.basename(file.path)}"`, - ]) + if (addFramework) { + for (const framework of addFramework) { + iosProject.addFramework(framework, { + customFramework: true, + }) + } } - } - if (addFramework) { - for (const framework of addFramework) { - iosProject.addFramework(framework, { - customFramework: true, - }) + if (addProject) { + for (const project of addProject) { + const projectAbsolutePath = path.join( + librariesPath, + project.path, + 'project.pbxproj' + ) + const options = { + addAsTargetDependency: project.addAsTargetDependency, + frameworks: project.frameworks, + projectAbsolutePath, + staticLibs: project.staticLibs, + } + iosProject.addProject( + project.path, + project.group, + target, + options + ) + } } - } - if (addProject) { - for (const project of addProject) { - const projectAbsolutePath = path.join( - librariesPath, - project.path, - 'project.pbxproj' - ) - const options = { - addAsTargetDependency: project.addAsTargetDependency, - frameworks: project.frameworks, - projectAbsolutePath, - staticLibs: project.staticLibs, + if (addStaticLibrary) { + for (const lib of addStaticLibrary) { + iosProject.addStaticLibrary(lib) } - iosProject.addProject(project.path, project.group, target, options) } - } - if (addStaticLibrary) { - for (const lib of addStaticLibrary) { - iosProject.addStaticLibrary(lib) + if (addHeaderSearchPath) { + for (const p of addHeaderSearchPath) { + iosProject.addToHeaderSearchPaths(p) + } } - } - if (addHeaderSearchPath) { - for (const p of addHeaderSearchPath) { - iosProject.addToHeaderSearchPaths(p) + if (addFrameworkReference) { + for (const frameworkReference of addFrameworkReference) { + iosProject.addFramework(frameworkReference, { + customFramework: true, + }) + } } - } - if (addFrameworkReference) { - for (const frameworkReference of addFrameworkReference) { - iosProject.addFramework(frameworkReference, { - customFramework: true, - }) + if (addEmbeddedFramework) { + for (const framework of addEmbeddedFramework) { + iosProject.addFramework(framework, { + customFramework: true, + embed: true, + }) + } } - } - if (addEmbeddedFramework) { - for (const framework of addEmbeddedFramework) { - iosProject.addFramework(framework, { - customFramework: true, - embed: true, - }) + if (addFrameworkSearchPath) { + for (const p of addFrameworkSearchPath) { + iosProject.addToFrameworkSearchPaths(p) + } } } + } - if (addFrameworkSearchPath) { - for (const p of addFrameworkSearchPath) { - iosProject.addToFrameworkSearchPaths(p) - } + if (podfile) { + if (plugin.name !== 'react-native') { + throw new Error( + `ios.podfile directive can only be used for react-native configuration` + ) } + const sourcePodfilePath = path.join(pluginConfig!.path!, podfile) + shell.cp(sourcePodfilePath, destPodfilePath) + } + + if (podspec) { + const sourcePodspecPath = path.join(pluginConfig!.path!, podspec) + const destPodspecPath = path.join(pathSpec.outputDir, podspec) + shell.cp(sourcePodspecPath, destPodspecPath) + } + + if (extraPods) { + additionalPods.push(...extraPods) } } + + await mustacheUtils.mustacheRenderToOutputFileUsingTemplateFile( + destPodfilePath, + { + extraPods: additionalPods.reduce((acc, cur) => `${acc}\n ${cur}`, ''), + }, + destPodfilePath + ) + injectPluginsKaxTask.succeed(injectPluginsTaskMsg) log.debug(`[=== Completed framework generation ===]`) diff --git a/ern-orchestrator/src/buildIosRunner.ts b/ern-orchestrator/src/buildIosRunner.ts index 7238d64ad..58bd9acd8 100644 --- a/ern-orchestrator/src/buildIosRunner.ts +++ b/ern-orchestrator/src/buildIosRunner.ts @@ -1,11 +1,20 @@ import { log } from 'ern-core' import { spawn } from 'child_process' +import fs from 'fs-extra' +import path from 'path' export async function buildIosRunner(pathToIosRunner: string, udid: string) { return new Promise((resolve, reject) => { + const extraBuildOptions = fs.existsSync( + path.join(pathToIosRunner, 'ErnRunner.xcworkspace') + ) + ? [`-workspace`, `ErnRunner.xcworkspace`] + : [] + const xcodebuildProc = spawn( 'xcodebuild', [ + ...extraBuildOptions, `-scheme`, 'ErnRunner', 'build', diff --git a/ern-orchestrator/src/codepush.ts b/ern-orchestrator/src/codepush.ts index 2ba283365..768884715 100644 --- a/ern-orchestrator/src/codepush.ts +++ b/ern-orchestrator/src/codepush.ts @@ -307,6 +307,11 @@ export async function performCodePushOtaUpdate( throw new Error('react-native-code-push plugin is not in native app !') } + const reactNative = _.find(plugins, p => p.name === 'react-native') + if (!codePushPlugin) { + throw new Error('react-native is not in native app !') + } + const tmpWorkingDir = createTmpDir() const miniAppsNativeDependenciesVersionAligned = await compatibility.areCompatible( @@ -433,7 +438,8 @@ export async function performCodePushOtaUpdate( const isHermesEnabled = conf?.androidConfig?.jsEngine === 'hermes' if (isHermesEnabled) { const hermesVersion = - conf.androidConfig.hermesVersion ?? android.DEFAULT_HERMES_VERSION + conf.androidConfig.hermesVersion ?? + android.getDefaultHermesVersion(reactNative?.version!) const hermesCli = await kax .task(`Installing hermes-engine@${hermesVersion}`) .run(HermesCli.fromVersion(hermesVersion)) diff --git a/ern-runner-gen-ios/src/IosRunnerGenerator.ts b/ern-runner-gen-ios/src/IosRunnerGenerator.ts index fe26abf65..15886b1fa 100644 --- a/ern-runner-gen-ios/src/IosRunnerGenerator.ts +++ b/ern-runner-gen-ios/src/IosRunnerGenerator.ts @@ -21,16 +21,26 @@ export default class IosRunerGenerator implements RunnerGenerator { const mustacheView = this.createMustacheView({ config }) shell.cp('-R', path.join(runnerHullPath, '*'), config.outDir) + const filesToMustache = [ - path.join(config.outDir, 'ErnRunner/RunnerConfig.m'), - path.join(config.outDir, 'ErnRunner.xcodeproj/project.pbxproj'), + 'ErnRunner/RunnerConfig.m', + 'ErnRunner.xcodeproj/project.pbxproj', ] + if ((mustacheView as any).RN_VERSION_LT_61) { + // Delete ErnRunner.xcworkspace directory as it is only needed for RN61+ + shell.rm('-rf', path.join(config.outDir, 'ErnRunner.xcworkspace')) + } else { + // Otherwise keep it and include the xcworkspacedata file for mustache + // processing as it contains some mustache template placeholders + filesToMustache.push('ErnRunner.xcworkspace/contents.xcworkspacedata') + } + for (const file of filesToMustache) { await mustacheUtils.mustacheRenderToOutputFileUsingTemplateFile( - file, + path.join(config.outDir, file), mustacheView, - file + path.join(config.outDir, file) ) } } diff --git a/ern-runner-gen-ios/src/hull/ErnRunner.xcworkspace/contents.xcworkspacedata b/ern-runner-gen-ios/src/hull/ErnRunner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..19f1a5de3 --- /dev/null +++ b/ern-runner-gen-ios/src/hull/ErnRunner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,13 @@ + + + + + + + + + diff --git a/yarn.lock b/yarn.lock index cfb1a6751..6bc8806b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6052,6 +6052,11 @@ mkdirp@0.5.1, mkdirp@0.5.x, mkdirp@0.x.x, mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~ dependencies: minimist "0.0.8" +mkdirp@~1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" + integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== + mkpath@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/mkpath/-/mkpath-0.1.0.tgz#7554a6f8d871834cc97b5462b122c4c124d6de91"