/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ import org.apache.tools.ant.taskdefs.condition.Os import org.gradle.internal.jvm.Jvm def config = project.hasProperty("react") ? project.react : [:]; def detectEntryFile(config) { if (System.getenv('ENTRY_FILE')) { return System.getenv('ENTRY_FILE') } else if (config.entryFile) { return config.entryFile } else if ((new File("${projectDir}/../../index.android.js")).exists()) { return "index.android.js" } return "index.js"; } /** * Detects CLI location in a similar fashion to the React Native CLI */ def detectCliPath(config) { if (config.cliPath) { return "${projectDir}/${config.cliPath}" } if (new File("${projectDir}/../../node_modules/react-native/cli.js").exists()) { return "${projectDir}/../../node_modules/react-native/cli.js" } throw new Exception("Couldn't determine CLI location. " + "Please set `project.ext.react.cliPath` to the path of the react-native cli.js file. This file typically resides in `node_modules/react-native/cli.js`"); } def composeSourceMapsPath = config.composeSourceMapsPath ?: "node_modules/react-native/scripts/compose-source-maps.js" def bundleAssetName = config.bundleAssetName ?: "index.android.bundle" def entryFile = detectEntryFile(config) def bundleCommand = config.bundleCommand ?: "bundle" def reactRoot = file(config.root ?: "../../") def inputExcludes = config.inputExcludes ?: ["android/**", "ios/**"] def bundleConfig = config.bundleConfig ? "${reactRoot}/${config.bundleConfig}" : null ; def enableVmCleanup = config.enableVmCleanup == null ? true : config.enableVmCleanup def hermesCommand = config.hermesCommand ?: "../../node_modules/hermes-engine/%OS-BIN%/hermesc" def reactNativeDevServerPort() { def value = project.getProperties().get("reactNativeDevServerPort") return value != null ? value : "8081" } def reactNativeInspectorProxyPort() { def value = project.getProperties().get("reactNativeInspectorProxyPort") return value != null ? value : reactNativeDevServerPort() } def getHermesOSBin() { 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"; throw new Exception("OS not recognized. Please set project.ext.react.hermesCommand " + "to the path of a working Hermes compiler."); } // Make sure not to inspect the Hermes config unless we need it, // to avoid breaking any JSC-only setups. def getHermesCommand = { // 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 .replaceAll("%OS-BIN%", getHermesOSBin()) .replace('/' as char, File.separatorChar); } // Set enableHermesForVariant to a function to configure per variant, // or set `enableHermes` to True/False to set all of them def enableHermesForVariant = config.enableHermesForVariant ?: { def variant -> config.enableHermes ?: false } // Set hermesFlagsForVariant to a function to configure per variant, // or set `hermesFlagsRelease` and `hermesFlagsDebug` to an array def hermesFlagsForVariant = config.hermesFlagsForVariant ?: { def variant -> def hermesFlags; if (variant.name.toLowerCase().contains("release")) { // Can't use ?: since that will also substitute valid empty lists hermesFlags = config.hermesFlagsRelease if (hermesFlags == null) hermesFlags = ["-O", "-output-source-map"] } else { hermesFlags = config.hermesFlagsDebug if (hermesFlags == null) hermesFlags = [] } return hermesFlags } // Set disableDevForVariant to a function to configure per variant, // defaults to `devDisabledIn${targetName}` or True for Release variants and False for debug variants def disableDevForVariant = config.disableDevForVariant ?: { def variant -> config."devDisabledIn${variant.name.capitalize()}" || variant.name.toLowerCase().contains("release") } // Set bundleForVariant to a function to configure per variant, // defaults to `bundleIn${targetName}` or True for Release variants and False for debug variants def bundleForVariant = config.bundleForVariant ?: { def variant -> config."bundleIn${variant.name.capitalize()}" || config."bundleIn${variant.buildType.name.capitalize()}" || variant.name.toLowerCase().contains("release") } // Set deleteDebugFilesForVariant to a function to configure per variant, // defaults to True for Release variants and False for debug variants def deleteDebugFilesForVariant = config.deleteDebugFilesForVariant ?: { def variant -> variant.name.toLowerCase().contains("release") } android { buildTypes.all { resValue "integer", "react_native_dev_server_port", reactNativeDevServerPort() resValue "integer", "react_native_inspector_proxy_port", reactNativeInspectorProxyPort() } } def jvmVersion = Jvm.current().javaVersion.majorVersion if (jvmVersion.toInteger() <= 8) { println "\n\n\n" println "**************************************************************************************************************" println "\n\n" println "ERROR: requires JDK11 or higher." println "Incompatible major version detected: '" + jvmVersion + "'" println "\n\n" println "**************************************************************************************************************" println "\n\n\n" System.exit(1) } afterEvaluate { def isAndroidLibrary = plugins.hasPlugin("com.android.library") def variants = isAndroidLibrary ? android.libraryVariants : android.applicationVariants variants.all { def variant -> // Create variant and target names def targetName = variant.name.capitalize() def targetPath = variant.dirName // React js bundle directories def jsBundleDir = file("$buildDir/generated/assets/react/${targetPath}") def resourcesDir = file("$buildDir/generated/res/react/${targetPath}") def jsBundleFile = file("$jsBundleDir/$bundleAssetName") def jsSourceMapsDir = file("$buildDir/generated/sourcemaps/react/${targetPath}") def jsIntermediateSourceMapsDir = file("$buildDir/intermediates/sourcemaps/react/${targetPath}") def jsPackagerSourceMapFile = file("$jsIntermediateSourceMapsDir/${bundleAssetName}.packager.map") def jsCompilerSourceMapFile = file("$jsIntermediateSourceMapsDir/${bundleAssetName}.compiler.map") def jsOutputSourceMapFile = file("$jsSourceMapsDir/${bundleAssetName}.map") // Additional node and packager commandline arguments def nodeExecutableAndArgs = config.nodeExecutableAndArgs ?: ["node"] def cliPath = detectCliPath(config) def execCommand = [] if (Os.isFamily(Os.FAMILY_WINDOWS)) { execCommand.addAll(["cmd", "/c", *nodeExecutableAndArgs, cliPath]) } else { execCommand.addAll([*nodeExecutableAndArgs, cliPath]) } def enableHermes = enableHermesForVariant(variant) def currentBundleTask = tasks.create( name: "bundle${targetName}JsAndAssets", type: Exec) { 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.deleteDir() jsBundleDir.mkdirs() resourcesDir.deleteDir() resourcesDir.mkdirs() jsIntermediateSourceMapsDir.deleteDir() jsIntermediateSourceMapsDir.mkdirs() jsSourceMapsDir.deleteDir() jsSourceMapsDir.mkdirs() } // Set up inputs and outputs so gradle can cache the result inputs.files fileTree(dir: reactRoot, excludes: inputExcludes) outputs.dir(jsBundleDir) outputs.dir(resourcesDir) // Set up the call to the react-native cli workingDir(reactRoot) // Set up dev mode def devEnabled = !disableDevForVariant(variant) def extraArgs = [] if (bundleConfig) { extraArgs.add("--config") extraArgs.add(bundleConfig) } // Hermes doesn't require JS minification. if (enableHermes && !devEnabled) { extraArgs.add("--minify") extraArgs.add("false") } if (config.extraPackagerArgs) { extraArgs.addAll(config.extraPackagerArgs) } commandLine(*execCommand, bundleCommand, "--platform", "android", "--dev", "${devEnabled}", "--reset-cache", "--entry-file", entryFile, "--bundle-output", jsBundleFile, "--assets-dest", resourcesDir, "--sourcemap-output", enableHermes ? jsPackagerSourceMapFile : jsOutputSourceMapFile, *extraArgs) if (enableHermes) { doLast { def hermesFlags = hermesFlagsForVariant(variant) def hbcTempFile = file("${jsBundleFile}.hbc") exec { if (Os.isFamily(Os.FAMILY_WINDOWS)) { commandLine("cmd", "/c", getHermesCommand(), "-emit-binary", "-out", hbcTempFile, jsBundleFile, *hermesFlags) } else { commandLine(getHermesCommand(), "-emit-binary", "-out", hbcTempFile, jsBundleFile, *hermesFlags) } } ant.move( file: hbcTempFile, toFile: jsBundleFile ); if (hermesFlags.contains("-output-source-map")) { ant.move( // Hermes will generate a source map with this exact name file: "${jsBundleFile}.hbc.map", tofile: jsCompilerSourceMapFile ); exec { // TODO: set task dependencies for caching // Set up the call to the compose-source-maps script workingDir(reactRoot) if (Os.isFamily(Os.FAMILY_WINDOWS)) { commandLine("cmd", "/c", *nodeExecutableAndArgs, composeSourceMapsPath, jsPackagerSourceMapFile, jsCompilerSourceMapFile, "-o", jsOutputSourceMapFile) } else { commandLine(*nodeExecutableAndArgs, composeSourceMapsPath, jsPackagerSourceMapFile, jsCompilerSourceMapFile, "-o", jsOutputSourceMapFile) } } } } } enabled bundleForVariant(variant) } // Expose a minimal interface on the application variant and the task itself: variant.ext.bundleJsAndAssets = currentBundleTask currentBundleTask.ext.generatedResFolders = files(resourcesDir).builtBy(currentBundleTask) currentBundleTask.ext.generatedAssetsFolders = files(jsBundleDir).builtBy(currentBundleTask) // registerGeneratedResFolders for Android plugin 3.x if (variant.respondsTo("registerGeneratedResFolders")) { variant.registerGeneratedResFolders(currentBundleTask.generatedResFolders) } else { variant.registerResGeneratingTask(currentBundleTask) } variant.mergeResourcesProvider.get().dependsOn(currentBundleTask) // packageApplication for Android plugin 3.x def packageTask = variant.hasProperty("packageApplication") ? variant.packageApplicationProvider.get() : tasks.findByName("package${targetName}") if (variant.hasProperty("packageLibrary")) { packageTask = variant.packageLibrary } // pre bundle build task for Android plugin 3.2+ def buildPreBundleTask = tasks.findByName("build${targetName}PreBundle") def resourcesDirConfigValue = config."resourcesDir${targetName}" if (resourcesDirConfigValue) { def currentCopyResTask = tasks.create( name: "copy${targetName}BundledResources", type: Copy) { group = "react" description = "copy bundled resources into custom location for ${targetName}." from(resourcesDir) into(file(resourcesDirConfigValue)) dependsOn(currentBundleTask) enabled(currentBundleTask.enabled) } packageTask.dependsOn(currentCopyResTask) if (buildPreBundleTask != null) { buildPreBundleTask.dependsOn(currentCopyResTask) } } def currentAssetsCopyTask = tasks.create( name: "copy${targetName}BundledJs", type: Copy) { group = "react" description = "copy bundled JS into ${targetName}." from(jsBundleDir) if (config."jsBundleDir${targetName}") { into(file(config."jsBundleDir${targetName}")) } else { into ("$buildDir/intermediates") if (isAndroidLibrary) { into ("library_assets/${variant.name}/out") } else { into ("assets/${targetPath}") // Workaround for Android Gradle Plugin 3.2+ new asset directory into ("merged_assets/${variant.name}/merge${targetName}Assets/out") // Workaround for Android Gradle Plugin 3.4+ new asset directory into ("merged_assets/${variant.name}/out") // Workaround for Android Gradle Plugin 7.1 asset directory into("$buildDir/intermediates/assets/${variant.name}/merge${targetName}Assets") } } // mergeAssets must run first, as it clears the intermediates directory dependsOn(variant.mergeAssetsProvider.get()) enabled(currentBundleTask.enabled) dependsOn(currentBundleTask) } // 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 def mergeResourcesTask = tasks.findByName("merge${targetName}Resources") mergeResourcesTask.dependsOn(currentAssetsCopyTask) packageTask.dependsOn(currentAssetsCopyTask) if (buildPreBundleTask != null) { 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. def cleanup = deleteDebugFilesForVariant(variant) def vmSelectionAction = { libDir -> fileTree(libDir).matching { if (enableHermes) { // For Hermes, delete all the libjsc* files include "**/libjsc*.so" if (cleanup) { // Reduce size by deleting the debugger/inspector 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 { details -> def targetVariant1 = ".*/transforms/[^/]*/${variant.name}/.*" def targetVariant2 = ".*/merged_native_libs/${variant.name}/out/lib/.*" def targetVariant3 = ".*/stripped_native_libs/${variant.name}/out/lib/.*" def path = details.file.getAbsolutePath().replace(File.separatorChar, '/' as char) if ((path.matches(targetVariant1) || path.matches(targetVariant2) || path.matches(targetVariant3)) && details.file.isFile()) { details.file.delete() } } } if (enableVmCleanup) { def task = tasks.findByName("package${targetName}") def transformsLibDir = "$buildDir/intermediates/transforms/" task.doFirst { vmSelectionAction(transformsLibDir) } def sTask = tasks.findByName("strip${targetName}DebugSymbols") if (sTask != null) { def strippedLibDir = "$buildDir/intermediates/stripped_native_libs/${variant.name}/out/lib/" sTask.doLast { vmSelectionAction(strippedLibDir) } } def mTask = tasks.findByName("merge${targetName}NativeLibs") if (mTask != null) { def mergedLibDir = "$buildDir/intermediates/merged_native_libs/${variant.name}/out/lib/" mTask.doLast { vmSelectionAction(mergedLibDir) } } } } }