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      '--no-warnings',
112      '--eval',
113      'require(\'expo-modules-autolinking\')(process.argv.slice(1))',
114      '--',
115      command,
116      '--platform',
117      'android'
118    ]
119
120    def searchPaths = options?.get("searchPaths", options?.get("modulesPaths", null))
121    if (searchPaths) {
122      args += searchPaths
123    }
124
125    if (options?.ignorePaths) {
126      args += '--ignore-paths'
127      args += options.ignorePaths
128    }
129
130    if (options?.exclude) {
131      args += '--exclude'
132      args += options.exclude
133    }
134
135    return args
136  }
137}
138
139class Colors {
140  static final String GREEN = "\u001B[32m"
141  static final String RESET = "\u001B[0m"
142}
143
144// We can't cast a manager that is created in `settings.gradle` to the `ExpoAutolinkingManager`
145// because if someone is using `buildSrc`, the `ExpoAutolinkingManager` class
146// will be loaded by two different class loader - `settings.gradle` will use a diffrent loader.
147// In the JVM, classes are equal only if were loaded by the same loader.
148// 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.
149def validateExpoAutolinkingManager(manager) {
150  assert ExpoAutolinkingManager.name == manager.getClass().name
151  return manager
152}
153
154// Here we split the implementation, depending on Gradle context.
155// `rootProject` is a `ProjectDescriptor` if this file is imported in `settings.gradle` context,
156// otherwise we can assume it is imported in `build.gradle`.
157if (rootProject instanceof ProjectDescriptor) {
158  // Method to be used in `settings.gradle`. Options passed here will have an effect in `build.gradle` context as well,
159  // i.e. adding the dependencies and generating the package list.
160  ext.useExpoModules = { Map options = [:] ->
161    ExpoAutolinkingManager manager = new ExpoAutolinkingManager(rootProject.projectDir, options)
162    ExpoModule[] modules = manager.getModules()
163
164    for (module in modules) {
165      for (moduleProject in module.projects) {
166        include(":${moduleProject.name}")
167        project(":${moduleProject.name}").projectDir = new File(moduleProject.sourceDir)
168      }
169    }
170
171    // Save the manager in the shared context, so that we can later use it in `build.gradle`.
172    gradle.ext.expoAutolinkingManager = manager
173  }
174} else {
175  def addModule = { DependencyHandler handler, String projectName, Boolean useAAR ->
176    Project dependency = rootProject.project(":${projectName}")
177
178    if (useAAR) {
179      handler.add('api', "${dependency.group}:${projectName}:${dependency.version}")
180    } else {
181      handler.add('api', dependency)
182    }
183  }
184
185  def addDependencies = { DependencyHandler handler, Project project ->
186    def manager = validateExpoAutolinkingManager(gradle.ext.expoAutolinkingManager)
187    def modules = manager.getModules()
188
189    if (!modules.length) {
190      return
191    }
192
193    println ''
194    println 'Using expo modules'
195
196    for (module in modules) {
197      // Don't link itself
198      if (module.name == project.name) {
199        continue
200      }
201      // Can remove this once we move all the interfaces into the core.
202      if (module.name.endsWith('-interface')) {
203        continue
204      }
205
206      for (moduleProject in module.projects) {
207        addModule(handler, moduleProject.name, manager.shouldUseAAR())
208        println "  - ${Colors.GREEN}${moduleProject.name}${Colors.RESET} (${module.version})"
209      }
210    }
211
212    println ''
213  }
214
215  // Adding dependencies
216  ext.addExpoModulesDependencies = { DependencyHandler handler, Project project ->
217    // Return early if `useExpoModules` was not called in `settings.gradle`
218    if (!gradle.ext.has('expoAutolinkingManager')) {
219      logger.error('Error: Autolinking is not set up in `settings.gradle`: expo modules won\'t be autolinked.')
220      return
221    }
222
223    def manager = validateExpoAutolinkingManager(gradle.ext.expoAutolinkingManager)
224
225    if (rootProject.findProject(':expo-modules-core')) {
226      // `expo` requires `expo-modules-core` as a dependency, even if autolinking is turned off.
227      addModule(handler, 'expo-modules-core', manager.shouldUseAAR())
228    } else {
229      logger.error('Error: `expo-modules-core` project is not included by autolinking.')
230    }
231
232    // If opted-in not to autolink modules as dependencies
233    if (manager.options == null) {
234      return
235    }
236
237    addDependencies(handler, project)
238  }
239
240  // Generating the package list
241  ext.generatedFilesSrcDir = ExpoAutolinkingManager.generatedFilesSrcDir
242
243  ext.generateExpoModulesPackageList = {
244    // Get options used in `settings.gradle` or null if it wasn't set up.
245    Map options = gradle.ext.has('expoAutolinkingManager') ? gradle.ext.expoAutolinkingManager.options : null
246
247    if (options == null) {
248      // TODO(@tsapeta): Temporarily muted this error — uncomment it once we start migrating from autolinking v1 to v2
249      // logger.error('Autolinking is not set up in `settings.gradle`: generated package list with expo modules will be empty.')
250    }
251    ExpoAutolinkingManager.generatePackageList(project, options)
252  }
253
254  ext.ensureDependeciesWereEvaluated = { Project project ->
255    if (!gradle.ext.has('expoAutolinkingManager')) {
256      return
257    }
258
259    def modules = gradle.ext.expoAutolinkingManager.getModules()
260    for (module in modules) {
261      for (moduleProject in module.projects) {
262        def dependency = project.findProject(":${moduleProject.name}")
263        if (dependency == null) {
264          logger.warn("Coudn't find project ${moduleProject.name}. Please, make sure that `useExpoModules` was called in `settings.gradle`.")
265          continue
266        }
267
268        // Prevent circular dependencies
269        if (moduleProject.name == project.name) {
270          continue
271        }
272
273        project.evaluationDependsOn(":${moduleProject.name}")
274      }
275    }
276  }
277}
278