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 the new architecture on Android #1603

Merged
merged 13 commits into from
May 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ And then:

```sh
cd /my/new/react-native/project/
yarn link "@react-native-community/cli-platform-ios" "@react-native-community/cli-platform-android" "@react-native-community/cli" "@react-native-community/cli-server-api" "@react-native-community/cli-types" "@react-native-community/cli-tools" "@react-native-community/cli-debugger-ui" "@react-native-community/cli-hermes" "@react-native-community/cli-plugin-metro"
yarn link "@react-native-community/cli-platform-ios" "@react-native-community/cli-platform-android" "@react-native-community/cli" "@react-native-community/cli-server-api" "@react-native-community/cli-types" "@react-native-community/cli-tools" "@react-native-community/cli-debugger-ui" "@react-native-community/cli-hermes" "@react-native-community/cli-plugin-metro" "@react-native-community/cli-clean"
```

Once you're done with testing and you'd like to get back to regular setup, run `yarn unlink` instead of `yarn link` from above command. Then `yarn install --force`.
Expand Down
28 changes: 28 additions & 0 deletions docs/autolinking.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ yarn react-native run-android
That's it. No more editing build config files to use native code.

Also, removing a library is similar to adding a library:

```sh
# uninstall
yarn remove react-native-webview
Expand Down Expand Up @@ -48,6 +49,10 @@ The [native_modules.gradle](https://github.com/react-native-community/cli/blob/m
1. At build time, before the build script is run:
1. A first Gradle plugin (in `settings.gradle`) runs `applyNativeModulesSettingsGradle` method. It uses the package metadata from `react-native config` to add Android projects.
1. A second Gradle plugin (in `app/build.gradle`) runs `applyNativeModulesAppBuildGradle` method. It creates a list of React Native packages to include in the generated `/android/build/generated/rn/src/main/java/com/facebook/react/PackageList.java` file.
1. When the new architecture is turned on, the `generateNewArchitectureFiles` task is fired, generating `/android/build/generated/rn/src/main/jni` directory with the following files:
- `Android-rncli.mk` – creates a list of codegen'd libs. Used by the project's `Android.mk`.
- `rncli.cpp` – registers codegen'd Turbo Modules and Fabric component providers. Used by `MainApplicationModuleProvider.cpp` and `MainComponentsRegistry.cpp`.
- `rncli.h` - a header file for `rncli.cpp`.
1. At runtime, the list of React Native packages generated in step 1.2 is registered by `getPackages` method of `ReactNativeHost` in `MainApplication.java`.
1. You can optionally pass in an instance of `MainPackageConfig` when initializing `PackageList` if you want to override the default configuration of `MainReactPackage`.

Expand Down Expand Up @@ -97,6 +102,27 @@ module.exports = {
};
```

## How can I disable autolinking for new architecture (Fabric, TurboModules)?

It happens that packages come with their own linking setup for the new architecture. To disable autolinking in such cases (currently `react-native-screens`, `react-native-safe-area-context`, `react-native-reanimated`, `react-native-gesture-handler`), update your `react-native.config.js`'s `dependencies` entry to look like this:

```js
// react-native.config.js
module.exports = {
dependencies: {
'fabric-or-tm-library': {
platforms: {
android: {
libraryName: null,
componentDescriptors: null,
androidMkPath: null,
},
},
},
},
};
```

## How can I autolink a local library?

We can leverage CLI configuration to make it "see" React Native libraries that are not part of our 3rd party dependencies. To do so, update your `react-native.config.js`'s `dependencies` entry to look like this:
Expand Down Expand Up @@ -124,6 +150,7 @@ correct location and update them accordingly:
- path to `native_modules.gradle` in your `android/app/build.gradle`

Dependencies are only linked if they are listed in the package.json of the mobile workspace, where "react-native" dependency is defined. For example, with this file structure:

```
/root
/packages
Expand All @@ -135,4 +162,5 @@ Dependencies are only linked if they are listed in the package.json of the mobil
package.json <-- Dependencies here are ignored when auto-linking
package.json
```

In this example, if you add a package with native code as a dependency of `components`, you need to also add it as a dependency of `mobile` for auto-linking to work.
21 changes: 21 additions & 0 deletions docs/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ type AndroidDependencyParams = {
packageImportPath?: string;
packageInstance?: string;
buildTypes?: string[];
libraryName?: string | null;
componentDescriptors?: string[] | null;
androidMkPath?: string | null;
};
```

Expand Down Expand Up @@ -119,3 +122,21 @@ An array of build variants or flavors which will include the dependency. If the

A string that defines which method other than `implementation` do you want to use
for autolinking inside `build.gradle` i.e: `'embed project(path: ":$dependencyName", configuration: "default")',` - `"dependencyName` will be replaced by the actual package's name. You can achieve the same result by directly defining this key per `dependency` _(without placeholder)_ and it will have higher priority than this option.

#### platforms.android.libraryName

> Note: Only applicable when new architecture is turned on.

A string indicating your custom library name. By default it's taken from the `libraryName` variable in your library's `build.gradle`.

#### platforms.android.componentDescriptors

> Note: Only applicable when new architecture is turned on.

An array of custom component descriptor strings. By default they're generated based on `codegenNativeComponent` calls.

#### platforms.android.androidMkPath

> Note: Only applicable when new architecture is turned on.

A relative path to a custom _Android.mk_ file not registered by codegen. Relative to `sourceDir`.
3 changes: 3 additions & 0 deletions docs/platforms.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,5 +118,8 @@ type AndroidDependencyConfig = {
packageInstance: string;
dependencyConfiguration?: string;
buildTypes: string[];
libraryName?: string | null;
componentDescriptors?: string[] | null;
androidMkPath?: string | null;
};
```
6 changes: 6 additions & 0 deletions packages/cli-config/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ export const dependencyConfig = t
packageInstance: t.string(),
dependencyConfiguration: t.string(),
buildTypes: t.array().items(t.string()).default([]),
libraryName: t.string().allow(null),
componentDescriptors: t.array().items(t.string()).allow(null),
androidMkPath: t.string().allow(null),
})
.default({}),
})
Expand Down Expand Up @@ -131,6 +134,9 @@ export const projectConfig = t
packageInstance: t.string(),
dependencyConfiguration: t.string(),
buildTypes: t.array().items(t.string()).default([]),
libraryName: t.string().allow(null),
componentDescriptors: t.array().items(t.string()).allow(null),
androidMkPath: t.string().allow(null),
})
.allow(null),
}),
Expand Down
6 changes: 6 additions & 0 deletions packages/cli-types/src/android.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ export type AndroidDependencyConfig = {
packageInstance: string;
dependencyConfiguration?: string;
buildTypes: string[];
libraryName?: string | null;
componentDescriptors?: string[] | null;
androidMkPath?: string | null;
};

export type AndroidDependencyParams = {
Expand All @@ -29,4 +32,7 @@ export type AndroidDependencyParams = {
packageImportPath?: string;
packageInstance?: string;
buildTypes?: string[];
libraryName?: string | null;
componentDescriptors?: string[] | null;
androidMkPath?: string | null;
};
165 changes: 165 additions & 0 deletions packages/platform-android/native_modules.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,67 @@ public class PackageList {
}
"""

def androidMkTemplate = """# This code was generated by [React Native CLI](https://www.npmjs.com/package/@react-native-community/cli)

{{ libraryIncludes }}

import-codegen-modules := \\
{{ 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 {

std::shared_ptr<TurboModule> rncli_ModuleProvider(const std::string moduleName, const JavaTurboModule::InitParams &params) {
{{ rncliCppModuleProviders }}
return nullptr;
}

void rncli_registerProviders(std::shared_ptr<ComponentDescriptorProviderRegistry const> providerRegistry) {
{{ rncliCppComponentDescriptors }}
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 &params);
void rncli_registerProviders(std::shared_ptr<ComponentDescriptorProviderRegistry const> providerRegistry);

} // namespace react
} // namespace facebook
"""

class ReactNativeModules {
private Logger logger
private String packageName
Expand Down Expand Up @@ -187,6 +248,91 @@ class ReactNativeModules {
}
}

void generateAndroidMkFile(File outputDir, String generatedFileName, String generatedFileContentsTemplate) {
ArrayList<HashMap<String, String>> packages = this.reactNativeModules
String packageName = this.packageName
String codegenLibPrefix = "libreact_codegen_"
String libraryIncludes = ""
String libraryModules = ""

if (packages.size() > 0) {
libraryIncludes = packages.collect {
it.androidMkPath ? "include ${it.androidMkPath}" : 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
String rncliCppIncludes = ""
String rncliCppModuleProviders = ""
String rncliCppComponentDescriptors = ""
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")
}

String generatedFileContents = generatedFileContentsTemplate
.replace("{{ rncliCppIncludes }}", rncliCppIncludes)
.replace("{{ rncliCppModuleProviders }}", rncliCppModuleProviders)
.replace("{{ rncliCppComponentDescriptors }}", rncliCppComponentDescriptors)

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.
Expand Down Expand Up @@ -272,6 +418,10 @@ class ReactNativeModules {
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("androidMkPath", androidConfig["androidMkPath"])

if (androidConfig["buildTypes"] && !androidConfig["buildTypes"].isEmpty()) {
reactNativeModulesBuildVariants.put(nameCleansed, androidConfig["buildTypes"])
}
Expand Down Expand Up @@ -326,15 +476,28 @@ ext.applyNativeModulesAppBuildGradle = { Project project, String root = null ->

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

Choose a reason for hiding this comment

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

I would refrain from using doLast. Those are terrible from the caching point of view (as they invalidate the gradle caches). We should move this to a proper task.

Not a blocker though :), just a heads up

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure! I'm not a heavy user of Gradle, so I'd appreciate any contributions in code or pointers on what to leverage exactly :)

autoModules.generateAndroidMkFile(generatedJniDir, "Android-rncli.mk", androidMkTemplate)
autoModules.generateRncliCpp(generatedJniDir, "rncli.cpp", rncliCppTemplate)
autoModules.generateRncliH(generatedJniDir, "rncli.h", rncliHTemplate)
}
}

preBuild.dependsOn generatePackageList

if (isNewArchitectureEnabled()) {
preBuild.dependsOn generateNewArchitectureFiles
}

android {
sourceSets {
main {
Expand All @@ -345,3 +508,5 @@ ext.applyNativeModulesAppBuildGradle = { Project project, String root = null ->
}
}
}


Loading