/* * 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. */ package com.facebook.react import com.facebook.react.model.ModelAutolinkingConfigJson import com.facebook.react.utils.JsonUtils import com.facebook.react.utils.windowsAwareCommandLine import java.io.File import java.math.BigInteger import java.security.MessageDigest import javax.inject.Inject import kotlin.math.min import org.gradle.api.GradleException import org.gradle.api.file.FileCollection import org.gradle.api.initialization.Settings import org.gradle.api.logging.Logging abstract class ReactSettingsExtension @Inject constructor(val settings: Settings) { private val outputFile = settings.layout.rootDirectory.file("build/generated/autolinking/autolinking.json").asFile private val outputFolder = settings.layout.rootDirectory.file("build/generated/autolinking/").asFile private val defaultConfigCommand: List = windowsAwareCommandLine(listOf("npx", "@react-native-community/cli", "config")).map { it.toString() } /** * Utility function to autolink libraries using an external command as source of truth. * * This should be invoked inside the `settings.gradle` file, and will make sure the Gradle project * is loading all the discovered libraries. * * @param command The command to execute to get the autolinking configuration. Default is * `npx @react-native-community/cli config`. * @param workingDirectory The directory where the command should be executed. * @param lockFiles The list of lock files to check for changes (if lockfiles are not changed, the * command will not be executed). */ @JvmOverloads public fun autolinkLibrariesFromCommand( command: List = defaultConfigCommand, workingDirectory: File? = settings.layout.rootDirectory.dir("../").asFile, lockFiles: FileCollection = settings.layout.rootDirectory .dir("../") .files("yarn.lock", "package-lock.json", "package.json", "react-native.config.js") ) { outputFile.parentFile.mkdirs() val updateConfig = object : GenerateConfig { override fun command(): List = command override fun execute(): Int { val execResult = settings.providers.exec { exec -> exec.commandLine(command) exec.workingDir = workingDirectory } outputFile.writeText(execResult.standardOutput.asText.get()) return execResult.result.get().exitValue } } checkAndUpdateCache(updateConfig, outputFile, outputFolder, lockFiles) linkLibraries(getLibrariesToAutolink(outputFile)) } /** * Utility function to autolink libraries using an external file as source of truth. * * The file should be a JSON file with the same structure as the one generated by the * `npx @react-native-community/cli config` command. * * @param autolinkConfigFile The file to read the autolinking configuration from. */ public fun autolinkLibrariesFromConfigFile( autolinkConfigFile: File, ) { // We copy the file to the build directory so that the various Gradle tasks can access it. autolinkConfigFile.copyTo(outputFile, overwrite = true) linkLibraries(getLibrariesToAutolink(autolinkConfigFile)) } /** * Utility function so that for each tuple :project-name -> project-dir, it instructs Gradle to * lad this extra module. */ private fun linkLibraries(input: Map) { input.forEach { (path, projectDir) -> settings.include(path) settings.project(path).projectDir = projectDir } } internal interface GenerateConfig { fun command(): List fun execute(): Int } companion object { private val md = MessageDigest.getInstance("SHA-256") /** * Determine if our cache is out-of-date * * @param cacheJsonConfig Our current cached autolinking.json config, which may exist * @param cacheFolder The folder we store our cached SHAs and config * @param lockFiles The [FileCollection] of the lockfiles to check. * @return `true` if the cache needs to be rebuilt, `false` otherwise */ internal fun isCacheDirty( cacheJsonConfig: File, cacheFolder: File, lockFiles: FileCollection, ): Boolean { if (cacheJsonConfig.exists().not() || cacheJsonConfig.length() == 0L) { return true } val lockFilesChanged = checkAndUpdateLockfiles(lockFiles, cacheFolder) if (lockFilesChanged) { return true } return isConfigModelInvalid(JsonUtils.fromAutolinkingConfigJson(cacheJsonConfig)) } /** * Utility function to update the settings cache only if it's entries are dirty * * @param updateJsonConfig A [GenerateConfig] to update the project's autolinking config * @param cacheJsonConfig Our current cached autolinking.json config, which may exist * @param cacheFolder The folder we store our cached SHAs and config * @param lockFiles The [FileCollection] of the lockfiles to check. */ internal fun checkAndUpdateCache( updateJsonConfig: GenerateConfig, cacheJsonConfig: File, cacheFolder: File, lockFiles: FileCollection, ) { if (isCacheDirty(cacheJsonConfig, cacheFolder, lockFiles)) { val exitValue = updateJsonConfig.execute() if (exitValue != 0) { val prefixCommand = "ERROR: autolinkLibrariesFromCommand: process ${updateJsonConfig.command().joinToString(" ")}" val message = "$prefixCommand exited with error code: $exitValue" val logger = Logging.getLogger("ReactSettingsExtension") logger.error(message) if (cacheJsonConfig.length() != 0L) { logger.error( cacheJsonConfig .readText() .substring(0, min(1024, cacheJsonConfig.length().toInt()))) } cacheJsonConfig.delete() throw GradleException(message) } else { // If cache was dirty, we executed the command and we need to update the lockfiles sha. checkAndUpdateLockfiles(lockFiles, cacheFolder) } } } /** * Utility function to check if the provided lockfiles have been updated or not. This function * will both check and update the lockfiles hashes if necessary. * * @param lockFiles The [FileCollection] of the lockfiles to check. * @param outputFolder The folder where the hashes will be stored. * @return `true` if the lockfiles have been updated, `false` otherwise. */ internal fun checkAndUpdateLockfiles(lockFiles: FileCollection, outputFolder: File): Boolean { var changed = false lockFiles.forEach { lockFile -> if (lockFile.exists()) { val sha = computeSha256(lockFile) val shaFile = File(outputFolder, "${lockFile.name}.sha") if (shaFile.exists().not() || shaFile.readText() != sha) { shaFile.writeText(sha) changed = true } } } return changed } internal fun getLibrariesToAutolink(buildFile: File): Map { val model = JsonUtils.fromAutolinkingConfigJson(buildFile) return model ?.dependencies ?.values // We handle scenarios where there are deps that are // iOS-only or missing the Android configs. ?.filter { it.platforms?.android?.sourceDir != null } // We want to skip dependencies that are pure C++ as they won't contain a .gradle file. ?.filterNot { it.platforms?.android?.isPureCxxDependency == true } ?.associate { deps -> ":${deps.nameCleansed}" to File(deps.platforms?.android?.sourceDir) } ?: emptyMap() } internal fun computeSha256(lockFile: File) = String.format("%032x", BigInteger(1, md.digest(lockFile.readBytes()))) internal fun isConfigModelInvalid(model: ModelAutolinkingConfigJson?) = model?.project?.android?.packageName == null } }