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