-
Notifications
You must be signed in to change notification settings - Fork 926
feat: autolinking for Android with Gradle #258
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f5f6271
9734a8f
3ab1c46
703098f
4f17675
32da7cf
942db96
dcb0362
93090f8
4afecc2
8f947ce
4af1cfe
830e929
505e4d0
64f3f5a
933512a
9815ff2
e50001a
2b3e016
bfc9ed7
2180a5a
7abcaeb
225d739
623b643
3c7f36b
b3cbf3b
db90f50
4b67b8b
34224eb
717687b
50674d7
752104b
8a491a8
1f1da28
089669b
c2c30e7
d71658b
5c13c1a
a575d6c
7b67f2e
6005390
5941d8f
8d08a00
b6e97cd
b5869f6
6a9ecd2
5ad00b3
a7fb13b
5c3c7c9
7a7115f
108b873
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -0,0 +1,250 @@ | ||||||
import groovy.json.JsonSlurper | ||||||
import org.gradle.initialization.DefaultSettings | ||||||
|
||||||
def generatedFileName = "PackageList.java" | ||||||
def generatedFileContentsTemplate = """ | ||||||
package com.facebook.react; | ||||||
|
||||||
import android.app.Application; | ||||||
import android.content.Context; | ||||||
import android.content.res.Resources; | ||||||
|
||||||
import com.facebook.react.ReactPackage; | ||||||
import com.facebook.react.shell.MainReactPackage; | ||||||
import java.util.Arrays; | ||||||
import java.util.List; | ||||||
|
||||||
{{ packageImports }} | ||||||
|
||||||
public class PackageList { | ||||||
private ReactNativeHost reactNativeHost; | ||||||
public PackageList(ReactNativeHost reactNativeHost) { | ||||||
this.reactNativeHost = reactNativeHost; | ||||||
} | ||||||
|
||||||
private ReactNativeHost getReactNativeHost() { | ||||||
return this.reactNativeHost; | ||||||
} | ||||||
|
||||||
private Resources getResources() { | ||||||
return this.getApplication().getResources(); | ||||||
} | ||||||
|
||||||
private Application getApplication() { | ||||||
return this.reactNativeHost.getApplication(); | ||||||
} | ||||||
|
||||||
private Context getApplicationContext() { | ||||||
return this.getApplication().getApplicationContext(); | ||||||
} | ||||||
|
||||||
public List<ReactPackage> getPackages() { | ||||||
return Arrays.<ReactPackage>asList( | ||||||
new MainReactPackage(){{ packageClassInstances }} | ||||||
); | ||||||
} | ||||||
} | ||||||
""" | ||||||
|
||||||
class ReactNativeModules { | ||||||
private Logger logger | ||||||
private Project project | ||||||
private DefaultSettings defaultSettings | ||||||
private ExtraPropertiesExtension extension | ||||||
private ArrayList<HashMap<String, String>> reactNativeModules | ||||||
|
||||||
private static String LOG_PREFIX = ":ReactNative:" | ||||||
private static String REACT_NATIVE_CLI_BIN = "node_modules${File.separator}@react-native-community${File.separator}cli${File.separator}build${File.separator}index.js" | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. RN can technically be in monorepo so we can't make assumptions about where node_modules with cli are. That's why in iOS implementation we spawn a Node process and use its There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is handled by the final arg here:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok, so it won't be seamless but at least that's supported. We need to add documentation on how the autolinking is working on how to configure it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @Salakar, we already have Also, you can just do |
||||||
private static String REACT_NATIVE_CONFIG_CMD = "node ${REACT_NATIVE_CLI_BIN} config" | ||||||
|
||||||
ReactNativeModules(Logger logger) { | ||||||
this.logger = logger | ||||||
} | ||||||
|
||||||
void applySettingsGradle(DefaultSettings defaultSettings, ExtraPropertiesExtension extraPropertiesExtension) { | ||||||
this.defaultSettings = defaultSettings | ||||||
this.extension = extraPropertiesExtension | ||||||
this.reactNativeModules = this.getReactNativeConfig() | ||||||
|
||||||
addReactNativeModuleProjects() | ||||||
} | ||||||
|
||||||
void applyBuildGradle(Project project, ExtraPropertiesExtension extraPropertiesExtension) { | ||||||
this.project = project | ||||||
this.extension = extraPropertiesExtension | ||||||
this.reactNativeModules = this.getReactNativeConfig() | ||||||
|
||||||
addReactNativeModuleDependencies() | ||||||
} | ||||||
|
||||||
/** | ||||||
* Include the react native modules android projects and specify their project directory | ||||||
*/ | ||||||
void addReactNativeModuleProjects() { | ||||||
reactNativeModules.forEach { reactNativeModule -> | ||||||
String name = reactNativeModule["name"] | ||||||
String androidSourceDir = reactNativeModule["androidSourceDir"] | ||||||
defaultSettings.include(":${name}") | ||||||
defaultSettings.project(":${name}").projectDir = new File("${androidSourceDir}") | ||||||
} | ||||||
} | ||||||
|
||||||
/** | ||||||
* Adds the react native modules as dependencies to the users `app` project | ||||||
*/ | ||||||
void addReactNativeModuleDependencies() { | ||||||
reactNativeModules.forEach { reactNativeModule -> | ||||||
def name = reactNativeModule["name"] | ||||||
project.dependencies { | ||||||
// TODO(salakar): are other dependency scope methods such as `api` required? | ||||||
implementation project(path: ":${name}") | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
/** | ||||||
* This returns the users project root (e.g. where the node_modules dir is located). | ||||||
* | ||||||
* This defaults to up one directory from the root android directory unless the user has defined | ||||||
* a `ext.reactNativeProjectRoot` extension property | ||||||
* | ||||||
* @return | ||||||
*/ | ||||||
File getReactNativeProjectRoot() { | ||||||
if (this.extension.has("reactNativeProjectRoot")) { | ||||||
File rnRoot = File(this.extension.get("reactNativeProjectRoot")) | ||||||
// allow custom React Native project roots for non-standard directory structures | ||||||
this.logger.debug("${LOG_PREFIX}Using custom React Native project root path '${rnRoot.toString()}'") | ||||||
return rnRoot | ||||||
} | ||||||
|
||||||
File androidRoot | ||||||
|
||||||
if (this.project) { | ||||||
androidRoot = this.project.rootProject.projectDir | ||||||
} else { | ||||||
androidRoot = this.defaultSettings.rootProject.projectDir | ||||||
} | ||||||
|
||||||
this.logger.debug("${LOG_PREFIX}Using default React Native project root path '${androidRoot.parentFile.toString()}'") | ||||||
return androidRoot.parentFile | ||||||
} | ||||||
|
||||||
/** | ||||||
* Code-gen a java file with all the detected ReactNativePackage instances automatically added | ||||||
* | ||||||
* @param outputDir | ||||||
* @param generatedFileName | ||||||
* @param generatedFileContentsTemplate | ||||||
* @param applicationId | ||||||
*/ | ||||||
void generatePackagesFile(File outputDir, String generatedFileName, String generatedFileContentsTemplate, String applicationId) { | ||||||
ArrayList<HashMap<String, String>>[] packages = this.reactNativeModules | ||||||
|
||||||
String packageImports = "" | ||||||
String packageClassInstances = "" | ||||||
|
||||||
if (packages.size() > 0) { | ||||||
packageImports = "import ${applicationId}.BuildConfig;\n\n" | ||||||
packageImports = packageImports + packages.collect { | ||||||
"// ${it.name}\n${it.packageImportPath}" | ||||||
}.join(';\n') | ||||||
packageClassInstances = ",\n " + packages.collect { it.packageInstance }.join(',') | ||||||
} | ||||||
|
||||||
String generatedFileContents = generatedFileContentsTemplate | ||||||
.replace("{{ packageImports }}", packageImports) | ||||||
.replace("{{ packageClassInstances }}", packageClassInstances) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am wondering if there's any way we can wrap an instance of Is there a way to wrap it or create our custom lazy package for each ReactPackage, that will lazily create it and return everything what's needed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the Would forcing every package to be lazy also cause issues for packages that are not inherently meant to be lazy, e.g. modules that have app initialization logic that must always be run? Not 100% sure |
||||||
|
||||||
outputDir.mkdirs() | ||||||
final FileTreeBuilder treeBuilder = new FileTreeBuilder(outputDir) | ||||||
treeBuilder.file(generatedFileName).newWriter().withWriter { w -> | ||||||
w << generatedFileContents | ||||||
} | ||||||
} | ||||||
|
||||||
/** | ||||||
* Runs a process to call the React Native CLI Config command and parses the output | ||||||
* | ||||||
* @return ArrayList < HashMap < String , String > > | ||||||
*/ | ||||||
ArrayList<HashMap<String, String>> getReactNativeConfig() { | ||||||
if (this.reactNativeModules != null) return this.reactNativeModules | ||||||
ArrayList<HashMap<String, String>> reactNativeModules = new ArrayList<HashMap<String, String>>() | ||||||
|
||||||
def cmdProcess | ||||||
|
||||||
try { | ||||||
cmdProcess = Runtime.getRuntime().exec(REACT_NATIVE_CONFIG_CMD, null, getReactNativeProjectRoot()) | ||||||
cmdProcess.waitFor() | ||||||
} catch (Exception exception) { | ||||||
this.logger.warn("${LOG_PREFIX}${exception.message}") | ||||||
this.logger.warn("${LOG_PREFIX}Automatic import of native modules failed. (UNKNOWN)") | ||||||
return reactNativeModules | ||||||
} | ||||||
|
||||||
def reactNativeConfigOutput = cmdProcess.in.text | ||||||
thymikee marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
def json = new JsonSlurper().parseText(reactNativeConfigOutput) | ||||||
def dependencies = json["dependencies"] | ||||||
|
||||||
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>() | ||||||
reactNativeModuleConfig.put("name", name) | ||||||
reactNativeModuleConfig.put("androidSourceDir", androidConfig["sourceDir"]) | ||||||
reactNativeModuleConfig.put("packageInstance", androidConfig["packageInstance"]) | ||||||
reactNativeModuleConfig.put("packageImportPath", androidConfig["packageImportPath"]) | ||||||
this.logger.trace("${LOG_PREFIX}'${name}': ${reactNativeModuleConfig.toMapString()}") | ||||||
|
||||||
reactNativeModules.add(reactNativeModuleConfig) | ||||||
} else { | ||||||
this.logger.info("${LOG_PREFIX}Skipping native module '${name}'") | ||||||
} | ||||||
} | ||||||
|
||||||
return reactNativeModules | ||||||
} | ||||||
} | ||||||
|
||||||
/** ----------------------- | ||||||
* Exported Extensions | ||||||
* ------------------------ */ | ||||||
|
||||||
def autoModules = new ReactNativeModules(logger) | ||||||
|
||||||
ext.applyNativeModulesSettingsGradle = { DefaultSettings defaultSettings -> | ||||||
autoModules.applySettingsGradle(defaultSettings, ext) | ||||||
} | ||||||
|
||||||
ext.applyNativeModulesAppBuildGradle = { Project project -> | ||||||
autoModules.applyBuildGradle(project, ext) | ||||||
|
||||||
def applicationId | ||||||
def generatedSrcDir = new File(buildDir, "generated/rncli/src/main/java/com/facebook/react") | ||||||
|
||||||
// TODO(salakar): not sure if this is the best way of getting the package name (used to import BuildConfig) | ||||||
project.android.applicationVariants.all { variant -> | ||||||
applicationId = [variant.mergedFlavor.applicationId, variant.buildType.applicationIdSuffix].findAll().join() | ||||||
} | ||||||
|
||||||
task generatePackageList << { | ||||||
autoModules.generatePackagesFile(generatedSrcDir, generatedFileName, generatedFileContentsTemplate, applicationId) | ||||||
} | ||||||
|
||||||
preBuild.dependsOn generatePackageList | ||||||
|
||||||
android { | ||||||
sourceSets { | ||||||
main { | ||||||
java { | ||||||
srcDirs += generatedSrcDir | ||||||
} | ||||||
} | ||||||
} | ||||||
} | ||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ | |
"xmldoc": "^0.4.0" | ||
}, | ||
"files": [ | ||
"build" | ||
"build", | ||
"native_modules.gradle" | ||
] | ||
} |
Uh oh!
There was an error while loading. Please reload this page.