Add react build app plugin and extension

Summary:
Changelog:
[Android][Added] - Basic definition for react gradle plugin

Adds plugin and build configuration + copies config from react.gradle to extension.
Copies internals of react.gradle to the plugin. Will be refactored in the next commits.

Reviewed By: mdvacca

Differential Revision: D25693008

fbshipit-source-id: b0feaa02cee8a1ee94d032426d19c501ff3b2534
This commit is contained in:
Andrei Shikov 2021-02-22 08:13:28 -08:00 committed by Facebook GitHub Bot
parent f303266d69
commit dbbc1c1624
10 changed files with 519 additions and 12 deletions

1
.gitignore vendored
View File

@ -25,6 +25,7 @@ project.xcworkspace
/build/
/packages/react-native-codegen/android/build/
/packages/react-native-codegen/android/gradlePlugin-build/gradlePlugin/build
/packages/react-native-gradle-plugin/build/
/packages/rn-tester/android/app/.cxx/
/packages/rn-tester/android/app/build/
/packages/rn-tester/android/app/gradle/

View File

@ -11,7 +11,7 @@ plugins {
gradlePlugin {
plugins {
greeting {
codegen {
id = 'com.facebook.react.codegen'
implementationClass = 'com.facebook.react.codegen.plugin.CodegenPlugin'
}

View File

@ -0,0 +1,30 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
plugins {
`java-gradle-plugin`
`kotlin-dsl`
kotlin("jvm") version "1.4.21"
}
repositories {
google()
jcenter()
}
gradlePlugin {
plugins {
create("reactApp") {
id = "com.facebook.react.app"
implementationClass = "com.facebook.react.ReactAppPlugin"
}
}
}
dependencies {
implementation("com.android.tools.build:gradle:4.1.0")
}

View File

@ -0,0 +1,21 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react
import com.android.build.gradle.BaseExtension
import org.gradle.api.Project
fun Project.configureDevPorts(androidExt: BaseExtension) {
val devServerPort = project.properties["reactNativeDevServerPort"]?.toString() ?: "8081"
val inspectorProxyPort = project.properties["reactNativeInspectorProxyPort"]?.toString() ?: devServerPort
androidExt.buildTypes.all {
resValue("integer", "react_native_dev_server_port", devServerPort)
resValue("integer", "react_native_inspector_proxy_port", inspectorProxyPort)
}
}

View File

@ -0,0 +1,107 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react
import com.android.build.gradle.api.BaseVariant
import org.apache.tools.ant.taskdefs.condition.Os
import java.io.File
open class ReactAppExtension(private val projectDir: File) {
var composeSourceMapsPath: String = "node_modules/react-native/scripts/compose-source-maps.js"
var bundleAssetName: String = "index.android.bundle"
var entryFile: File? = null
var bundleCommand: String = "bundle"
var reactRoot: File = File(projectDir, "../../")
var inputExcludes: List<String> = listOf("android/**", "ios/**")
var bundleConfig: String? = null
var enableVmCleanup: Boolean = true
var hermesCommand: String = "node_modules/hermes-engine/%OS-BIN%/hermesc"
var cliPath: String? = null
var nodeExecutableAndArgs: List<String> = listOf("node")
var enableHermes: Boolean = false
var enableHermesForVariant: (BaseVariant) -> Boolean = { enableHermes }
var devDisabledInVariants: List<String> = emptyList()
// todo maybe lambda as for hermes?
var bundleIn: Map<String, Boolean> = emptyMap()
var extraPackagerArgs: List<String> = emptyList()
var hermesFlagsDebug: List<String> = emptyList()
var hermesFlagsRelease: List<String> = listOf("-O", "-output-source-map")
var resourcesDir: Map<String, File> = emptyMap()
var jsBundleDir: Map<String, File> = emptyMap()
internal val detectedEntryFile: File
get() = detectEntryFile(entryFile = entryFile, reactRoot = reactRoot)
internal val detectedCliPath: String
get() = detectCliPath(
projectDir = projectDir,
reactRoot = reactRoot,
preconfuredCliPath = cliPath
)
internal val osAwareHermesCommand: String
get() = getOSAwareHermesCommand(hermesCommand)
private fun detectEntryFile(entryFile: File?, reactRoot: File): File = when {
System.getenv("ENTRY_FILE") != null -> File(System.getenv("ENTRY_FILE"))
entryFile != null -> entryFile
File(reactRoot, "index.android.js").exists() -> File(reactRoot, "index.android.js")
else -> File(reactRoot, "index.android.js")
}
private fun detectCliPath(projectDir: File, reactRoot: File, preconfuredCliPath: String?): String {
// 1. preconfigured path
if (preconfuredCliPath != null) return preconfuredCliPath
// 2. node module path
val nodeProcess = Runtime.getRuntime().exec(
arrayOf("node", "-e", "console.log(require('react-native/cli').bin);"),
emptyArray(),
projectDir
)
val nodeProcessOutput = nodeProcess.inputStream.use {
it.bufferedReader().readText().trim()
}
if (nodeProcessOutput.isNotEmpty()) {
return nodeProcessOutput
}
// 3. cli.js in the root folder
val rootCliJs = File(reactRoot, "node_modules/react-native/cli.js")
if (rootCliJs.exists()) {
return rootCliJs.absolutePath
}
error("Couldn't determine CLI location. " +
"Please set `project.react.cliPath` to the path of the react-native cli.js")
}
// Make sure not to inspect the Hermes config unless we need it,
// to avoid breaking any JSC-only setups.
private fun getOSAwareHermesCommand(hermesCommand: String): String {
// If the project specifies a Hermes command, don't second guess it.
if (!hermesCommand.contains("%OS-BIN%")) {
return hermesCommand
}
// Execution on Windows fails with / as separator
return hermesCommand
.replace("%OS-BIN%", getHermesOSBin())
.replace('/', File.separatorChar)
}
private fun getHermesOSBin(): String {
if (Os.isFamily(Os.FAMILY_WINDOWS)) return "win64-bin"
if (Os.isFamily(Os.FAMILY_MAC)) return "osx-bin"
if (Os.isOs(null, "linux", "amd64", null)) return "linux64-bin"
error("OS not recognized. Please set project.react.hermesCommand " +
"to the path of a working Hermes compiler.")
}
}

View File

@ -0,0 +1,41 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react
import com.android.build.gradle.AppExtension
import com.android.build.gradle.BaseExtension
import com.android.build.gradle.LibraryExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.create
import org.gradle.kotlin.dsl.getByType
class ReactAppPlugin : Plugin<Project> {
override fun apply(project: Project) {
// todo rename codegen or combine extensions
val config = project.extensions.create<ReactAppExtension>("reactApp", project.projectDir)
project.afterEvaluate {
val androidConfiguration = extensions.getByType<BaseExtension>()
configureDevPorts(androidConfiguration)
val isAndroidLibrary = plugins.hasPlugin("com.android.library")
val variants = if (isAndroidLibrary) {
extensions.getByType<LibraryExtension>().libraryVariants
} else {
extensions.getByType<AppExtension>().applicationVariants
}
variants.all {
configureReactTasks(
variant = this,
config = config
)
}
}
}
}

View File

@ -0,0 +1,283 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react
import com.android.build.gradle.api.ApplicationVariant
import com.android.build.gradle.api.BaseVariant
import com.android.build.gradle.api.LibraryVariant
import com.facebook.react.tasks.windowsAwareCommandLine
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.Exec
import org.gradle.kotlin.dsl.withGroovyBuilder
import org.gradle.kotlin.dsl.create
import java.io.File
@Suppress("SpreadOperator")
internal fun Project.configureReactTasks(variant: BaseVariant, config: ReactAppExtension) {
val targetName = variant.name.capitalize()
val isRelease = variant.isRelease
val targetPath = variant.dirName
// React js bundle directories
val jsBundleDir = File(buildDir, "generated/assets/react/$targetPath")
val resourcesDir = File(buildDir, "generated/res/react/$targetPath")
val jsBundleFile = File(jsBundleDir, config.bundleAssetName)
val jsSourceMapsDir = File(buildDir, "generated/sourcemaps/react/$targetPath")
val jsIntermediateSourceMapsDir = File(buildDir, "intermediates/sourcemaps/react/$targetPath")
val jsPackagerSourceMapFile = File(jsIntermediateSourceMapsDir, "${config.bundleAssetName}.packager.map")
val jsCompilerSourceMapFile = File(jsIntermediateSourceMapsDir, "${config.bundleAssetName}.compiler.map")
val jsOutputSourceMapFile = File(jsSourceMapsDir, "${config.bundleAssetName}.map")
// Additional node and packager commandline arguments
val nodeExecutableAndArgs = config.nodeExecutableAndArgs.toTypedArray()
val cliPath = config.detectedCliPath
val execCommand = nodeExecutableAndArgs + cliPath
val enableHermes = config.enableHermesForVariant(variant)
val bundleEnabled = variant.checkBundleEnabled(config)
val currentBundleTask = tasks.create<Exec>("bundle${targetName}JsAndAssets") {
group = "react"
description = "bundle JS and assets for $targetName."
// Create dirs if they are not there (e.g. the "clean" task just ran)
doFirst {
jsBundleDir.deleteRecursively()
jsBundleDir.mkdirs()
resourcesDir.deleteRecursively()
resourcesDir.mkdirs()
jsIntermediateSourceMapsDir.deleteRecursively()
jsIntermediateSourceMapsDir.mkdirs()
jsSourceMapsDir.deleteRecursively()
jsSourceMapsDir.mkdirs()
}
// Set up inputs and outputs so gradle can cache the result
inputs.files(
fileTree(config.reactRoot) {
setExcludes(config.inputExcludes)
}
)
outputs.dir(jsBundleDir)
outputs.dir(resourcesDir)
// Set up the call to the react-native cli
workingDir(config.reactRoot)
// Set up dev mode
val devEnabled = !(variant.name in config.devDisabledInVariants || variant.isRelease)
val extraArgs = mutableListOf<String>()
if (config.bundleConfig != null) {
extraArgs.add("--config")
extraArgs.add(config.bundleConfig.orEmpty())
}
// Hermes doesn't require JS minification.
if (enableHermes && !devEnabled) {
extraArgs.add("--minify")
extraArgs.add("false")
}
extraArgs.addAll(config.extraPackagerArgs)
windowsAwareCommandLine(
*execCommand,
config.bundleCommand,
"--platform", "android",
"--dev", "$devEnabled",
"--reset-cache",
"--entry-file", config.detectedEntryFile,
"--bundle-output", jsBundleFile,
"--assets-dest", resourcesDir,
"--sourcemap-output", if (enableHermes) jsPackagerSourceMapFile else jsOutputSourceMapFile,
*extraArgs.toTypedArray()
)
if (enableHermes) {
doLast {
val hermesFlags = if (isRelease) {
config.hermesFlagsRelease
} else {
config.hermesFlagsDebug
}.toTypedArray()
val hbcTempFile = file("$jsBundleFile.hbc")
exec {
windowsAwareCommandLine(
config.osAwareHermesCommand,
"-emit-binary",
"-out", hbcTempFile, jsBundleFile,
*hermesFlags
)
}
ant.withGroovyBuilder {
"move"(
"file" to hbcTempFile,
"toFile" to jsBundleFile
)
}
if (hermesFlags.contains("-output-source-map")) {
ant.withGroovyBuilder {
"move"(
"file" to "$jsBundleFile.hbc.map",
"toFile" to jsCompilerSourceMapFile
)
}
exec {
// TODO: set task dependencies for caching
// Set up the call to the compose-source-maps script
workingDir(config.reactRoot)
windowsAwareCommandLine(
*nodeExecutableAndArgs,
config.composeSourceMapsPath,
jsPackagerSourceMapFile,
jsCompilerSourceMapFile,
"-o", jsOutputSourceMapFile)
}
}
}
}
enabled = bundleEnabled
}
// todo expose bundle task and its generated folders
val generatedResFolders = files(resourcesDir).builtBy(currentBundleTask)
// val generatedAssetsFolders = files(jsBundleDir).builtBy(currentBundleTask)
variant.registerGeneratedResFolders(generatedResFolders)
variant.mergeResourcesProvider.get().dependsOn(currentBundleTask)
val packageTask = when (variant) {
is ApplicationVariant -> variant.packageApplicationProvider.get()
is LibraryVariant -> variant.packageLibraryProvider.get()
else -> tasks.findByName("package$targetName")!!
}
// pre bundle build task for Android plugin 3.2+
val buildPreBundleTask = tasks.findByName("build${targetName}PreBundle")
val resourcesDirConfigValue = config.resourcesDir[variant.name]
if (resourcesDirConfigValue != null) {
val currentCopyResTask = tasks.create<Copy>("copy${targetName}BundledResources") {
group = "react"
description = "copy bundled resources into custom location for $targetName."
from(resourcesDir)
into(file(resourcesDirConfigValue))
dependsOn(currentBundleTask)
enabled = currentBundleTask.enabled
}
packageTask.dependsOn(currentCopyResTask)
buildPreBundleTask?.dependsOn(currentCopyResTask)
}
val currentAssetsCopyTask = tasks.create<Copy>("copy${targetName}BundledJs") {
group = "react"
description = "copy bundled JS into $targetName."
val jsBundleDirConfigValue = config.jsBundleDir[targetName]
if (jsBundleDirConfigValue != null) {
from(jsBundleDir)
into(jsBundleDirConfigValue)
} else {
into("$buildDir/intermediates")
into("assets/$targetPath") {
from(jsBundleDir)
}
// Workaround for Android Gradle Plugin 3.2+ new asset directory
into("merged_assets/${variant.name}/merge${targetName}Assets/out") {
from(jsBundleDir)
}
// Workaround for Android Gradle Plugin 3.4+ new asset directory
into("merged_assets/${variant.name}/out") {
from(jsBundleDir)
}
}
// mergeAssets must run first, as it clears the intermediates directory
dependsOn(variant.mergeAssetsProvider.get())
enabled = currentBundleTask.enabled
}
// mergeResources task runs before the bundle file is copied to the intermediate asset directory from Android plugin 4.1+.
// This ensures to copy the bundle file before mergeResources task starts
val mergeResourcesTask = tasks.findByName("merge${targetName}Resources")
mergeResourcesTask?.dependsOn(currentAssetsCopyTask)
packageTask.dependsOn(currentAssetsCopyTask)
buildPreBundleTask?.dependsOn(currentAssetsCopyTask)
// Delete the VM related libraries that this build doesn't need.
// The application can manage this manually by setting 'enableVmCleanup: false'
//
// This should really be done by packaging all Hermes related libs into
// two separate HermesDebug and HermesRelease AARs, but until then we'll
// kludge it by deleting the .so files out of the /transforms/ directory.
val libDir = "$buildDir/intermediates/transforms/"
val vmSelectionAction = Action<Task> {
fileTree(libDir) {
if (enableHermes) {
// For Hermes, delete all the libjsc* files
include("**/libjsc*.so")
if (isRelease) {
// Reduce size by deleting the debugger/inspector
include("**/libhermes-inspector.so")
include("**/libhermes-executor-debug.so")
} else {
// Release libs take precedence and must be removed
// to allow debugging
include("**/libhermes-executor-release.so")
}
} else {
// For JSC, delete all the libhermes* files
include("**/libhermes*.so")
}
}.visit {
val targetVariant = ".*/transforms/[^/]*/$targetPath/.*".toRegex()
val path = file.absolutePath.replace(File.separatorChar, '/')
if (path.matches(targetVariant) && file.isFile()) {
file.delete()
}
}
}
if (config.enableVmCleanup) {
val task = tasks.findByName("package$targetName")
task?.doFirst(vmSelectionAction)
}
}
private fun BaseVariant.checkBundleEnabled(config: ReactAppExtension): Boolean {
if (name in config.bundleIn) {
return config.bundleIn.getValue(name)
}
if (buildType.name in config.bundleIn) {
return config.bundleIn.getValue(buildType.name)
}
return isRelease
}
private val BaseVariant.isRelease: Boolean
get() = name.toLowerCase().contains("release")

View File

@ -0,0 +1,20 @@
/*
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
package com.facebook.react.tasks
import org.apache.tools.ant.taskdefs.condition.Os
import org.gradle.process.ExecSpec
@Suppress("SpreadOperator")
internal fun ExecSpec.windowsAwareCommandLine(vararg args: Any) {
if (Os.isFamily(Os.FAMILY_WINDOWS)) {
commandLine("cmd", "/c", *args)
} else {
commandLine(*args)
}
}

View File

@ -8,6 +8,7 @@
plugins {
id("com.android.application")
id("com.facebook.react.codegen")
id("com.facebook.react.app")
}
/**
@ -75,21 +76,23 @@ plugins {
* ]
*/
reactApp {
cliPath = "$rootDir/cli.js"
bundleAssetName = "RNTesterApp.android.bundle"
entryFile = file("../../js/RNTesterApp.android.js")
reactRoot = rootDir
inputExcludes = ["android/**", "./**", ".gradle/**"]
composeSourceMapsPath = "$rootDir/scripts/compose-source-maps.js"
hermesCommand = "$rootDir/node_modules/hermes-engine/%OS-BIN%/hermesc"
enableHermesForVariant { def v -> v.name.contains("hermes") }
}
project.ext.react = [
cliPath: "$rootDir/cli.js",
bundleAssetName: "RNTesterApp.android.bundle",
entryFile: file("../../js/RNTesterApp.android.js"),
root: "$rootDir",
inputExcludes: ["android/**", "./**", ".gradle/**"],
composeSourceMapsPath: "$rootDir/scripts/compose-source-maps.js",
hermesCommand: "$rootDir/node_modules/hermes-engine/%OS-BIN%/hermesc",
enableHermesForVariant: { def v -> v.name.contains("hermes") },
jsRootDir: "$rootDir/RNTester",
enableCodegen: true, // Keep this here until it's sync'ed to Android template.
enableFabric: (System.getenv('USE_FABRIC') ?: '0').toBoolean(),
enableFabric: (System.getenv('USE_FABRIC') ?: '0').toBoolean()
]
apply from: "../../../../react.gradle"
//apply from: "../../../../react.gradle"
/**
* Set this to true to create three separate APKs instead of one:

View File

@ -21,3 +21,4 @@ include(
// Include this to enable codegen Gradle plugin.
includeBuild("packages/react-native-codegen/android")
includeBuild("packages/react-native-gradle-plugin/")