541 lines
20 KiB
Groovy
541 lines
20 KiB
Groovy
import groovy.json.JsonSlurper
|
|
import org.gradle.initialization.DefaultSettings
|
|
import org.apache.tools.ant.taskdefs.condition.Os
|
|
|
|
def generatedFileName = "PackageList.java"
|
|
def generatedFilePackage = "com.facebook.react"
|
|
def generatedFileContentsTemplate = """
|
|
package $generatedFilePackage;
|
|
|
|
import android.app.Application;
|
|
import android.content.Context;
|
|
import android.content.res.Resources;
|
|
|
|
import com.facebook.react.ReactPackage;
|
|
import com.facebook.react.shell.MainPackageConfig;
|
|
import com.facebook.react.shell.MainReactPackage;
|
|
import java.util.Arrays;
|
|
import java.util.ArrayList;
|
|
|
|
{{ packageImports }}
|
|
|
|
public class PackageList {
|
|
private Application application;
|
|
private ReactNativeHost reactNativeHost;
|
|
private MainPackageConfig mConfig;
|
|
|
|
public PackageList(ReactNativeHost reactNativeHost) {
|
|
this(reactNativeHost, null);
|
|
}
|
|
|
|
public PackageList(Application application) {
|
|
this(application, null);
|
|
}
|
|
|
|
public PackageList(ReactNativeHost reactNativeHost, MainPackageConfig config) {
|
|
this.reactNativeHost = reactNativeHost;
|
|
mConfig = config;
|
|
}
|
|
|
|
public PackageList(Application application, MainPackageConfig config) {
|
|
this.reactNativeHost = null;
|
|
this.application = application;
|
|
mConfig = config;
|
|
}
|
|
|
|
private ReactNativeHost getReactNativeHost() {
|
|
return this.reactNativeHost;
|
|
}
|
|
|
|
private Resources getResources() {
|
|
return this.getApplication().getResources();
|
|
}
|
|
|
|
private Application getApplication() {
|
|
if (this.reactNativeHost == null) return this.application;
|
|
return this.reactNativeHost.getApplication();
|
|
}
|
|
|
|
private Context getApplicationContext() {
|
|
return this.getApplication().getApplicationContext();
|
|
}
|
|
|
|
public ArrayList<ReactPackage> getPackages() {
|
|
return new ArrayList<>(Arrays.<ReactPackage>asList(
|
|
new MainReactPackage(mConfig){{ packageClassInstances }}
|
|
));
|
|
}
|
|
}
|
|
"""
|
|
|
|
def cmakeTemplate = """# This code was generated by [React Native CLI](https://www.npmjs.com/package/@react-native-community/cli)
|
|
|
|
cmake_minimum_required(VERSION 3.13)
|
|
set(CMAKE_VERBOSE_MAKEFILE on)
|
|
|
|
{{ libraryIncludes }}
|
|
|
|
set(AUTOLINKED_LIBRARIES
|
|
{{ libraryModules }}
|
|
)
|
|
"""
|
|
|
|
def rncliCppTemplate = """/**
|
|
* This code was generated by [React Native CLI](https://www.npmjs.com/package/@react-native-community/cli).
|
|
*
|
|
* Do not edit this file as changes may cause incorrect behavior and will be lost
|
|
* once the code is regenerated.
|
|
*
|
|
*/
|
|
|
|
#include "rncli.h"
|
|
{{ rncliCppIncludes }}
|
|
|
|
namespace facebook {
|
|
namespace react {
|
|
|
|
{{ rncliReactLegacyComponentNames }}
|
|
|
|
std::shared_ptr<TurboModule> rncli_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams ¶ms) {
|
|
{{ rncliCppModuleProviders }}
|
|
return nullptr;
|
|
}
|
|
|
|
void rncli_registerProviders(std::shared_ptr<ComponentDescriptorProviderRegistry const> providerRegistry) {
|
|
{{ rncliCppComponentDescriptors }}
|
|
{{ rncliReactLegacyComponentDescriptors }}
|
|
return;
|
|
}
|
|
|
|
} // namespace react
|
|
} // namespace facebook
|
|
"""
|
|
|
|
def rncliHTemplate = """/**
|
|
* This code was generated by [React Native CLI](https://www.npmjs.com/package/@react-native-community/cli).
|
|
*
|
|
* Do not edit this file as changes may cause incorrect behavior and will be lost
|
|
* once the code is regenerated.
|
|
*
|
|
*/
|
|
|
|
#pragma once
|
|
|
|
#include <ReactCommon/JavaTurboModule.h>
|
|
#include <ReactCommon/TurboModule.h>
|
|
#include <jsi/jsi.h>
|
|
#include <react/renderer/componentregistry/ComponentDescriptorProviderRegistry.h>
|
|
|
|
namespace facebook {
|
|
namespace react {
|
|
|
|
std::shared_ptr<TurboModule> rncli_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams ¶ms);
|
|
void rncli_registerProviders(std::shared_ptr<ComponentDescriptorProviderRegistry const> providerRegistry);
|
|
|
|
} // namespace react
|
|
} // namespace facebook
|
|
"""
|
|
|
|
class ReactNativeModules {
|
|
private Logger logger
|
|
private String packageName
|
|
private File root
|
|
private ArrayList<HashMap<String, String>> reactNativeModules
|
|
private ArrayList<String> unstable_reactLegacyComponentNames
|
|
private HashMap<String, ArrayList> reactNativeModulesBuildVariants
|
|
|
|
private static String LOG_PREFIX = ":ReactNative:"
|
|
|
|
ReactNativeModules(Logger logger, File root) {
|
|
this.logger = logger
|
|
this.root = root
|
|
|
|
def (nativeModules, reactNativeModulesBuildVariants, androidProject) = this.getReactNativeConfig()
|
|
this.reactNativeModules = nativeModules
|
|
this.reactNativeModulesBuildVariants = reactNativeModulesBuildVariants
|
|
this.packageName = androidProject["packageName"]
|
|
this.unstable_reactLegacyComponentNames = androidProject["unstable_reactLegacyComponentNames"]
|
|
}
|
|
|
|
/**
|
|
* Include the react native modules android projects and specify their project directory
|
|
*/
|
|
void addReactNativeModuleProjects(DefaultSettings defaultSettings) {
|
|
reactNativeModules.forEach { reactNativeModule ->
|
|
String nameCleansed = reactNativeModule["nameCleansed"]
|
|
String androidSourceDir = reactNativeModule["androidSourceDir"]
|
|
defaultSettings.include(":${nameCleansed}")
|
|
defaultSettings.project(":${nameCleansed}").projectDir = new File("${androidSourceDir}")
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds the react native modules as dependencies to the users `app` project
|
|
*/
|
|
void addReactNativeModuleDependencies(Project appProject) {
|
|
reactNativeModules.forEach { reactNativeModule ->
|
|
def nameCleansed = reactNativeModule["nameCleansed"]
|
|
def dependencyConfiguration = reactNativeModule["dependencyConfiguration"]
|
|
appProject.dependencies {
|
|
if (reactNativeModulesBuildVariants.containsKey(nameCleansed)) {
|
|
reactNativeModulesBuildVariants
|
|
.get(nameCleansed)
|
|
.forEach { buildVariant ->
|
|
if(dependencyConfiguration != null) {
|
|
"${buildVariant}${dependencyConfiguration}"
|
|
} else {
|
|
"${buildVariant}Implementation" project(path: ":${nameCleansed}")
|
|
}
|
|
}
|
|
} else {
|
|
if(dependencyConfiguration != null) {
|
|
"${dependencyConfiguration}"
|
|
} else {
|
|
implementation project(path: ":${nameCleansed}")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Code-gen a java file with all the detected ReactNativePackage instances automatically added
|
|
*
|
|
* @param outputDir
|
|
* @param generatedFileName
|
|
* @param generatedFileContentsTemplate
|
|
*/
|
|
void generatePackagesFile(File outputDir, String generatedFileName, String generatedFileContentsTemplate) {
|
|
ArrayList<HashMap<String, String>> packages = this.reactNativeModules
|
|
String packageName = this.packageName
|
|
|
|
String packageImports = ""
|
|
String packageClassInstances = ""
|
|
|
|
if (packages.size() > 0) {
|
|
def interpolateDynamicValues = {
|
|
it
|
|
// Before adding the package replacement mechanism,
|
|
// BuildConfig and R classes were imported automatically
|
|
// into the scope of the file. We want to replace all
|
|
// non-FQDN references to those classes with the package name
|
|
// of the MainApplication.
|
|
//
|
|
// We want to match "R" or "BuildConfig":
|
|
// - new Package(R.string…),
|
|
// - Module.configure(BuildConfig);
|
|
// ^ hence including (BuildConfig|R)
|
|
// but we don't want to match "R":
|
|
// - new Package(getResources…),
|
|
// - new PackageR…,
|
|
// - new Royal…,
|
|
// ^ hence excluding \w before and after matches
|
|
// and "BuildConfig" that has FQDN reference:
|
|
// - Module.configure(com.acme.BuildConfig);
|
|
// ^ hence excluding . before the match.
|
|
.replaceAll(~/([^.\w])(BuildConfig|R)([^\w])/, {
|
|
wholeString, prefix, className, suffix ->
|
|
"${prefix}${packageName}.${className}${suffix}"
|
|
})
|
|
}
|
|
packageImports = packages.collect {
|
|
"// ${it.name}\n${interpolateDynamicValues(it.packageImportPath)}"
|
|
}.join('\n')
|
|
packageClassInstances = ",\n " + packages.collect {
|
|
interpolateDynamicValues(it.packageInstance)
|
|
}.join(",\n ")
|
|
}
|
|
|
|
String generatedFileContents = generatedFileContentsTemplate
|
|
.replace("{{ packageImports }}", packageImports)
|
|
.replace("{{ packageClassInstances }}", packageClassInstances)
|
|
|
|
outputDir.mkdirs()
|
|
final FileTreeBuilder treeBuilder = new FileTreeBuilder(outputDir)
|
|
treeBuilder.file(generatedFileName).newWriter().withWriter { w ->
|
|
w << generatedFileContents
|
|
}
|
|
}
|
|
|
|
void generateCmakeFile(File outputDir, String generatedFileName, String generatedFileContentsTemplate) {
|
|
ArrayList<HashMap<String, String>> packages = this.reactNativeModules
|
|
String packageName = this.packageName
|
|
String codegenLibPrefix = "react_codegen_"
|
|
String libraryIncludes = ""
|
|
String libraryModules = ""
|
|
|
|
if (packages.size() > 0) {
|
|
libraryIncludes = packages.collect {
|
|
if (it.libraryName != null && it.cmakeListsPath != null) {
|
|
// If user provided a custom cmakeListsPath, let's honor it.
|
|
String nativeFolderPath = it.cmakeListsPath.replace("CMakeLists.txt", "")
|
|
"add_subdirectory($nativeFolderPath ${it.libraryName}_autolinked_build)"
|
|
} else {
|
|
null
|
|
}
|
|
}.minus(null).join('\n')
|
|
libraryModules = packages.collect {
|
|
it.libraryName ? "${codegenLibPrefix}${it.libraryName}" : null
|
|
}.minus(null).join('\n ')
|
|
}
|
|
|
|
String generatedFileContents = generatedFileContentsTemplate
|
|
.replace("{{ libraryIncludes }}", libraryIncludes)
|
|
.replace("{{ libraryModules }}", libraryModules)
|
|
|
|
outputDir.mkdirs()
|
|
final FileTreeBuilder treeBuilder = new FileTreeBuilder(outputDir)
|
|
treeBuilder.file(generatedFileName).newWriter().withWriter { w ->
|
|
w << generatedFileContents
|
|
}
|
|
}
|
|
|
|
void generateRncliCpp(File outputDir, String generatedFileName, String generatedFileContentsTemplate) {
|
|
ArrayList<HashMap<String, String>> packages = this.reactNativeModules
|
|
ArrayList<String> unstable_reactLegacyComponentNames = this.unstable_reactLegacyComponentNames
|
|
String rncliCppIncludes = ""
|
|
String rncliCppModuleProviders = ""
|
|
String rncliCppComponentDescriptors = ""
|
|
String rncliReactLegacyComponentDescriptors = ""
|
|
String rncliReactLegacyComponentNames = ""
|
|
String codegenComponentDescriptorsHeaderFile = "ComponentDescriptors.h"
|
|
String codegenReactComponentsDir = "react/renderer/components"
|
|
|
|
if (packages.size() > 0) {
|
|
rncliCppIncludes = packages.collect {
|
|
if (!it.libraryName) {
|
|
return null
|
|
}
|
|
def result = "#include <${it.libraryName}.h>"
|
|
if (it.componentDescriptors && it.componentDescriptors.size() > 0) {
|
|
result += "\n#include <${codegenReactComponentsDir}/${it.libraryName}/${codegenComponentDescriptorsHeaderFile}>"
|
|
}
|
|
result
|
|
}.minus(null).join('\n')
|
|
rncliCppModuleProviders = packages.collect {
|
|
it.libraryName ? """ auto module_${it.libraryName} = ${it.libraryName}_ModuleProvider(moduleName, params);
|
|
if (module_${it.libraryName} != nullptr) {
|
|
return module_${it.libraryName};
|
|
}""" : null
|
|
}.minus(null).join("\n")
|
|
rncliCppComponentDescriptors = packages.collect {
|
|
def result = ""
|
|
if (it.componentDescriptors && it.componentDescriptors.size() > 0) {
|
|
result += it.componentDescriptors.collect {
|
|
" providerRegistry->add(concreteComponentDescriptorProvider<${it}>());"
|
|
}.join('\n')
|
|
}
|
|
result
|
|
}.join("\n")
|
|
}
|
|
|
|
rncliReactLegacyComponentDescriptors = unstable_reactLegacyComponentNames.collect {
|
|
" providerRegistry->add(concreteComponentDescriptorProvider<UnstableLegacyViewManagerInteropComponentDescriptor<${it}>>());"
|
|
}.join("\n")
|
|
rncliReactLegacyComponentNames = unstable_reactLegacyComponentNames.collect {
|
|
"extern const char ${it}[] = \"${it}\";"
|
|
}.join("\n")
|
|
if (unstable_reactLegacyComponentNames && unstable_reactLegacyComponentNames.size() > 0) {
|
|
rncliCppIncludes += "\n#include <react/renderer/components/legacyviewmanagerinterop/UnstableLegacyViewManagerInteropComponentDescriptor.h>"
|
|
}
|
|
|
|
String generatedFileContents = generatedFileContentsTemplate
|
|
.replace("{{ rncliCppIncludes }}", rncliCppIncludes)
|
|
.replace("{{ rncliCppModuleProviders }}", rncliCppModuleProviders)
|
|
.replace("{{ rncliCppComponentDescriptors }}", rncliCppComponentDescriptors)
|
|
.replace("{{ rncliReactLegacyComponentDescriptors }}", rncliReactLegacyComponentDescriptors)
|
|
.replace("{{ rncliReactLegacyComponentNames }}", rncliReactLegacyComponentNames)
|
|
|
|
outputDir.mkdirs()
|
|
final FileTreeBuilder treeBuilder = new FileTreeBuilder(outputDir)
|
|
treeBuilder.file(generatedFileName).newWriter().withWriter { w ->
|
|
w << generatedFileContents
|
|
}
|
|
}
|
|
|
|
void generateRncliH(File outputDir, String generatedFileName, String generatedFileContentsTemplate) {
|
|
String generatedFileContents = generatedFileContentsTemplate
|
|
|
|
outputDir.mkdirs()
|
|
final FileTreeBuilder treeBuilder = new FileTreeBuilder(outputDir)
|
|
treeBuilder.file(generatedFileName).newWriter().withWriter { w ->
|
|
w << generatedFileContents
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Runs a specified command using Runtime exec() in a specified directory.
|
|
* Throws when the command result is empty.
|
|
*/
|
|
String getCommandOutput(String[] command, File directory) {
|
|
try {
|
|
def output = ""
|
|
def cmdProcess = Runtime.getRuntime().exec(command, null, directory)
|
|
def bufferedReader = new BufferedReader(new InputStreamReader(cmdProcess.getInputStream()))
|
|
def buff = ""
|
|
def readBuffer = new StringBuffer()
|
|
while ((buff = bufferedReader.readLine()) != null) {
|
|
readBuffer.append(buff)
|
|
}
|
|
output = readBuffer.toString()
|
|
if (!output) {
|
|
this.logger.error("${LOG_PREFIX}Unexpected empty result of running '${command}' command.")
|
|
def bufferedErrorReader = new BufferedReader(new InputStreamReader(cmdProcess.getErrorStream()))
|
|
def errBuff = ""
|
|
def readErrorBuffer = new StringBuffer()
|
|
while ((errBuff = bufferedErrorReader.readLine()) != null) {
|
|
readErrorBuffer.append(errBuff)
|
|
}
|
|
throw new Exception(readErrorBuffer.toString())
|
|
}
|
|
return output
|
|
} catch (Exception exception) {
|
|
this.logger.error("${LOG_PREFIX}Running '${command}' command failed.")
|
|
throw exception
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Runs a process to call the React Native CLI Config command and parses the output
|
|
*/
|
|
ArrayList<HashMap<String, String>> getReactNativeConfig() {
|
|
if (this.reactNativeModules != null) return this.reactNativeModules
|
|
|
|
ArrayList<HashMap<String, String>> reactNativeModules = new ArrayList<HashMap<String, String>>()
|
|
HashMap<String, ArrayList> reactNativeModulesBuildVariants = new HashMap<String, ArrayList>()
|
|
|
|
/**
|
|
* Resolve the CLI location from Gradle file
|
|
*
|
|
* @todo: Sometimes Gradle can be called outside of the JavaScript hierarchy (-p flag) which
|
|
* will fail to resolve the script and the dependencies. We should resolve this soon.
|
|
*
|
|
* @todo: `fastlane` has been reported to not work too.
|
|
*/
|
|
def cliResolveScript = "try {console.log(require('@react-native-community/cli').bin);} catch (e) {console.log(require('react-native/cli').bin);}"
|
|
String[] nodeCommand = ["node", "-e", cliResolveScript]
|
|
def cliPath = this.getCommandOutput(nodeCommand, this.root)
|
|
|
|
String[] reactNativeConfigCommand = ["node", cliPath, "config"]
|
|
def reactNativeConfigOutput = this.getCommandOutput(reactNativeConfigCommand, this.root)
|
|
|
|
def json
|
|
try {
|
|
json = new JsonSlurper().parseText(reactNativeConfigOutput)
|
|
} catch (Exception exception) {
|
|
throw new Exception("Calling `${reactNativeConfigCommand}` finished with an exception. Error message: ${exception.toString()}. Output: ${reactNativeConfigOutput}");
|
|
}
|
|
def dependencies = json["dependencies"]
|
|
def project = json["project"]["android"]
|
|
|
|
if (project == null) {
|
|
throw new Exception("React Native CLI failed to determine Android project configuration. This is likely due to misconfiguration. Config output:\n${json.toMapString()}")
|
|
}
|
|
|
|
def engine = new groovy.text.SimpleTemplateEngine()
|
|
|
|
dependencies.each { name, value ->
|
|
def platformsConfig = value["platforms"];
|
|
def androidConfig = platformsConfig["android"]
|
|
|
|
if (androidConfig != null && androidConfig["sourceDir"] != null) {
|
|
this.logger.info("${LOG_PREFIX}Automatically adding native module '${name}'")
|
|
|
|
HashMap reactNativeModuleConfig = new HashMap<String, String>()
|
|
def nameCleansed = name.replaceAll('[~*!\'()]+', '_').replaceAll('^@([\\w-.]+)/', '$1_')
|
|
reactNativeModuleConfig.put("name", name)
|
|
reactNativeModuleConfig.put("nameCleansed", nameCleansed)
|
|
reactNativeModuleConfig.put("androidSourceDir", androidConfig["sourceDir"])
|
|
reactNativeModuleConfig.put("packageInstance", androidConfig["packageInstance"])
|
|
reactNativeModuleConfig.put("packageImportPath", androidConfig["packageImportPath"])
|
|
reactNativeModuleConfig.put("libraryName", androidConfig["libraryName"])
|
|
reactNativeModuleConfig.put("componentDescriptors", androidConfig["componentDescriptors"])
|
|
reactNativeModuleConfig.put("cmakeListsPath", androidConfig["cmakeListsPath"])
|
|
|
|
if (androidConfig["buildTypes"] && !androidConfig["buildTypes"].isEmpty()) {
|
|
reactNativeModulesBuildVariants.put(nameCleansed, androidConfig["buildTypes"])
|
|
}
|
|
if(androidConfig.containsKey("dependencyConfiguration")) {
|
|
reactNativeModuleConfig.put("dependencyConfiguration", androidConfig["dependencyConfiguration"])
|
|
} else if (project.containsKey("dependencyConfiguration")) {
|
|
def bindings = ["dependencyName": nameCleansed]
|
|
def template = engine.createTemplate(project["dependencyConfiguration"]).make(bindings)
|
|
|
|
reactNativeModuleConfig.put("dependencyConfiguration", template.toString())
|
|
}
|
|
|
|
this.logger.trace("${LOG_PREFIX}'${name}': ${reactNativeModuleConfig.toMapString()}")
|
|
|
|
reactNativeModules.add(reactNativeModuleConfig)
|
|
} else {
|
|
this.logger.info("${LOG_PREFIX}Skipping native module '${name}'")
|
|
}
|
|
}
|
|
|
|
return [reactNativeModules, reactNativeModulesBuildVariants, json["project"]["android"]];
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
* Sometimes Gradle can be called outside of JavaScript hierarchy. Detect the directory
|
|
* where build files of an active project are located.
|
|
*/
|
|
def projectRoot = rootProject.projectDir
|
|
|
|
def autoModules = new ReactNativeModules(logger, projectRoot)
|
|
|
|
/** -----------------------
|
|
* Exported Extensions
|
|
* ------------------------ */
|
|
|
|
ext.applyNativeModulesSettingsGradle = { DefaultSettings defaultSettings, String root = null ->
|
|
if (root != null) {
|
|
logger.warn("${ReactNativeModules.LOG_PREFIX}Passing custom root is deprecated. CLI detects root automatically now.");
|
|
logger.warn("${ReactNativeModules.LOG_PREFIX}Please remove second argument to `applyNativeModulesSettingsGradle`.");
|
|
}
|
|
autoModules.addReactNativeModuleProjects(defaultSettings)
|
|
}
|
|
|
|
ext.applyNativeModulesAppBuildGradle = { Project project, String root = null ->
|
|
if (root != null) {
|
|
logger.warn("${ReactNativeModules.LOG_PREFIX}Passing custom root is deprecated. CLI detects root automatically now");
|
|
logger.warn("${ReactNativeModules.LOG_PREFIX}Please remove second argument to `applyNativeModulesAppBuildGradle`.");
|
|
}
|
|
autoModules.addReactNativeModuleDependencies(project)
|
|
|
|
def generatedSrcDir = new File(buildDir, "generated/rncli/src/main/java")
|
|
def generatedCodeDir = new File(generatedSrcDir, generatedFilePackage.replace('.', '/'))
|
|
def generatedJniDir = new File(buildDir, "generated/rncli/src/main/jni")
|
|
|
|
task generatePackageList {
|
|
doLast {
|
|
autoModules.generatePackagesFile(generatedCodeDir, generatedFileName, generatedFileContentsTemplate)
|
|
}
|
|
}
|
|
|
|
task generateNewArchitectureFiles {
|
|
doLast {
|
|
autoModules.generateCmakeFile(generatedJniDir, "Android-rncli.cmake", cmakeTemplate)
|
|
autoModules.generateRncliCpp(generatedJniDir, "rncli.cpp", rncliCppTemplate)
|
|
autoModules.generateRncliH(generatedJniDir, "rncli.h", rncliHTemplate)
|
|
}
|
|
}
|
|
|
|
preBuild.dependsOn generatePackageList
|
|
|
|
if (project.hasProperty("newArchEnabled") && project.newArchEnabled == "true") {
|
|
preBuild.dependsOn generateNewArchitectureFiles
|
|
}
|
|
|
|
android {
|
|
sourceSets {
|
|
main {
|
|
java {
|
|
srcDirs += generatedSrcDir
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|