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' 32 static String generatedPackageListFilename = 'ExpoModulesPackageList.java' 33 static String generatedFilesSrcDir = 'generated/expo/src/main/java' 34 35 ExpoAutolinkingManager(File projectDir, Map options = [:]) { 36 this.projectDir = projectDir 37 this.options = options 38 } 39 40 Object resolve() { 41 if (cachedResolvingResults) { 42 return cachedResolvingResults 43 } 44 String[] args = convertOptionsToCommandArgs('resolve', this.options) 45 args += ['--json'] 46 47 String output = exec(args) 48 Object json = new JsonSlurper().parseText(output) 49 50 cachedResolvingResults = json 51 return json 52 } 53 54 boolean shouldUseAAR() { 55 return options?.useAAR == true 56 } 57 58 ExpoModule[] getModules() { 59 Object json = resolve() 60 return json.modules.collect { new ExpoModule(it) } 61 } 62 63 static void generatePackageList(Project project, Map options) { 64 String[] args = convertOptionsToCommandArgs('generate-package-list', options) 65 66 // Construct absolute path to generated package list. 67 def generatedFilePath = Paths.get( 68 project.buildDir.toString(), 69 generatedFilesSrcDir, 70 generatedPackageListNamespace.replace('.', '/'), 71 generatedPackageListFilename 72 ) 73 74 args += [ 75 '--namespace', 76 generatedPackageListNamespace, 77 '--target', 78 generatedFilePath.toString() 79 ] 80 81 if (options == null) { 82 // Options are provided only when settings.gradle was configured. 83 // If not or opted-out from autolinking, the generated list should be empty. 84 args += '--empty' 85 } 86 87 exec(args) 88 } 89 90 static String exec(String[] commandArgs) { 91 Process proc = commandArgs.execute() 92 StringBuffer outputStream = new StringBuffer() 93 proc.waitForProcessOutput(outputStream, System.err) 94 return outputStream.toString() 95 } 96 97 static private String[] convertOptionsToCommandArgs(String command, Map options) { 98 String[] args = [ 99 'node', 100 '--eval', 101 'require(\'expo-modules-autolinking\')(process.argv.slice(1))', 102 '--', 103 command, 104 '--platform', 105 'android' 106 ] 107 108 def searchPaths = options?.get("searchPaths", options?.get("modulesPaths", null)) 109 if (searchPaths) { 110 args += 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 147 // Save the manager in the shared context, so that we can later use it in `build.gradle`. 148 gradle.ext.expoAutolinkingManager = manager 149 } 150} else { 151 def addModule = { DependencyHandler handler, String projectName, Boolean useAAR -> 152 Project dependency = rootProject.project(":${projectName}") 153 154 if (useAAR) { 155 handler.add('api', "${dependency.group}:${projectName}:${dependency.version}") 156 } else { 157 handler.add('api', dependency) 158 } 159 } 160 161 def addDependencies = { DependencyHandler handler, Project project -> 162 ExpoAutolinkingManager manager = gradle.ext.expoAutolinkingManager 163 def modules = manager.getModules() 164 165 if (!modules.length) { 166 return 167 } 168 169 println 'Using expo modules' 170 171 for (module in modules) { 172 // Don't link itself 173 if (module.name == project.name) { 174 continue 175 } 176 177 addModule(handler, module.projectName, manager.shouldUseAAR()) 178 179 // Can remove this once we move all the interfaces into the core. 180 if (module.name.endsWith('-interface')) { 181 continue 182 } 183 184 println "— ${Colors.GREEN}${module.name}${Colors.RESET} (${module.version})" 185 } 186 } 187 188 // Adding dependencies 189 ext.addExpoModulesDependencies = { DependencyHandler handler, Project project -> 190 // Return early if `useExpoModules` was not called in `settings.gradle` 191 if (!gradle.ext.has('expoAutolinkingManager')) { 192 logger.error('Error: Autolinking is not set up in `settings.gradle`: expo modules won\'t be autolinked.') 193 return 194 } 195 196 ExpoAutolinkingManager manager = gradle.ext.expoAutolinkingManager 197 198 if (rootProject.findProject(':expo-modules-core')) { 199 // `expo` requires `expo-modules-core` as a dependency, even if autolinking is turned off. 200 addModule(handler, 'expo-modules-core', manager.shouldUseAAR()) 201 } else { 202 logger.error('Error: `expo-modules-core` project is not included by autolinking.') 203 } 204 205 // If opted-in not to autolink modules as dependencies 206 if (manager.options == null) { 207 return 208 } 209 210 addDependencies(handler, project) 211 } 212 213 // Generating the package list 214 ext.generatedFilesSrcDir = ExpoAutolinkingManager.generatedFilesSrcDir 215 216 ext.generateExpoModulesPackageList = { 217 // Get options used in `settings.gradle` or null if it wasn't set up. 218 Map options = gradle.ext.has('expoAutolinkingManager') ? gradle.ext.expoAutolinkingManager.options : null 219 220 if (options == null) { 221 // TODO(@tsapeta): Temporarily muted this error — uncomment it once we start migrating from autolinking v1 to v2 222 // logger.error('Autolinking is not set up in `settings.gradle`: generated package list with expo modules will be empty.') 223 } 224 ExpoAutolinkingManager.generatePackageList(project, options) 225 } 226 227 ext.ensureDependeciesWereEvaluated = { Project project -> 228 if (!gradle.ext.has('expoAutolinkingManager')) { 229 return 230 } 231 232 def modules = gradle.ext.expoAutolinkingManager.getModules() 233 for (module in modules) { 234 def dependency = project.findProject(":${module.projectName}") 235 if (dependency == null) { 236 logger.warn("Coudn't find project ${module.projectName}. Please, make sure that `useExpoModules` was called in `settings.gradle`.") 237 continue 238 } 239 240 // Prevent circular dependencies 241 if (module.projectName == project.name) { 242 continue 243 } 244 245 project.evaluationDependsOn(":${module.projectName}") 246 } 247 } 248} 249