1import groovy.json.JsonSlurper 2import java.nio.file.Paths 3 4 5// Object representing a gradle project. 6class ExpoModuleGradleProject { 7 // Name of the Android project 8 String name 9 10 // Path to the folder with Android project 11 String sourceDir 12 13 ExpoModuleGradleProject(Object data) { 14 this.name = data.name 15 this.sourceDir = data.sourceDir 16 } 17} 18 19// Object representing a gradle plugin 20class ExpoModuleGradlePlugin { 21 // ID of the gradle plugin 22 String id 23 24 // Artifact group 25 String group 26 27 // Path to the plugin folder 28 String sourceDir 29 30 ExpoModuleGradlePlugin(Object data) { 31 this.id = data.id 32 this.group = data.group 33 this.sourceDir = data.sourceDir 34 } 35} 36 37// Object representing a module. 38class ExpoModule { 39 // Name of the JavaScript package 40 String name 41 42 // Version of the package, loaded from `package.json` 43 String version 44 45 // Gradle projects 46 ExpoModuleGradleProject[] projects 47 48 // Gradle plugins 49 ExpoModuleGradlePlugin[] plugins 50 51 ExpoModule(Object data) { 52 this.name = data.packageName 53 this.version = data.packageVersion 54 this.projects = data.projects.collect { new ExpoModuleGradleProject(it) } 55 this.plugins = data.plugins.collect { new ExpoModuleGradlePlugin(it) } 56 } 57} 58 59class ExpoAutolinkingManager { 60 private File projectDir 61 private Map options 62 private Object cachedResolvingResults 63 64 static String generatedPackageListNamespace = 'expo.modules' 65 static String generatedPackageListFilename = 'ExpoModulesPackageList.java' 66 static String generatedFilesSrcDir = 'generated/expo/src/main/java' 67 68 ExpoAutolinkingManager(File projectDir, Map options = [:]) { 69 this.projectDir = projectDir 70 this.options = options 71 } 72 73 Object resolve() { 74 if (cachedResolvingResults) { 75 return cachedResolvingResults 76 } 77 String[] args = convertOptionsToCommandArgs('resolve', this.options) 78 args += ['--json'] 79 80 String output = exec(args, projectDir) 81 Object json = new JsonSlurper().parseText(output) 82 83 cachedResolvingResults = json 84 return json 85 } 86 87 boolean shouldUseAAR() { 88 return options?.useAAR == true 89 } 90 91 ExpoModule[] getModules() { 92 Object json = resolve() 93 return json.modules.collect { new ExpoModule(it) } 94 } 95 96 static void generatePackageList(Project project, Map options) { 97 String[] args = convertOptionsToCommandArgs('generate-package-list', options) 98 99 // Construct absolute path to generated package list. 100 def generatedFilePath = Paths.get( 101 project.buildDir.toString(), 102 generatedFilesSrcDir, 103 generatedPackageListNamespace.replace('.', '/'), 104 generatedPackageListFilename 105 ) 106 107 args += [ 108 '--namespace', 109 generatedPackageListNamespace, 110 '--target', 111 generatedFilePath.toString() 112 ] 113 114 if (options == null) { 115 // Options are provided only when settings.gradle was configured. 116 // If not or opted-out from autolinking, the generated list should be empty. 117 args += '--empty' 118 } 119 120 exec(args, project.rootDir) 121 } 122 123 static String exec(String[] commandArgs, File dir) { 124 Process proc = commandArgs.execute(null, dir) 125 StringBuffer outputStream = new StringBuffer() 126 proc.waitForProcessOutput(outputStream, System.err) 127 return outputStream.toString() 128 } 129 130 static private String[] convertOptionsToCommandArgs(String command, Map options) { 131 String[] args = [ 132 'node', 133 '--no-warnings', 134 '--eval', 135 'require(\'expo-modules-autolinking\')(process.argv.slice(1))', 136 '--', 137 command, 138 '--platform', 139 'android' 140 ] 141 142 def searchPaths = options?.get("searchPaths", options?.get("modulesPaths", null)) 143 if (searchPaths) { 144 args += searchPaths 145 } 146 147 if (options?.ignorePaths) { 148 args += '--ignore-paths' 149 args += options.ignorePaths 150 } 151 152 if (options?.exclude) { 153 args += '--exclude' 154 args += options.exclude 155 } 156 157 return args 158 } 159} 160 161class Colors { 162 static final String GREEN = "\u001B[32m" 163 static final String YELLOW = "\u001B[33m" 164 static final String RESET = "\u001B[0m" 165} 166class Emojis { 167 static final String INFORMATION = "\u2139\uFE0F" 168} 169 170// We can't cast a manager that is created in `settings.gradle` to the `ExpoAutolinkingManager` 171// because if someone is using `buildSrc`, the `ExpoAutolinkingManager` class 172// will be loaded by two different class loader - `settings.gradle` will use a diffrent loader. 173// In the JVM, classes are equal only if were loaded by the same loader. 174// There is nothing that we can do in that case, but to make our code safer, we check if the class name is the same. 175def validateExpoAutolinkingManager(manager) { 176 assert ExpoAutolinkingManager.name == manager.getClass().name 177 return manager 178} 179 180// Here we split the implementation, depending on Gradle context. 181// `rootProject` is a `ProjectDescriptor` if this file is imported in `settings.gradle` context, 182// otherwise we can assume it is imported in `build.gradle`. 183if (rootProject instanceof ProjectDescriptor) { 184 // Method to be used in `settings.gradle`. Options passed here will have an effect in `build.gradle` context as well, 185 // i.e. adding the dependencies and generating the package list. 186 ext.useExpoModules = { Map options = [:] -> 187 ExpoAutolinkingManager manager = new ExpoAutolinkingManager(rootProject.projectDir, options) 188 ExpoModule[] modules = manager.getModules() 189 190 for (module in modules) { 191 for (moduleProject in module.projects) { 192 include(":${moduleProject.name}") 193 project(":${moduleProject.name}").projectDir = new File(moduleProject.sourceDir) 194 } 195 for (modulePlugin in module.plugins) { 196 includeBuild(new File(modulePlugin.sourceDir)) 197 } 198 } 199 200 // Add plugin classpath to the root project 201 gradle.beforeProject { project -> 202 if (project === project.rootProject) { 203 for (module in modules) { 204 for (modulePlugin in module.plugins) { 205 project.buildscript.dependencies.add('classpath', "${modulePlugin.group}:${modulePlugin.id}") 206 } 207 } 208 } 209 } 210 211 // Apply plugins for all app projects 212 gradle.afterProject { project -> 213 if (!project.plugins.hasPlugin('com.android.application')) { 214 return 215 } 216 for (module in modules) { 217 for (modulePlugin in module.plugins) { 218 println " ${Emojis.INFORMATION} ${Colors.YELLOW}Applying gradle plugin${Colors.RESET} '${Colors.GREEN}${modulePlugin.id}${Colors.RESET}' (${module.name}@${module.version})" 219 project.plugins.apply(modulePlugin.id) 220 } 221 } 222 } 223 224 // Save the manager in the shared context, so that we can later use it in `build.gradle`. 225 gradle.ext.expoAutolinkingManager = manager 226 } 227} else { 228 def addModule = { DependencyHandler handler, String projectName, Boolean useAAR -> 229 Project dependency = rootProject.project(":${projectName}") 230 231 if (useAAR) { 232 handler.add('api', "${dependency.group}:${projectName}:${dependency.version}") 233 } else { 234 handler.add('api', dependency) 235 } 236 } 237 238 def addDependencies = { DependencyHandler handler, Project project -> 239 def manager = validateExpoAutolinkingManager(gradle.ext.expoAutolinkingManager) 240 def modules = manager.getModules() 241 242 if (!modules.length) { 243 return 244 } 245 246 println '' 247 println 'Using expo modules' 248 249 for (module in modules) { 250 // Don't link itself 251 if (module.name == project.name) { 252 continue 253 } 254 // Can remove this once we move all the interfaces into the core. 255 if (module.name.endsWith('-interface')) { 256 continue 257 } 258 259 for (moduleProject in module.projects) { 260 addModule(handler, moduleProject.name, manager.shouldUseAAR()) 261 println " - ${Colors.GREEN}${moduleProject.name}${Colors.RESET} (${module.version})" 262 } 263 } 264 265 println '' 266 } 267 268 // Adding dependencies 269 ext.addExpoModulesDependencies = { DependencyHandler handler, Project project -> 270 // Return early if `useExpoModules` was not called in `settings.gradle` 271 if (!gradle.ext.has('expoAutolinkingManager')) { 272 logger.error('Error: Autolinking is not set up in `settings.gradle`: expo modules won\'t be autolinked.') 273 return 274 } 275 276 def manager = validateExpoAutolinkingManager(gradle.ext.expoAutolinkingManager) 277 278 if (rootProject.findProject(':expo-modules-core')) { 279 // `expo` requires `expo-modules-core` as a dependency, even if autolinking is turned off. 280 addModule(handler, 'expo-modules-core', manager.shouldUseAAR()) 281 } else { 282 logger.error('Error: `expo-modules-core` project is not included by autolinking.') 283 } 284 285 // If opted-in not to autolink modules as dependencies 286 if (manager.options == null) { 287 return 288 } 289 290 addDependencies(handler, project) 291 } 292 293 // Generating the package list 294 ext.generatedFilesSrcDir = ExpoAutolinkingManager.generatedFilesSrcDir 295 296 ext.generateExpoModulesPackageList = { 297 // Get options used in `settings.gradle` or null if it wasn't set up. 298 Map options = gradle.ext.has('expoAutolinkingManager') ? gradle.ext.expoAutolinkingManager.options : null 299 300 if (options == null) { 301 // TODO(@tsapeta): Temporarily muted this error — uncomment it once we start migrating from autolinking v1 to v2 302 // logger.error('Autolinking is not set up in `settings.gradle`: generated package list with expo modules will be empty.') 303 } 304 ExpoAutolinkingManager.generatePackageList(project, options) 305 } 306 307 ext.ensureDependeciesWereEvaluated = { Project project -> 308 if (!gradle.ext.has('expoAutolinkingManager')) { 309 return 310 } 311 312 def modules = gradle.ext.expoAutolinkingManager.getModules() 313 for (module in modules) { 314 for (moduleProject in module.projects) { 315 def dependency = project.findProject(":${moduleProject.name}") 316 if (dependency == null) { 317 logger.warn("Coudn't find project ${moduleProject.name}. Please, make sure that `useExpoModules` was called in `settings.gradle`.") 318 continue 319 } 320 321 // Prevent circular dependencies 322 if (moduleProject.name == project.name) { 323 continue 324 } 325 326 project.evaluationDependsOn(":${moduleProject.name}") 327 } 328 } 329 } 330} 331