Skip to content
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

feat: autolinking for Android with Gradle #258

Merged
merged 51 commits into from
Apr 19, 2019
Merged
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
f5f6271
Initial commit
grabbou Mar 20, 2019
9734a8f
Clean up main entry point
grabbou Mar 20, 2019
3ab1c46
Fixes some typos
orta Mar 20, 2019
703098f
Update configuration
grabbou Mar 20, 2019
4f17675
Resolve conflicts
grabbou Mar 20, 2019
32da7cf
update
grabbou Mar 20, 2019
942db96
Tweaks to types
grabbou Mar 20, 2019
dcb0362
types and tweaks
grabbou Mar 20, 2019
93090f8
Save another chunk of work
grabbou Mar 20, 2019
4afecc2
add initial native modules gradle script
Salakar Mar 21, 2019
8f947ce
[config] expose android sourceDir for react native module packages
Salakar Mar 21, 2019
4af1cfe
rework config reader to new schema + add logging
Salakar Mar 21, 2019
830e929
handle null android sourceDir
Salakar Mar 21, 2019
505e4d0
add support for Packages that accept various app instance arguments
Salakar Mar 23, 2019
64f3f5a
add build config support (for packages that use it in args)
Salakar Mar 23, 2019
933512a
WIP part 2'
grabbou Mar 26, 2019
9815ff2
Simplify diff by removing things we can tweak later
grabbou Mar 26, 2019
e50001a
Wip 3: Legacy link config
grabbou Mar 26, 2019
2b3e016
Updates
grabbou Mar 26, 2019
bfc9ed7
Update name:
grabbou Mar 26, 2019
2180a5a
Add deprecation messages for soon-to-be-deprecated rnpm configuration
grabbou Mar 26, 2019
7abcaeb
Both array and string were acceptable in rnpm configuration
grabbou Mar 26, 2019
225d739
Share root
grabbou Mar 26, 2019
623b643
Documentation and refining the code
grabbou Mar 26, 2019
3c7f36b
Start wrapping it up
grabbou Mar 26, 2019
b3cbf3b
Fix some simple flow errors
grabbou Mar 26, 2019
db90f50
Fix flow errors except for tests
grabbou Mar 26, 2019
4b67b8b
Override configuration
grabbou Mar 27, 2019
34224eb
Simplify typing changes
grabbou Mar 27, 2019
717687b
Fix remaining flow errors
grabbou Mar 27, 2019
50674d7
Validate dependencies separately
grabbou Mar 27, 2019
752104b
Use Joi to validate schema
grabbou Mar 27, 2019
8a491a8
Read Joi validated config
grabbou Mar 27, 2019
1f1da28
Rewrite configuration once again
grabbou Mar 28, 2019
089669b
Further simplification
grabbou Mar 28, 2019
c2c30e7
Simplify code again
grabbou Mar 28, 2019
d71658b
Print deprecation warning
grabbou Mar 28, 2019
5c13c1a
Fix flow
grabbou Mar 28, 2019
a575d6c
Add basic error handling of Joi errors
grabbou Mar 28, 2019
7b67f2e
wip
grabbou Mar 29, 2019
6005390
chore: setup e2e tests (#264)
thymikee Mar 28, 2019
5941d8f
Add first snapshot test using e2e utils
grabbou Mar 29, 2019
8d08a00
Test for root too
grabbou Mar 29, 2019
b6e97cd
Merge remote-tracking branch 'origin/master' into feat/config
thymikee Mar 29, 2019
b5869f6
add initial native modules gradle script
Salakar Apr 18, 2019
6a9ecd2
Merge branch 'master' of https://github.com/react-native-community/re…
Salakar Apr 18, 2019
5ad00b3
move to platform-android
Salakar Apr 18, 2019
a7fb13b
cleanup
Salakar Apr 18, 2019
5c3c7c9
update to new config output structure
Salakar Apr 18, 2019
7a7115f
switch to using local cli path
Salakar Apr 18, 2019
108b873
add `native_modules.gradle` to package files
Salakar Apr 18, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
250 changes: 250 additions & 0 deletions packages/platform-android/native_modules.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
import groovy.json.JsonSlurper
Salakar marked this conversation as resolved.
Show resolved Hide resolved
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"
Copy link
Member

Choose a reason for hiding this comment

The 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 require.resolve to output the resolved path to stdout

Copy link
Member Author

@Salakar Salakar Apr 18, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is handled by the final arg here:

cmdProcess = Runtime.getRuntime().exec(REACT_NATIVE_CONFIG_CMD, null, getReactNativeProjectRoot())
- the process is spawned relative to the project root, which can also be overridden here:
if (this.extension.has("reactNativeProjectRoot")) {

Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Salakar, we already have config.root in our config outputted from react-native config. You could use that one.

Also, you can just do console.log(require.resolve('react-native')) script and exec node to get the exact location. I think this is what we use on iOS.

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)
Copy link
Member

Choose a reason for hiding this comment

The 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 ReactPackage here to LazyReactPackage. By default, packageClassInstance is e.g. new CodePushPackage() and in order to convert it to a LazyPackage, we would have to update that package itself.

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?

Copy link
Member Author

@Salakar Salakar Apr 18, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the CodePushPackage class itself would need to extend LazyReactPackage instead of ReactPackage for this to work? Not something we can do? 🤷‍♂️

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
}
}
}
}
}