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