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