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