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