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 // Resolve the `expo` > `expo-modules-autolinking` chain from the project root 157 'require(require.resolve(\'expo-modules-autolinking\', { paths: [require.resolve(\'expo\')] }))(process.argv.slice(1))', 158 '--', 159 command, 160 '--platform', 161 'android' 162 ] 163 164 def searchPaths = options?.get("searchPaths", options?.get("modulesPaths", null)) 165 if (searchPaths) { 166 args += searchPaths 167 } 168 169 if (options?.ignorePaths) { 170 args += '--ignore-paths' 171 args += options.ignorePaths 172 } 173 174 if (options?.exclude) { 175 args += '--exclude' 176 args += options.exclude 177 } 178 179 return args 180 } 181} 182 183class Colors { 184 static final String GREEN = "\u001B[32m" 185 static final String YELLOW = "\u001B[33m" 186 static final String RESET = "\u001B[0m" 187} 188class Emojis { 189 static final String INFORMATION = "\u2139\uFE0F" 190} 191 192// We can't cast a manager that is created in `settings.gradle` to the `ExpoAutolinkingManager` 193// because if someone is using `buildSrc`, the `ExpoAutolinkingManager` class 194// will be loaded by two different class loader - `settings.gradle` will use a diffrent loader. 195// In the JVM, classes are equal only if were loaded by the same loader. 196// 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. 197def validateExpoAutolinkingManager(manager) { 198 assert ExpoAutolinkingManager.name == manager.getClass().name 199 return manager 200} 201 202// Here we split the implementation, depending on Gradle context. 203// `rootProject` is a `ProjectDescriptor` if this file is imported in `settings.gradle` context, 204// otherwise we can assume it is imported in `build.gradle`. 205if (rootProject instanceof ProjectDescriptor) { 206 // Method to be used in `settings.gradle`. Options passed here will have an effect in `build.gradle` context as well, 207 // i.e. adding the dependencies and generating the package list. 208 ext.useExpoModules = { Map options = [:] -> 209 ExpoAutolinkingManager manager = new ExpoAutolinkingManager(rootProject.projectDir, options) 210 ExpoModule[] modules = manager.getModules() 211 MavenRepo[] extraMavenRepos = manager.getExtraMavenRepos() 212 213 for (module in modules) { 214 for (moduleProject in module.projects) { 215 include(":${moduleProject.name}") 216 project(":${moduleProject.name}").projectDir = new File(moduleProject.sourceDir) 217 } 218 for (modulePlugin in module.plugins) { 219 includeBuild(new File(modulePlugin.sourceDir)) 220 } 221 } 222 223 gradle.beforeProject { project -> 224 if (project !== project.rootProject) { 225 return 226 } 227 def rootProject = project 228 229 // Add plugin classpath to the root project 230 for (module in modules) { 231 for (modulePlugin in module.plugins) { 232 rootProject.buildscript.dependencies.add('classpath', "${modulePlugin.group}:${modulePlugin.id}") 233 } 234 } 235 236 // Add extra maven repositories to allprojects 237 for (mavenRepo in extraMavenRepos) { 238 println "Adding extra maven repository - '${mavenRepo.url}'" 239 } 240 rootProject.allprojects { eachProject -> 241 eachProject.repositories { 242 for (mavenRepo in extraMavenRepos) { 243 maven { url = mavenRepo.url } 244 } 245 } 246 } 247 } 248 249 // Apply plugins for all app projects 250 gradle.afterProject { project -> 251 if (!project.plugins.hasPlugin('com.android.application')) { 252 return 253 } 254 for (module in modules) { 255 for (modulePlugin in module.plugins) { 256 println " ${Emojis.INFORMATION} ${Colors.YELLOW}Applying gradle plugin${Colors.RESET} '${Colors.GREEN}${modulePlugin.id}${Colors.RESET}' (${module.name}@${module.version})" 257 project.plugins.apply(modulePlugin.id) 258 } 259 } 260 } 261 262 // Save the manager in the shared context, so that we can later use it in `build.gradle`. 263 gradle.ext.expoAutolinkingManager = manager 264 } 265} else { 266 def addModule = { DependencyHandler handler, String projectName, Boolean useAAR -> 267 Project dependency = rootProject.project(":${projectName}") 268 269 if (useAAR) { 270 handler.add('api', "${dependency.group}:${projectName}:${dependency.version}") 271 } else { 272 handler.add('api', dependency) 273 } 274 } 275 276 def addDependencies = { DependencyHandler handler, Project project -> 277 def manager = validateExpoAutolinkingManager(gradle.ext.expoAutolinkingManager) 278 def modules = manager.getModules(true) 279 280 if (!modules.length) { 281 return 282 } 283 284 println '' 285 println 'Using expo modules' 286 287 for (module in modules) { 288 // Don't link itself 289 if (module.name == project.name) { 290 continue 291 } 292 // Can remove this once we move all the interfaces into the core. 293 if (module.name.endsWith('-interface')) { 294 continue 295 } 296 297 for (moduleProject in module.projects) { 298 addModule(handler, moduleProject.name, manager.shouldUseAAR()) 299 println " - ${Colors.GREEN}${moduleProject.name}${Colors.RESET} (${module.version})" 300 } 301 } 302 303 println '' 304 } 305 306 // Adding dependencies 307 ext.addExpoModulesDependencies = { DependencyHandler handler, Project project -> 308 // Return early if `useExpoModules` was not called in `settings.gradle` 309 if (!gradle.ext.has('expoAutolinkingManager')) { 310 logger.error('Error: Autolinking is not set up in `settings.gradle`: expo modules won\'t be autolinked.') 311 return 312 } 313 314 def manager = validateExpoAutolinkingManager(gradle.ext.expoAutolinkingManager) 315 316 if (rootProject.findProject(':expo-modules-core')) { 317 // `expo` requires `expo-modules-core` as a dependency, even if autolinking is turned off. 318 addModule(handler, 'expo-modules-core', manager.shouldUseAAR()) 319 } else { 320 logger.error('Error: `expo-modules-core` project is not included by autolinking.') 321 } 322 323 // If opted-in not to autolink modules as dependencies 324 if (manager.options == null) { 325 return 326 } 327 328 addDependencies(handler, project) 329 } 330 331 // Generating the package list 332 ext.generatedFilesSrcDir = ExpoAutolinkingManager.generatedFilesSrcDir 333 334 ext.generateExpoModulesPackageList = { 335 // Get options used in `settings.gradle` or null if it wasn't set up. 336 Map options = gradle.ext.has('expoAutolinkingManager') ? gradle.ext.expoAutolinkingManager.options : null 337 338 if (options == null) { 339 // TODO(@tsapeta): Temporarily muted this error — uncomment it once we start migrating from autolinking v1 to v2 340 // logger.error('Autolinking is not set up in `settings.gradle`: generated package list with expo modules will be empty.') 341 } 342 ExpoAutolinkingManager.generatePackageList(project, options) 343 } 344 345 ext.getGenerateExpoModulesPackagesListPath = { 346 return ExpoAutolinkingManager.getGeneratedFilePath(project) 347 } 348 349 ext.getModulesConfig = { 350 if (!gradle.ext.has('expoAutolinkingManager')) { 351 return null 352 } 353 354 def modules = gradle.ext.expoAutolinkingManager.resolve(true).modules 355 return modules.toString() 356 } 357 358 ext.ensureDependeciesWereEvaluated = { Project project -> 359 if (!gradle.ext.has('expoAutolinkingManager')) { 360 return 361 } 362 363 def modules = gradle.ext.expoAutolinkingManager.getModules(true) 364 for (module in modules) { 365 for (moduleProject in module.projects) { 366 def dependency = project.findProject(":${moduleProject.name}") 367 if (dependency == null) { 368 logger.warn("Coudn't find project ${moduleProject.name}. Please, make sure that `useExpoModules` was called in `settings.gradle`.") 369 continue 370 } 371 372 // Prevent circular dependencies 373 if (moduleProject.name == project.name) { 374 continue 375 } 376 377 project.evaluationDependsOn(":${moduleProject.name}") 378 } 379 } 380 } 381} 382