native_modules.gradle 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. import groovy.json.JsonSlurper
  2. import org.gradle.initialization.DefaultSettings
  3. import org.apache.tools.ant.taskdefs.condition.Os
  4. def generatedFileName = "PackageList.java"
  5. def generatedFilePackage = "com.facebook.react"
  6. def generatedFileContentsTemplate = """
  7. package $generatedFilePackage;
  8. import android.app.Application;
  9. import android.content.Context;
  10. import android.content.res.Resources;
  11. import com.facebook.react.ReactPackage;
  12. import com.facebook.react.shell.MainPackageConfig;
  13. import com.facebook.react.shell.MainReactPackage;
  14. import java.util.Arrays;
  15. import java.util.ArrayList;
  16. {{ packageImports }}
  17. public class PackageList {
  18. private Application application;
  19. private ReactNativeHost reactNativeHost;
  20. private MainPackageConfig mConfig;
  21. public PackageList(ReactNativeHost reactNativeHost) {
  22. this(reactNativeHost, null);
  23. }
  24. public PackageList(Application application) {
  25. this(application, null);
  26. }
  27. public PackageList(ReactNativeHost reactNativeHost, MainPackageConfig config) {
  28. this.reactNativeHost = reactNativeHost;
  29. mConfig = config;
  30. }
  31. public PackageList(Application application, MainPackageConfig config) {
  32. this.reactNativeHost = null;
  33. this.application = application;
  34. mConfig = config;
  35. }
  36. private ReactNativeHost getReactNativeHost() {
  37. return this.reactNativeHost;
  38. }
  39. private Resources getResources() {
  40. return this.getApplication().getResources();
  41. }
  42. private Application getApplication() {
  43. if (this.reactNativeHost == null) return this.application;
  44. return this.reactNativeHost.getApplication();
  45. }
  46. private Context getApplicationContext() {
  47. return this.getApplication().getApplicationContext();
  48. }
  49. public ArrayList<ReactPackage> getPackages() {
  50. return new ArrayList<>(Arrays.<ReactPackage>asList(
  51. new MainReactPackage(mConfig){{ packageClassInstances }}
  52. ));
  53. }
  54. }
  55. """
  56. class ReactNativeModules {
  57. private Logger logger
  58. private String packageName
  59. private File root
  60. private ArrayList<HashMap<String, String>> reactNativeModules
  61. private static String LOG_PREFIX = ":ReactNative:"
  62. ReactNativeModules(Logger logger, File root) {
  63. this.logger = logger
  64. this.root = root
  65. def (nativeModules, packageName) = this.getReactNativeConfig()
  66. this.reactNativeModules = nativeModules
  67. this.packageName = packageName
  68. }
  69. /**
  70. * Include the react native modules android projects and specify their project directory
  71. */
  72. void addReactNativeModuleProjects(DefaultSettings defaultSettings) {
  73. reactNativeModules.forEach { reactNativeModule ->
  74. String nameCleansed = reactNativeModule["nameCleansed"]
  75. String androidSourceDir = reactNativeModule["androidSourceDir"]
  76. defaultSettings.include(":${nameCleansed}")
  77. defaultSettings.project(":${nameCleansed}").projectDir = new File("${androidSourceDir}")
  78. }
  79. }
  80. /**
  81. * Adds the react native modules as dependencies to the users `app` project
  82. */
  83. void addReactNativeModuleDependencies(Project appProject) {
  84. reactNativeModules.forEach { reactNativeModule ->
  85. def nameCleansed = reactNativeModule["nameCleansed"]
  86. appProject.dependencies {
  87. // TODO(salakar): are other dependency scope methods such as `api` required?
  88. implementation project(path: ":${nameCleansed}")
  89. }
  90. }
  91. }
  92. /**
  93. * Code-gen a java file with all the detected ReactNativePackage instances automatically added
  94. *
  95. * @param outputDir
  96. * @param generatedFileName
  97. * @param generatedFileContentsTemplate
  98. */
  99. void generatePackagesFile(File outputDir, String generatedFileName, String generatedFileContentsTemplate) {
  100. ArrayList<HashMap<String, String>>[] packages = this.reactNativeModules
  101. String packageName = this.packageName
  102. String packageImports = ""
  103. String packageClassInstances = ""
  104. if (packages.size() > 0) {
  105. def interpolateDynamicValues = {
  106. it
  107. // Before adding the package replacement mechanism,
  108. // BuildConfig and R classes were imported automatically
  109. // into the scope of the file. We want to replace all
  110. // non-FQDN references to those classes with the package name
  111. // of the MainApplication.
  112. //
  113. // We want to match "R" or "BuildConfig":
  114. // - new Package(R.string…),
  115. // - Module.configure(BuildConfig);
  116. // ^ hence including (BuildConfig|R)
  117. // but we don't want to match "R":
  118. // - new Package(getResources…),
  119. // - new PackageR…,
  120. // - new Royal…,
  121. // ^ hence excluding \w before and after matches
  122. // and "BuildConfig" that has FQDN reference:
  123. // - Module.configure(com.acme.BuildConfig);
  124. // ^ hence excluding . before the match.
  125. .replaceAll(~/([^.\w])(BuildConfig|R)([^\w])/, {
  126. wholeString, prefix, className, suffix ->
  127. "${prefix}${packageName}.${className}${suffix}"
  128. })
  129. }
  130. packageImports = packages.collect {
  131. "// ${it.name}\n${interpolateDynamicValues(it.packageImportPath)}"
  132. }.join('\n')
  133. packageClassInstances = ",\n " + packages.collect {
  134. interpolateDynamicValues(it.packageInstance)
  135. }.join(",\n ")
  136. }
  137. String generatedFileContents = generatedFileContentsTemplate
  138. .replace("{{ packageImports }}", packageImports)
  139. .replace("{{ packageClassInstances }}", packageClassInstances)
  140. outputDir.mkdirs()
  141. final FileTreeBuilder treeBuilder = new FileTreeBuilder(outputDir)
  142. treeBuilder.file(generatedFileName).newWriter().withWriter { w ->
  143. w << generatedFileContents
  144. }
  145. }
  146. /**
  147. * Runs a specified command using Runtime exec() in a specified directory.
  148. * Throws when the command result is empty.
  149. */
  150. String getCommandOutput(String[] command, File directory) {
  151. try {
  152. def output = ""
  153. def cmdProcess = Runtime.getRuntime().exec(command, null, directory)
  154. def bufferedReader = new BufferedReader(new InputStreamReader(cmdProcess.getInputStream()))
  155. def buff = ""
  156. def readBuffer = new StringBuffer()
  157. while ((buff = bufferedReader.readLine()) != null) {
  158. readBuffer.append(buff)
  159. }
  160. output = readBuffer.toString()
  161. if (!output) {
  162. this.logger.error("${LOG_PREFIX}Unexpected empty result of running '${command}' command.")
  163. def bufferedErrorReader = new BufferedReader(new InputStreamReader(cmdProcess.getErrorStream()))
  164. def errBuff = ""
  165. def readErrorBuffer = new StringBuffer()
  166. while ((errBuff = bufferedErrorReader.readLine()) != null) {
  167. readErrorBuffer.append(errBuff)
  168. }
  169. throw new Exception(readErrorBuffer.toString())
  170. }
  171. return output
  172. } catch (Exception exception) {
  173. this.logger.error("${LOG_PREFIX}Running '${command}' command failed.")
  174. throw exception
  175. }
  176. }
  177. /**
  178. * Runs a process to call the React Native CLI Config command and parses the output
  179. */
  180. ArrayList<HashMap<String, String>> getReactNativeConfig() {
  181. if (this.reactNativeModules != null) return this.reactNativeModules
  182. ArrayList<HashMap<String, String>> reactNativeModules = new ArrayList<HashMap<String, String>>()
  183. /**
  184. * Resolve the CLI location from Gradle file
  185. *
  186. * @todo: Sometimes Gradle can be called outside of the JavaScript hierarchy (-p flag) which
  187. * will fail to resolve the script and the dependencies. We should resolve this soon.
  188. *
  189. * @todo: `fastlane` has been reported to not work too.
  190. */
  191. def cliResolveScript = "console.log(require('react-native/cli').bin);"
  192. String[] nodeCommand = ["node", "-e", cliResolveScript]
  193. def cliPath = this.getCommandOutput(nodeCommand, this.root)
  194. String[] reactNativeConfigCommand = ["node", cliPath, "config"]
  195. def reactNativeConfigOutput = this.getCommandOutput(reactNativeConfigCommand, this.root)
  196. def json
  197. try {
  198. json = new JsonSlurper().parseText(reactNativeConfigOutput)
  199. } catch (Exception exception) {
  200. throw new Exception("Calling `${reactNativeConfigCommand}` finished with an exception. Error message: ${exception.toString()}. Output: ${reactNativeConfigOutput}");
  201. }
  202. def dependencies = json["dependencies"]
  203. def project = json["project"]["android"]
  204. if (project == null) {
  205. throw new Exception("React Native CLI failed to determine Android project configuration. This is likely due to misconfiguration. Config output:\n${json.toMapString()}")
  206. }
  207. dependencies.each { name, value ->
  208. def platformsConfig = value["platforms"];
  209. def androidConfig = platformsConfig["android"]
  210. if (androidConfig != null && androidConfig["sourceDir"] != null) {
  211. this.logger.info("${LOG_PREFIX}Automatically adding native module '${name}'")
  212. HashMap reactNativeModuleConfig = new HashMap<String, String>()
  213. reactNativeModuleConfig.put("name", name)
  214. reactNativeModuleConfig.put("nameCleansed", name.replaceAll('[~*!\'()]+', '_').replaceAll('^@([\\w-.]+)/', '$1_'))
  215. reactNativeModuleConfig.put("androidSourceDir", androidConfig["sourceDir"])
  216. reactNativeModuleConfig.put("packageInstance", androidConfig["packageInstance"])
  217. reactNativeModuleConfig.put("packageImportPath", androidConfig["packageImportPath"])
  218. this.logger.trace("${LOG_PREFIX}'${name}': ${reactNativeModuleConfig.toMapString()}")
  219. reactNativeModules.add(reactNativeModuleConfig)
  220. } else {
  221. this.logger.info("${LOG_PREFIX}Skipping native module '${name}'")
  222. }
  223. }
  224. return [reactNativeModules, json["project"]["android"]["packageName"]];
  225. }
  226. }
  227. /*
  228. * Sometimes Gradle can be called outside of JavaScript hierarchy. Detect the directory
  229. * where build files of an active project are located.
  230. */
  231. def projectRoot = rootProject.projectDir
  232. def autoModules = new ReactNativeModules(logger, projectRoot)
  233. /** -----------------------
  234. * Exported Extensions
  235. * ------------------------ */
  236. ext.applyNativeModulesSettingsGradle = { DefaultSettings defaultSettings, String root = null ->
  237. if (root != null) {
  238. logger.warn("${ReactNativeModules.LOG_PREFIX}Passing custom root is deprecated. CLI detects root automatically now.");
  239. logger.warn("${ReactNativeModules.LOG_PREFIX}Please remove second argument to `applyNativeModulesSettingsGradle`.");
  240. }
  241. autoModules.addReactNativeModuleProjects(defaultSettings)
  242. }
  243. ext.applyNativeModulesAppBuildGradle = { Project project, String root = null ->
  244. if (root != null) {
  245. logger.warn("${ReactNativeModules.LOG_PREFIX}Passing custom root is deprecated. CLI detects root automatically now");
  246. logger.warn("${ReactNativeModules.LOG_PREFIX}Please remove second argument to `applyNativeModulesAppBuildGradle`.");
  247. }
  248. autoModules.addReactNativeModuleDependencies(project)
  249. def generatedSrcDir = new File(buildDir, "generated/rncli/src/main/java")
  250. def generatedCodeDir = new File(generatedSrcDir, generatedFilePackage.replace('.', '/'))
  251. task generatePackageList {
  252. doLast {
  253. autoModules.generatePackagesFile(generatedCodeDir, generatedFileName, generatedFileContentsTemplate)
  254. }
  255. }
  256. preBuild.dependsOn generatePackageList
  257. android {
  258. sourceSets {
  259. main {
  260. java {
  261. srcDirs += generatedSrcDir
  262. }
  263. }
  264. }
  265. }
  266. }