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