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